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:
Kenil Shah 2024-11-26 11:55:47 +05:30 committed by GitHub
parent d1b8b9a966
commit 09db201a57
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
26 changed files with 405 additions and 96 deletions

View File

@ -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
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}

View File

@ -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()) {

View File

@ -75,7 +75,8 @@
"active",
"activeError",
"stopped",
"success"
"success",
"stopInProgress"
]
},
"failure": {

View File

@ -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;
}

View File

@ -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();
});
});

View File

@ -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;

View File

@ -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();
}}
/>
)}
</>
);
}
);

View File

@ -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();
});
});

View File

@ -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}}?",

View File

@ -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}}?",

View File

@ -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}}?",

View File

@ -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}}?",

View File

@ -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}}?",

View File

@ -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}}?",

View File

@ -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}}?",

View File

@ -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?",

View File

@ -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}}؟",

View File

@ -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}}?",

View File

@ -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}}?",

View File

@ -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}}?",

View File

@ -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}}?",

View File

@ -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}}?",

View File

@ -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)}`);
};