Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
17 changes: 17 additions & 0 deletions core/src/main/java/dev/vml/es/acm/core/gui/SpaSettings.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import org.osgi.service.metatype.annotations.AttributeDefinition;
import org.osgi.service.metatype.annotations.Designate;
import org.osgi.service.metatype.annotations.ObjectClassDefinition;
import org.osgi.service.metatype.annotations.Option;

@Component(service = SpaSettings.class, immediate = true)
@Designate(ocd = SpaSettings.Config.class)
Expand All @@ -18,6 +19,8 @@ public class SpaSettings implements Serializable {

private int executionCodeOutputChunkSize;

private String executionReviewOutputsPolicy;

private long scriptStatsLimit;

@Activate
Expand All @@ -26,6 +29,7 @@ protected void activate(Config config) {
this.appStateInterval = config.appStateInterval();
this.executionPollInterval = config.executionPollInterval();
this.executionCodeOutputChunkSize = config.executionCodeOutputChunkSize();
this.executionReviewOutputsPolicy = config.executionReviewOutputsPolicy();
this.scriptStatsLimit = config.scriptStatsLimit();
}

Expand All @@ -45,6 +49,10 @@ public long getScriptStatsLimit() {
return scriptStatsLimit;
}

public String getExecutionReviewOutputsPolicy() {
return executionReviewOutputsPolicy;
}

@ObjectClassDefinition(name = "AEM Content Manager - SPA Settings")
public @interface Config {

Expand All @@ -61,6 +69,15 @@ public long getScriptStatsLimit() {
@AttributeDefinition(name = "Execution Code Output Chunk Size", description = "In bytes. Default is 2 MB.")
int executionCodeOutputChunkSize() default 2 * 1024 * 1024;

@AttributeDefinition(
name = "Execution Review Outputs Policy",
description =
"Controls if the review outputs dialog opens automatically after a script execution succeeds with generated outputs. "
+ "Manual: user opens it explicitly via the 'Review' button. Auto: it opens by itself once outputs are ready. "
+ "Applies to script executions only; console executions always stay manual.",
options = {@Option(label = "Manual", value = "manual"), @Option(label = "Auto", value = "auto")})
String executionReviewOutputsPolicy() default "auto";

@AttributeDefinition(
name = "Script Stats Limit",
description =
Expand Down
14 changes: 12 additions & 2 deletions ui.frontend/src/components/ExecutionReviewOutputsButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import Help from '@spectrum-icons/workflow/Help';
import Info from '@spectrum-icons/workflow/Info';
import Preview from '@spectrum-icons/workflow/Preview';
import Print from '@spectrum-icons/workflow/Print';
import React, { useState } from 'react';
import React, { useEffect, useRef, useState } from 'react';
import { Execution } from '../types/execution.ts';
import { FileOutput, Output, OutputNames, TextOutput } from '../types/output.ts';
import { ToastTimeoutQuick } from '../utils/spectrum.ts';
Expand All @@ -18,16 +18,26 @@ import Markdown from './Markdown.tsx';

interface ExecutionReviewOutputsButtonProps extends Omit<React.ComponentProps<typeof Button>, 'onPress'> {
execution: Execution;
autoOpen?: boolean;
}

const ExecutionReviewOutputsButton: React.FC<ExecutionReviewOutputsButtonProps> = ({ execution, ...buttonProps }) => {
const ExecutionReviewOutputsButton: React.FC<ExecutionReviewOutputsButtonProps> = ({ execution, autoOpen = false, ...buttonProps }) => {
const [dialogOpen, setDialogOpen] = useState(false);
const autoOpenedRef = useRef(false);

const outputs = execution.outputs || {};
const outputValues = Object.values(outputs);
const outputFiles = outputValues.filter((output) => output.type === 'FILE') as FileOutput[];
const outputTexts = outputValues.filter((output) => output.type === 'TEXT') as TextOutput[];

// Opens the dialog once per execution when it just completed with outputs to review
useEffect(() => {
if (autoOpen && outputValues.length > 0 && !autoOpenedRef.current) {
autoOpenedRef.current = true;
setDialogOpen(true);
}
}, [autoOpen, outputValues.length]);
Comment thread
krystian-panek-vmltech marked this conversation as resolved.

const handleOpenDialog = () => {
setDialogOpen(true);
};
Expand Down
29 changes: 27 additions & 2 deletions ui.frontend/src/hooks/execution.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { ToastQueue } from '@react-spectrum/toast';
import { useState } from 'react';
import { useEffect, useRef, useState } from 'react';
import { useInterval } from 'react-use';
import { isExecutableScript } from '../types/executable';
import { Execution, ExecutionStatus, isExecutionPending } from '../types/execution';
import { QueueOutput } from '../types/main';
import { apiRequest } from '../utils/api';
Expand All @@ -14,6 +15,7 @@ export const useExecutionPolling = (executionId: string | undefined | null, poll
const [executing, setExecuting] = useState<boolean>(!!executionId);
const [loading, setLoading] = useState<boolean>(true);
const [wasPending, setWasPending] = useState<boolean>(false);
const [justCompleted, setJustCompleted] = useState<boolean>(false);
Comment thread
krystian-panek-vmltech marked this conversation as resolved.
const formatter = useFormatter();

const pollExecutionState = async (executionId: string) => {
Expand All @@ -37,6 +39,7 @@ export const useExecutionPolling = (executionId: string | undefined | null, poll

const recentlyCompleted = formatter.isRecent(queuedExecution.endDate, 2 * pollInterval);
if (recentlyCompleted || wasPending) {
setJustCompleted(true);
if (queuedExecution.status === ExecutionStatus.FAILED) {
ToastQueue.negative('Code execution failed!', { timeout: ToastTimeoutQuick });
} else if (queuedExecution.status === ExecutionStatus.SKIPPED) {
Expand All @@ -63,7 +66,29 @@ export const useExecutionPolling = (executionId: string | undefined | null, poll
executing && executionId ? appState.spaSettings.executionPollInterval : null,
);

return { execution, setExecution, executing, setExecuting, loading };
return { execution, setExecution, executing, setExecuting, loading, justCompleted };
};

// Signals a script execution just succeeded with a 'auto' review policy, exactly once per execution id
export const useExecutionReviewAutoOpen = (execution: Execution | null, justCompleted: boolean): boolean => {
const appState = useAppState();
const autoOpenedIdRef = useRef<string | null>(null);

const autoOpen =
!!execution &&
appState.spaSettings.executionReviewOutputsPolicy === 'auto' &&
isExecutableScript(execution.executable.id) &&
execution.status === ExecutionStatus.SUCCEEDED &&
justCompleted &&
autoOpenedIdRef.current !== execution.id;

useEffect(() => {
if (autoOpen && execution) {
autoOpenedIdRef.current = execution.id;
}
}, [autoOpen, execution]);

return autoOpen;
};

export const pollExecutionPending = async (executionId: string, pollInterval: number): Promise<Execution> => {
Expand Down
9 changes: 5 additions & 4 deletions ui.frontend/src/pages/ExecutionView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,11 @@ import Toggle from '../components/Toggle.tsx';
import ThreeColumnBar from '../components/ThreeColumnBar';
import UserInfo from '../components/UserInfo';
import { useAppState } from '../hooks/app.ts';
import { useExecutionPolling } from '../hooks/execution';
import { useExecutionPolling, useExecutionReviewAutoOpen } from '../hooks/execution';
import { useFormatter } from '../hooks/formatter';
import { useNavigationTab } from '../hooks/navigation';
import { isExecutableConsole, isExecutableScript } from '../types/executable.ts';
import { isExecutionPending } from '../types/execution.ts';
import { ExecutionStatus, isExecutionPending } from '../types/execution.ts';
import { GROOVY_LANGUAGE_ID } from '../utils/monaco/groovy.ts';
import { LOG_LANGUAGE_ID } from '../utils/monaco/log.ts';
import { ToastTimeoutQuick } from '../utils/spectrum.ts';
Expand All @@ -37,9 +37,10 @@ const ExecutionView = () => {
const { executionId } = useParams<{ executionId: string }>();
const formatter = useFormatter();
const [autoscrollOutput, setAutoscrollOutput] = useState<boolean>(true);
const { execution, setExecution, loading } = useExecutionPolling(executionId, appState.spaSettings.executionPollInterval);
const { execution, setExecution, loading, justCompleted } = useExecutionPolling(executionId, appState.spaSettings.executionPollInterval);
const [selectedTab, handleTabChange] = useNavigationTab('details');
const navigate = useNavigate();
const autoOpenReview = useExecutionReviewAutoOpen(execution, justCompleted);

if (loading) {
return (
Expand Down Expand Up @@ -170,7 +171,7 @@ const ExecutionView = () => {
<ExecutionAbortButton execution={execution} onComplete={setExecution} />
</Toggle>
<Toggle when={!isExecutionPending(execution.status)}>
<ExecutionReviewOutputsButton variant="cta" execution={execution} />
<ExecutionReviewOutputsButton variant="cta" execution={execution} autoOpen={autoOpenReview} />
</Toggle>
<ExecutionCopyOutputButton output={executionOutput} />
</ButtonGroup>
Expand Down
2 changes: 2 additions & 0 deletions ui.frontend/src/types/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ export const StateDefault: State = {
appStateInterval: 3000,
executionPollInterval: 1400,
scriptStatsLimit: 20,
executionReviewOutputsPolicy: 'manual',
},
healthStatus: {
healthy: true,
Expand Down Expand Up @@ -88,6 +89,7 @@ export type SpaSettings = {
appStateInterval: number;
executionPollInterval: number;
scriptStatsLimit: number;
executionReviewOutputsPolicy: 'manual' | 'auto';
};

export type InstanceSettings = {
Expand Down
Loading