diff --git a/ui.frontend/src/components/ExecutionAbortButton.tsx b/ui.frontend/src/components/ExecutionAbortButton.tsx
index 4eeaad9c..7dd630bc 100644
--- a/ui.frontend/src/components/ExecutionAbortButton.tsx
+++ b/ui.frontend/src/components/ExecutionAbortButton.tsx
@@ -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 = ({ 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 = ({ 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({
- method: 'DELETE',
- url: `/apps/acm/api/queue-code.json?${params.toString()}`,
- operation: 'Abort executions',
+ await apiRequest({
+ 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({
+ 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 = () => (
- <>
-
- Confirmation
-
-
-
- Are you sure you want to abort the selected executions? This action cannot be undone.
-
-
-
-
-
- >
- );
-
return (
-
-
-
-
+
);
};
diff --git a/ui.frontend/src/components/ExecutionCopyOutputButton.tsx b/ui.frontend/src/components/ExecutionCopyOutputButton.tsx
new file mode 100644
index 00000000..001248d8
--- /dev/null
+++ b/ui.frontend/src/components/ExecutionCopyOutputButton.tsx
@@ -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 = ({ 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 (
+
+ );
+};
+
+export default ExecutionCopyOutputButton;
\ No newline at end of file
diff --git a/ui.frontend/src/components/ExecutionProgressBar.tsx b/ui.frontend/src/components/ExecutionProgressBar.tsx
index bd0837c6..53e31e8f 100644
--- a/ui.frontend/src/components/ExecutionProgressBar.tsx
+++ b/ui.frontend/src/components/ExecutionProgressBar.tsx
@@ -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 = ({ execution, active }) => {
@@ -23,13 +24,15 @@ const ExecutionProgressBar: React.FC = ({ execution,
}
};
+ const formatter = useFormatter();
+
return (
<>
{execution ? (
active || isExecutionPending(execution.status) ? (
) : (
-
+
)
) : (
diff --git a/ui.frontend/src/components/ExecutionStatusBadge.tsx b/ui.frontend/src/components/ExecutionStatusBadge.tsx
index 6461ad88..ca194395 100644
--- a/ui.frontend/src/components/ExecutionStatusBadge.tsx
+++ b/ui.frontend/src/components/ExecutionStatusBadge.tsx
@@ -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';
@@ -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;
const getVariant = (status: ExecutionStatus): 'positive' | 'negative' | 'neutral' | 'info' | 'yellow' => {
switch (status) {
@@ -52,12 +52,12 @@ const getIcon = (status: ExecutionStatus) => {
}
};
-const ExecutionStatusBadge: React.FC = ({ value }) => {
+const ExecutionStatusBadge: React.FC = ({ value, ...props }) => {
const variant = getVariant(value);
const icon = getIcon(value);
return (
-
+
{value.toLowerCase()}
{icon}
diff --git a/ui.frontend/src/components/ExecutionsAbortButton.tsx b/ui.frontend/src/components/ExecutionsAbortButton.tsx
new file mode 100644
index 00000000..563fb652
--- /dev/null
+++ b/ui.frontend/src/components/ExecutionsAbortButton.tsx
@@ -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 = ({ 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({
+ 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 = () => (
+ <>
+
+ Confirmation
+
+
+
+ Are you sure you want to abort the selected executions? This action cannot be undone.
+
+
+
+
+
+ >
+ );
+
+ return (
+
+
+
+
+ );
+};
+
+export default ExecutionsAbortButton;
\ No newline at end of file
diff --git a/ui.frontend/src/components/ScriptExecutor.tsx b/ui.frontend/src/components/ScriptExecutor.tsx
index b13a5172..39fd0431 100644
--- a/ui.frontend/src/components/ScriptExecutor.tsx
+++ b/ui.frontend/src/components/ScriptExecutor.tsx
@@ -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 = () => {
@@ -70,7 +70,7 @@ const ScriptExecutor = () => {
-
+
-
- Be aware that aborting execution may leave data in an inconsistent state.
-
-
-
-
-
- Close
-
-
-
- )}
-
+
+ -
+
+
+
+
+
+
+
+ setAutoscroll((prev) => !prev)}>
+ Autoscroll
+
+
+
+
+
+
+
+
+
+ Help
+
+ {(close) => (
+
+ )}
+
+
+
-
-
-
-
-
-
+
+
+
+
);
};
-export default ConsolePage;
+export default ConsolePage;
\ No newline at end of file
diff --git a/ui.frontend/src/pages/DashboardPage.tsx b/ui.frontend/src/pages/DashboardPage.tsx
index 69ad989c..5b9e1c40 100644
--- a/ui.frontend/src/pages/DashboardPage.tsx
+++ b/ui.frontend/src/pages/DashboardPage.tsx
@@ -9,7 +9,7 @@ import Star from '@spectrum-icons/workflow/Star';
const DashboardPage = () => {
return (
{
-
+
-
+
- New Approach
- Experience a different way of using Groovy scripts. ACM ensures the instance is healthy before scripts decide when to run: once, periodically, or at an exact date and time. Execute scripts in parallel or sequentially, offering a complete change in paradigm. Unlike traditional methods, ACM allows scripts to run at specific moments offering unmatched flexibility and control.
+ All-in-one
+ ACM may be a good alternative to tools like APM, AECU, AEM Groovy Console, and AC Tool. Groovy language is ideal to manage all things in content including permissions. In tools like APM/AC Tool, there is a need to learn custom YAML syntax or languages/grammars. In ACM, you only need Groovy, which almost every Java developer knows! Enjoy a single, painless tool setup in AEM projects with no hooks and POM updates.
-
+
-
+
- All-in-one
- ACM may be a good alternative to tools like APM, AECU, AEM Groovy Console, and AC Tool. Groovy language is ideal to manage all things in content including permissions. In tools like APM/AC Tool, there is a need to learn custom YAML syntax or languages/grammars. In ACM, you only need Groovy, which almost every Java developer knows! Enjoy a single, painless tool setup in AEM projects with no hooks and POM updates.
+ New Approach
+ Experience a different way of using Groovy scripts. ACM ensures the instance is healthy before scripts decide when to run: once, periodically, or at an exact date and time. Execute scripts in parallel or sequentially, offering a complete change in paradigm. Unlike traditional methods, ACM allows scripts to run at specific moments offering unmatched flexibility and control.
diff --git a/ui.frontend/src/pages/ExecutionView.tsx b/ui.frontend/src/pages/ExecutionView.tsx
index 22150aa7..ab16730f 100644
--- a/ui.frontend/src/pages/ExecutionView.tsx
+++ b/ui.frontend/src/pages/ExecutionView.tsx
@@ -1,4 +1,4 @@
-import { Button, ButtonGroup, Content, Flex, IllustratedMessage, Item, LabeledValue, TabList, TabPanels, Tabs, Text, View, ProgressBar } from '@adobe/react-spectrum';
+import { Button, ButtonGroup, Content, Flex, IllustratedMessage, Item, LabeledValue, ProgressBar, Switch, TabList, TabPanels, Tabs, Text, View } from '@adobe/react-spectrum';
import { Field } from '@react-spectrum/label';
import { ToastQueue } from '@react-spectrum/toast';
import NotFound from '@spectrum-icons/illustrations/NotFound';
@@ -6,183 +6,182 @@ import Copy from '@spectrum-icons/workflow/Copy';
import FileCode from '@spectrum-icons/workflow/FileCode';
import History from '@spectrum-icons/workflow/History';
import Print from '@spectrum-icons/workflow/Print';
-import React, { useEffect, useState } from 'react';
+import { useContext, useEffect, useState } from 'react';
import { useParams } from 'react-router-dom';
-import ExecutableValue from '../components/ExecutableValue.tsx';
-import ExecutionStatusBadge from '../components/ExecutionStatusBadge.tsx';
-import ImmersiveEditor from '../components/ImmersiveEditor.tsx';
+import { AppContext } from '../AppContext.tsx';
+import ExecutableValue from '../components/ExecutableValue';
+import ExecutionStatusBadge from '../components/ExecutionStatusBadge';
+import ImmersiveEditor from '../components/ImmersiveEditor';
import { toastRequest } from '../utils/api';
-import { Execution, ExecutionOutput } from '../utils/api.types';
+import {Execution, ExecutionOutput, isExecutionPending} from '../utils/api.types';
import { useFormatter } from '../utils/hooks/formatter';
import { useNavigationTab } from '../utils/hooks/navigation';
+import ExecutionProgressBar from "../components/ExecutionProgressBar";
+import ExecutionCopyOutputButton from "../components/ExecutionCopyOutputButton";
+import ExecutionAbortButton from "../components/ExecutionAbortButton";
const toastTimeout = 3000;
const ExecutionView = () => {
- const [execution, setExecution] = useState(null);
- const [loading, setLoading] = useState(true);
- const executionId = decodeURIComponent(useParams<{ executionId: string }>().executionId as string);
- const formatter = useFormatter();
+ const [execution, setExecution] = useState(null);
+ const [autoscrollOutput, setAutoscrollOutput] = useState(true);
+ const executionId = decodeURIComponent(useParams<{ executionId: string }>().executionId as string);
+ const formatter = useFormatter();
+ const appState = useContext(AppContext);
+ const executionInQueue = !!appState?.queuedExecutions.find((execution) => execution.id === executionId);
+ const [loading, setLoading] = useState(executionInQueue);
- useEffect(() => {
- const fetchExecution = async () => {
- setLoading(true);
- try {
- const response = await toastRequest({
- method: 'GET',
- url: `/apps/acm/api/execution.json?id=${executionId}`,
- operation: `Execution loading`,
- positive: false,
- });
- setExecution(response.data.data.list[0]);
- } catch (error) {
- console.error(`Execution cannot be loaded '${executionId}':`, error);
- } finally {
- setLoading(false);
- }
- };
- fetchExecution();
- }, [executionId]);
+ useEffect(() => {
+ const fetchExecution = async () => {
+ try {
+ const response = await toastRequest({
+ method: 'GET',
+ url: `/apps/acm/api/execution.json?id=${executionId}`,
+ operation: `Execution loading`,
+ positive: false,
+ });
- const [selectedTab, handleTabChange] = useNavigationTab(executionId ? `/executions/view/${encodeURIComponent(executionId)}` : null, 'details');
+ setExecution(response.data.data.list[0]);
+ } catch (error) {
+ console.error(`Execution cannot be loaded '${executionId}':`, error);
+ } finally {
+ setLoading(false);
+ }
+ };
+ fetchExecution();
- if (loading) {
- return (
-
-
-
- );
- }
+ if (executionInQueue) {
+ const intervalId = setInterval(fetchExecution, 500);
- if (!execution) {
- return (
-
-
-
- Execution not found
-
-
- );
+ return () => clearInterval(intervalId);
}
+ }, [executionInQueue, executionId]);
- const executionOutput = ((execution.output ?? '') + '\n' + (execution.error ?? '')).trim();
+ const [selectedTab, handleTabChange] = useNavigationTab(executionId ? `/executions/view/${encodeURIComponent(executionId)}` : null, 'details');
- const onCopyExecutionOutput = () => {
- if (executionOutput) {
- navigator.clipboard
- .writeText(executionOutput)
- .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,
- });
- }
- };
-
- const onCopyExecutableCode = () => {
- navigator.clipboard
- .writeText(execution.executable.content)
- .then(() => {
- ToastQueue.info('Execution code copied to clipboard!', {
- timeout: toastTimeout,
- });
- })
- .catch(() => {
- ToastQueue.negative('Failed to copy execution code!', {
- timeout: toastTimeout,
- });
- });
- };
+ if (loading) {
+ return (
+
+
+
+ );
+ }
+ if (!execution) {
return (
-
-
-
- -
-
- Execution
-
- -
-
- Code
-
- -
-
- Output
-
-
-
- -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- -
-
-
-
-
-
-
- Copy
-
-
-
-
-
-
-
- -
-
-
-
-
-
-
- Copy
-
-
-
-
-
-
-
-
-
-
+
+
+
+ Execution not found
+
+
);
+ }
+
+ const executionOutput = ((execution.output ?? '') + '\n' + (execution.error ?? '')).trim();
+
+ const onCopyExecutableCode = () => {
+ navigator.clipboard
+ .writeText(execution.executable.content)
+ .then(() => {
+ ToastQueue.info('Execution code copied to clipboard!', {
+ timeout: toastTimeout,
+ });
+ })
+ .catch(() => {
+ ToastQueue.negative('Failed to copy execution code!', {
+ timeout: toastTimeout,
+ });
+ });
+ };
+
+ return (
+
+
+
+ -
+
+ Execution
+
+ -
+
+ Code
+
+ -
+
+ Output
+
+
+
+ -
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+ Copy
+
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
+ setAutoscrollOutput((prev) => !prev)}>
+ Autoscroll
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
};
-export default ExecutionView;
\ No newline at end of file
+export default ExecutionView;
diff --git a/ui.frontend/src/utils/api.types.ts b/ui.frontend/src/utils/api.types.ts
index 6e12ecf3..90695509 100644
--- a/ui.frontend/src/utils/api.types.ts
+++ b/ui.frontend/src/utils/api.types.ts
@@ -24,15 +24,15 @@ export enum ExecutionStatus {
SUCCEEDED = 'SUCCEEDED',
}
-export function isExecutionNegative(status: ExecutionStatus | null) {
+export function isExecutionNegative(status: ExecutionStatus | null | undefined) {
return status === ExecutionStatus.FAILED || status === ExecutionStatus.ABORTED;
}
-export function isExecutionPending(status: ExecutionStatus | null) {
+export function isExecutionPending(status: ExecutionStatus | null | undefined) {
return status === ExecutionStatus.QUEUED || status === ExecutionStatus.ACTIVE;
}
-export function isExecutionCompleted(status: ExecutionStatus | null) {
+export function isExecutionCompleted(status: ExecutionStatus | null | undefined) {
return status === ExecutionStatus.FAILED || status === ExecutionStatus.SUCCEEDED;
}
diff --git a/ui.frontend/src/utils/hooks/formatter.ts b/ui.frontend/src/utils/hooks/formatter.ts
index 5d06cea0..4c5063da 100644
--- a/ui.frontend/src/utils/hooks/formatter.ts
+++ b/ui.frontend/src/utils/hooks/formatter.ts
@@ -51,6 +51,19 @@ class Formatter {
return result;
}
+ public durationShort(milliseconds: number): string {
+ if (milliseconds < 1000) {
+ return `${milliseconds} ms`;
+ }
+ const duration = intervalToDuration({ start: 0, end: milliseconds });
+ const parts = [];
+ if (duration.days) parts.push(`${duration.days}d`);
+ if (duration.hours) parts.push(`${duration.hours}h`);
+ if (duration.minutes) parts.push(`${duration.minutes}m`);
+ if (duration.seconds) parts.push(`${duration.seconds}s`);
+ return parts.join(' ');
+ }
+
public durationExplained(milliseconds: number): string {
const duration = intervalToDuration({ start: 0, end: milliseconds });
let result = `${milliseconds} ms`;