Skip to content

Commit 4a6b540

Browse files
Merge pull request #66 from wttech/spa-settings
SPA settings
2 parents 89c9280 + 6cfc42b commit 4a6b540

File tree

14 files changed

+146
-64
lines changed

14 files changed

+146
-64
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package com.wttech.aem.acm.core.gui;
2+
3+
import java.io.Serializable;
4+
import org.osgi.service.component.annotations.Activate;
5+
import org.osgi.service.component.annotations.Component;
6+
import org.osgi.service.component.annotations.Modified;
7+
import org.osgi.service.metatype.annotations.AttributeDefinition;
8+
import org.osgi.service.metatype.annotations.Designate;
9+
import org.osgi.service.metatype.annotations.ObjectClassDefinition;
10+
11+
@Component(service = SpaSettings.class, immediate = true)
12+
@Designate(ocd = SpaSettings.Config.class)
13+
public class SpaSettings implements Serializable {
14+
15+
private long appStateInterval;
16+
17+
private long executionPollInterval;
18+
19+
@Activate
20+
@Modified
21+
protected void activate(Config config) {
22+
this.appStateInterval = config.appStateInterval();
23+
this.executionPollInterval = config.executionPollInterval();
24+
}
25+
26+
public long getAppStateInterval() {
27+
return appStateInterval;
28+
}
29+
30+
public long getExecutionPollInterval() {
31+
return executionPollInterval;
32+
}
33+
34+
@ObjectClassDefinition(name = "AEM Content Manager - SPA Settings")
35+
public @interface Config {
36+
37+
@AttributeDefinition(
38+
name = "Application State Interval",
39+
description = "Interval in milliseconds to check application state.")
40+
long appStateInterval() default 3000;
41+
42+
@AttributeDefinition(
43+
name = "Execution Poll Interval",
44+
description = "Interval in milliseconds to poll execution status.")
45+
long executionPollInterval() default 1000;
46+
}
47+
}

core/src/main/java/com/wttech/aem/acm/core/servlet/StateServlet.java

