fix: hide app configuration for external app type (#13636)

This commit is contained in:
karanh37 2023-10-21 20:15:36 +05:30 committed by GitHub
parent 6c252b5722
commit 0fee1ccb3a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
35 changed files with 483 additions and 128 deletions

View File

@ -109,7 +109,7 @@ public class AbstractNativeApplication implements NativeApplication {
// Get Pipeline
IngestionPipeline dataInsightPipeline =
getIngestionPipeline(createPipelineRequest, String.format("%sBot", app.getName()), "admin")
.withProvider(ProviderType.SYSTEM);
.withProvider(ProviderType.USER);
ingestionPipelineRepository.setFullyQualifiedName(dataInsightPipeline);
ingestionPipelineRepository.initializeEntity(dataInsightPipeline);
@ -121,7 +121,7 @@ public class AbstractNativeApplication implements NativeApplication {
dataInsightPipeline.getId(),
Entity.APPLICATION,
Entity.INGESTION_PIPELINE,
Relationship.CONTAINS.ordinal());
Relationship.HAS.ordinal());
} catch (Exception ex) {
LOG.error("[IngestionPipelineResource] Failed in Creating Reindex and Insight Pipeline", ex);

View File

@ -111,22 +111,27 @@ public class DataInsightsReportApp extends AbstractNativeApplication {
emails.add(user.getEmail());
}
}
DataInsightTotalAssetTemplate totalAssetTemplate =
createTotalAssetTemplate(searchClient, team.getName(), scheduleTime, currentTime, numberOfDaysChange);
DataInsightDescriptionAndOwnerTemplate descriptionTemplate =
createDescriptionTemplate(searchClient, team.getName(), scheduleTime, currentTime, numberOfDaysChange);
DataInsightDescriptionAndOwnerTemplate ownershipTemplate =
createOwnershipTemplate(searchClient, team.getName(), scheduleTime, currentTime, numberOfDaysChange);
DataInsightDescriptionAndOwnerTemplate tierTemplate =
createTierTemplate(searchClient, team.getName(), scheduleTime, currentTime, numberOfDaysChange);
EmailUtil.sendDataInsightEmailNotificationToUser(
emails,
totalAssetTemplate,
descriptionTemplate,
ownershipTemplate,
tierTemplate,
EmailUtil.getDataInsightReportSubject(),
EmailUtil.DATA_INSIGHT_REPORT_TEMPLATE);
try {
DataInsightTotalAssetTemplate totalAssetTemplate =
createTotalAssetTemplate(searchClient, team.getName(), scheduleTime, currentTime, numberOfDaysChange);
DataInsightDescriptionAndOwnerTemplate descriptionTemplate =
createDescriptionTemplate(searchClient, team.getName(), scheduleTime, currentTime, numberOfDaysChange);
DataInsightDescriptionAndOwnerTemplate ownershipTemplate =
createOwnershipTemplate(searchClient, team.getName(), scheduleTime, currentTime, numberOfDaysChange);
DataInsightDescriptionAndOwnerTemplate tierTemplate =
createTierTemplate(searchClient, team.getName(), scheduleTime, currentTime, numberOfDaysChange);
EmailUtil.sendDataInsightEmailNotificationToUser(
emails,
totalAssetTemplate,
descriptionTemplate,
ownershipTemplate,
tierTemplate,
EmailUtil.getDataInsightReportSubject(),
EmailUtil.DATA_INSIGHT_REPORT_TEMPLATE);
} catch (Exception ex) {
LOG.error("[DataInsightReport] Failed for Team: {}, Reason : {}", team.getName(), ex.getMessage());
}
}
}
}
@ -135,23 +140,27 @@ public class DataInsightsReportApp extends AbstractNativeApplication {
throws ParseException, IOException, TemplateException {
// Get Admins
Set<String> emailList = getAdminsData(CreateEventSubscription.SubscriptionType.DATA_INSIGHT);
DataInsightTotalAssetTemplate totalAssetTemplate =
createTotalAssetTemplate(searchClient, null, scheduleTime, currentTime, numberOfDaysChange);
DataInsightDescriptionAndOwnerTemplate descriptionTemplate =
createDescriptionTemplate(searchClient, null, scheduleTime, currentTime, numberOfDaysChange);
DataInsightDescriptionAndOwnerTemplate ownershipTemplate =
createOwnershipTemplate(searchClient, null, scheduleTime, currentTime, numberOfDaysChange);
DataInsightDescriptionAndOwnerTemplate tierTemplate =
createTierTemplate(searchClient, null, scheduleTime, currentTime, numberOfDaysChange);
EmailUtil.sendDataInsightEmailNotificationToUser(
emailList,
totalAssetTemplate,
descriptionTemplate,
ownershipTemplate,
tierTemplate,
EmailUtil.getDataInsightReportSubject(),
EmailUtil.DATA_INSIGHT_REPORT_TEMPLATE);
try {
// Build Insights Report
DataInsightTotalAssetTemplate totalAssetTemplate =
createTotalAssetTemplate(searchClient, null, scheduleTime, currentTime, numberOfDaysChange);
DataInsightDescriptionAndOwnerTemplate descriptionTemplate =
createDescriptionTemplate(searchClient, null, scheduleTime, currentTime, numberOfDaysChange);
DataInsightDescriptionAndOwnerTemplate ownershipTemplate =
createOwnershipTemplate(searchClient, null, scheduleTime, currentTime, numberOfDaysChange);
DataInsightDescriptionAndOwnerTemplate tierTemplate =
createTierTemplate(searchClient, null, scheduleTime, currentTime, numberOfDaysChange);
EmailUtil.sendDataInsightEmailNotificationToUser(
emailList,
totalAssetTemplate,
descriptionTemplate,
ownershipTemplate,
tierTemplate,
EmailUtil.getDataInsightReportSubject(),
EmailUtil.DATA_INSIGHT_REPORT_TEMPLATE);
} catch (Exception ex) {
LOG.error("[DataInsightReport] Failed for Admin, Reason : {}", ex.getMessage(), ex);
}
}
private List<Kpi> getAvailableKpi() {

View File

@ -1,5 +1,6 @@
package org.openmetadata.service.jdbi3;
import static org.openmetadata.schema.type.Include.ALL;
import static org.openmetadata.service.resources.teams.UserResource.getUser;
import java.util.ArrayList;
@ -52,6 +53,17 @@ public class AppRepository extends EntityRepository<App> {
return entity.withBot(getBotUser(entity));
}
protected List<EntityReference> getIngestionPipelines(App service) {
List<CollectionDAO.EntityRelationshipRecord> pipelines =
findToRecords(service.getId(), entityType, Relationship.HAS, Entity.INGESTION_PIPELINE);
List<EntityReference> ingestionPipelines = new ArrayList<>();
for (CollectionDAO.EntityRelationshipRecord entityRelationshipRecord : pipelines) {
ingestionPipelines.add(
Entity.getEntityReferenceById(Entity.INGESTION_PIPELINE, entityRelationshipRecord.getId(), ALL));
}
return ingestionPipelines;
}
public AppMarketPlaceRepository getMarketPlace() {
return (AppMarketPlaceRepository) Entity.getEntityRepository(Entity.APP_MARKET_PLACE_DEF);
}
@ -194,6 +206,15 @@ public class AppRepository extends EntityRepository<App> {
}
}
@Override
protected void cleanup(App app) {
// Remove the Pipelines for Application
List<EntityReference> pipelineRef = getIngestionPipelines(app);
pipelineRef.forEach(
(reference) -> Entity.deleteEntity("admin", reference.getType(), reference.getId(), true, true));
super.cleanup(app);
}
public AppRunRecord getLatestAppRuns(UUID appId) {
String json = daoCollection.appExtensionTimeSeriesDao().getLatestAppRun(appId);
return JsonUtils.readValue(json, AppRunRecord.class);

View File

@ -2,7 +2,7 @@
"name": "DataInsightsApplication",
"fullyQualifiedName": "DataInsightsApplication",
"displayName": "Data Insights",
"description": "Open-metadata Data Insights Application runs a Data Insights Pipeline on the Ingestion Framework.",
"description": "OpenMetadata Data Insights Application runs a Data Insights Pipeline on the Ingestion Framework.",
"features": "Run Ingestion Framework Data Insight Module.",
"appType": "external",
"appScreenshots": ["DataInsightsPic1.png"],

View File

@ -2,7 +2,7 @@
"name": "DataInsightsReportApplication",
"fullyQualifiedName": "DataInsightsReportApplication",
"displayName": "Data Insights Report",
"description": "Open-metadata Data Insights Report Application sends a data insight report.",
"description": "OpenMetadata Data Insights Report Application sends a data insight report.",
"features": "Data Insights Application. Send Email Reports.",
"appType": "internal",
"appScreenshots": ["DataInsightsReportPic1.png"],

View File

@ -2,8 +2,8 @@
"name": "SearchIndexingApplication",
"fullyQualifiedName": "SearchIndexingApplication",
"displayName": "Search Indexing",
"description": "Open-metadata connects with Elastic/Open Search to provide search feature for Data Assets. This application provides additional features related to ES/OS.",
"features": "Sync Open-metadata and Elastic Search and Recreate Indexes.",
"description": "OpenMetadata connects with Elastic/Open Search to provide search feature for Data Assets. This application provides additional features related to ES/OS.",
"features": "Sync OpenMetadata and Elastic Search and Recreate Indexes.",
"appType": "internal",
"appScreenshots": ["SearchIndexPic1.png"],
"developer": "Collate Inc.",

View File

@ -11,7 +11,7 @@
"name": "ApplicationBotRule-Allow",
"description" : "Allow ingestion bots to read data entities",
"resources" : ["All"],
"operations": ["ViewAll"],
"operations": ["Create", "EditAll", "ViewAll", "Delete"],
"effect": "allow"
}
]

