Skip to content

Commit

Permalink
feat: cron job picker support in JobPlugin
Browse files Browse the repository at this point in the history
  • Loading branch information
collinlokken committed Oct 27, 2023
1 parent 7a989a9 commit d65c041
Show file tree
Hide file tree
Showing 6 changed files with 273 additions and 41 deletions.
11 changes: 11 additions & 0 deletions packages/dm-core-plugins/src/job/Colors.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { tokens } from '@equinor/eds-tokens'

const { colors } = tokens

export const backgroundColorDefault = colors.ui.background__default.rgba
export const backgroundColorLight = colors.ui.background__light.rgba
export const primary = colors.infographic.primary__moss_green_100.rgba
export const primaryGray = '#f7f7f7'
export const lightGray = '#f6f6f6'
export const linkPrimary = colors.interactive.primary__resting.rgba
export const linkHoverPrimary = colors.interactive.primary__hover.rgba
187 changes: 187 additions & 0 deletions packages/dm-core-plugins/src/job/CronJob.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
import React, { useEffect, useState } from 'react'
import { Button, Label } from '@equinor/eds-core-react'
import { StyledSelect } from './Input'
import DateRangePicker from './DateRangePicker'
import styled from 'styled-components'
import { TGenericObject, TSchedule } from '@development-framework/dm-core'

enum EInterval {
HOURLY = 'Hourly',
DAILY = 'Daily',
WEEKLY = 'Weekly',
MONTHLY = 'Monthly',
}

// Creates a list like ["00:00",...,"24:00"]
function generateSelectableTimes(): string[] {
const selectableTimes = []
for (let i = 0; i < 25; i++) {
selectableTimes.push(`${i}:00`)
}
return selectableTimes
}

const InputWrapper = styled.div`
flex-direction: row;
display: flex;
column-gap: 10px;
padding-top: 20px;
`

const ButtonWrapper = styled.div`
display: flex;
justify-content: center;
column-gap: 30px;
margin-top: 30px;
`

export function CreateReoccurringJob(props: {
close: () => void
removeJob: () => void
// TODO: Export TCronJob from dm-core
setCronJob: (job: TSchedule) => void
cronJob?: TGenericObject | undefined
}) {
const { close, removeJob, setCronJob, cronJob } = props
const [schedule, setSchedule] = useState<string | null>(null)
const [dateRange, setDateRange] = useState<{
startDate: string
endDate: string
}>({ startDate: '', endDate: '' })
const [interval, setInterval] = useState<EInterval>(EInterval.HOURLY)
const [hour, setHour] = useState<string>('23')
const [hourStep, setHourStep] = useState<string>('1')
const [minute, setMinute] = useState<string>('30')

const getLabel = () => {
if (interval === 'Weekly') {
return <small>Will run on sunday in every week</small>
} else if (interval === 'Monthly') {
return <small>Will run on the 1st on every month</small>
} else if (interval === 'Hourly') {
return <small>Will run every {hourStep} hour</small>
}
}

useEffect(() => {
const newMinute = minute
let newHour = hour
let dayOfMonth = '*'
const month = '*'
let dayOfWeek = '*'

switch (interval) {
case EInterval.WEEKLY:
dayOfMonth = '*'
dayOfWeek = '6'
break
case EInterval.MONTHLY:
dayOfMonth = '1'
break
case EInterval.HOURLY:
newHour = `*/${hourStep}`
}
setSchedule(`${newMinute} ${newHour} ${dayOfMonth} ${month} ${dayOfWeek}`)
}, [interval, hour, minute, hourStep])
return (
<div>
<div>
<div style={{ paddingBottom: '10px' }}>
{cronJob && Object.keys(cronJob).length > 0
? 'A job is already scheduled. You can update it here.'
: ''}
</div>
<DateRangePicker
setDateRange={(dateRange) => setDateRange(dateRange)}
value={dateRange}
/>
<InputWrapper>
<div style={{ display: 'flex', flexDirection: 'column' }}>
<Label label="Interval" />
<StyledSelect
onChange={(e) => {
//@ts-ignore
// TODO fix type
setInterval(e.target.value)
}}
value={interval}
>
{Object.entries(EInterval).map(([key, value]: any) => (
<option key={key} value={value}>
{value}
</option>
))}
</StyledSelect>
</div>
{interval !== EInterval.HOURLY && (
<div style={{ display: 'flex', flexDirection: 'column' }}>
<Label label="Time" />
<StyledSelect
onChange={(e) => {
const hourAndMinute = e.target.value
const [newHour, newMinute] = hourAndMinute.split(':')
setMinute(newMinute)
setHour(newHour)
}}
>
{generateSelectableTimes().map((value: string, index) => (
<option key={index} value={value}>
{value}
</option>
))}
</StyledSelect>
</div>
)}
{interval === EInterval.HOURLY && (
<div style={{ display: 'flex', flexDirection: 'column' }}>
<Label label="Hour step" />
<StyledSelect
onChange={(e) => {
setHourStep(e.target.value)
}}
>
{/*TODO fix type error*/}
{/*@ts-ignore*/}
{[...Array(12).keys()].map((value: number, index) => (
<option key={index} value={value + 1}>
{value + 1}
</option>
))}
</StyledSelect>
</div>
)}
</InputWrapper>
</div>
<div style={{ paddingTop: '10px', height: '20px' }}>{getLabel()}</div>
<ButtonWrapper>
<Button
disabled={cronJob && Object.keys(cronJob).length === 0}
color="danger"
variant="outlined"
onClick={() => {
removeJob()
close()
}}
>
Remove
</Button>
<Button
onClick={() => {
setCronJob({
//TODO fix type error
//@ts-ignore
cron: schedule,
//@ts-ignore
startDate: dateRange.startDate,
//@ts-ignore
endDate: dateRange.endDate,
})
close()
}}
>
Set
</Button>
</ButtonWrapper>
</div>
)
}
35 changes: 35 additions & 0 deletions packages/dm-core-plugins/src/job/DateRangePicker.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import React from 'react'
import { Label, TextField } from '@equinor/eds-core-react'

