From 67f053b77438f2101102c52d1bbc7e05e8bac936 Mon Sep 17 00:00:00 2001 From: Stig Ofstad Date: Wed, 22 Nov 2023 11:01:24 +0100 Subject: [PATCH] feat(job): create recurring job --- .../signals_simple/case.recipe.json | 20 +- example/docker-compose.yaml | 1 + example/job_handlers/signal-app/__init__.py | 4 +- .../marmo-ui/containers/views/SignalPlot.tsx | 2 - packages/dm-core-plugins/src/job/CronJob.tsx | 86 ++++---- .../src/job/JobControlButton.tsx | 22 ++- .../dm-core-plugins/src/job/JobLogsDialog.tsx | 3 - .../dm-core-plugins/src/job/JobPlugin.tsx | 184 ++++++------------ .../src/job/templateEntities.ts | 75 +++++++ packages/dm-core/src/Enums.ts | 3 + .../dm-core/src/components/EntityView.tsx | 3 - packages/dm-core/src/hooks/useJob.tsx | 17 +- packages/dm-core/src/types.ts | 7 +- 13 files changed, 215 insertions(+), 212 deletions(-) create mode 100644 packages/dm-core-plugins/src/job/templateEntities.ts diff --git a/example/app/data/DemoDataSource/recipes/apps/signal_app/recipe_links/signals_simple/case.recipe.json b/example/app/data/DemoDataSource/recipes/apps/signal_app/recipe_links/signals_simple/case.recipe.json index 1e3794e3a..9778303d3 100644 --- a/example/app/data/DemoDataSource/recipes/apps/signal_app/recipe_links/signals_simple/case.recipe.json +++ b/example/app/data/DemoDataSource/recipes/apps/signal_app/recipe_links/signals_simple/case.recipe.json @@ -5,21 +5,14 @@ "name": "Dashboard", "type": "CORE:UiRecipe", "description": "ESS Plot", + "showRefreshButton": true, "plugin": "@development-framework/dm-core-plugins/grid", "config": { "type": "PLUGINS:dm-core-plugins/grid/GridPluginConfig", "size": { "type": "PLUGINS:dm-core-plugins/grid/GridSize", "columns": 2, - "rows": 2, - "rowSizes": [ - "4fr", - "1fr" - ], - "columnSizes": [ - "1fr", - "2fr" - ] + "rows": 4 }, "showItemBorders": false, "items": [ @@ -33,7 +26,7 @@ "type": "PLUGINS:dm-core-plugins/grid/GridArea", "rowStart": 1, "columnStart": 1, - "rowEnd": 1, + "rowEnd": 3, "columnEnd": 1 } }, @@ -48,7 +41,7 @@ "type": "PLUGINS:dm-core-plugins/grid/GridArea", "rowStart": 1, "columnStart": 2, - "rowEnd": 1, + "rowEnd": 3, "columnEnd": 2 } }, @@ -60,9 +53,9 @@ }, "gridArea": { "type": "PLUGINS:dm-core-plugins/grid/GridArea", - "rowStart": 2, + "rowStart": 4, "columnStart": 1, - "rowEnd": 2, + "rowEnd": 4, "columnEnd": 2 } } @@ -112,7 +105,6 @@ "plugin": "@development-framework/dm-core-plugins/job/single_job", "config": { "type": "PLUGINS:dm-core-plugins/job/JobConfig", - "recurring": false, "jobTargetAddress": { "type": "PLUGINS:dm-core-plugins/job/TargetAddress", "targetAddress": ".job", diff --git a/example/docker-compose.yaml b/example/docker-compose.yaml index 4bd05f1bd..5b1bc5fb3 100644 --- a/example/docker-compose.yaml +++ b/example/docker-compose.yaml @@ -8,6 +8,7 @@ services: environment: AUTH_ENABLED: 0 ENVIRONMENT: local +# RESET_DATA_SOURCE: off MONGO_INITDB_ROOT_USERNAME: root MONGO_INITDB_ROOT_PASSWORD: xd7wCEhEx4kszsecYFfC SECRET_KEY: sg9aeUM5i1JO4gNN8fQadokJa3_gXQMLBjSGGYcfscs= # Don't reuse this in production... diff --git a/example/job_handlers/signal-app/__init__.py b/example/job_handlers/signal-app/__init__.py index 1ee2ccabe..447eaad7b 100644 --- a/example/job_handlers/signal-app/__init__.py +++ b/example/job_handlers/signal-app/__init__.py @@ -39,11 +39,11 @@ def start(self) -> str: logger.info("Job started") self.job.status = JobStatus.RUNNING logger.info("after sleep") - application_input_reference = self.job.entity['applicationInput']['address'] + application_input_reference = self.job.application_input['address'] input_entity = self._get_by_id(application_input_reference) signal_length = int(input_entity["duration"] / input_entity["timeStep"]) new_signal_value = [random.randint(-50, 50) for value in range(signal_length)] - signal_reference: str = self.job.entity['outputTarget'] + signal_reference: str = self.job.outputTarget signal_entity = self._get_by_id(signal_reference) signal_entity["value"] = new_signal_value self._update(f"{signal_reference}", {"data": signal_entity}) diff --git a/example/src/plugins/marmo-ui/containers/views/SignalPlot.tsx b/example/src/plugins/marmo-ui/containers/views/SignalPlot.tsx index f653eacda..cbdb0fa7d 100644 --- a/example/src/plugins/marmo-ui/containers/views/SignalPlot.tsx +++ b/example/src/plugins/marmo-ui/containers/views/SignalPlot.tsx @@ -11,8 +11,6 @@ import Plot from 'react-plotly.js' const ESSPlotPlugin = (props: { document: TGenericObject }) => { const { document } = props - console.log(document) - const yData = document.value const xData: number[] = [] diff --git a/packages/dm-core-plugins/src/job/CronJob.tsx b/packages/dm-core-plugins/src/job/CronJob.tsx index 9ce5f23b9..f157d572f 100644 --- a/packages/dm-core-plugins/src/job/CronJob.tsx +++ b/packages/dm-core-plugins/src/job/CronJob.tsx @@ -1,8 +1,8 @@ import React, { useEffect, useState } from 'react' -import { Autocomplete, Button } from '@equinor/eds-core-react' +import { Autocomplete } from '@equinor/eds-core-react' import DateRangePicker from './DateRangePicker' import styled from 'styled-components' -import { EBlueprint, TSchedule } from '@development-framework/dm-core' +import { TSchedule } from '@development-framework/dm-core' enum EInterval { HOURLY = 'Hourly', @@ -27,26 +27,22 @@ const InputWrapper = styled.div` padding-top: 1rem; ` -const ButtonWrapper = styled.div` - display: flex; - justify-content: flex-end; - gap: 0.5rem; - margin-top: 1rem; -` +const getIntervall = ({ cron }: { cron: string }): EInterval => { + const [, hour, dayOfMonth, , dayOfWeek] = cron.split(' ') + if (hour.includes('/')) return EInterval.HOURLY + if (dayOfMonth !== '*') return EInterval.MONTHLY + if (dayOfWeek !== '*') return EInterval.WEEKLY + if (dayOfWeek == '*') return EInterval.DAILY + return EInterval.DAILY +} -export function CreateRecurringJob(props: { - close: () => void - removeJob: () => void +export function ConfigureSchedule(props: { + isRegistered: boolean // TODO: Export TCronJob from dm-core - setCronJob: (job: TSchedule) => void - cronJob?: TSchedule | undefined + setSchedule: (s: TSchedule) => void + schedule: TSchedule }) { - const { close, removeJob, setCronJob, cronJob } = props - const [schedule, setSchedule] = useState(null) - const [dateRange, setDateRange] = useState<{ - startDate: string - endDate: string - }>({ startDate: cronJob?.startDate ?? '', endDate: cronJob?.endDate ?? '' }) + const { setSchedule, schedule, isRegistered } = props const [interval, setInterval] = useState(EInterval.HOURLY) const [hour, setHour] = useState('23') const [hourStep, setHourStep] = useState('1') @@ -88,7 +84,10 @@ export function CreateRecurringJob(props: { case EInterval.HOURLY: newHour = hourStep ? `*/${hourStep}` : '*' } - setSchedule(`${newMinute} ${newHour} ${dayOfMonth} ${month} ${dayOfWeek}`) + setSchedule({ + ...schedule, + cron: `${newMinute} ${newHour} ${dayOfMonth} ${month} ${dayOfWeek}`, + }) }, [interval, hour, minute, hourStep]) return ( @@ -100,19 +99,26 @@ export function CreateRecurringJob(props: { }} >
-
- {cronJob && Object.keys(cronJob).length > 0 - ? 'A job is already scheduled. You can update it here.' - : ''} -
+ {isRegistered && ( +
+ A job is already scheduled. You can update it here. +
+ )} setDateRange(dateRange)} - value={dateRange} + setDateRange={(dateRange) => + setSchedule({ + ...schedule, + startDate: dateRange.startDate, + endDate: dateRange.endDate, + }) + } + value={{ startDate: schedule.startDate, endDate: schedule.endDate }} /> { const chosenIntervalType = Object.entries(EInterval) .filter((l) => l.length > 0 && l[1] == label) @@ -143,32 +149,6 @@ export function CreateRecurringJob(props: {
{getLabel()}
- - - - ) } diff --git a/packages/dm-core-plugins/src/job/JobControlButton.tsx b/packages/dm-core-plugins/src/job/JobControlButton.tsx index a6e5a65a0..16e01382e 100644 --- a/packages/dm-core-plugins/src/job/JobControlButton.tsx +++ b/packages/dm-core-plugins/src/job/JobControlButton.tsx @@ -6,10 +6,12 @@ import React, { MutableRefObject, useRef, useState } from 'react' export const JobControlButton = (props: { jobStatus: JobStatus createJob: () => void + remove: () => void asCronJob: boolean disabled: boolean + exists: boolean }) => { - const { jobStatus, createJob, asCronJob, disabled } = props + const { jobStatus, createJob, asCronJob, disabled, exists, remove } = props const [hovering, setHovering] = useState(false) const buttonRef: MutableRefObject = useRef() buttonRef.current?.addEventListener('mouseenter', () => setHovering(true)) @@ -17,12 +19,13 @@ export const JobControlButton = (props: { const buttonText = () => { switch (jobStatus) { - case 'running': + case JobStatus.Running: return hovering ? 'Stop' : 'Running' - case 'completed': - return 'Re-run' - case 'failed': + case JobStatus.Failed: + case JobStatus.Completed: return 'Re-run' + case JobStatus.Registered: + return 'Remove' default: return asCronJob ? 'Schedule' : 'Run' } @@ -30,11 +33,13 @@ export const JobControlButton = (props: { const buttonIcon = () => { switch (jobStatus) { + case JobStatus.Removed: case JobStatus.Failed: return case JobStatus.Running: + case JobStatus.Registered: return - case JobStatus.Removed || JobStatus.NotStarted: + case JobStatus.NotStarted: return default: return @@ -44,7 +49,10 @@ export const JobControlButton = (props: { return ( )} - + {config.showGetResult && ( + + )} {status} - {showLogs && ( - -
- - - {result && ( - - )} -
-
- )} ({ + type: EBlueprint.CRON_JOB, + cron: '0 8 * * *', + startDate: new Date().toISOString().slice(0, 16), + endDate: new Date().toISOString().slice(0, 16), + runs: [], +}) +export const getJobTemplate = ({ + config, + username, + idReference, +}: any): TJob => { + let jobInputAddress: string = idReference + config.jobInput.targetAddress + + if ((config.jobInput.addressScope ?? 'local') !== 'local') { + jobInputAddress = config.jobInput.targetAddress + } else if (['self', '.'].includes(config.jobInput.targetAddress)) { + jobInputAddress = idReference + } + + const jobEntity: TJob = { + type: EBlueprint.JOB, + label: config?.label, + status: JobStatus.NotStarted, + triggeredBy: username, + applicationInput: { + type: EBlueprint.REFERENCE, + referenceType: 'link', + address: jobInputAddress, + }, + runner: config?.runner, + } + + if (config?.outputTarget) jobEntity.outputTarget = config.outputTarget + return jobEntity +} + +export const getRecurringJobTemplate = ({ + config, + username, + idReference, + schedule, +}: any): TRecurringJob => { + return { + type: EBlueprint.RECURRING_JOB, + label: config?.label, + status: JobStatus.NotStarted, + triggeredBy: username, + applicationInput: getJobTemplate({ config, username, idReference }), + runner: { + type: EBlueprint.RECURRING_JOB_HANDLER, + }, + schedule: schedule, + } +} + +export const getNewJobDocument = ({ + config, + username, + idReference, + asCronJob, + schedule, +}: any): TJob | TRecurringJob => { + if (asCronJob) + return getRecurringJobTemplate({ config, username, idReference, schedule }) + return getJobTemplate({ config, username, idReference }) +} diff --git a/packages/dm-core/src/Enums.ts b/packages/dm-core/src/Enums.ts index be0c6703f..99937e605 100644 --- a/packages/dm-core/src/Enums.ts +++ b/packages/dm-core/src/Enums.ts @@ -16,6 +16,9 @@ export enum EBlueprint { ENUM = 'dmss://system/SIMOS/Enum', /** Path to the Job blueprint */ JOB = 'dmss://WorkflowDS/Blueprints/Job', + RECURRING_JOB = 'dmss://WorkflowDS/Blueprints/RecurringJob', + RECURRING_JOB_HANDLER = 'dmss://WorkflowDS/Blueprints/RecurringJobHandler', + CRON_JOB = 'dmss://WorkflowDS/Blueprints/CronJob', REFERENCE = 'dmss://system/SIMOS/Reference', FILE = 'dmss://system/SIMOS/File', diff --git a/packages/dm-core/src/components/EntityView.tsx b/packages/dm-core/src/components/EntityView.tsx index a415486c6..1be95369b 100644 --- a/packages/dm-core/src/components/EntityView.tsx +++ b/packages/dm-core/src/components/EntityView.tsx @@ -28,9 +28,7 @@ function UiPlugin({ onSubmit, onOpen, config, - key, }: IUIPlugin & { - key: number getPlugin: (name: string) => (p: IUIPlugin) => React.ReactElement pluginName: string }) { @@ -42,7 +40,6 @@ function UiPlugin({ onSubmit={onSubmit} onOpen={onOpen} config={config || {}} - key={key} /> ) } diff --git a/packages/dm-core/src/hooks/useJob.tsx b/packages/dm-core/src/hooks/useJob.tsx index 9741ce677..6d68a5f4b 100644 --- a/packages/dm-core/src/hooks/useJob.tsx +++ b/packages/dm-core/src/hooks/useJob.tsx @@ -20,6 +20,7 @@ interface IUseJob { logs: string isLoading: boolean error: ErrorResponse | undefined + exists: boolean } /** @@ -92,9 +93,11 @@ export function useJob(entityId?: string, jobId?: string): IUseJob { // @ts-ignore .then((response: AxiosResponse) => { if (response.data?.uid) { - // The job must be started before it has an UID - setHookJobId(response.data.uid) setStatus(response.data.status) + if (response.data.status !== JobStatus.Removed) { + // The job must be started before it has an UID + setHookJobId(response.data.uid) + } } }) .catch((error: AxiosError) => { @@ -108,7 +111,7 @@ export function useJob(entityId?: string, jobId?: string): IUseJob { // The interval is deregistered if the status of the job is not "Running" useEffect(() => { if (!hookJobId) return - statusIntervalId = setInterval(fetchStatusAndLogs, 2000) + statusIntervalId = setInterval(fetchStatusAndLogs, 5000) return () => clearInterval(statusIntervalId) }, [hookJobId]) @@ -161,7 +164,11 @@ export function useJob(entityId?: string, jobId?: string): IUseJob { return dmJobApi .jobStatus({ jobUid: hookJobId }) .then((response: AxiosResponse) => { - setLogs(response.data.log ?? '') + setLogs( + response.data.log ?? + response.data.message ?? + 'No logs or status returned from job handler' + ) if (response.data.status !== status) setStatus(response.data.status) if ( ([JobStatus.Failed, JobStatus.Completed] as JobStatus[]).includes( @@ -175,6 +182,7 @@ export function useJob(entityId?: string, jobId?: string): IUseJob { }) .catch((error: AxiosError) => { setError(error.response?.data) + clearInterval(statusIntervalId) return null }) .finally(() => setIsLoading(false)) @@ -232,5 +240,6 @@ export function useJob(entityId?: string, jobId?: string): IUseJob { logs, isLoading, error, + exists: !!hookJobId, } } diff --git a/packages/dm-core/src/types.ts b/packages/dm-core/src/types.ts index e33c9df51..e8439eb5e 100644 --- a/packages/dm-core/src/types.ts +++ b/packages/dm-core/src/types.ts @@ -100,7 +100,11 @@ export type TJob = { ended?: string outputTarget?: string referenceTarget?: string - schedule?: TSchedule +} + +export type TRecurringJob = TJob & { + applicationInput: TJob + schedule: TSchedule } export type TSchedule = { @@ -108,6 +112,7 @@ export type TSchedule = { cron: string startDate: string endDate: string + runs: TJob[] } export type TJobWithRunner = TJob & {