Binary file not shown.

Before

Width:  |  Height:  |  Size: 568 KiB

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 858 KiB

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 642 KiB

After

Width:  |  Height:  |  Size: 1.6 MiB

View File

@ -46,6 +46,10 @@ export interface TestSuiteSchedulerProps {
onSubmit: (repeatFrequency: string) => void;
onCancel: () => void;
isQuartzCron?: boolean;
buttonProps?: {
okText: string;
cancelText: string;
};
}
export interface RightPanelProps {

View File

@ -19,6 +19,7 @@ import { TestSuiteSchedulerProps } from '../AddDataQualityTest.interface';
const TestSuiteScheduler: React.FC<TestSuiteSchedulerProps> = ({
initialData,
buttonProps,
onCancel,
onSubmit,
isQuartzCron = false,
@ -44,12 +45,14 @@ const TestSuiteScheduler: React.FC<TestSuiteSchedulerProps> = ({
</Col>
<Col span={24}>
<Space className="w-full justify-end" size={16}>
<Button onClick={onCancel}>{t('label.back')}</Button>
<Button onClick={onCancel}>
{buttonProps?.cancelText ?? t('label.back')}
</Button>
<Button
data-testid="deploy-button"
type="primary"
onClick={() => onSubmit(repeatFrequency || '')}>
{t('label.submit')}
{buttonProps?.okText ?? t('label.submit')}
</Button>
</Space>
</Col>

View File

@ -50,9 +50,11 @@ import {
import { ServiceCategory } from '../../../enums/service.enum';
import {
App,
AppType,
ScheduleTimeline,
} from '../../../generated/entity/applications/app';
import {
deployApp,
getApplicationByName,
patchApplication,
triggerOnDemandApp,
@ -86,7 +88,7 @@ const AppDetails = () => {
const fetchAppDetails = useCallback(async () => {
setIsLoading(true);
try {
const data = await getApplicationByName(fqn, 'owner');
const data = await getApplicationByName(fqn, ['owner', 'pipelines']);
setAppData(data);
const schema = await import(
`../../../utils/ApplicationSchemas/${fqn}.json`
@ -223,7 +225,25 @@ const AppDetails = () => {
const onDemandTrigger = async () => {
try {
await triggerOnDemandApp(appData?.fullyQualifiedName ?? '');
showSuccessToast(t('message.application-trigger-successfully'));
showSuccessToast(
t('message.application-action-successfully', {
action: t('label.triggered-lowercase'),
})
);
} catch (error) {
showErrorToast(error as AxiosError);
}
};
const onDeployTrigger = async () => {
try {
await deployApp(appData?.fullyQualifiedName ?? '');
showSuccessToast(
t('message.application-action-successfully', {
action: t('label.deploy'),
})
);
fetchAppDetails();
} catch (error) {
showErrorToast(error as AxiosError);
}
@ -231,7 +251,9 @@ const AppDetails = () => {
const tabs = useMemo(() => {
const tabConfiguration =
appData && appData.appConfiguration && jsonSchema
appData?.appConfiguration &&
appData.appType === AppType.Internal &&
jsonSchema
? [
{
label: (
@ -276,6 +298,7 @@ const AppDetails = () => {
appData={appData}
onCancel={onBrowseAppsClick}
onDemandTrigger={onDemandTrigger}
onDeployTrigger={onDeployTrigger}
onSave={onAppScheduleSave}
/>
)}
@ -283,17 +306,24 @@ const AppDetails = () => {
),
},
...tabConfiguration,
{
label: (
<TabsLabel id={ApplicationTabs.HISTORY} name={t('label.history')} />
),
key: ApplicationTabs.HISTORY,
children: (
<div className="p-y-md">
<AppRunsHistory />
</div>
),
},
...(appData?.appType === AppType.Internal
? [
{
label: (
<TabsLabel
id={ApplicationTabs.HISTORY}
name={t('label.history')}
/>
),
key: ApplicationTabs.HISTORY,
children: (
<div className="p-y-md">
<AppRunsHistory />
</div>
),
},
]
: []),
];
}, [appData, jsonSchema]);

View File

@ -0,0 +1,22 @@
/*
* Copyright 2023 Collate.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { PipelineStatus } from '../../../generated/entity/services/ingestionPipelines/ingestionPipeline';
export interface DataInsightLatestRun {
lastIngestionLogs: {
data_insight_task: string;
total: string;
};
pipelineStatus: PipelineStatus;
}

View File

@ -39,6 +39,7 @@ import { AppInstallVerifyCardProps } from './AppInstallVerifyCard.interface';
const AppInstallVerifyCard = ({
appData,
nextButtonLabel,
onCancel,
onSave,
}: AppInstallVerifyCardProps) => {
@ -135,7 +136,7 @@ const AppInstallVerifyCard = ({
key="save-btn"
type="primary"
onClick={onSave}>
{t('label.configure')}
{nextButtonLabel}
</Button>
</div>
</Card>

View File

@ -14,6 +14,7 @@ import { AppMarketPlaceDefinition } from '../../../generated/entity/applications
export interface AppInstallVerifyCardProps {
appData: AppMarketPlaceDefinition;
nextButtonLabel: string;
onSave: () => void;
onCancel: () => void;
}

View File

@ -10,7 +10,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Button, Col, Row } from 'antd';
import { Button, Col, Row, Typography } from 'antd';
import { ColumnsType } from 'antd/lib/table';
import { AxiosError } from 'axios';
import { capitalize, isNull } from 'lodash';
@ -23,13 +23,20 @@ import React, {
useState,
} from 'react';
import { useTranslation } from 'react-i18next';
import { useParams } from 'react-router-dom';
import { useHistory, useParams } from 'react-router-dom';
import { NO_DATA_PLACEHOLDER } from '../../../constants/constants';
import { GlobalSettingOptions } from '../../../constants/GlobalSettings.constants';
import { AppType } from '../../../generated/entity/applications/app';
import { Status } from '../../../generated/entity/applications/appRunRecord';
import { Paging } from '../../../generated/type/paging';
import { usePaging } from '../../../hooks/paging/usePaging';
import { getApplicationRuns } from '../../../rest/applicationAPI';
import {
getApplicationRuns,
getLatestApplicationRuns,
} from '../../../rest/applicationAPI';
import { getStatusTypeForApplication } from '../../../utils/ApplicationUtils';
import { formatDateTime } from '../../../utils/date-time/DateTimeUtils';
import { getLogsViewerPath } from '../../../utils/RouterUtils';
import { showErrorToast } from '../../../utils/ToastUtils';
import ErrorPlaceHolder from '../../common/error-with-placeholder/ErrorPlaceHolder';
import NextPrevious from '../../common/next-previous/NextPrevious';
@ -44,7 +51,10 @@ import {
} from './AppRunsHistory.interface';
const AppRunsHistory = forwardRef(
({ maxRecords, showPagination = true }: AppRunsHistoryProps, ref) => {
(
{ appData, maxRecords, showPagination = true }: AppRunsHistoryProps,
ref
) => {
const { t } = useTranslation();
const { fqn } = useParams<{ fqn: string }>();
const [isLoading, setIsLoading] = useState(true);
@ -62,9 +72,25 @@ const AppRunsHistory = forwardRef(
handlePageSizeChange,
} = usePaging();
const history = useHistory();
const isExternalApp = useMemo(
() => appData?.appType === AppType.External,
[appData]
);
const handleRowExpandable = useCallback(
(key?: string) => {
if (key) {
if (isExternalApp && appData) {
return history.push(
getLogsViewerPath(
GlobalSettingOptions.APPLICATIONS,
appData.name ?? '',
appData.name ?? ''
)
);
}
if (expandedRowKeys.includes(key)) {
setExpandedRowKeys((prev) => prev.filter((item) => item !== key));
} else {
@ -95,6 +121,9 @@ const AppRunsHistory = forwardRef(
title: t('label.run-type'),
dataIndex: 'runType',
key: 'runType',
render: (runType) => (
<Typography.Text>{runType ?? NO_DATA_PLACEHOLDER}</Typography.Text>
),
},
{
title: t('label.status'),
@ -143,18 +172,33 @@ const AppRunsHistory = forwardRef(
async (pagingOffset?: Paging) => {
try {
setIsLoading(true);
const { data, paging } = await getApplicationRuns(fqn, {
offset: pagingOffset?.offset ?? 0,
limit: maxRecords ?? pageSize,
});
setAppRunsHistoryData(
data.map((item) => ({
...item,
id: `${item.appId}-${item.runType}-${item.timestamp}`,
}))
);
handlePagingChange(paging);
if (isExternalApp) {
const res = await getLatestApplicationRuns(fqn);
setAppRunsHistoryData([
{
...res,
timestamp: res.pipelineStatus.timestamp,
status: (res.pipelineStatus.pipelineState ??
Status.Failed) as Status,
id: `${res.pipelineStatus.runId}-${res.pipelineStatus.timestamp}`,
},
]);
} else {
const { data, paging } = await getApplicationRuns(fqn, {
offset: pagingOffset?.offset ?? 0,
limit: maxRecords ?? pageSize,
});
setAppRunsHistoryData(
data.map((item) => ({
...item,
id: `${item.appId}-${item.runType}-${item.timestamp}`,
}))
);
handlePagingChange(paging);
}
} catch (err) {
showErrorToast(err as AxiosError);
} finally {

View File

@ -11,6 +11,7 @@
* limitations under the License.
*/
import { App } from '../../../generated/entity/applications/app';
import { AppRunRecord } from '../../../generated/entity/applications/appRunRecord';
export interface AppRunRecordWithId extends AppRunRecord {
@ -24,4 +25,5 @@ export interface AppRunsHistoryRef {
export interface AppRunsHistoryProps {
maxRecords?: number;
showPagination?: boolean;
appData?: App;
}

View File

@ -12,12 +12,23 @@
*/
import { Button, Col, Divider, Modal, Row, Space, Typography } from 'antd';
import cronstrue from 'cronstrue';
import React, { useMemo, useRef, useState } from 'react';
import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import { useTranslation } from 'react-i18next';
import { AppScheduleClass } from '../../../generated/entity/applications/app';
import {
AppScheduleClass,
AppType,
} from '../../../generated/entity/applications/app';
import { PipelineType } from '../../../generated/entity/services/ingestionPipelines/ingestionPipeline';
import { getIngestionPipelineByFqn } from '../../../rest/ingestionPipelineAPI';
import { getIngestionFrequency } from '../../../utils/CommonUtils';
import TestSuiteScheduler from '../../AddDataQualityTest/components/TestSuiteScheduler';
import Loader from '../../Loader/Loader';
import AppRunsHistory from '../AppRunsHistory/AppRunsHistory.component';
import { AppRunsHistoryRef } from '../AppRunsHistory/AppRunsHistory.interface';
import { AppScheduleProps } from './AppScheduleProps.interface';
@ -26,10 +37,35 @@ const AppSchedule = ({
appData,
onSave,
onDemandTrigger,
onDeployTrigger,
}: AppScheduleProps) => {
const { t } = useTranslation();
const [showModal, setShowModal] = useState(false);
const appRunsHistoryRef = useRef<AppRunsHistoryRef>(null);
const [isPipelineDeployed, setIsPipelineDeployed] = useState(false);
const [isLoading, setIsLoading] = useState(true);
const fetchPipelineDetails = useCallback(async () => {
setIsLoading(true);
try {
if (
appData.appType === AppType.External &&
appData.pipelines &&
appData.pipelines.length > 0
) {
const fqn = appData.pipelines[0].fullyQualifiedName ?? '';
const pipelineData = await getIngestionPipelineByFqn(fqn);
setIsPipelineDeployed(pipelineData.deployed ?? false);
} else {
setIsPipelineDeployed(false);
}
} catch (error) {
setIsPipelineDeployed(false);
} finally {
setIsLoading(false);
}
}, [appData]);
const cronString = useMemo(() => {
if (appData.appSchedule) {
@ -58,6 +94,33 @@ const AppSchedule = ({
appRunsHistoryRef.current?.refreshAppHistory();
};
const appRunHistory = useMemo(() => {
if (appData.appType === AppType.Internal || isPipelineDeployed) {
return (
<AppRunsHistory
appData={appData}
maxRecords={1}
ref={appRunsHistoryRef}
showPagination={false}
/>
);
}
return (
<Typography.Text>
{t('message.no-ingestion-pipeline-found')}
</Typography.Text>
);
}, [appData, isPipelineDeployed, appRunsHistoryRef]);
useEffect(() => {
fetchPipelineDetails();
}, []);
if (isLoading) {
return <Loader />;
}
return (
<>
<Row>
@ -90,6 +153,15 @@ const AppSchedule = ({
</Col>
<Col className="d-flex items-center justify-end" flex="200px">
<Space>
{appData.appType === AppType.External && (
<Button
data-testid="deploy-button"
type="primary"
onClick={onDeployTrigger}>
{t('label.deploy')}
</Button>
)}
<Button
data-testid="edit-button"
type="primary"
@ -97,7 +169,7 @@ const AppSchedule = ({
{t('label.edit')}
</Button>
<Button
data-testid="deploy-button"
data-testid="run-now-button"
type="primary"
onClick={onAppTrigger}>
{t('label.run-now')}
@ -107,24 +179,24 @@ const AppSchedule = ({
<Divider />
<Col span={24}>
<AppRunsHistory
maxRecords={1}
ref={appRunsHistoryRef}
showPagination={false}
/>
</Col>
<Col span={24}>{appRunHistory}</Col>
</Row>
<Modal
destroyOnClose
className="update-schedule-modal"
closable={false}
data-testid="update-schedule-modal"
footer={null}
maskClosable={false}
okText={t('label.save')}
open={showModal}
title={t('label.update-entity', { entity: t('label.schedule') })}>
<TestSuiteScheduler
isQuartzCron
buttonProps={{
cancelText: t('label.cancel'),
okText: t('label.save'),
}}
initialData={getIngestionFrequency(PipelineType.Application)}
onCancel={onDialogCancel}
onSubmit={onDialogSave}

View File

@ -17,4 +17,5 @@ export interface AppScheduleProps {
onCancel: () => void;
onSave: (cron: string) => void;
onDemandTrigger: () => void;
onDeployTrigger: () => void;
}

View File

@ -47,7 +47,9 @@ const MarketPlaceAppDetails = () => {
);
const imageSrc = imageModule.default;
return <img alt={screenshotName} src={imageSrc} />;
return (
<img alt={screenshotName} src={imageSrc} style={{ height: '320px' }} />
);
} catch (error) {
return <></>;
}

View File

@ -351,7 +351,7 @@ export const AssetSelectionModal = ({
{isLoading && <Loader />}
{!isLoading && items.length > 0 && (
<List loading={{ spinning: isLoading, indicator: <Loader /> }}>
<List>
<VirtualList
data={items}
height={500}

View File

@ -162,7 +162,7 @@ export const PAGE_HEADERS = {
},
APPLICATION: {
header: i18n.t('label.extend-open-meta-data'),
subHeader: i18n.t('message.tools-to-improve-workflow'),
subHeader: i18n.t('message.application-to-improve-data'),
},
CUSTOM_PAGE: {
header: i18n.t('label.customize-entity', {

View File

@ -403,7 +403,7 @@
"explore-now": "Jetzt erkunden",
"export": "Exportieren",
"export-entity": "{{entity}} exportieren",
"extend-open-meta-data": "Extend OpenMetaData",
"extend-open-meta-data": "Extend OpenMetadata",
"failed": "Fehlgeschlagen",
"failure-context": "Fehlerkontext",
"feature": "Funktion",
@ -1059,6 +1059,7 @@
"tree": "Baum",
"trigger": "Auslöser",
"trigger-type": "Auslösertyp",
"triggered-lowercase": "triggered",
"triggering-lowercase": "Auslösen",
"try-again": "Erneut versuchen",
"tuesday": "Dienstag",
@ -1170,7 +1171,8 @@
"app-disabled-successfully": "Application disabled successfully",
"app-installed-successfully": "Application installed successfully",
"app-uninstalled-successfully": "Application uninstalled successfully",
"application-trigger-successfully": "Application triggered successfully",
"application-action-successfully": "Application {{action}} successfully",
"application-to-improve-data": "Find applications to improve your data",
"are-you-sure": "Sind Sie sicher?",
"are-you-sure-action-property": "Are you sure you want to {{action}} {{propertyName}}?",
"are-you-sure-delete-entity": "Sind Sie sicher, dass Sie die Eigenschaft {{entity}} dauerhaft löschen möchten?",
@ -1394,6 +1396,7 @@
"no-info-about-joined-tables": "Keine Informationen über verbundene Tabellen.",
"no-ingestion-available": "Keine Eingabedaten verfügbar",
"no-ingestion-description": "Um Ingestion-Daten anzuzeigen, führen Sie die Metadaten-Ingestion aus. Bitte beziehen Sie sich auf dieses Dokument, um die <0>{{link}}</0> zu planen.",
"no-ingestion-pipeline-found": "No ingestion pipeline found. Please click on Deploy button to setup the ingestion pipeline.",
"no-inherited-roles-found": "Keine vererbten Rollen gefunden",
"no-installed-applications-found": "No applications are currently installed. Click the 'Add Apps' button to install one.",
"no-kpi-available-add-new-one": "Keine KPIs verfügbar. Klicken Sie auf die Schaltfläche 'KPI hinzufügen', um einen hinzuzufügen.",
@ -1542,7 +1545,6 @@
"to-add-new-line": "um eine neue Zeile hinzuzufügen",
"token-has-no-expiry": "Dieses Token hat kein Ablaufdatum.",
"token-security-description": "Jeder, der Ihr JWT-Token hat, kann REST-API-Anfragen an den OpenMetadata Server senden. Geben Sie das JWT-Token nicht in Ihrem Anwendungscode preis. Teilen Sie es nicht auf GitHub oder an anderer Stelle online.",
"tools-to-improve-workflow": "Find tools to improve your workflow",
"total-entity-insight": "Anzeigen der neuesten Anzahl von Datenvermögenswerten nach Typ.",
"tour-follow-step": "Folgen Sie einem Datenvermögen, um über Änderungen informiert zu bleiben. In Ihrem Aktivitäts-Feed werden alle Änderungen am von Ihnen verfolgten Datenvermögen angezeigt. Sie erhalten auch Benachrichtigungen, wenn dieses Datenvermögen Datenqualitätsprobleme aufweist.",
"tour-high-level-assets-information-step": "Auf der Seite für Datenvermögen erhalten Sie einen 360-Grad-Blick auf das Datenvermögen, um alle Kontexte der Daten zu verstehen und optimal zu nutzen.",

View File

@ -403,7 +403,7 @@
"explore-now": "Explore Now",
"export": "Export",
"export-entity": "Export {{entity}}",
"extend-open-meta-data": "Extend OpenMetaData",
"extend-open-meta-data": "Extend OpenMetadata",
"failed": "Failed",
"failure-context": "Failure Context",
"feature": "Feature",
@ -1059,6 +1059,7 @@
"tree": "Tree",
"trigger": "Trigger",
"trigger-type": "Trigger type",
"triggered-lowercase": "triggered",
"triggering-lowercase": "triggering",
"try-again": "Try Again",
"tuesday": "Tuesday",
@ -1170,7 +1171,8 @@
"app-disabled-successfully": "Application disabled successfully",
"app-installed-successfully": "Application installed successfully",
"app-uninstalled-successfully": "Application uninstalled successfully",
"application-trigger-successfully": "Application triggered successfully",
"application-action-successfully": "Application {{action}} successfully",
"application-to-improve-data": "Find applications to improve your data",
"are-you-sure": "Are you sure?",
"are-you-sure-action-property": "Are you sure you want to {{action}} {{propertyName}}?",
"are-you-sure-delete-entity": "Are you sure you want to delete the property {{entity}}",
@ -1394,6 +1396,7 @@
"no-info-about-joined-tables": "No information about joined tables.",
"no-ingestion-available": "No ingestion data available",
"no-ingestion-description": "To view Ingestion Data, run the metadata ingestion. Please refer to this doc to schedule the <0>{{link}}</0>",
"no-ingestion-pipeline-found": "No ingestion pipeline found. Please click on Deploy button to setup the ingestion pipeline.",
"no-inherited-roles-found": "No inherited roles found",
"no-installed-applications-found": "No applications are currently installed. Click the 'Add Apps' button to install one.",
"no-kpi-available-add-new-one": "No KPIs are available. Click on the Add KPI button to add one.",
@ -1542,7 +1545,6 @@
"to-add-new-line": "to add a new line",
"token-has-no-expiry": "This token has no expiration date.",
"token-security-description": "Anyone who has your JWT Token will be able to send REST API requests to the OpenMetadata Server. Do not expose the JWT Token in your application code. Do not share it on GitHub or anywhere else online.",
"tools-to-improve-workflow": "Find tools to improve your workflow",
"total-entity-insight": "Display the latest number of data assets by type.",
"tour-follow-step": "Follow a data asset to stay informed about the changes to it. In your activity feed all the changes to the data asset you are following are shown. You will also get alerts if this data asset has data quality issues.",
"tour-high-level-assets-information-step": "In Data Assets details page, you get 360-degree view of the data asset to help you understand the all the context of the data and make the best use of it.",

View File

@ -403,7 +403,7 @@
"explore-now": "Explorar ahora",
"export": "Exportar",
"export-entity": "Export {{entity}}",
"extend-open-meta-data": "Extend OpenMetaData",
"extend-open-meta-data": "Extend OpenMetadata",
"failed": "Falló",
"failure-context": "Contexto del error",
"feature": "Funcionalidad",
@ -1059,6 +1059,7 @@
"tree": "Árbol",
"trigger": "Desencadenador",
"trigger-type": "Trigger type",
"triggered-lowercase": "triggered",
"triggering-lowercase": "desencadenamiento",
"try-again": "Try Again",
"tuesday": "Martes",
@ -1170,7 +1171,8 @@
"app-disabled-successfully": "Application disabled successfully",
"app-installed-successfully": "Application installed successfully",
"app-uninstalled-successfully": "Application uninstalled successfully",
"application-trigger-successfully": "Application triggered successfully",
"application-action-successfully": "Application {{action}} successfully",
"application-to-improve-data": "Find applications to improve your data",
"are-you-sure": "¿Estás seguro?",
"are-you-sure-action-property": "Are you sure you want to {{action}} {{propertyName}}?",
"are-you-sure-delete-entity": "¿Estás seguro de que quieres eliminar la propiedad {{entity}}?",
@ -1394,6 +1396,7 @@
"no-info-about-joined-tables": "No hay información sobre JOINs.",
"no-ingestion-available": "No hay datos de ingesta disponibles",
"no-ingestion-description": "Para ver los datos, ejecuta la ingesta de metadatos. Consulta este documento para programar la <0>{{link}}</0>",
"no-ingestion-pipeline-found": "No ingestion pipeline found. Please click on Deploy button to setup the ingestion pipeline.",
"no-inherited-roles-found": "No se encontraron roles heredados",
"no-installed-applications-found": "No applications are currently installed. Click the 'Add Apps' button to install one.",
"no-kpi-available-add-new-one": "No hay KPIs disponibles. Haz clic en el botón Agregar KPI para agregar uno.",
@ -1542,7 +1545,6 @@
"to-add-new-line": "para añadir una nueva línea",
"token-has-no-expiry": "Este token no tiene fecha de caducidad.",
"token-security-description": "Cualquier persona que tenga su token JWT podrá enviar solicitudes REST API al servidor OpenMetadata. No exponga el token JWT en el código de su aplicación. No lo comparta en GitHub ni en ningún otro lugar online.",
"tools-to-improve-workflow": "Find tools to improve your workflow",
"total-entity-insight": "Muestra el último número de activos de datos por tipo.",
"tour-follow-step": "Follow a data asset to stay informed about the changes to it. In your activity feed all the changes to the data asset you are following are shown. You will also get alerts if this data asset has data quality issues.",
"tour-high-level-assets-information-step": "In Data Assets details page, you get 360-degree view of the data asset to help you understand the all the context of the data and make the best use of it.",

View File

@ -403,7 +403,7 @@
"explore-now": "Explorer Maintenant",
"export": "Exporter",
"export-entity": "Exporter {{entity}}",
"extend-open-meta-data": "Extend OpenMetaData",
"extend-open-meta-data": "Extend OpenMetadata",
"failed": "Échec",
"failure-context": "Contexte de l'Échec",
"feature": "Fonctionnalité",
@ -1059,6 +1059,7 @@
"tree": "Arbre",
"trigger": "Déclencheur",
"trigger-type": "Type de Déclencheur",
"triggered-lowercase": "triggered",
"triggering-lowercase": "déclenchement",
"try-again": "Réessayer",
"tuesday": "Mardi",
@ -1170,7 +1171,8 @@
"app-disabled-successfully": "Application disabled successfully",
"app-installed-successfully": "Application installed successfully",
"app-uninstalled-successfully": "Application uninstalled successfully",
"application-trigger-successfully": "Application triggered successfully",
"application-action-successfully": "Application {{action}} successfully",
"application-to-improve-data": "Find applications to improve your data",
"are-you-sure": "Êtes-vous sûr?",
"are-you-sure-action-property": "Are you sure you want to {{action}} {{propertyName}}?",
"are-you-sure-delete-entity": "Êtes-vous sûr de vouloir supprimer le bien {{entity}}",
@ -1394,6 +1396,7 @@
"no-info-about-joined-tables": "Aucune information sur les tables jointes.",
"no-ingestion-available": "Aucune donnée d'ingestion disponible",
"no-ingestion-description": "Pour afficher les données d'ingestion, exécutez l'ingestion de métadonnées. Vous pouvez consulter la documentation sur la manière de planifier <0>{{link}}</0>",
"no-ingestion-pipeline-found": "No ingestion pipeline found. Please click on Deploy button to setup the ingestion pipeline.",
"no-inherited-roles-found": "Aucun rôle hérité trouvé",
"no-installed-applications-found": "No applications are currently installed. Click the 'Add Apps' button to install one.",
"no-kpi-available-add-new-one": "Aucun KPI n'est disponible, ajoutez un KPI en cliquant sur le bouton \"Ajouter un KPI\".",
@ -1542,7 +1545,6 @@
"to-add-new-line": "pour ajouter une nouvelle ligne",
"token-has-no-expiry": "Ce jeton n'a pas de date d'expiration.",
"token-security-description": "Toute personne en possession de votre Jeton JWT pourra envoyer des requêtes à l'API REST d'OpenMetadata. Ne surtout pas rendre public le Jeton JWT dans le code de votre application. Ne le partagez pas sur Github ou ailleurs en ligne.",
"tools-to-improve-workflow": "Find tools to improve your workflow",
"total-entity-insight": "Montre le nombre le plus récent de ressources de données par type.",
"tour-follow-step": "Suivre un actif de données pour rester informé des modifications qui lui sont apportées. Dans votre flux d'activité, toutes les modifications apportées à l'actif de données que vous suivez sont affichées. Vous recevrez également des alertes si cet actif de données présente des problèmes de qualité des données.",
"tour-high-level-assets-information-step": "Dans la page Détails des actifs de données, vous obtenez une vue à 360 degrés de lactif de données pour vous aider à comprendre tout le contexte des données et à en tirer le meilleur parti.",

View File

@ -403,7 +403,7 @@
"explore-now": "いますぐ探索する",
"export": "エクスポート",
"export-entity": "Export {{entity}}",
"extend-open-meta-data": "Extend OpenMetaData",
"extend-open-meta-data": "Extend OpenMetadata",
"failed": "失敗",
"failure-context": "Failure Context",
"feature": "Feature",
@ -1059,6 +1059,7 @@
"tree": "ツリー",
"trigger": "トリガー",
"trigger-type": "Trigger type",
"triggered-lowercase": "triggered",
"triggering-lowercase": "triggering",
"try-again": "Try Again",
"tuesday": "火曜日",
@ -1170,7 +1171,8 @@
"app-disabled-successfully": "Application disabled successfully",
"app-installed-successfully": "Application installed successfully",
"app-uninstalled-successfully": "Application uninstalled successfully",
"application-trigger-successfully": "Application triggered successfully",
"application-action-successfully": "Application {{action}} successfully",
"application-to-improve-data": "Find applications to improve your data",
"are-you-sure": "よろしいですか?",
"are-you-sure-action-property": "Are you sure you want to {{action}} {{propertyName}}?",
"are-you-sure-delete-entity": "本当にプロパティ {{propertyName}} を削除してよろしいですか?",
@ -1394,6 +1396,7 @@
"no-info-about-joined-tables": "結合テーブルの情報はありません。",
"no-ingestion-available": "利用可能なインジェスチョンデータはありません。",
"no-ingestion-description": "To view Ingestion Data, run the metadata ingestion. Please refer to this doc to schedule the <0>{{link}}</0>",
"no-ingestion-pipeline-found": "No ingestion pipeline found. Please click on Deploy button to setup the ingestion pipeline.",
"no-inherited-roles-found": "継承されたロールは見つかりませんでした",
"no-installed-applications-found": "No applications are currently installed. Click the 'Add Apps' button to install one.",
"no-kpi-available-add-new-one": "No KPIs are available. Click on the Add KPI button to add one.",
@ -1542,7 +1545,6 @@
"to-add-new-line": "to add a new line",
"token-has-no-expiry": "This token has no expiration date.",
"token-security-description": "Anyone who has your JWT Token will be able to send REST API requests to the OpenMetadata Server. Do not expose the JWT Token in your application code. Do not share it on GitHub or anywhere else online.",
"tools-to-improve-workflow": "Find tools to improve your workflow",
"total-entity-insight": "最新のデータアセットの数をタイプ別に表示します。",
"tour-follow-step": "Follow a data asset to stay informed about the changes to it. In your activity feed all the changes to the data asset you are following are shown. You will also get alerts if this data asset has data quality issues.",
"tour-high-level-assets-information-step": "In Data Assets details page, you get 360-degree view of the data asset to help you understand the all the context of the data and make the best use of it.",

View File

@ -403,7 +403,7 @@
"explore-now": "Explorar agora",
"export": "Exportar",
"export-entity": "Export {{entity}}",
"extend-open-meta-data": "Extend OpenMetaData",
"extend-open-meta-data": "Extend OpenMetadata",
"failed": "Falhou",
"failure-context": "Contexto da falha",
"feature": "Função",
@ -1059,6 +1059,7 @@
"tree": "Árvore",
"trigger": "Disparador",
"trigger-type": "Trigger type",
"triggered-lowercase": "triggered",
"triggering-lowercase": "disparador",
"try-again": "Try Again",
"tuesday": "Terça-feira",
@ -1170,7 +1171,8 @@
"app-disabled-successfully": "Application disabled successfully",
"app-installed-successfully": "Application installed successfully",
"app-uninstalled-successfully": "Application uninstalled successfully",
"application-trigger-successfully": "Application triggered successfully",
"application-action-successfully": "Application {{action}} successfully",
"application-to-improve-data": "Find applications to improve your data",
"are-you-sure": "Você tem certeza?",
"are-you-sure-action-property": "Are you sure you want to {{action}} {{propertyName}}?",
"are-you-sure-delete-entity": "Tem certeza de que deseja excluir a propriedade {{entity}}",
@ -1394,6 +1396,7 @@
"no-info-about-joined-tables": "Nenhuma informação sobre tabelas unidas.",
"no-ingestion-available": "Nenhum dado de ingestão disponível",
"no-ingestion-description": "Para visualizar os dados de ingestão, execute a ingestão de metadados. Consulte este documento para agendar a <0>{{link}}</0>",
"no-ingestion-pipeline-found": "No ingestion pipeline found. Please click on Deploy button to setup the ingestion pipeline.",
"no-inherited-roles-found": "Nenhum papel herdado encontrado",
"no-installed-applications-found": "No applications are currently installed. Click the 'Add Apps' button to install one.",
"no-kpi-available-add-new-one": "Não há KPIs disponíveis. Clique no botão Adicionar KPI para adicionar um.",
@ -1542,7 +1545,6 @@
"to-add-new-line": "para adicionar uma nova linha",
"token-has-no-expiry": "Este token não tem data de expiração.",
"token-security-description": "Qualquer pessoa que possua seu token JWT poderá enviar solicitações REST API para o servidor OpenMetadata. Não exponha o token JWT em seu código de aplicativo. Não compartilhe em GitHub ou em qualquer outro lugar online.",
"tools-to-improve-workflow": "Find tools to improve your workflow",
"total-entity-insight": "Exibir o número mais recente de ativos de dados por tipo.",
"tour-follow-step": "Siga um Ativo de Dados para se manter informado sobre as suas alterações. No seu feed de atividades são mostradas todas as alterações ao Ativo de Dados que está seguindo. Também receberá alertas se este Ativo de Dados tiver problemas de qualidade de dados.",
"tour-high-level-assets-information-step": "Na página de detalhes dos ativos de dados, você tem uma visão de 360 graus do ativo de dados para ajudá-lo a entender todo o contexto dos dados e fazer o melhor uso deles.",

View File

@ -403,7 +403,7 @@
"explore-now": "Исследовать сейчас",
"export": "Экспортировать",
"export-entity": "Экспортировать {{entity}}",
"extend-open-meta-data": "Extend OpenMetaData",
"extend-open-meta-data": "Extend OpenMetadata",
"failed": "Неуспешно",
"failure-context": "Контекст отказа",
"feature": "Свойство",
@ -1059,6 +1059,7 @@
"tree": "Дерево",
"trigger": "Триггер",
"trigger-type": "Тип триггера",
"triggered-lowercase": "triggered",
"triggering-lowercase": "триггер",
"try-again": "Попробуйте снова",
"tuesday": "Вторник",
@ -1170,7 +1171,8 @@
"app-disabled-successfully": "Application disabled successfully",
"app-installed-successfully": "Application installed successfully",
"app-uninstalled-successfully": "Application uninstalled successfully",
"application-trigger-successfully": "Application triggered successfully",
"application-action-successfully": "Application {{action}} successfully",
"application-to-improve-data": "Find applications to improve your data",
"are-you-sure": "Вы уверены?",
"are-you-sure-action-property": "Are you sure you want to {{action}} {{propertyName}}?",
"are-you-sure-delete-entity": "Вы уверены, что хотите удалить свойство {{entity}}?",
@ -1394,6 +1396,7 @@
"no-info-about-joined-tables": "Нет информации о соединенных таблицах.",
"no-ingestion-available": "Нет доступных данных о получении",
"no-ingestion-description": "Чтобы просмотреть данные получения, запустите получение метаданных. Пожалуйста, обратитесь к этому документу, чтобы запланировать <0>{{link}}</0>",
"no-ingestion-pipeline-found": "No ingestion pipeline found. Please click on Deploy button to setup the ingestion pipeline.",
"no-inherited-roles-found": "Унаследованные роли не найдены",
"no-installed-applications-found": "No applications are currently installed. Click the 'Add Apps' button to install one.",
"no-kpi-available-add-new-one": "Нет доступных KPI. Нажмите кнопку «Добавить KPI», чтобы добавить его.",
@ -1542,7 +1545,6 @@
"to-add-new-line": "чтобы добавить новую строку",
"token-has-no-expiry": "Этот токен не имеет срока действия.",
"token-security-description": "Любой, у кого есть ваш токен JWT, сможет отправлять запросы REST API на сервер OpenMetadata. Не раскрывайте токен JWT в коде приложения. Не делитесь им на GitHub или где-либо еще в Интернете.",
"tools-to-improve-workflow": "Find tools to improve your workflow",
"total-entity-insight": "Отображение количества объектов данных по типу.",
"tour-follow-step": "Подпишитесь на объект данных, чтобы быть в курсе изменений в нем. В вашей ленте действий отображаются все изменения в объекте данных, за которым вы следите. Вы также получите оповещения, если у этого объекта данных есть проблемы с качеством данных.",
"tour-high-level-assets-information-step": "На странице сведений об объектах данных вы получаете их 360-градусный обзор, который поможет вам понять весь контекст данных и наилучшим образом их использовать.",

View File

@ -403,7 +403,7 @@
"explore-now": "现在探索",
"export": "导出",
"export-entity": "导出{{entity}}",
"extend-open-meta-data": "Extend OpenMetaData",
"extend-open-meta-data": "Extend OpenMetadata",
"failed": "失败",
"failure-context": "失败上下文",
"feature": "特点",
@ -1059,6 +1059,7 @@
"tree": "树",
"trigger": "触发器",
"trigger-type": "触发器类型",
"triggered-lowercase": "triggered",
"triggering-lowercase": "触发",
"try-again": "再试",
"tuesday": "星期二",
@ -1170,7 +1171,8 @@
"app-disabled-successfully": "Application disabled successfully",
"app-installed-successfully": "Application installed successfully",
"app-uninstalled-successfully": "Application uninstalled successfully",
"application-trigger-successfully": "Application triggered successfully",
"application-action-successfully": "Application {{action}} successfully",
"application-to-improve-data": "Find applications to improve your data",
"are-you-sure": "您确定吗?",
"are-you-sure-action-property": "Are you sure you want to {{action}} {{propertyName}}?",
"are-you-sure-delete-entity": "您确定要删除属性{{entity}}吗?",
@ -1394,6 +1396,7 @@
"no-info-about-joined-tables": "无相关表信息",
"no-ingestion-available": "没有可用的提取数据",
"no-ingestion-description": "要查看提取数据,请先运行元数据提取工作流!详情请参阅此文档 <0>{{link}}</0>",
"no-ingestion-pipeline-found": "No ingestion pipeline found. Please click on Deploy button to setup the ingestion pipeline.",
"no-inherited-roles-found": "没有找到继承的角色",
"no-installed-applications-found": "No applications are currently installed. Click the 'Add Apps' button to install one.",
"no-kpi-available-add-new-one": "没有可用的 KPI请单击添加 KPI 按钮添加一个",
@ -1542,7 +1545,6 @@
"to-add-new-line": "添加新行",
"token-has-no-expiry": "此令牌没有过期日期",
"token-security-description": "任何拥有您的 JWT 令牌的人都可以向 OpenMetadata 服务器发送 REST API 请求。请不要在源代码中公开 JWT 令牌。不要在 GitHub 或任何其他网络上分享它。",
"tools-to-improve-workflow": "Find tools to improve your workflow",
"total-entity-insight": "按类型显示最新的数据资产数量",
"tour-follow-step": "选择关注一个数据资产以获取其最新变更信息。这些信息将会展示在活动信息流页面,并能在数据质控出现问题时提醒您。",
"tour-high-level-assets-information-step": "在数据资产详情页,您可以 360 度全方位了解数据资产及其上下文环境,更好的挖掘其潜在价值。",

View File

@ -15,7 +15,7 @@ import { RJSFSchema } from '@rjsf/utils';
import validator from '@rjsf/validator-ajv8';
import { Col, Row, Typography } from 'antd';
import { AxiosError } from 'axios';
import React, { useCallback, useEffect, useState } from 'react';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useHistory, useParams } from 'react-router-dom';
import TestSuiteScheduler from '../../components/AddDataQualityTest/components/TestSuiteScheduler';
@ -31,6 +31,7 @@ import {
GlobalSettingsMenuCategory,
} from '../../constants/GlobalSettings.constants';
import { ServiceCategory } from '../../enums/service.enum';
import { AppType } from '../../generated/entity/applications/app';
import {
CreateAppRequest,
ScheduleTimeline,
@ -61,6 +62,20 @@ const AppInstall = () => {
const [appConfiguration, setAppConfiguration] = useState();
const [jsonSchema, setJsonSchema] = useState<RJSFSchema>();
const isExternalApp = useMemo(
() => appData?.appType === AppType.External,
[appData]
);
const stepperList = useMemo(
() =>
isExternalApp
? STEPS_FOR_APP_INSTALL.filter((item) => item.step !== 2)
: STEPS_FOR_APP_INSTALL,
[isExternalApp]
);
const fetchAppDetails = useCallback(async () => {
setIsLoading(true);
try {
@ -92,7 +107,7 @@ const AppInstall = () => {
const onSubmit = async (repeatFrequency: string) => {
try {
const data: CreateAppRequest = {
appConfiguration: appConfiguration,
appConfiguration: appConfiguration ?? appData?.appConfiguration,
appSchedule: {
scheduleType: ScheduleTimeline.Custom,
cronExpression: repeatFrequency,
@ -125,10 +140,14 @@ const AppInstall = () => {
return (
<AppInstallVerifyCard
appData={appData}
nextButtonLabel={
isExternalApp ? t('label.schedule') : t('label.configure')
}
onCancel={onCancel}
onSave={() => setActiveServiceStep(2)}
onSave={() => setActiveServiceStep(isExternalApp ? 3 : 2)}
/>
);
case 2:
return (
<div className="w-500 p-md border rounded-4">
@ -155,7 +174,7 @@ const AppInstall = () => {
<TestSuiteScheduler
isQuartzCron
initialData={getIngestionFrequency(PipelineType.Application)}
onCancel={() => setActiveServiceStep(2)}
onCancel={() => setActiveServiceStep(isExternalApp ? 1 : 2)}
onSubmit={onSubmit}
/>
</div>
@ -189,7 +208,7 @@ const AppInstall = () => {
<Col span={24}>
<IngestionStepper
activeStep={activeServiceStep}
steps={STEPS_FOR_APP_INSTALL}
steps={stepperList}
/>
</Col>
<Col span={24}>

View File

@ -11,11 +11,12 @@
* limitations under the License.
*/
import { Button, Col, Row, Space, Typography } from 'antd';
import { Button, Col, Row, Space, Tag, Typography } from 'antd';
import { AxiosError } from 'axios';
import { isEmpty, isNil, isUndefined, toNumber } from 'lodash';
import { isEmpty, isNil, isUndefined, startCase, toNumber } from 'lodash';
import React, {
Fragment,
useCallback,
useEffect,
useLayoutEffect,
useMemo,
@ -24,14 +25,25 @@ import React, {
import { useTranslation } from 'react-i18next';
import { LazyLog } from 'react-lazylog';
import { useParams } from 'react-router-dom';
import { DataInsightLatestRun } from '../../components/Applications/AppDetails/AppDetails.interface';
import { CopyToClipboardButton } from '../../components/buttons/CopyToClipboardButton/CopyToClipboardButton';
import TitleBreadcrumb from '../../components/common/title-breadcrumb/title-breadcrumb.component';
import PageLayoutV1 from '../../components/containers/PageLayoutV1';
import { IngestionRecentRuns } from '../../components/Ingestion/IngestionRecentRun/IngestionRecentRuns.component';
import Loader from '../../components/Loader/Loader';
import { GlobalSettingOptions } from '../../constants/GlobalSettings.constants';
import { PIPELINE_INGESTION_RUN_STATUS } from '../../constants/pipeline.constants';
import { PipelineType } from '../../generated/api/services/ingestionPipelines/createIngestionPipeline';
import { IngestionPipeline } from '../../generated/entity/services/ingestionPipelines/ingestionPipeline';
import { App, AppScheduleClass } from '../../generated/entity/applications/app';
import {
IngestionPipeline,
PipelineState,
} from '../../generated/entity/services/ingestionPipelines/ingestionPipeline';
import { Paging } from '../../generated/type/paging';
import {
getApplicationByName,
getLatestApplicationRuns,
} from '../../rest/applicationAPI';
import {
getIngestionPipelineByName,
getIngestionPipelineLogById,
@ -51,13 +63,27 @@ const LogsViewer = () => {
const [isLoading, setIsLoading] = useState<boolean>(false);
const [logs, setLogs] = useState<string>('');
const [ingestionDetails, setIngestionDetails] = useState<IngestionPipeline>();
const [appData, setAppData] = useState<App>();
const [appLatestRun, setAppLatestRun] = useState<DataInsightLatestRun>();
const [paging, setPaging] = useState<Paging>();
const isApplicationType = useMemo(
() => logEntityType === GlobalSettingOptions.APPLICATIONS,
[logEntityType]
);
const fetchLogs = async (
ingestionId?: string,
pipelineType?: PipelineType
) => {
try {
if (isApplicationType) {
const res = await getLatestApplicationRuns(ingestionName);
setAppLatestRun(res);
setLogs(res.lastIngestionLogs.data_insight_task);
return;
}
const res = await getIngestionPipelineLogById(
ingestionId || ingestionDetails?.id || '',
paging?.total !== paging?.after ? paging?.after : ''
@ -134,6 +160,19 @@ const LogsViewer = () => {
}
};
const fetchAppDetails = useCallback(async () => {
setIsLoading(true);
try {
const data = await getApplicationByName(ingestionName, 'owner');
setAppData(data);
fetchLogs();
} catch (error) {
showErrorToast(error as AxiosError);
} finally {
setIsLoading(false);
}
}, [ingestionName]);
const fetchMoreLogs = () => {
fetchLogs(ingestionDetails?.id, ingestionDetails?.pipelineType);
setPaging({
@ -143,7 +182,11 @@ const LogsViewer = () => {
};
useEffect(() => {
fetchIngestionDetailsByName();
if (isApplicationType) {
fetchAppDetails();
} else {
fetchIngestionDetailsByName();
}
}, []);
const handleScroll = (event: Event) => {
@ -210,17 +253,43 @@ const LogsViewer = () => {
}
};
const recentRuns = useMemo(() => {
if (isApplicationType) {
return (
<Tag
className="ingestion-run-badge latest"
color={
PIPELINE_INGESTION_RUN_STATUS[
appLatestRun?.pipelineStatus?.pipelineState ??
PipelineState.Failed
]
}
data-testid="pipeline-status">
{startCase(appLatestRun?.pipelineStatus?.pipelineState)}
</Tag>
);
}
if (ingestionDetails?.fullyQualifiedName) {
return <IngestionRecentRuns ingestion={ingestionDetails} />;
}
return '--';
}, [logEntityType, appLatestRun, ingestionDetails]);
const logSummaries = useMemo(() => {
const scheduleClass = appData?.appSchedule as AppScheduleClass;
return {
Type: ingestionDetails?.pipelineType || '--',
Schedule: ingestionDetails?.airflowConfig.scheduleInterval || '--',
['Recent Runs']: ingestionDetails?.fullyQualifiedName ? (
<IngestionRecentRuns ingestion={ingestionDetails} />
) : (
'--'
),
Type:
ingestionDetails?.pipelineType ?? scheduleClass?.scheduleType ?? '--',
Schedule:
ingestionDetails?.airflowConfig.scheduleInterval ??
scheduleClass?.cronExpression ??
'--',
['Recent Runs']: recentRuns,
};
}, [ingestionDetails]);
}, [ingestionDetails, appData, recentRuns]);
if (isLoading) {
return <Loader />;
@ -240,7 +309,7 @@ const LogsViewer = () => {
</Space>
<Space>
<Typography.Title level={5}>
{ingestionDetails?.name}
{ingestionDetails?.name ?? appData?.name}
</Typography.Title>
</Space>
</Space>

View File

@ -13,6 +13,7 @@
import { AxiosResponse } from 'axios';
import { Operation } from 'fast-json-patch';
import { PagingResponse } from 'Models';
import { DataInsightLatestRun } from '../components/Applications/AppDetails/AppDetails.interface';
import { App } from '../generated/entity/applications/app';
import { AppRunRecord } from '../generated/entity/applications/appRunRecord';
import { CreateAppRequest } from '../generated/entity/applications/createAppRequest';
@ -68,6 +69,14 @@ export const getApplicationRuns = async (
return response.data;
};
export const getLatestApplicationRuns = async (appName: string) => {
const response = await APIClient.get<DataInsightLatestRun>(
`${BASE_URL}/name/${appName}/runs/latest`
);
return response.data;
};
export const uninstallApp = (appName: string, hardDelete = false) => {
return APIClient.delete(`${BASE_URL}/name/${appName}`, {
params: { hardDelete },
@ -91,3 +100,7 @@ export const patchApplication = async (id: string, patch: Operation[]) => {
export const triggerOnDemandApp = (appName: string): Promise<AxiosResponse> => {
return APIClient.post(`${BASE_URL}/trigger/${appName}`, {});
};
export const deployApp = (appName: string): Promise<AxiosResponse> => {
return APIClient.post(`${BASE_URL}/deploy/${appName}`);
};

View File

@ -14,6 +14,10 @@
import { isUndefined, startCase } from 'lodash';
import { TableProfilerTab } from '../components/ProfilerDashboard/profilerDashboard.interface';
import { getTableTabPath } from '../constants/constants';
import {
GlobalSettingOptions,
GlobalSettingsMenuCategory,
} from '../constants/GlobalSettings.constants';
import { OPEN_METADATA } from '../constants/service-guide.constant';
import { EntityTabs } from '../enums/entity.enum';
import { Pipeline } from '../generated/api/services/ingestionPipelines/createIngestionPipeline';
@ -23,7 +27,12 @@ import { getNameFromFQN } from './CommonUtils';
import Fqn from './Fqn';
import i18n from './i18next/LocalUtil';
import { getSettingsPathFromPipelineType } from './IngestionUtils';
import { getDataQualityPagePath, getLogEntityPath } from './RouterUtils';
import {
getApplicationDetailsPath,
getDataQualityPagePath,
getLogEntityPath,
getSettingPath,
} from './RouterUtils';
import { getEncodedFqn } from './StringsUtils';
/**
@ -56,6 +65,23 @@ export const getLogBreadCrumbs = (
},
];
}
if (serviceType === GlobalSettingOptions.APPLICATIONS) {
return [
{
name: startCase(serviceType),
url: getSettingPath(
GlobalSettingsMenuCategory.INTEGRATIONS,
GlobalSettingOptions.APPLICATIONS
),
},
{
name: ingestionName,
url: getApplicationDetailsPath(ingestionName),
},
];
}
if (isUndefined(ingestionDetails)) {
return [];
}