const DateRangePicker = (props: {
setDateRange: (dateRange: { startDate: string; endDate: string }) => void
value: any
}): JSX.Element => {
const { setDateRange, value } = props

return (
<div>
<Label label="Specify date range" />
<TextField
id="startDate"
defaultValue={value.startDate}
type="datetime-local"
onChange={(e: any) =>
setDateRange({ ...value, startDate: e.target.value })
}
label={'Valid from'}
/>
<TextField
id="endDate"
defaultValue={value.endDate}
type="datetime-local"
onChange={(e: any) =>
setDateRange({ ...value, endDate: e.target.value })
}
label={'Valid to'}
/>
</div>
)
}

export default DateRangePicker
15 changes: 15 additions & 0 deletions packages/dm-core-plugins/src/job/Input.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { primaryGray } from './Colors'
import styled from 'styled-components'

// This should be used instead of the EDS SingleSelect as that one is broken
export const StyledSelect = styled.select`
position: relative;
font-size: medium;
padding: 8px 16px;
border: 0;
border-bottom: 2px solid #dbdbdb;
cursor: pointer;
width: fit-content;
background-color: ${primaryGray};
min-width: 150px;
`
10 changes: 8 additions & 2 deletions packages/dm-core-plugins/src/job/JobControlButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ export const JobControlButton = (props: {
jobStatus: JobStatus
createJob: () => void
asCronJob: boolean
disabled: boolean
}) => {
const { jobStatus, createJob, asCronJob } = props
const { jobStatus, createJob, asCronJob, disabled } = props
const [hovering, setHovering] = useState(false)
const buttonRef: MutableRefObject<HTMLButtonElement | undefined> = useRef()
buttonRef.current?.addEventListener('mouseenter', () => setHovering(true))
Expand Down Expand Up @@ -41,7 +42,12 @@ export const JobControlButton = (props: {
}

return (
<Button ref={buttonRef} onClick={createJob} style={{ width: '7rem' }}>
<Button
ref={buttonRef}
onClick={createJob}
style={{ width: '7rem' }}
disabled={disabled}
>
{jobStatus === JobStatus.Running && !hovering ? (
<CircularProgress size={16} variant="indeterminate" />
) : (
Expand Down
56 changes: 17 additions & 39 deletions packages/dm-core-plugins/src/job/JobPlugin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,13 @@ import React, {
useState,
ChangeEvent,
} from 'react'
import {
Button,
Card,
Icon,
Switch,
TextField,
Typography,
} from '@equinor/eds-core-react'
import { Button, Card, Icon, Switch, Typography } from '@equinor/eds-core-react'
import { JobControlButton } from './JobControlButton'
import { refresh } from '@equinor/eds-icons'
import styled from 'styled-components'
import { AxiosError } from 'axios'
import { AuthContext } from 'react-oauth2-code-pkce'
import { CreateReoccurringJob } from './CronJob'

const JobButtonWrapper = styled.div`
display: flex;
Expand Down Expand Up @@ -58,6 +52,7 @@ export const JobPlugin = (props: IUIPlugin & { config: JobPluginConfig }) => {
idReference,
}: { config: JobPluginConfig; idReference: string } = props
const DmssApi = useDMSS()
const defaultCron = '0 8 * * *'

const jobTargetAddress = useMemo((): string => {
if ((config.jobTargetAddress.addressScope ?? 'local') !== 'local') {
Expand Down Expand Up @@ -86,16 +81,18 @@ export const JobPlugin = (props: IUIPlugin & { config: JobPluginConfig }) => {
const [asCronJob, setAsCronJob] = useState<boolean>(false)
const [jobSchedule, setJobSchedule] = useState<TSchedule>({
type: EBlueprint.CRON_JOB,
cron: '',
cron: defaultCron,
startDate: '',
endDate: '',
})
const canSubmit =
!asCronJob || Boolean(jobSchedule.startDate && jobSchedule.endDate)
const {
document: jobDocument,
isLoading,
error: jobEntityError,
updateDocument,
} = useDocument(jobTargetAddress, 0, false)
} = useDocument<TJob>(jobTargetAddress, 0, false)

const { start, error, fetchResult, fetchStatusAndLogs, logs, status } =
useJob(jobTargetAddress)
Expand Down Expand Up @@ -145,6 +142,7 @@ export const JobPlugin = (props: IUIPlugin & { config: JobPluginConfig }) => {
useEffect(() => {
if (!jobDocument) return
if (Object.keys(jobDocument).length) setJobExists(true)
if (jobDocument.schedule) setJobSchedule(jobDocument.schedule)
}, [isLoading, jobEntityError, jobDocument])

return (
Expand All @@ -158,41 +156,21 @@ export const JobPlugin = (props: IUIPlugin & { config: JobPluginConfig }) => {
checked={asCronJob}
/>
{asCronJob && (
<>
<TextField
id="cronString"
onChange={(e: ChangeEvent<HTMLInputElement>) =>
setJobSchedule({ ...jobSchedule, cron: e.target.value })
}
placeholder="String with cron syntax, e.g. '30 12 * * 3'"
defaultValue={'0 8 * * *'}
label={'Cron string'}
/>
<TextField
id="startDate"
defaultValue={jobSchedule.startDate}
type="datetime-local"
onChange={(e: any) =>
setJobSchedule({ ...jobSchedule, startDate: e.target.value })
}
label={'Valid from'}
/>
<TextField
id="endDate"
defaultValue={jobSchedule.endDate}
type="datetime-local"
onChange={(e: any) =>
setJobSchedule({ ...jobSchedule, endDate: e.target.value })
}
label={'Valid to'}
/>
</>
<CreateReoccurringJob
close={() => console.log('close')}
removeJob={() => console.log('remove')}
setCronJob={(e: TSchedule) => {
console.log(e)
setJobSchedule(e)
}}
/>
)}
<JobButtonWrapper>
<JobControlButton
jobStatus={status}
createJob={createAndStartJob}
asCronJob={asCronJob}
disabled={!canSubmit}
/>
{status === JobStatus.Running && (
<Button
Expand Down

0 comments on commit d65c041

Please sign in to comment.