diff --git a/web/vtadmin/src/api/http.ts b/web/vtadmin/src/api/http.ts index 64d75b23a42..030455c652d 100644 --- a/web/vtadmin/src/api/http.ts +++ b/web/vtadmin/src/api/http.ts @@ -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 ({ 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. diff --git a/web/vtadmin/src/components/routes/Workflows.tsx b/web/vtadmin/src/components/routes/Workflows.tsx index 32ddfcfb825..558d82ab8e8 100644 --- a/web/vtadmin/src/components/routes/Workflows.tsx +++ b/web/vtadmin/src/components/routes/Workflows.tsx @@ -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(); @@ -180,6 +186,17 @@ export const Workflows = () => {
{formatDateTime(row.timeUpdated)}
{formatRelativeTime(row.timeUpdated)}
+ + + + + + ); }); @@ -199,7 +216,7 @@ export const Workflows = () => { /> diff --git a/web/vtadmin/src/components/routes/workflows/WorkflowAction.tsx b/web/vtadmin/src/components/routes/workflows/WorkflowAction.tsx new file mode 100644 index 00000000000..8ff08801dff --- /dev/null +++ b/web/vtadmin/src/components/routes/workflows/WorkflowAction.tsx @@ -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; + 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 = ({ + 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 ( + +
+ {!hasRun && body} + {mutation.data && !mutation.error && ( +
+ + + +
{successText}
+ {successBody} +
+ )} + {mutation.error && ( +
+ + + +
{errorText}
+
+ )} +
+
+ ); +}; + +export default WorkflowAction; diff --git a/web/vtadmin/src/components/routes/workflows/WorkflowActions.tsx b/web/vtadmin/src/components/routes/workflows/WorkflowActions.tsx new file mode 100644 index 00000000000..f437fb9e785 --- /dev/null +++ b/web/vtadmin/src/components/routes/workflows/WorkflowActions.tsx @@ -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 = ({ refetchWorkflows, keyspace, clusterID, name }) => { + const [currentDialog, setCurrentDialog] = useState(''); + const closeDialog = () => setCurrentDialog(''); + + const startWorkflowMutation = useStartWorkflow({ keyspace, clusterID, name }); + + const stopWorkflowMutation = useStopWorkflow({ keyspace, clusterID, name }); + + return ( +
+ + setCurrentDialog('Start Workflow')}>Start Workflow + setCurrentDialog('Stop Workflow')}>Stop Workflow + + + {startWorkflowMutation.data && startWorkflowMutation.data.summary && ( +
{startWorkflowMutation.data.summary}
+ )} +
+ } + body={ +
+ Start the {name} workflow. +
+ } + /> + + {stopWorkflowMutation.data && stopWorkflowMutation.data.summary && ( +
{stopWorkflowMutation.data.summary}
+ )} + + } + body={ +
+ Stop the {name} workflow. +
+ } + /> + + ); +}; + +export default WorkflowActions; diff --git a/web/vtadmin/src/hooks/api.ts b/web/vtadmin/src/hooks/api.ts index 536293702c1..f3772ed4ab0 100644 --- a/web/vtadmin/src/hooks/api.ts +++ b/web/vtadmin/src/hooks/api.ts @@ -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'; @@ -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[0], + options?: UseMutationOptions>, Error> +) => { + return useMutation>, Error>(() => { + return startWorkflow(params); + }, options); +}; + +/** + * useStopWorkflow is a mutate hook that stops a workflow. + */ +export const useStopWorkflow = ( + params: Parameters[0], + options?: UseMutationOptions>, Error> +) => { + return useMutation>, 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.