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

Auto refresh execution #12

Merged
merged 11 commits into from
Mar 18, 2025
116 changes: 53 additions & 63 deletions ui.frontend/src/components/ExecutionAbortButton.tsx
Original file line number Diff line number Diff line change
@@ -1,80 +1,70 @@
import React, { useState } from 'react';
import {
Button,
ButtonGroup,
Content,
Dialog,
DialogTrigger,
Divider,
Heading,
Text
} from '@adobe/react-spectrum';
import { toastRequest } from '../utils/api';
import { Button, Text } from '@adobe/react-spectrum';
import Cancel from '@spectrum-icons/workflow/Cancel';
import Checkmark from '@spectrum-icons/workflow/Checkmark';
import {QueueOutput} from "../utils/api.types.ts";
import { ToastQueue } from '@react-spectrum/toast';
import { apiRequest } from '../utils/api';
import {Execution, ExecutionStatus, isExecutionPending, QueueOutput} from '../utils/api.types';

type ExecutionAbortButtonProps = {
selectedKeys: string[];
onAbort?: () => void;
};

const ExecutionAbortButton: React.FC<ExecutionAbortButtonProps> = ({ selectedKeys, onAbort }) => {
const [abortDialogOpen, setAbortDialogOpen] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const executionPollInterval = 1000;
const toastTimeout = 3000;

const handleConfirm = async () => {
setIsLoading(true);
const ids = Array.from(selectedKeys);
interface ExecutionAbortButtonProps {
execution: Execution | null;
onComplete: (execution: Execution | null) => void;
}

const params = new URLSearchParams();
ids.forEach((id) => params.append('jobId', id));
const ExecutionAbortButton: React.FC<ExecutionAbortButtonProps> = ({ execution, onComplete }) => {
const [isAborting, setIsAborting] = useState(false);

const onAbort = async () => {
if (!execution?.id) {
console.warn('Code execution cannot be aborted as it is not running!');
return;
}
setIsAborting(true);
try {
await toastRequest<QueueOutput>({
method: 'DELETE',
url: `/apps/acm/api/queue-code.json?${params.toString()}`,
operation: 'Abort executions',
await apiRequest<QueueOutput>({
operation: 'Code execution aborting',
url: `/apps/acm/api/queue-code.json?jobId=${execution.id}`,
method: 'delete',
});
if (onAbort) onAbort();

let queuedExecution: Execution | null = null;
while (queuedExecution === null || isExecutionPending(queuedExecution.status)) {
const response = await apiRequest<QueueOutput>({
operation: 'Code execution state',
url: `/apps/acm/api/queue-code.json?jobId=${execution.id}`,
method: 'get',
});
queuedExecution = response.data.data.executions[0]!;
onComplete(queuedExecution);
await new Promise((resolve) => setTimeout(resolve, executionPollInterval));
}
if (queuedExecution.status === ExecutionStatus.ABORTED) {
ToastQueue.positive('Code execution aborted successfully!', {
timeout: toastTimeout,
});
} else {
console.warn('Code execution aborting failed!');
ToastQueue.negative('Code execution aborting failed!', {
timeout: toastTimeout,
});
}
} catch (error) {
console.error('Abort executions error:', error);
console.error('Code execution aborting error:', error);
ToastQueue.negative('Code execution aborting failed!', {
timeout: toastTimeout,
});
} finally {
setIsLoading(false);
setAbortDialogOpen(false);
setIsAborting(false);
}
};

const renderAbortDialog = () => (
<>
<Heading>
<Text>Confirmation</Text>
</Heading>
<Divider />
<Content>
<Text>Are you sure you want to abort the selected executions? This action cannot be undone.</Text>
</Content>
<ButtonGroup>
<Button variant="secondary" onPress={() => setAbortDialogOpen(false)} isDisabled={isLoading}>
<Cancel />
<Text>Cancel</Text>
</Button>
<Button variant="negative" style="fill" onPress={handleConfirm} isPending={isLoading}>
<Checkmark />
<Text>Confirm</Text>
</Button>
</ButtonGroup>
</>
);

return (
<DialogTrigger isOpen={abortDialogOpen} onOpenChange={setAbortDialogOpen}>
<Button variant="negative" style="fill" isDisabled={selectedKeys.length === 0} onPress={() => setAbortDialogOpen(true)}>
<Cancel />
<Text>Abort</Text>
</Button>
<Dialog>{renderAbortDialog()}</Dialog>
</DialogTrigger>
<Button variant="negative" isDisabled={!execution || !isExecutionPending(execution.status) || isAborting} onPress={onAbort}>
<Cancel />
<Text>Abort</Text>
</Button>
);
};

Expand Down
42 changes: 42 additions & 0 deletions ui.frontend/src/components/ExecutionCopyOutputButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import React from 'react';
import { Button, ButtonGroup, Text } from '@adobe/react-spectrum';
import { ToastQueue } from '@react-spectrum/toast';
import Copy from '@spectrum-icons/workflow/Copy';

const toastTimeout = 3000;

interface ExecutionCopyOutputButtonProps {
output: string;
}

const ExecutionCopyOutputButton: React.FC<ExecutionCopyOutputButtonProps> = ({ output }) => {
const onCopyExecutionOutput = () => {
if (output) {
navigator.clipboard
.writeText(output)
.then(() => {
ToastQueue.info('Execution output copied to clipboard!', {
timeout: toastTimeout,
});
})
.catch(() => {
ToastQueue.negative('Failed to copy execution output!', {
timeout: toastTimeout,
});
});
} else {
ToastQueue.negative('No execution output to copy!', {
timeout: toastTimeout,
});
}
};

return (
<Button variant="secondary" isDisabled={!output} onPress={onCopyExecutionOutput}>
<Copy />
<Text>Copy</Text>
</Button>
);
};

export default ExecutionCopyOutputButton;
7 changes: 5 additions & 2 deletions ui.frontend/src/components/ExecutionProgressBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@ import { Meter, ProgressBar } from '@adobe/react-spectrum';
import React from 'react';
import { Execution, isExecutionPending } from '../utils/api.types.ts';
import { Strings } from '../utils/strings.ts';
import {useFormatter} from "../utils/hooks/formatter.ts";

interface ExecutionProgressBarProps {
execution: Execution | null;
active: boolean;
active?: boolean;
}

const ExecutionProgressBar: React.FC<ExecutionProgressBarProps> = ({ execution, active }) => {
Expand All @@ -23,13 +24,15 @@ const ExecutionProgressBar: React.FC<ExecutionProgressBarProps> = ({ execution,
}
};

const formatter = useFormatter();

return (
<>
{execution ? (
active || isExecutionPending(execution.status) ? (
<ProgressBar aria-label="Executing" showValueLabel={false} label="Executing…" isIndeterminate />
) : (
<Meter aria-label="Executed" variant={variant()} showValueLabel={false} value={100} label={`${Strings.capitalize(execution.status)} after ${execution.duration} ms`} />
<Meter aria-label="Executed" variant={variant()} showValueLabel={false} value={100} label={`${Strings.capitalize(execution.status)} after ${formatter.durationShort(execution.duration)}`} />
)
) : (
<Meter aria-label="Not executing" label="Not executing" showValueLabel={false} value={0} />
Expand Down
10 changes: 5 additions & 5 deletions ui.frontend/src/components/ExecutionStatusBadge.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {Badge, ProgressCircle, Text} from '@adobe/react-spectrum';
import { Badge, ProgressCircle, SpectrumBadgeProps, Text } from '@adobe/react-spectrum';
import Alert from '@spectrum-icons/workflow/Alert';
import Cancel from '@spectrum-icons/workflow/Cancel';
import Checkmark from '@spectrum-icons/workflow/Checkmark';
Expand All @@ -7,9 +7,9 @@ import Pause from '@spectrum-icons/workflow/Pause';
import React from 'react';
import { ExecutionStatus } from '../utils/api.types';

interface ExecutionStatusProps {
type ExecutionStatusProps = {
value: ExecutionStatus;
}
} & Partial<SpectrumBadgeProps>;

const getVariant = (status: ExecutionStatus): 'positive' | 'negative' | 'neutral' | 'info' | 'yellow' => {
switch (status) {
Expand Down Expand Up @@ -52,12 +52,12 @@ const getIcon = (status: ExecutionStatus) => {
}
};

const ExecutionStatusBadge: React.FC<ExecutionStatusProps> = ({ value }) => {
const ExecutionStatusBadge: React.FC<ExecutionStatusProps> = ({ value, ...props }) => {
const variant = getVariant(value);
const icon = getIcon(value);

return (
<Badge variant={variant}>
<Badge variant={variant} {...props}>
<Text>{value.toLowerCase()}</Text>
{icon}
</Badge>
Expand Down
81 changes: 81 additions & 0 deletions ui.frontend/src/components/ExecutionsAbortButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import React, { useState } from 'react';
import {
Button,
ButtonGroup,
Content,
Dialog,
DialogTrigger,
Divider,
Heading,
Text
} from '@adobe/react-spectrum';
import { toastRequest } from '../utils/api';
import Cancel from '@spectrum-icons/workflow/Cancel';
import Checkmark from '@spectrum-icons/workflow/Checkmark';
import {QueueOutput} from "../utils/api.types.ts";

type ExecutionsAbortButtonProps = {
selectedKeys: string[];
onAbort?: () => void;
};

const ExecutionsAbortButton: React.FC<ExecutionsAbortButtonProps> = ({ selectedKeys, onAbort }) => {
const [abortDialogOpen, setAbortDialogOpen] = useState(false);
const [isLoading, setIsLoading] = useState(false);

const handleConfirm = async () => {
setIsLoading(true);
const ids = Array.from(selectedKeys);

const params = new URLSearchParams();
ids.forEach((id) => params.append('jobId', id));

try {
await toastRequest<QueueOutput>({
method: 'DELETE',
url: `/apps/acm/api/queue-code.json?${params.toString()}`,
operation: 'Abort executions',
});
if (onAbort) onAbort();
} catch (error) {
console.error('Abort executions error:', error);
} finally {
setIsLoading(false);
setAbortDialogOpen(false);
}
};

const renderAbortDialog = () => (
<>
<Heading>
<Text>Confirmation</Text>
</Heading>
<Divider />
<Content>
<Text>Are you sure you want to abort the selected executions? This action cannot be undone.</Text>
</Content>
<ButtonGroup>
<Button variant="secondary" onPress={() => setAbortDialogOpen(false)} isDisabled={isLoading}>
<Cancel />
<Text>Cancel</Text>
</Button>
<Button variant="negative" style="fill" onPress={handleConfirm} isPending={isLoading}>
<Checkmark />
<Text>Confirm</Text>
</Button>
</ButtonGroup>
</>
);

return (
<DialogTrigger isOpen={abortDialogOpen} onOpenChange={setAbortDialogOpen}>
<Button variant="negative" style="fill" isDisabled={selectedKeys.length === 0} onPress={() => setAbortDialogOpen(true)}>
<Cancel />
<Text>Abort</Text>
</Button>
<Dialog>{renderAbortDialog()}</Dialog>
</DialogTrigger>
);
};

export default ExecutionsAbortButton;
4 changes: 2 additions & 2 deletions ui.frontend/src/components/ScriptExecutor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ import Replay from "@spectrum-icons/workflow/Replay";
import Checkmark from "@spectrum-icons/workflow/Checkmark";
import Cancel from "@spectrum-icons/workflow/Cancel";
import Settings from "@spectrum-icons/workflow/Settings";
import ExecutionAbortButton from './ExecutionAbortButton';
import ExecutionsAbortButton from './ExecutionsAbortButton.tsx';
import Code from "@spectrum-icons/workflow/Code";

const ScriptExecutor = () => {
Expand Down Expand Up @@ -70,7 +70,7 @@ const ScriptExecutor = () => {
<Flex direction="row" justifyContent="space-between" alignItems="center">
<Flex flex="1" alignItems="center">
<ButtonGroup>
<ExecutionAbortButton selectedKeys={selectedIds(selectedKeys)}/>
<ExecutionsAbortButton selectedKeys={selectedIds(selectedKeys)}/>
<MenuTrigger>
<Button variant="negative">
<Settings />
Expand Down
Loading