+5-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
import com.wttech.aem.acm.core.code.ExecutionQueue;
88
import com.wttech.aem.acm.core.code.ExecutionSummary;
9+
import com.wttech.aem.acm.core.gui.SpaSettings;
910
import com.wttech.aem.acm.core.instance.HealthChecker;
1011
import com.wttech.aem.acm.core.instance.HealthStatus;
1112
import com.wttech.aem.acm.core.osgi.InstanceInfo;
@@ -45,13 +46,16 @@ public class StateServlet extends SlingAllMethodsServlet {
4546
@Reference
4647
private HealthChecker healthChecker;
4748

49+
@Reference
50+
private SpaSettings spaSettings;
51+
4852
@Override
4953
protected void doGet(SlingHttpServletRequest request, SlingHttpServletResponse response) throws IOException {
5054
try {
5155
HealthStatus healthStatus = healthChecker.checkStatus();
5256
List<ExecutionSummary> queuedExecutions =
5357
executionQueue.findAllSummaries().collect(Collectors.toList());
54-
State state = new State(healthStatus, instanceInfo.getInstanceSettings(), queuedExecutions);
58+
State state = new State(spaSettings, healthStatus, instanceInfo.getInstanceSettings(), queuedExecutions);
5559

5660
respondJson(response, ok("State read successfully", state));
5761
} catch (Exception e) {

core/src/main/java/com/wttech/aem/acm/core/state/State.java

+12-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.wttech.aem.acm.core.state;
22

33
import com.wttech.aem.acm.core.code.ExecutionSummary;
4+
import com.wttech.aem.acm.core.gui.SpaSettings;
45
import com.wttech.aem.acm.core.instance.HealthStatus;
56
import com.wttech.aem.acm.core.instance.InstanceSettings;
67
import java.io.Serializable;
@@ -14,8 +15,14 @@ public class State implements Serializable {
1415

1516
private final List<ExecutionSummary> queuedExecutions;
1617

18+
private final SpaSettings spaSettings;
19+
1720
public State(
18-
HealthStatus healthStatus, InstanceSettings instanceSettings, List<ExecutionSummary> queuedExecutions) {
21+
SpaSettings spaSettings,
22+
HealthStatus healthStatus,
23+
InstanceSettings instanceSettings,
24+
List<ExecutionSummary> queuedExecutions) {
25+
this.spaSettings = spaSettings;
1926
this.healthStatus = healthStatus;
2027
this.instanceSettings = instanceSettings;
2128
this.queuedExecutions = queuedExecutions;
@@ -32,4 +39,8 @@ public InstanceSettings getInstanceSettings() {
3239
public List<ExecutionSummary> getQueuedExecutions() {
3340
return queuedExecutions;
3441
}
42+
43+
public SpaSettings getSpaSettings() {
44+
return spaSettings;
45+
}
3546
}

ui.frontend/src/App.tsx

+8-6
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,14 @@ import Header from './components/Header';
99
import router from './router';
1010
import { apiRequest } from './utils/api';
1111
import { InstanceRole, InstanceType, State } from './utils/api.types';
12-
13-
const AppStateFetchTimeout = 2500;
14-
const AppStateFetchInterval = 3000;
12+
import { intervalToTimeout } from './utils/spectrum.ts';
1513

1614
function App() {
1715
const [state, setState] = useState<State>({
16+
spaSettings: {
17+
appStateInterval: 3000,
18+
executionPollInterval: 1000,
19+
},
1820
healthStatus: {
1921
healthy: true,
2022
issues: [],
@@ -42,7 +44,7 @@ function App() {
4244
operation: 'Fetch application state',
4345
url: '/apps/acm/api/state.json',
4446
method: 'get',
45-
timeout: AppStateFetchTimeout,
47+
timeout: intervalToTimeout(state.spaSettings.appStateInterval),
4648
quiet: true,
4749
});
4850
setState(response.data.data);
@@ -54,9 +56,9 @@ function App() {
5456
};
5557

5658
fetchState();
57-
const intervalId = setInterval(fetchState, AppStateFetchInterval);
59+
const intervalId = setInterval(fetchState, state.spaSettings.appStateInterval);
5860
return () => clearInterval(intervalId);
59-
}, []);
61+
}, [state.spaSettings.appStateInterval]);
6062

6163
return (
6264
<Provider

ui.frontend/src/components/ExecutionAbortButton.tsx

+3-1
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,15 @@ import { pollExecutionPending } from '../hooks/execution';
66
import { apiRequest } from '../utils/api';
77
import { Execution, ExecutionStatus, isExecutionPending, QueueOutput } from '../utils/api.types';
88
import { ToastTimeoutQuick } from '../utils/spectrum.ts';
9+
import {useAppState} from "../hooks/app.ts";
910

1011
interface ExecutionAbortButtonProps {
1112
execution: Execution | null;
1213
onComplete: (execution: Execution | null) => void;
1314
}
1415

1516
const ExecutionAbortButton: React.FC<ExecutionAbortButtonProps> = ({ execution, onComplete }) => {
17+
const appState = useAppState();
1618
const [isAborting, setIsAborting] = useState(false);
1719

1820
const onAbort = async () => {
@@ -28,7 +30,7 @@ const ExecutionAbortButton: React.FC<ExecutionAbortButtonProps> = ({ execution,
2830
method: 'delete',
2931
});
3032

31-
const queuedExecution = await pollExecutionPending(execution.id);
33+
const queuedExecution = await pollExecutionPending(execution.id, appState.spaSettings.executionPollInterval);
3234
if (queuedExecution?.status === ExecutionStatus.ABORTED) {
3335
ToastQueue.positive('Code execution aborted successfully!', {
3436
timeout: ToastTimeoutQuick,

ui.frontend/src/components/HealthChecker.tsx

+4-9
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,13 @@ import Close from '@spectrum-icons/workflow/Close';
55
import Help from '@spectrum-icons/workflow/Help';
66
import Replay from '@spectrum-icons/workflow/Replay';
77
import Settings from '@spectrum-icons/workflow/Settings';
8-
import { useContext } from 'react';
9-
import { AppContext } from '../AppContext';
8+
import { useAppState } from '../hooks/app.ts';
109
import { HealthIssueSeverity, InstanceType } from '../utils/api.types';
1110
import { isProduction } from '../utils/node.ts';
1211

1312
const HealthChecker = () => {
14-
const context = useContext(AppContext);
15-
const healthIssues = context?.healthStatus.issues || [];
13+
const appState = useAppState();
14+
const healthIssues = appState.healthStatus.issues;
1615
const prefix = isProduction() ? '' : 'http://localhost:4502';
1716

1817
const getSeverityVariant = (severity: HealthIssueSeverity): 'negative' | 'yellow' | 'neutral' => {
@@ -38,11 +37,7 @@ const HealthChecker = () => {
3837
<View>
3938
<Flex direction="row" justifyContent="space-between" alignItems="center">
4039
<Flex flex="1" alignItems="center">
41-
<Button
42-
variant="negative"
43-
isDisabled={!context || context.instanceSettings.type === InstanceType.CLOUD_CONTAINER}
44-
onPress={() => window.open(`${prefix}/system/console/configMgr/com.wttech.aem.acm.core.instance.HealthChecker`, '_blank')}
45-
>
40+
<Button variant="negative" isDisabled={appState.instanceSettings.type === InstanceType.CLOUD_CONTAINER} onPress={() => window.open(`${prefix}/system/console/configMgr/com.wttech.aem.acm.core.instance.HealthChecker`, '_blank')}>
4641
<Settings />
4742
<Text>Configure</Text>
4843
</Button>

ui.frontend/src/components/ScriptExecutor.tsx

+5-5
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@ import Code from '@spectrum-icons/workflow/Code';
1010
import Help from '@spectrum-icons/workflow/Help';
1111
import Replay from '@spectrum-icons/workflow/Replay';
1212
import Settings from '@spectrum-icons/workflow/Settings';
13-
import { useContext, useState } from 'react';
13+
import { useState } from 'react';
1414
import { useNavigate } from 'react-router-dom';
15-
import { AppContext } from '../AppContext';
15+
import { useAppState } from '../hooks/app.ts';
1616
import { InstanceType } from '../utils/api.types.ts';
1717
import { isProduction } from '../utils/node';
1818
import DateExplained from './DateExplained';
@@ -24,8 +24,8 @@ const ScriptExecutor = () => {
2424
const prefix = isProduction() ? '' : 'http://localhost:4502';
2525
const navigate = useNavigate();
2626

27-
const context = useContext(AppContext);
28-
const executions = context?.queuedExecutions || [];
27+
const appState = useAppState();
28+
const executions = appState.queuedExecutions;
2929

3030
const [selectedKeys, setSelectedKeys] = useState<Selection>(new Set<Key>());
3131
const selectedIds = (selectedKeys: Selection): string[] => {
@@ -51,7 +51,7 @@ const ScriptExecutor = () => {
5151
<ButtonGroup>
5252
<ExecutionsAbortButton selectedKeys={selectedIds(selectedKeys)} />
5353
<MenuTrigger>
54-
<Button variant="negative" isDisabled={!context || context.instanceSettings.type === InstanceType.CLOUD_CONTAINER}>
54+
<Button variant="negative" isDisabled={appState.instanceSettings.type === InstanceType.CLOUD_CONTAINER}>
5555
<Settings />
5656
<Text>Configure</Text>
5757
</Button>

ui.frontend/src/components/ScriptList.tsx

+17-19
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@ import { Button, ButtonGroup, Cell, Column, Content, ContextualHelp, Flex, Headi
22
import { Key, Selection } from '@react-types/shared';
33
import NotFound from '@spectrum-icons/illustrations/NotFound';
44
import Magnify from '@spectrum-icons/workflow/Magnify';
5-
import React, { useCallback, useContext, useEffect, useState } from 'react';
5+
import React, { useCallback, useEffect, useState } from 'react';
66
import { useNavigate } from 'react-router-dom';
7-
import { AppContext } from '../AppContext.tsx';
7+
import { useAppState } from '../hooks/app.ts';
88
import { toastRequest } from '../utils/api';
99
import { InstanceRole, isExecutionNegative, ScriptOutput, ScriptType } from '../utils/api.types';
1010
import DateExplained from './DateExplained.tsx';
@@ -20,7 +20,7 @@ type ScriptListProps = {
2020
};
2121

2222
const ScriptList: React.FC<ScriptListProps> = ({ type }) => {
23-
const appContext = useContext(AppContext);
23+
const appState = useAppState();
2424
const navigate = useNavigate();
2525

2626
const [scripts, setScripts] = useState<ScriptOutput | null>(null);
@@ -76,27 +76,25 @@ const ScriptList: React.FC<ScriptListProps> = ({ type }) => {
7676
{type === ScriptType.ENABLED || type === ScriptType.DISABLED ? (
7777
<>
7878
<ScriptToggleButton type={type} selectedKeys={selectedIds(selectedKeys)} onToggle={loadScripts} />
79-
{appContext && appContext.instanceSettings.role == InstanceRole.AUTHOR && <ScriptSynchronizeButton selectedKeys={selectedIds(selectedKeys)} onSync={loadScripts} />}
79+
{appState.instanceSettings.role == InstanceRole.AUTHOR && <ScriptSynchronizeButton selectedKeys={selectedIds(selectedKeys)} onSync={loadScripts} />}
8080
</>
8181
) : null}
8282
</ButtonGroup>
8383
</Flex>
8484
<Flex flex="1" justifyContent="center" alignItems="center">
85-
{appContext && (
86-
<StatusLight variant={appContext.healthStatus.healthy ? 'positive' : 'negative'}>
87-
{appContext.healthStatus.healthy ? (
88-
<Text>Executor active</Text>
89-
) : (
90-
<>
91-
<Text>Executor paused</Text>
92-
<Text>&nbsp;&mdash;&nbsp;</Text>
93-
<Link isQuiet onPress={() => navigate('/maintenance?tab=health-checker')}>
94-
See health issues
95-
</Link>
96-
</>
97-
)}
98-
</StatusLight>
99-
)}
85+
<StatusLight variant={appState.healthStatus.healthy ? 'positive' : 'negative'}>
86+
{appState.healthStatus.healthy ? (
87+
<Text>Executor active</Text>
88+
) : (
89+
<>
90+
<Text>Executor paused</Text>
91+
<Text>&nbsp;&mdash;&nbsp;</Text>
92+
<Link isQuiet onPress={() => navigate('/maintenance?tab=health-checker')}>
93+
See health issues
94+
</Link>
95+
</>
96+
)}
97+
</StatusLight>
10098
</Flex>
10199
<Flex flex="1" justifyContent="end" alignItems="center">
102100
{type === ScriptType.MANUAL ? <ScriptsManualHelpButton /> : type === ScriptType.EXTENSION ? <ScriptsExtensionHelpButton /> : <ScriptsAutomaticHelpButton />}

ui.frontend/src/hooks/app.ts

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { useContext } from 'react';
2+
import { AppContext } from '../AppContext';
3+
import { State } from '../utils/api.types.ts';
4+
5+
export function useAppState(): State {
6+
const state = useContext(AppContext);
7+
if (!state) {
8+
throw new Error('Application state is not available!');
9+
}
10+
return state;
11+
}

ui.frontend/src/hooks/execution.ts

+10-11
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,12 @@ import { useState } from 'react';
33
import { useInterval } from 'react-use';
44
import { apiRequest } from '../utils/api';
55
import { Execution, ExecutionStatus, isExecutionPending, QueueOutput } from '../utils/api.types';
6-
import { ToastTimeoutQuick } from '../utils/spectrum.ts';
6+
import { intervalToTimeout, ToastTimeoutQuick } from '../utils/spectrum';
7+
import { useAppState } from './app';
78
import { useFormatter } from './formatter';
89

9-
const ExecutionPollInterval = 1000;
10-
const ExecutionPollTimeout = 900;
11-
12-
export const useExecutionPolling = (executionId: string | undefined | null) => {
10+
export const useExecutionPolling = (executionId: string | undefined | null, pollInterval: number) => {
11+
const appState = useAppState();
1312
const [execution, setExecution] = useState<Execution | null>(null);
1413
const [executing, setExecuting] = useState<boolean>(!!executionId);
1514
const [loading, setLoading] = useState<boolean>(true);
@@ -22,7 +21,7 @@ export const useExecutionPolling = (executionId: string | undefined | null) => {
2221
operation: 'Code execution state',
2322
url: `/apps/acm/api/queue-code.json?executionId=${executionId}`,
2423
method: 'get',
25-
timeout: ExecutionPollTimeout,
24+
timeout: intervalToTimeout(pollInterval),
2625
});
2726
const queuedExecution = response.data.data.executions.find((e: Execution) => e.id === executionId)!;
2827
setExecution(queuedExecution);
@@ -34,7 +33,7 @@ export const useExecutionPolling = (executionId: string | undefined | null) => {
3433
setExecuting(false);
3534
setWasPending(false);
3635

37-
const recentlyCompleted = formatter.isRecent(queuedExecution.endDate, 2 * ExecutionPollInterval);
36+
const recentlyCompleted = formatter.isRecent(queuedExecution.endDate, 2 * pollInterval);
3837
if (recentlyCompleted || wasPending) {
3938
if (queuedExecution.status === ExecutionStatus.FAILED) {
4039
ToastQueue.negative('Code execution failed!', { timeout: ToastTimeoutQuick });
@@ -57,24 +56,24 @@ export const useExecutionPolling = (executionId: string | undefined | null) => {
5756
pollExecutionState(executionId);
5857
}
5958
},
60-
executing && executionId ? ExecutionPollInterval : null,
59+
executing && executionId ? appState.spaSettings.executionPollInterval : null,
6160
);
6261

6362
return { execution, setExecution, executing, setExecuting, loading };
6463
};
6564

66-
export const pollExecutionPending = async (executionId: string): Promise<Execution> => {
65+
export const pollExecutionPending = async (executionId: string, pollInterval: number): Promise<Execution> => {
6766
let queuedExecution: Execution | null = null;
6867

6968
while (queuedExecution === null || isExecutionPending(queuedExecution.status)) {
7069
const response = await apiRequest<QueueOutput>({
7170
operation: 'Code execution state',
7271
url: `/apps/acm/api/queue-code.json?executionId=${executionId}`,
7372
method: 'get',
74-
timeout: ExecutionPollTimeout,
73+
timeout: intervalToTimeout(pollInterval),
7574
});
7675
queuedExecution = response.data.data.executions[0]!;
77-
await new Promise((resolve) => setTimeout(resolve, ExecutionPollInterval));
76+
await new Promise((resolve) => setTimeout(resolve, pollInterval));
7877
}
7978

8079
return queuedExecution;

0 commit comments

Comments
 (0)