mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-08-24 08:58:06 +00:00
UI: Add option to stop the running application (#18759)
* Add Stop functionality for Applications * show kill button by default * Add option to stop the running application * fix: show stop button for ruuning & activeerror status * add localisation files * added unit tests * update localisation files --------- Co-authored-by: Sriharsha Chintalapani <harsha@getcollate.io>
This commit is contained in:
parent
d1b8b9a966
commit
09db201a57
@ -48,7 +48,6 @@ public class AbstractNativeApplication implements NativeApplication {
|
||||
protected CollectionDAO collectionDAO;
|
||||
private App app;
|
||||
protected SearchRepository searchRepository;
|
||||
protected boolean isJobInterrupted = false;
|
||||
|
||||
// Default service that contains external apps' Ingestion Pipelines
|
||||
private static final String SERVICE_NAME = "OpenMetadata";
|
||||
@ -299,6 +298,11 @@ public class AbstractNativeApplication implements NativeApplication {
|
||||
@Override
|
||||
public void interrupt() throws UnableToInterruptJobException {
|
||||
LOG.info("Interrupting the job for app: {}", this.app.getName());
|
||||
isJobInterrupted = true;
|
||||
stop();
|
||||
}
|
||||
|
||||
protected void stop() {
|
||||
LOG.info("Default stop behavior for app: {}", this.app.getName());
|
||||
// Default implementation: no-op or generic cleanup logic
|
||||
}
|
||||
}
|
||||
|
@ -161,12 +161,13 @@ public class SearchIndexApp extends AbstractNativeApplication {
|
||||
|
||||
@Getter private EventPublisherJob jobData;
|
||||
private final Object jobDataLock = new Object();
|
||||
private volatile boolean stopped = false;
|
||||
private ExecutorService producerExecutor;
|
||||
private final ExecutorService jobExecutor = Executors.newCachedThreadPool();
|
||||
private BlockingQueue<Runnable> producerQueue = new LinkedBlockingQueue<>(100);
|
||||
private final AtomicReference<Stats> searchIndexStats = new AtomicReference<>();
|
||||
private final AtomicReference<Integer> batchSize = new AtomicReference<>(5);
|
||||
private JobExecutionContext jobExecutionContext;
|
||||
private volatile boolean stopped = false;
|
||||
|
||||
public SearchIndexApp(CollectionDAO collectionDAO, SearchRepository searchRepository) {
|
||||
super(collectionDAO, searchRepository);
|
||||
@ -190,6 +191,7 @@ public class SearchIndexApp extends AbstractNativeApplication {
|
||||
@Override
|
||||
public void startApp(JobExecutionContext jobExecutionContext) {
|
||||
try {
|
||||
this.jobExecutionContext = jobExecutionContext;
|
||||
initializeJob(jobExecutionContext);
|
||||
String runType =
|
||||
(String) jobExecutionContext.getJobDetail().getJobDataMap().get("triggerType");
|
||||
@ -533,11 +535,17 @@ public class SearchIndexApp extends AbstractNativeApplication {
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public void stopJob() {
|
||||
@Override
|
||||
public void stop() {
|
||||
LOG.info("Stopping reindexing job.");
|
||||
stopped = true;
|
||||
jobData.setStatus(EventPublisherJob.Status.STOP_IN_PROGRESS);
|
||||
sendUpdates(jobExecutionContext);
|
||||
shutdownExecutor(jobExecutor, "JobExecutor", 60, TimeUnit.SECONDS);
|
||||
shutdownExecutor(producerExecutor, "ProducerExecutor", 60, TimeUnit.SECONDS);
|
||||
LOG.info("Stopped reindexing job.");
|
||||
jobData.setStatus(EventPublisherJob.Status.STOPPED);
|
||||
sendUpdates(jobExecutionContext);
|
||||
}
|
||||
|
||||
private void processTask(IndexingTask<?> task, JobExecutionContext jobExecutionContext) {
|
||||
@ -596,7 +604,9 @@ public class SearchIndexApp extends AbstractNativeApplication {
|
||||
}
|
||||
LOG.error("Unexpected error during processing task for entity {}", entityType, e);
|
||||
} finally {
|
||||
sendUpdates(jobExecutionContext);
|
||||
if (!stopped) {
|
||||
sendUpdates(jobExecutionContext);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -263,30 +263,52 @@ public class AppScheduler {
|
||||
}
|
||||
|
||||
public void stopApplicationRun(App application) {
|
||||
if (application.getFullyQualifiedName() == null) {
|
||||
throw new IllegalArgumentException("Application's fullyQualifiedName is null.");
|
||||
}
|
||||
try {
|
||||
// Interrupt any scheduled job
|
||||
JobDetail jobDetailScheduled =
|
||||
scheduler.getJobDetail(new JobKey(application.getName(), APPS_JOB_GROUP));
|
||||
if (jobDetailScheduled != null) {
|
||||
LOG.debug("Stopping Scheduled Execution for App : {}", application.getName());
|
||||
scheduler.interrupt(jobDetailScheduled.getKey());
|
||||
}
|
||||
|
||||
// Interrupt any on-demand job
|
||||
JobDetail jobDetailOnDemand =
|
||||
scheduler.getJobDetail(
|
||||
new JobKey(
|
||||
String.format("%s-%s", application.getName(), ON_DEMAND_JOB), APPS_JOB_GROUP));
|
||||
|
||||
if (jobDetailOnDemand != null) {
|
||||
LOG.debug("Stopping On Demand Execution for App : {}", application.getName());
|
||||
scheduler.interrupt(jobDetailOnDemand.getKey());
|
||||
boolean isJobRunning = false;
|
||||
// Check if the job is already running
|
||||
List<JobExecutionContext> currentJobs = scheduler.getCurrentlyExecutingJobs();
|
||||
for (JobExecutionContext context : currentJobs) {
|
||||
if ((jobDetailScheduled != null
|
||||
&& context.getJobDetail().getKey().equals(jobDetailScheduled.getKey()))
|
||||
|| (jobDetailOnDemand != null
|
||||
&& context.getJobDetail().getKey().equals(jobDetailOnDemand.getKey()))) {
|
||||
isJobRunning = true;
|
||||
}
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
LOG.error("Failed to stop job execution.", ex);
|
||||
if (!isJobRunning) {
|
||||
throw new UnhandledServerException("There is no job running for the application.");
|
||||
}
|
||||
JobKey scheduledJobKey = new JobKey(application.getName(), APPS_JOB_GROUP);
|
||||
if (jobDetailScheduled != null) {
|
||||
LOG.debug("Stopping Scheduled Execution for App: {}", application.getName());
|
||||
scheduler.interrupt(scheduledJobKey);
|
||||
try {
|
||||
scheduler.deleteJob(scheduledJobKey);
|
||||
} catch (SchedulerException ex) {
|
||||
LOG.error("Failed to delete scheduled job: {}", scheduledJobKey, ex);
|
||||
}
|
||||
} else {
|
||||
JobKey onDemandJobKey =
|
||||
new JobKey(
|
||||
String.format("%s-%s", application.getName(), ON_DEMAND_JOB), APPS_JOB_GROUP);
|
||||
if (jobDetailOnDemand != null) {
|
||||
LOG.debug("Stopping On Demand Execution for App: {}", application.getName());
|
||||
scheduler.interrupt(onDemandJobKey);
|
||||
try {
|
||||
scheduler.deleteJob(onDemandJobKey);
|
||||
} catch (SchedulerException ex) {
|
||||
LOG.error("Failed to delete on-demand job: {}", onDemandJobKey, ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (SchedulerException ex) {
|
||||
LOG.error("Failed to stop job execution for app: {}", application.getName(), ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -141,11 +141,7 @@ public class AppRepository extends EntityRepository<App> {
|
||||
public void storeEntity(App entity, boolean update) {
|
||||
List<EntityReference> ownerRefs = entity.getOwners();
|
||||
entity.withOwners(null);
|
||||
|
||||
// Store
|
||||
store(entity, update);
|
||||
|
||||
// Restore entity fields
|
||||
entity.withOwners(ownerRefs);
|
||||
}
|
||||
|
||||
@ -178,7 +174,6 @@ public class AppRepository extends EntityRepository<App> {
|
||||
}
|
||||
|
||||
public final List<App> listAll() {
|
||||
// forward scrolling, if after == null then first page is being asked
|
||||
List<String> jsons = dao.listAfterWithOffset(Integer.MAX_VALUE, 0);
|
||||
List<App> entities = new ArrayList<>();
|
||||
for (String json : jsons) {
|
||||
@ -214,7 +209,6 @@ public class AppRepository extends EntityRepository<App> {
|
||||
.listAppExtensionCountByName(app.getName(), extensionType.toString());
|
||||
List<T> entities = new ArrayList<>();
|
||||
if (limitParam > 0) {
|
||||
// forward scrolling, if after == null then first page is being asked
|
||||
List<String> jsons =
|
||||
daoCollection
|
||||
.appExtensionTimeSeriesDao()
|
||||
@ -274,7 +268,6 @@ public class AppRepository extends EntityRepository<App> {
|
||||
app.getName(), startTime, extensionType.toString());
|
||||
List<T> entities = new ArrayList<>();
|
||||
if (limitParam > 0) {
|
||||
// forward scrolling, if after == null then first page is being asked
|
||||
List<String> jsons =
|
||||
daoCollection
|
||||
.appExtensionTimeSeriesDao()
|
||||
@ -287,7 +280,6 @@ public class AppRepository extends EntityRepository<App> {
|
||||
|
||||
return new ResultList<>(entities, offset, total);
|
||||
} else {
|
||||
// limit == 0 , return total count of entity.
|
||||
return new ResultList<>(entities, null, total);
|
||||
}
|
||||
}
|
||||
|
@ -269,13 +269,13 @@ public class AppResource extends EntityResource<App, AppRepository> {
|
||||
@DefaultValue("10")
|
||||
@QueryParam("limit")
|
||||
@Min(0)
|
||||
@Max(1000000)
|
||||
@Max(1000)
|
||||
int limitParam,
|
||||
@Parameter(description = "Offset records. (0 to 1000000, default = 0)")
|
||||
@DefaultValue("0")
|
||||
@QueryParam("offset")
|
||||
@Min(0)
|
||||
@Max(1000000)
|
||||
@Max(1000)
|
||||
int offset,
|
||||
@Parameter(
|
||||
description = "Filter pipeline status after the given start timestamp",
|
||||
@ -1013,9 +1013,9 @@ public class AppResource extends EntityResource<App, AppRepository> {
|
||||
App app = repository.getByName(uriInfo, name, fields);
|
||||
if (Boolean.TRUE.equals(app.getSupportsInterrupt())) {
|
||||
if (app.getAppType().equals(AppType.Internal)) {
|
||||
AppScheduler.getInstance().stopApplicationRun(app);
|
||||
new Thread(() -> AppScheduler.getInstance().stopApplicationRun(app)).start();
|
||||
return Response.status(Response.Status.OK)
|
||||
.entity("Application will be stopped in some time.")
|
||||
.entity("Application stop in progress. Please check status via.")
|
||||
.build();
|
||||
} else {
|
||||
if (!app.getPipelines().isEmpty()) {
|
||||
|
@ -75,7 +75,8 @@
|
||||
"active",
|
||||
"activeError",
|
||||
"stopped",
|
||||
"success"
|
||||
"success",
|
||||
"stopInProgress"
|
||||
]
|
||||
},
|
||||
"failure": {
|
||||
|
@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright 2022 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.
|
||||
*/
|
||||
|
||||
export interface StopScheduleRunModalProps {
|
||||
appName: string;
|
||||
isModalOpen: boolean;
|
||||
displayName: string;
|
||||
onClose: () => void;
|
||||
onStopWorkflowsUpdate?: () => void;
|
||||
}
|
@ -0,0 +1,87 @@
|
||||
/*
|
||||
* Copyright 2024 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 { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { stopApp } from '../../../rest/applicationAPI';
|
||||
import { showErrorToast } from '../../../utils/ToastUtils';
|
||||
import StopScheduleModal from './StopScheduleRunModal';
|
||||
|
||||
jest.mock('../../../rest/applicationAPI', () => ({
|
||||
stopApp: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../../../utils/ToastUtils', () => ({
|
||||
showErrorToast: jest.fn(),
|
||||
showSuccessToast: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('StopScheduleModal', () => {
|
||||
const mockProps = {
|
||||
appName: 'test-app',
|
||||
displayName: 'Test App',
|
||||
isModalOpen: true,
|
||||
onClose: jest.fn(),
|
||||
onStopWorkflowsUpdate: jest.fn(),
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should render the modal', () => {
|
||||
render(<StopScheduleModal {...mockProps} />);
|
||||
|
||||
expect(screen.getByTestId('stop-modal')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should call stop app and display success toast on confirm', async () => {
|
||||
(stopApp as jest.Mock).mockResolvedValueOnce({ status: 200 });
|
||||
|
||||
render(<StopScheduleModal {...mockProps} />);
|
||||
|
||||
const confirmButton = screen.getByText('label.confirm');
|
||||
fireEvent.click(confirmButton);
|
||||
|
||||
expect(stopApp).toHaveBeenCalledWith('test-app');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockProps.onStopWorkflowsUpdate).toHaveBeenCalled();
|
||||
expect(mockProps.onClose).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('should call stop app and display error toast on failure', async () => {
|
||||
(stopApp as jest.Mock).mockRejectedValueOnce(new Error('API Error'));
|
||||
|
||||
render(<StopScheduleModal {...mockProps} />);
|
||||
|
||||
const confirmButton = screen.getByText('label.confirm');
|
||||
fireEvent.click(confirmButton);
|
||||
|
||||
expect(stopApp).toHaveBeenCalledWith('test-app');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(showErrorToast).toHaveBeenCalledWith(new Error('API Error'));
|
||||
expect(mockProps.onClose).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('should call onClose when cancel button is clicked', () => {
|
||||
render(<StopScheduleModal {...mockProps} />);
|
||||
|
||||
const cancelButton = screen.getByText('label.cancel');
|
||||
fireEvent.click(cancelButton);
|
||||
|
||||
expect(mockProps.onClose).toHaveBeenCalled();
|
||||
});
|
||||
});
|
@ -0,0 +1,76 @@
|
||||
/*
|
||||
* Copyright 2022 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 { Modal, Typography } from 'antd';
|
||||
import { AxiosError } from 'axios';
|
||||
import React, { FC, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { stopApp } from '../../../rest/applicationAPI';
|
||||
import { showErrorToast, showSuccessToast } from '../../../utils/ToastUtils';
|
||||
import { StopScheduleRunModalProps } from './StopScheduleRunModal.interface';
|
||||
|
||||
const StopScheduleModal: FC<StopScheduleRunModalProps> = ({
|
||||
appName,
|
||||
isModalOpen,
|
||||
displayName,
|
||||
onClose,
|
||||
onStopWorkflowsUpdate,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
|
||||
const handleConfirm = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const { status } = await stopApp(appName);
|
||||
if (status === 200) {
|
||||
showSuccessToast(
|
||||
t('message.application-stop', {
|
||||
pipelineName: displayName,
|
||||
})
|
||||
);
|
||||
onStopWorkflowsUpdate?.();
|
||||
}
|
||||
} catch (error) {
|
||||
// catch block error is unknown type so we have to cast it to respective type
|
||||
showErrorToast(error as AxiosError);
|
||||
} finally {
|
||||
onClose();
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
destroyOnClose
|
||||
cancelText={t('label.cancel')}
|
||||
closable={false}
|
||||
confirmLoading={isLoading}
|
||||
data-testid="stop-modal"
|
||||
maskClosable={false}
|
||||
okText={t('label.confirm')}
|
||||
open={isModalOpen}
|
||||
title={`${t('label.stop')} ${displayName} ?`}
|
||||
onCancel={onClose}
|
||||
onOk={handleConfirm}>
|
||||
<Typography.Text data-testid="stop-modal-body">
|
||||
{t('message.are-you-sure-action-property', {
|
||||
action: 'Stop',
|
||||
propertyName: displayName,
|
||||
})}
|
||||
</Typography.Text>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default StopScheduleModal;
|
@ -59,6 +59,7 @@ import { PagingHandlerParams } from '../../../common/NextPrevious/NextPrevious.i
|
||||
import StatusBadge from '../../../common/StatusBadge/StatusBadge.component';
|
||||
import { StatusType } from '../../../common/StatusBadge/StatusBadge.interface';
|
||||
import Table from '../../../common/Table/Table';
|
||||
import StopScheduleModal from '../../../Modals/StopScheduleRun/StopScheduleRunModal';
|
||||
import AppLogsViewer from '../AppLogsViewer/AppLogsViewer.component';
|
||||
import {
|
||||
AppRunRecordWithId,
|
||||
@ -78,6 +79,7 @@ const AppRunsHistory = forwardRef(
|
||||
AppRunRecordWithId[]
|
||||
>([]);
|
||||
const [expandedRowKeys, setExpandedRowKeys] = useState<string[]>([]);
|
||||
const [isStopModalOpen, setIsStopModalOpen] = useState<boolean>(false);
|
||||
|
||||
const {
|
||||
currentPage,
|
||||
@ -132,29 +134,35 @@ const AppRunsHistory = forwardRef(
|
||||
|
||||
const getActionButton = useCallback(
|
||||
(record: AppRunRecordWithId, index: number) => {
|
||||
if (appData?.appType === AppType.Internal) {
|
||||
if (
|
||||
appData?.appType === AppType.Internal ||
|
||||
(isExternalApp && index === 0)
|
||||
) {
|
||||
return (
|
||||
<Button
|
||||
className="p-0"
|
||||
data-testid="logs"
|
||||
disabled={showLogAction(record)}
|
||||
size="small"
|
||||
type="link"
|
||||
onClick={() => handleRowExpandable(record.id)}>
|
||||
{t('label.log-plural')}
|
||||
</Button>
|
||||
);
|
||||
} else if (isExternalApp && index === 0) {
|
||||
return (
|
||||
<Button
|
||||
className="p-0"
|
||||
data-testid="logs"
|
||||
disabled={showLogAction(record)}
|
||||
size="small"
|
||||
type="link"
|
||||
onClick={() => handleRowExpandable(record.id)}>
|
||||
{t('label.log-plural')}
|
||||
</Button>
|
||||
<>
|
||||
<Button
|
||||
className="p-0"
|
||||
data-testid="logs"
|
||||
disabled={showLogAction(record)}
|
||||
size="small"
|
||||
type="link"
|
||||
onClick={() => handleRowExpandable(record.id)}>
|
||||
{t('label.log-plural')}
|
||||
</Button>
|
||||
{/* For status running or activewitherror and supportsInterrupt is true, show stop button */}
|
||||
{(record.status === Status.Running ||
|
||||
record.status === Status.ActiveError) &&
|
||||
Boolean(appData?.supportsInterrupt) && (
|
||||
<Button
|
||||
className="m-l-xs p-0"
|
||||
data-testid="stop-button"
|
||||
size="small"
|
||||
type="link"
|
||||
onClick={() => setIsStopModalOpen(true)}>
|
||||
{t('label.stop')}
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
return NO_DATA_PLACEHOLDER;
|
||||
@ -347,47 +355,62 @@ const AppRunsHistory = forwardRef(
|
||||
}, [socket]);
|
||||
|
||||
return (
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col span={24}>
|
||||
<Table
|
||||
bordered
|
||||
columns={tableColumn}
|
||||
data-testid="app-run-history-table"
|
||||
dataSource={appRunsHistoryData}
|
||||
expandable={{
|
||||
expandedRowRender: (record) => (
|
||||
<AppLogsViewer
|
||||
data={record}
|
||||
scrollHeight={maxRecords !== 1 ? 200 : undefined}
|
||||
/>
|
||||
),
|
||||
showExpandColumn: false,
|
||||
rowExpandable: (record) => !showLogAction(record),
|
||||
expandedRowKeys,
|
||||
}}
|
||||
loading={isLoading}
|
||||
locale={{
|
||||
emptyText: <ErrorPlaceHolder className="m-y-md" />,
|
||||
}}
|
||||
pagination={false}
|
||||
rowKey="id"
|
||||
size="small"
|
||||
/>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
{showPagination && paginationVisible && (
|
||||
<NextPrevious
|
||||
isNumberBased
|
||||
currentPage={currentPage}
|
||||
isLoading={isLoading}
|
||||
pageSize={pageSize}
|
||||
paging={paging}
|
||||
pagingHandler={handleAppHistoryPageChange}
|
||||
onShowSizeChange={handlePageSizeChange}
|
||||
<>
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col span={24}>
|
||||
<Table
|
||||
bordered
|
||||
columns={tableColumn}
|
||||
data-testid="app-run-history-table"
|
||||
dataSource={appRunsHistoryData}
|
||||
expandable={{
|
||||
expandedRowRender: (record) => (
|
||||
<AppLogsViewer
|
||||
data={record}
|
||||
scrollHeight={maxRecords !== 1 ? 200 : undefined}
|
||||
/>
|
||||
),
|
||||
showExpandColumn: false,
|
||||
rowExpandable: (record) => !showLogAction(record),
|
||||
expandedRowKeys,
|
||||
}}
|
||||
loading={isLoading}
|
||||
locale={{
|
||||
emptyText: <ErrorPlaceHolder className="m-y-md" />,
|
||||
}}
|
||||
pagination={false}
|
||||
rowKey="id"
|
||||
size="small"
|
||||
/>
|
||||
)}
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
{showPagination && paginationVisible && (
|
||||
<NextPrevious
|
||||
isNumberBased
|
||||
currentPage={currentPage}
|
||||
isLoading={isLoading}
|
||||
pageSize={pageSize}
|
||||
paging={paging}
|
||||
pagingHandler={handleAppHistoryPageChange}
|
||||
onShowSizeChange={handlePageSizeChange}
|
||||
/>
|
||||
)}
|
||||
</Col>
|
||||
</Row>
|
||||
{isStopModalOpen && (
|
||||
<StopScheduleModal
|
||||
appName={fqn}
|
||||
displayName={appData?.displayName ?? ''}
|
||||
isModalOpen={isStopModalOpen}
|
||||
onClose={() => {
|
||||
setIsStopModalOpen(false);
|
||||
}}
|
||||
onStopWorkflowsUpdate={() => {
|
||||
fetchAppHistory();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
@ -142,6 +142,15 @@ const mockProps2 = {
|
||||
},
|
||||
};
|
||||
|
||||
const mockProps3 = {
|
||||
...mockProps1,
|
||||
appData: {
|
||||
...mockProps1.appData,
|
||||
supportsInterrupt: true,
|
||||
status: Status.Running,
|
||||
},
|
||||
};
|
||||
|
||||
describe('AppRunsHistory component', () => {
|
||||
it('should contain all necessary elements based on mockProps1', async () => {
|
||||
render(<AppRunsHistory {...mockProps1} />);
|
||||
@ -160,6 +169,11 @@ describe('AppRunsHistory component', () => {
|
||||
expect(screen.queryByText('--')).not.toBeInTheDocument();
|
||||
|
||||
expect(screen.getByText('NextPrevious')).toBeInTheDocument();
|
||||
|
||||
// Verify Stop button is not present as initial status is success
|
||||
const stopButton = screen.queryByTestId('stop-button');
|
||||
|
||||
expect(stopButton).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should show the error toast if fail in fetching app history', async () => {
|
||||
@ -247,4 +261,32 @@ describe('AppRunsHistory component', () => {
|
||||
|
||||
expect(screen.getByText('--')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render the stop button when conditions are met', async () => {
|
||||
const mockRunRecordWithStopButton = {
|
||||
...mockApplicationData,
|
||||
status: Status.Running, // Ensures Stop button condition is met
|
||||
supportsInterrupt: true,
|
||||
};
|
||||
mockGetApplicationRuns.mockReturnValueOnce({
|
||||
data: [mockRunRecordWithStopButton],
|
||||
paging: {
|
||||
offset: 0,
|
||||
total: 1,
|
||||
},
|
||||
});
|
||||
|
||||
render(<AppRunsHistory {...mockProps3} />);
|
||||
await waitForElementToBeRemoved(() => screen.getByText('TableLoader'));
|
||||
|
||||
const stopButton = screen.getByTestId('stop-button');
|
||||
|
||||
expect(stopButton).toBeInTheDocument();
|
||||
|
||||
act(() => {
|
||||
userEvent.click(stopButton);
|
||||
});
|
||||
|
||||
expect(screen.getByTestId('stop-modal')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
@ -1172,6 +1172,7 @@
|
||||
"status": "Status",
|
||||
"stay-up-to-date": "Bleiben Sie auf dem neuesten Stand",
|
||||
"step": "Step",
|
||||
"stop": "Stopp",
|
||||
"stop-re-index-all": "Stoppen Sie die erneute Indexierung aller",
|
||||
"stopped": "Gestoppt",
|
||||
"storage": "Speicher",
|
||||
@ -1430,6 +1431,7 @@
|
||||
"appearance-configuration-message": "Customize OpenMetadata with your company logo, monogram, favicon and brand color.",
|
||||
"application-action-successfully": "Application {{action}} successfully",
|
||||
"application-disabled-message": "Application is currently disabled. Click on the ellipsis menu in header to enable it.",
|
||||
"application-stop": "Anwendung wird gestoppt",
|
||||
"application-to-improve-data": "Improve your data using Applications for MetaPilot, Data Insights, and Search Indexing",
|
||||
"are-you-sure": "Sind Sie sicher?",
|
||||
"are-you-sure-action-property": "Are you sure you want to {{action}} {{propertyName}}?",
|
||||
|
@ -1172,6 +1172,7 @@
|
||||
"status": "Status",
|
||||
"stay-up-to-date": "Stay Up-to-date",
|
||||
"step": "Step",
|
||||
"stop": "Stop",
|
||||
"stop-re-index-all": "Stop Re-Index",
|
||||
"stopped": "Stopped",
|
||||
"storage": "Storage",
|
||||
@ -1430,6 +1431,7 @@
|
||||
"appearance-configuration-message": "Customize OpenMetadata with your company logo, monogram, favicon and brand color.",
|
||||
"application-action-successfully": "Application {{action}} successfully",
|
||||
"application-disabled-message": "Application is currently disabled. Click on the ellipsis menu in header to enable it.",
|
||||
"application-stop": "Application stop is in progresss",
|
||||
"application-to-improve-data": "Improve your data using Applications for MetaPilot, Data Insights, and Search Indexing",
|
||||
"are-you-sure": "Are you sure?",
|
||||
"are-you-sure-action-property": "Are you sure you want to {{action}} {{propertyName}}?",
|
||||
|
@ -1172,6 +1172,7 @@
|
||||
"status": "Estado",
|
||||
"stay-up-to-date": "Manténgase Actualizado",
|
||||
"step": "Paso",
|
||||
"stop": "Detener",
|
||||
"stop-re-index-all": "Parar el reindexado",
|
||||
"stopped": "Parado",
|
||||
"storage": "Almacenamiento",
|
||||
@ -1430,6 +1431,7 @@
|
||||
"appearance-configuration-message": "Customize OpenMetadata with your company logo, monogram, favicon and brand color.",
|
||||
"application-action-successfully": "Aplicación {{action}} exitosamente",
|
||||
"application-disabled-message": "La aplicación está actualmente deshabilitada. Haz clic en el menú de puntos suspensivos en la cabecera para habilitarla.",
|
||||
"application-stop": "La detención de la aplicación está en progreso",
|
||||
"application-to-improve-data": "Mejora tus datos utilizando Aplicaciones para MetaPilot, Data Insights y Indexación de Búsqueda",
|
||||
"are-you-sure": "¿Estás seguro?",
|
||||
"are-you-sure-action-property": "¿Estás seguro de que quieres {{action}} {{propertyName}}?",
|
||||
|
@ -1172,6 +1172,7 @@
|
||||
"status": "Statut",
|
||||
"stay-up-to-date": "Rester à Jour",
|
||||
"step": "Step",
|
||||
"stop": "Arrêter",
|
||||
"stop-re-index-all": "Arrêter la Ré-indexation de Tout",
|
||||
"stopped": "Arrêté",
|
||||
"storage": "Stockage",
|
||||
@ -1430,6 +1431,7 @@
|
||||
"appearance-configuration-message": "Personnalisez OpenMetadata avec votre logo d'entreprise, monogramme, favicon et couleur de marque.",
|
||||
"application-action-successfully": "Application {{action}} avec succès",
|
||||
"application-disabled-message": "Application actuellement désactivée. Cliques sur l'ellipse dans l'en-tête pour l'activer.",
|
||||
"application-stop": "L'arrêt de l'application est en cours",
|
||||
"application-to-improve-data": "Améliorez vous données en utilisant les Applications pour MetaPilot, les Data Insights, et l'indexation de recherche.",
|
||||
"are-you-sure": "Êtes-vous sûr?",
|
||||
"are-you-sure-action-property": "Êtes-vous sûr de vouloir {{action}} {{propertyName}}?",
|
||||
|
@ -1172,6 +1172,7 @@
|
||||
"status": "Estado",
|
||||
"stay-up-to-date": "Mantente ao día",
|
||||
"step": "Paso",
|
||||
"stop": "Parar",
|
||||
"stop-re-index-all": "Deteñer reindexación",
|
||||
"stopped": "Detido",
|
||||
"storage": "Almacenamento",
|
||||
@ -1430,6 +1431,7 @@
|
||||
"appearance-configuration-message": "Personaliza OpenMetadata co logotipo da túa empresa, monograma, favicon e cor da marca.",
|
||||
"application-action-successfully": "A aplicación {{action}} correctamente",
|
||||
"application-disabled-message": "A aplicación está desactivada actualmente. Fai clic no menú de puntos suspensivos no encabezado para activala.",
|
||||
"application-stop": "A parada da aplicación está en progreso",
|
||||
"application-to-improve-data": "Mellora os teus datos usando aplicacións para MetaPilot, Perspectivas de Datos, e Indexación de Busca",
|
||||
"are-you-sure": "Estás seguro?",
|
||||
"are-you-sure-action-property": "Estás seguro de que queres {{action}} {{propertyName}}?",
|
||||
|
@ -1172,6 +1172,7 @@
|
||||
"status": "סטטוס",
|
||||
"stay-up-to-date": "הישאר מעודכן",
|
||||
"step": "Step",
|
||||
"stop": "עצור",
|
||||
"stop-re-index-all": "עצור Re-Index",
|
||||
"stopped": "נעצר",
|
||||
"storage": "אחסון",
|
||||
@ -1430,6 +1431,7 @@
|
||||
"appearance-configuration-message": "Customize OpenMetadata with your company logo, monogram, favicon and brand color.",
|
||||
"application-action-successfully": "האפליקציה {{action}} בהצלחה",
|
||||
"application-disabled-message": "האפליקציה מושבתת כרגע. לחץ על תפריט הנקודות התלתול בכותרת כדי להפעיל אותה.",
|
||||
"application-stop": "עצירת היישום מתבצעת",
|
||||
"application-to-improve-data": "Improve your data using Applications for MetaPilot, Data Insights, and Search Indexing",
|
||||
"are-you-sure": "האם אתה בטוח?",
|
||||
"are-you-sure-action-property": "האם אתה בטוח שברצונך {{action}} {{propertyName}}?",
|
||||
|
@ -1172,6 +1172,7 @@
|
||||
"status": "ステータス",
|
||||
"stay-up-to-date": "最新を維持",
|
||||
"step": "Step",
|
||||
"stop": "停止",
|
||||
"stop-re-index-all": "Stop Re-Index",
|
||||
"stopped": "Stopped",
|
||||
"storage": "Storage",
|
||||
@ -1430,6 +1431,7 @@
|
||||
"appearance-configuration-message": "Customize OpenMetadata with your company logo, monogram, favicon and brand color.",
|
||||
"application-action-successfully": "Application {{action}} successfully",
|
||||
"application-disabled-message": "Application is currently disabled. Click on the ellipsis menu in header to enable it.",
|
||||
"application-stop": "アプリケーションの停止が進行中です",
|
||||
"application-to-improve-data": "Improve your data using Applications for MetaPilot, Data Insights, and Search Indexing",
|
||||
"are-you-sure": "よろしいですか?",
|
||||
"are-you-sure-action-property": "Are you sure you want to {{action}} {{propertyName}}?",
|
||||
|
@ -1172,6 +1172,7 @@
|
||||
"status": "Status",
|
||||
"stay-up-to-date": "Blijf Up-to-date",
|
||||
"step": "Step",
|
||||
"stop": "Stop",
|
||||
"stop-re-index-all": "Stop herindexeren",
|
||||
"stopped": "Gestopt",
|
||||
"storage": "Storage",
|
||||
@ -1430,6 +1431,7 @@
|
||||
"appearance-configuration-message": "Customize OpenMetadata with your company logo, monogram, favicon and brand color.",
|
||||
"application-action-successfully": "Applicatie {{action}} succesvol",
|
||||
"application-disabled-message": "De applicatie is momenteel uitgeschakeld. Klik op het ellipsismenu bovenin om het in te schakelen.",
|
||||
"application-stop": "Het stoppen van de applicatie is aan de gang",
|
||||
"application-to-improve-data": "Verbeter je gegevens met behulp van toepassingen voor MetaPilot, data-inzichten en zoekindexering",
|
||||
"are-you-sure": "Weet u het zeker?",
|
||||
"are-you-sure-action-property": "Weet u zeker dat u {{action}} {{propertyName}} wilt?",
|
||||
|
@ -1172,6 +1172,7 @@
|
||||
"status": "وضعیت",
|
||||
"stay-up-to-date": "در جریان بمانید",
|
||||
"step": "مرحله",
|
||||
"stop": "توقف",
|
||||
"stop-re-index-all": "متوقف کردن دوباره ایندکس",
|
||||
"stopped": "متوقف شد",
|
||||
"storage": "ذخیرهسازی",
|
||||
@ -1430,6 +1431,7 @@
|
||||
"appearance-configuration-message": "OpenMetadata را با لوگوی شرکت، مونوگرام، favicon و رنگ برند خود سفارشی کنید.",
|
||||
"application-action-successfully": "برنامه {{action}} با موفقیت انجام شد.",
|
||||
"application-disabled-message": "برنامه در حال حاضر غیرفعال است. برای فعالسازی آن روی منوی سهنقطه در سربرگ کلیک کنید.",
|
||||
"application-stop": "توقف برنامه در حال انجام است",
|
||||
"application-to-improve-data": "دادههای خود را با استفاده از برنامهها برای MetaPilot، بینش داده و ایندکس جستجو بهبود دهید.",
|
||||
"are-you-sure": "آیا مطمئن هستید؟",
|
||||
"are-you-sure-action-property": "آیا مطمئن هستید که میخواهید {{action}} {{propertyName}}؟",
|
||||
|
@ -1172,6 +1172,7 @@
|
||||
"status": "Status",
|
||||
"stay-up-to-date": "Mantenha-se Atualizado",
|
||||
"step": "Step",
|
||||
"stop": "Parar",
|
||||
"stop-re-index-all": "Parar Reindexação",
|
||||
"stopped": "Parado",
|
||||
"storage": "Armazenamento",
|
||||
@ -1430,6 +1431,7 @@
|
||||
"appearance-configuration-message": "Customize OpenMetadata with your company logo, monogram, favicon and brand color.",
|
||||
"application-action-successfully": "Aplicativo {{action}} com sucesso",
|
||||
"application-disabled-message": "O aplicativo está atualmente desativado. Clique no menu de reticências no cabeçalho para ativá-lo.",
|
||||
"application-stop": "A parada do aplicativo está em andamento",
|
||||
"application-to-improve-data": "Aprimore seus dados utilizando Aplicações como MetaPilot, Data Insights e Indexação de Pesquisa",
|
||||
"are-you-sure": "Você tem certeza?",
|
||||
"are-you-sure-action-property": "Você tem certeza que deseja {{action}} {{propertyName}}?",
|
||||
|
@ -1172,6 +1172,7 @@
|
||||
"status": "Status",
|
||||
"stay-up-to-date": "Mantenha-se Atualizado",
|
||||
"step": "Step",
|
||||
"stop": "Parar",
|
||||
"stop-re-index-all": "Parar Reindexação",
|
||||
"stopped": "Parado",
|
||||
"storage": "Armazenamento",
|
||||
@ -1430,6 +1431,7 @@
|
||||
"appearance-configuration-message": "Customize OpenMetadata with your company logo, monogram, favicon and brand color.",
|
||||
"application-action-successfully": "Aplicativo {{action}} com sucesso",
|
||||
"application-disabled-message": "O aplicativo está atualmente desativado. Clique no menu de reticências no cabeçalho para ativá-lo.",
|
||||
"application-stop": "A paragem da aplicação está em progresso",
|
||||
"application-to-improve-data": "Aprimore seus dados utilizando Aplicações como MetaPilot, Data Insights e Indexação de Pesquisa",
|
||||
"are-you-sure": "Você tem certeza?",
|
||||
"are-you-sure-action-property": "Você tem certeza que deseja {{action}} {{propertyName}}?",
|
||||
|
@ -1172,6 +1172,7 @@
|
||||
"status": "Статус",
|
||||
"stay-up-to-date": "Будьте в курсе последних событий",
|
||||
"step": "Step",
|
||||
"stop": "Остановить",
|
||||
"stop-re-index-all": "Остановить ре-индексирование",
|
||||
"stopped": "Остановлено",
|
||||
"storage": "Хранилище",
|
||||
@ -1430,6 +1431,7 @@
|
||||
"appearance-configuration-message": "Customize OpenMetadata with your company logo, monogram, favicon and brand color.",
|
||||
"application-action-successfully": "Application {{action}} successfully",
|
||||
"application-disabled-message": "Application is currently disabled. Click on the ellipsis menu in header to enable it.",
|
||||
"application-stop": "Остановка приложения выполняется",
|
||||
"application-to-improve-data": "Improve your data using Applications for MetaPilot, Data Insights, and Search Indexing",
|
||||
"are-you-sure": "Вы уверены?",
|
||||
"are-you-sure-action-property": "Are you sure you want to {{action}} {{propertyName}}?",
|
||||
|
@ -1172,6 +1172,7 @@
|
||||
"status": "สถานะ",
|
||||
"stay-up-to-date": "อัปเดตอยู่เสมอ",
|
||||
"step": "ขั้นตอน",
|
||||
"stop": "หยุด",
|
||||
"stop-re-index-all": "หยุดการสร้างดัชนีใหม่ทั้งหมด",
|
||||
"stopped": "หยุดแล้ว",
|
||||
"storage": "ที่จัดเก็บ",
|
||||
@ -1430,6 +1431,7 @@
|
||||
"appearance-configuration-message": "ปรับแต่ง OpenMetadata ด้วยโลโก้ของบริษัท, ตัวอักษร, favicon และสีแบรนด์",
|
||||
"application-action-successfully": "แอปพลิเคชัน {{action}} สำเร็จ",
|
||||
"application-disabled-message": "แอปพลิเคชันถูกปิดการใช้งานในขณะนี้ คลิกที่เมนูสามจุดในหัวข้อเพื่อเปิดใช้งาน",
|
||||
"application-stop": "การหยุดแอปพลิเคชันกำลังดำเนินการ",
|
||||
"application-to-improve-data": "ปรับปรุงข้อมูลของคุณโดยใช้แอปพลิเคชันสำหรับ MetaPilot, ข้อมูลเชิงลึก, และการจัดทำดัชนีการค้นหา",
|
||||
"are-you-sure": "คุณแน่ใจหรือไม่?",
|
||||
"are-you-sure-action-property": "คุณแน่ใจหรือไม่ว่าต้องการ {{action}} {{propertyName}}?",
|
||||
|
@ -1172,6 +1172,7 @@
|
||||
"status": "状态",
|
||||
"stay-up-to-date": "保持最新",
|
||||
"step": "步骤",
|
||||
"stop": "停止",
|
||||
"stop-re-index-all": "停止重新索引",
|
||||
"stopped": "已停止",
|
||||
"storage": "存储",
|
||||
@ -1430,6 +1431,7 @@
|
||||
"appearance-configuration-message": "用您公司的徽标、字母图案、图标和品牌颜色自定义 OpenMetadata",
|
||||
"application-action-successfully": "应用{{action}}成功",
|
||||
"application-disabled-message": "应用程序目前处于禁用状态, 点击标题中的省略号菜单即可启用",
|
||||
"application-stop": "应用程序停止进行中",
|
||||
"application-to-improve-data": "使用 MetaPilot、数据洞察和搜索索引应用改进数据",
|
||||
"are-you-sure": "您确定吗?",
|
||||
"are-you-sure-action-property": "您确定要{{action}} {{propertyName}}?",
|
||||
|
@ -135,3 +135,7 @@ export const restoreApp = async (id: string) => {
|
||||
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const stopApp = async (name: string) => {
|
||||
return await APIClient.post(`${BASE_URL}/stop/${getEncodedFqn(name)}`);
|
||||
};
|
||||
|
Loading…
x
Reference in New Issue
Block a user