Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

VTAdmin(web): Add workflow start/stop actions #16675

Merged
merged 4 commits into from
Sep 7, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions web/vtadmin/src/api/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -448,6 +448,28 @@ export const fetchWorkflowStatus = async (params: { clusterID: string; keyspace:
return vtctldata.WorkflowStatusResponse.create(result);
};

export interface WorkflowActionParams {
clusterID: string;
keyspace: string;
name: string;
}

export const startWorkflow = async ({ clusterID, keyspace, name }: WorkflowActionParams) => {
const { result } = await vtfetch(`/api/workflow/${clusterID}/${keyspace}/${name}/start`);
const err = vtctldata.WorkflowUpdateResponse.verify(result);
if (err) throw Error(err);

return vtctldata.WorkflowUpdateResponse.create(result);
};

export const stopWorkflow = async ({ clusterID, keyspace, name }: WorkflowActionParams) => {
const { result } = await vtfetch(`/api/workflow/${clusterID}/${keyspace}/${name}/stop`);
const err = vtctldata.WorkflowUpdateResponse.verify(result);
if (err) throw Error(err);

return vtctldata.WorkflowUpdateResponse.create(result);
};

export const fetchVTExplain = async <R extends pb.IVTExplainRequest>({ cluster, keyspace, sql }: R) => {
// As an easy enhancement for later, we can also validate the request parameters on the front-end
// instead of defaulting to '', to save a round trip.
Expand Down
19 changes: 18 additions & 1 deletion web/vtadmin/src/components/routes/Workflows.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,15 @@ import { Tooltip } from '../tooltip/Tooltip';
import { KeyspaceLink } from '../links/KeyspaceLink';
import { QueryLoadingPlaceholder } from '../placeholders/QueryLoadingPlaceholder';
import { UseQueryResult } from 'react-query';
import { ReadOnlyGate } from '../ReadOnlyGate';
import WorkflowActions from './workflows/WorkflowActions';
import { isReadOnlyMode } from '../../util/env';

export const ThrottleThresholdSeconds = 60;

const COLUMNS = ['Workflow', 'Source', 'Target', 'Streams', 'Last Updated', 'Actions'];
const READ_ONLY_COLUMNS = ['Workflow', 'Source', 'Target', 'Streams', 'Last Updated'];

export const Workflows = () => {
useDocumentTitle('Workflows');
const workflowsQuery = useWorkflows();
Expand Down Expand Up @@ -180,6 +186,17 @@ export const Workflows = () => {
<div className="font-sans whitespace-nowrap">{formatDateTime(row.timeUpdated)}</div>
<div className="font-sans text-sm text-secondary">{formatRelativeTime(row.timeUpdated)}</div>
</DataCell>

<ReadOnlyGate>
<DataCell>
<WorkflowActions
refetchWorkflows={workflowsQuery.refetch}
keyspace={row.keyspace as string}
clusterID={row.clusterID as string}
name={row.name as string}
/>
</DataCell>
</ReadOnlyGate>
</tr>
);
});
Expand All @@ -199,7 +216,7 @@ export const Workflows = () => {
/>

<DataTable
columns={['Workflow', 'Source', 'Target', 'Streams', 'Last Updated']}
columns={isReadOnlyMode() ? READ_ONLY_COLUMNS : COLUMNS}
data={sortedData}
renderRows={renderRows}
/>
Expand Down
89 changes: 89 additions & 0 deletions web/vtadmin/src/components/routes/workflows/WorkflowAction.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import React from 'react';
import { Icon, Icons } from '../../Icon';
import Dialog from '../../dialog/Dialog';
import { UseMutationResult } from 'react-query';

interface WorkflowActionProps {
isOpen: boolean;
mutation: UseMutationResult<any, any, any>;
title: string;
confirmText: string;
successText: string;
errorText: string;
loadingText: string;
description?: string;
body?: JSX.Element;
successBody?: JSX.Element;
refetchWorkflows: Function;
closeDialog: () => void;
}

const WorkflowAction: React.FC<WorkflowActionProps> = ({
isOpen,
closeDialog,
mutation,
title,
confirmText,
description,
successText,
successBody,
loadingText,
errorText,
refetchWorkflows,
body,
}) => {
const onCloseDialog = () => {
setTimeout(mutation.reset, 500);
closeDialog();
};

const hasRun = mutation.data || mutation.error;
const onConfirm = () => {
mutation.mutate(
{},
{
onSuccess: () => {
refetchWorkflows();
},
}
);
};
return (
<Dialog
isOpen={isOpen}
confirmText={hasRun ? 'Close' : confirmText}
cancelText="Cancel"
onConfirm={hasRun ? onCloseDialog : onConfirm}
loadingText={loadingText}
loading={mutation.isLoading}
onCancel={onCloseDialog}
onClose={onCloseDialog}
hideCancel={hasRun}
title={hasRun ? undefined : title}
description={hasRun ? undefined : description}
>
<div className="w-full">
{!hasRun && body}
{mutation.data && !mutation.error && (
<div className="w-full flex flex-col justify-center items-center">
<span className="flex h-12 w-12 relative items-center justify-center">
<Icon className="fill-current text-green-500" icon={Icons.checkSuccess} />
</span>
<div className="text-lg mt-3 font-bold text-center">{successText}</div>
{successBody}
</div>
)}
{mutation.error && (
<div className="w-full flex flex-col justify-center items-center">
<span className="flex h-12 w-12 relative items-center justify-center">
<Icon className="fill-current text-red-500" icon={Icons.alertFail} />
</span>
<div className="text-lg mt-3 font-bold text-center">{errorText}</div>
</div>
)}
</div>
</Dialog>
);
};

export default WorkflowAction;
79 changes: 79 additions & 0 deletions web/vtadmin/src/components/routes/workflows/WorkflowActions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import React, { useState } from 'react';
import Dropdown from '../../dropdown/Dropdown';
import MenuItem from '../../dropdown/MenuItem';
import { Icons } from '../../Icon';
import WorkflowAction from './WorkflowAction';
import { useStartWorkflow, useStopWorkflow } from '../../../hooks/api';

interface WorkflowActionsProps {
refetchWorkflows: Function;
keyspace: string;
clusterID: string;
name: string;
}

const WorkflowActions: React.FC<WorkflowActionsProps> = ({ refetchWorkflows, keyspace, clusterID, name }) => {
const [currentDialog, setCurrentDialog] = useState<string>('');
const closeDialog = () => setCurrentDialog('');

const startWorkflowMutation = useStartWorkflow({ keyspace, clusterID, name });

const stopWorkflowMutation = useStopWorkflow({ keyspace, clusterID, name });

return (
<div className="w-min inline-block">
<Dropdown dropdownButton={Icons.info} position="bottom-right">
<MenuItem onClick={() => setCurrentDialog('Start Workflow')}>Start Workflow</MenuItem>
<MenuItem onClick={() => setCurrentDialog('Stop Workflow')}>Stop Workflow</MenuItem>
</Dropdown>
<WorkflowAction
title="Start Workflow"
confirmText="Start"
loadingText="Starting"
mutation={startWorkflowMutation}
successText="Started workflow"
errorText={`Error occured while starting workflow ${name}`}
closeDialog={closeDialog}
isOpen={currentDialog === 'Start Workflow'}
refetchWorkflows={refetchWorkflows}
successBody={
<div className="text-sm">
{startWorkflowMutation.data && startWorkflowMutation.data.summary && (
<div className="text-sm">{startWorkflowMutation.data.summary}</div>
)}
</div>
}
body={
<div className="text-sm mt-3">
Starts the <span className="font-mono bg-gray-300">{name}</span> workflow.
beingnoble03 marked this conversation as resolved.
Show resolved Hide resolved
</div>
}
/>
<WorkflowAction
title="Stop Workflow"
confirmText="Stop"
loadingText="Stopping"
mutation={stopWorkflowMutation}
successText="Stopped workflow"
errorText={`Error occured while stopping workflow ${name}`}
closeDialog={closeDialog}
isOpen={currentDialog === 'Stop Workflow'}
refetchWorkflows={refetchWorkflows}
successBody={
<div className="text-sm">
{stopWorkflowMutation.data && stopWorkflowMutation.data.summary && (
<div className="text-sm">{stopWorkflowMutation.data.summary}</div>
)}
</div>
}
body={
<div className="text-sm mt-3">
Stops the <span className="font-mono bg-gray-300">{name}</span> workflow.
</div>
}
/>
</div>
);
};

export default WorkflowActions;
26 changes: 26 additions & 0 deletions web/vtadmin/src/hooks/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ import {
GetFullStatusParams,
validateVersionShard,
ValidateVersionShardParams,
startWorkflow,
stopWorkflow,
} from '../api/http';
import { vtadmin as pb, vtctldata } from '../proto/vtadmin';
import { formatAlias } from '../util/tablets';
Expand Down Expand Up @@ -460,6 +462,30 @@ export const useWorkflowStatus = (
return useQuery(['workflow_status', params], () => fetchWorkflowStatus(params));
};

/**
* useStartWorkflow is a mutate hook that starts a workflow.
*/
export const useStartWorkflow = (
params: Parameters<typeof startWorkflow>[0],
options?: UseMutationOptions<Awaited<ReturnType<typeof startWorkflow>>, Error>
) => {
return useMutation<Awaited<ReturnType<typeof startWorkflow>>, Error>(() => {
return startWorkflow(params);
}, options);
};

/**
* useStopWorkflow is a mutate hook that stops a workflow.
*/
export const useStopWorkflow = (
params: Parameters<typeof stopWorkflow>[0],
options?: UseMutationOptions<Awaited<ReturnType<typeof stopWorkflow>>, Error>
) => {
return useMutation<Awaited<ReturnType<typeof stopWorkflow>>, Error>(() => {
return stopWorkflow(params);
}, options);
};

/**
* useReloadSchema is a mutate hook that reloads schemas in one or more
* keyspaces, shards, or tablets in the cluster, depending on the request parameters.
Expand Down