Skip to content

Commit

Permalink
[Feature] Add ability to set a slug for a deployment schedule (#2920)
Browse files Browse the repository at this point in the history
* Add ability to set a slug for a deployment schedule

* Add validation for schedule slug
  • Loading branch information
desertaxle authored Feb 6, 2025
1 parent 27c793f commit 10476ae
Show file tree
Hide file tree
Showing 19 changed files with 96 additions and 11 deletions.
1 change: 1 addition & 0 deletions src/components/DeploymentDetails.vue
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@
schedule: updatedSchedule.schedule,
jobVariables: updatedSchedule.jobVariables,
parameters: updatedSchedule.parameters,
slug: updatedSchedule.slug,
})
showToast(localization.success.updateDeploymentSchedule, 'success')
emit('update')
Expand Down
13 changes: 11 additions & 2 deletions src/components/DeploymentScheduleCard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<p-list-item class="deployment-schedule-card">
<p-tooltip :text="deploymentSchedule.schedule.toString({ verbose: true })">
<div class="deployment-schedule-card__content">
{{ deploymentSchedule.schedule.toString({ verbose: false }) }}
{{ scheduleDisplay }}
</div>
</p-tooltip>
<div class="deployment-schedule-card__action">
Expand All @@ -23,15 +23,24 @@
<script lang="ts" setup>
import { DeploymentScheduleMenu, DeploymentScheduleToggle } from '@/components'
import { Deployment, DeploymentSchedule } from '@/models'
import { computed } from 'vue'
defineProps<{
const props = defineProps<{
deployment: Deployment,
deploymentSchedule: DeploymentSchedule,
}>()
defineEmits<{
(event: 'update'): void,
}>()
const scheduleDisplay = computed(() => {
if (props.deploymentSchedule.slug) {
return props.deploymentSchedule.slug
}
return props.deploymentSchedule.schedule.toString({ verbose: false })
})
</script>

<style>
Expand Down
4 changes: 4 additions & 0 deletions src/components/DeploymentScheduleMenu.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,12 @@
<ScheduleFormModal
ref="scheduleFormModalRef"
v-bind="schedule"
:slug="schedule.slug"
:deployment-parameters="deployment.parameters"
:schedule-parameters="schedule.parameters"
:parameter-open-api-schema="deployment.parameterOpenApiSchema"
:deployment="deployment"
:deployment-schedule-id="schedule.id"
@submit="updateSchedule"
/>

Expand Down Expand Up @@ -77,6 +80,7 @@
schedule: updatedSchedule.schedule,
jobVariables: updatedSchedule.jobVariables,
parameters: updatedSchedule.parameters,
slug: updatedSchedule.slug,
},
)
showToast(localization.success.updateDeploymentSchedule, 'success')
Expand Down
2 changes: 2 additions & 0 deletions src/components/DeploymentSchedulesFieldset.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@

<ScheduleFormModal
v-if="deployment.can.update"
:slug="null"
:active="null"
:schedule="null"
:job-variables="{}"
:deployment-parameters="deployment.parameters"
:schedule-parameters="{}"
:parameter-open-api-schema="deployment.parameterOpenApiSchema"
:deployment="deployment"
@submit="createSchedule"
>
<template #default="{ open }">
Expand Down
1 change: 1 addition & 0 deletions src/components/ScheduleFieldset.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
<template v-if="!readonly">
<div class="schedule-fieldset__buttons">
<ScheduleFormModal
:slug="null"
:active="null"
:schedule="internalValue"
:job-variables="{}"
Expand Down
51 changes: 45 additions & 6 deletions src/components/ScheduleFormModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@
<slot :open="open" :close="close" />

<p-modal v-model:showModal="showModal" :title="schedule ? 'Edit schedule' : 'Add schedule'" @update:show-modal="resetIfFalse">
<p-label label="Slug (Optional)" :message="slugError" :state="slugState">
<p-text-input v-model="internalSlug" placeholder="Enter a unique identifier for this schedule" :state="slugState" />
</p-label>

<p-label label="Schedule type">
<p-button-group v-model="scheduleForm" :options="scheduleFormOptions" small />
</p-label>
Expand Down Expand Up @@ -57,10 +61,11 @@
import CronScheduleForm from '@/components/CronScheduleForm.vue'
import FlowRunJobVariableOverridesLabeledInput from '@/components/FlowRunJobVariableOverridesLabeledInput.vue'
import IntervalScheduleForm from '@/components/IntervalScheduleForm.vue'
import { useCan, useShowModal } from '@/compositions'
import { useCan, useShowModal, useWorkspaceApi } from '@/compositions'
import { localization } from '@/localization'
import {
CronSchedule,
Deployment,
DeploymentScheduleCompatible,
IntervalSchedule,
Schedule,
Expand All @@ -71,11 +76,15 @@
} from '@/models'
import { SchemaInputV2, SchemaV2, SchemaValuesV2 } from '@/schemas'
import { useSchemaValidation } from '@/schemas/compositions/useSchemaValidation'
import { isEmptyObject, omit, stringify } from '@/utilities'
import { isEmptyObject, isEmptyString, isNull, isSlug, omit, stringify, timeout } from '@/utilities'
import { ButtonGroupOption } from '@prefecthq/prefect-design'
import { useValidationObserver } from '@prefecthq/vue-compositions'
import {
ValidationRule,
useValidation,
useValidationObserver
} from '@prefecthq/vue-compositions'
import merge from 'lodash.merge'
import { computed, reactive, ref, watch } from 'vue'
import { computed, ref, watch } from 'vue'
defineOptions({
inheritAttrs: false,
Expand All @@ -90,19 +99,47 @@
defineExpose({ publicOpen })
const props = defineProps<{
slug: string | null,
active: boolean | null,
schedule: Schedule | null,
jobVariables: Record<string, unknown> | undefined,
deploymentParameters: SchemaValuesV2,
scheduleParameters?: SchemaValuesV2 | null,
parameterOpenApiSchema: SchemaV2,
deployment?: Deployment,
deploymentScheduleId?: string,
}>()
const can = useCan()
const internalSlug = ref<string | null>(props.slug)
const internalActive = ref<boolean>(props.active ?? true)
const { validate } = useValidationObserver()
const slugIsUniqueForDeployment: ValidationRule<string | null> = async (value) => {
if (isNull(value) || isEmptyString(value)) {
return true
}
if (!props.deployment) {
return true
}
return props.deployment.schedules.some(
(schedule) => schedule.slug !== null &&
props.deploymentScheduleId !== schedule.id &&
schedule.slug === value,
)
? localization.error.scheduleSlugAlreadyExists
: true
}
const { state: slugState, error: slugError } = useValidation(
internalSlug,
'Slug',
[isSlug, slugIsUniqueForDeployment],
)
const internalJobVariables = ref<string | undefined>(
props.jobVariables ? stringify(props.jobVariables) : undefined,
)
Expand All @@ -112,7 +149,7 @@
}>()
// Parameters-related refs and compositions
const internalParameters = ref<SchemaValuesV2>(props.scheduleParameters ?? { })
const internalParameters = ref<SchemaValuesV2>(props.scheduleParameters ?? {})
const selectedProperties = ref<string[]>(Object.keys(internalParameters.value))
const properties = computed(
() => props.parameterOpenApiSchema.properties ?? {},
Expand All @@ -133,8 +170,9 @@
// Reset values to the initial values when the modal is opened
watch(showModal, () => {
if (showModal.value) {
internalParameters.value = props.scheduleParameters ?? { }
internalParameters.value = props.scheduleParameters ?? {}
selectedProperties.value = Object.keys(internalParameters.value)
internalSlug.value = props.slug ?? null
}
})
Expand Down Expand Up @@ -188,6 +226,7 @@
schedule,
jobVariables,
parameters,
slug: internalSlug.value ?? null,
}
emit('submit', deploymentSchedule)
Expand Down
2 changes: 2 additions & 0 deletions src/localization/locale/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,13 @@ export const en = {
resumeFlowRun: 'Failed to resume flow run',
retryRun: 'Failed to retry flow run',
scheduleFlowRun: 'Failed to schedule flow run',
scheduleSlugAlreadyExists: 'A schedule with this slug already exists for this deployment',
arrayValueTooLong: (property: string, max: number) => `${property} must have fewer than ${max} items`,
stringValueTooLong: (property: string, max: number) => `${property} must be less than or equal to ${max} characters`,
numberValueTooLarge: (property: string, max: number) => `${property} must be less than or equal to ${max}`,
valueTooLarge: (property: string, max: number) => `${property} must be less than or equal to ${max}`,
mustBeSnakeCase: (property: string) => `${property} may only contain letters, numbers, and underscores and may not begin or end with an underscore`,
mustBeSlug: (property: string) => `${property} may only contain letters, numbers, dashes, and underscores and may not begin or end with a dash or underscore`,
submitNotification: 'Failed to submit notification',
suspendFlowRun: 'Failed to suspend flow run',
updateBlock: 'Failed to update block',
Expand Down
1 change: 1 addition & 0 deletions src/maps/deploymentSchedule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export const mapDeploymentScheduleResponseToDeploymentSchedule: MapFunction<Depl
id: source.id,
created: this.map('string', source.created, 'Date'),
updated: this.map('string', source.updated, 'Date'),
slug: source.slug ?? null,
active: source.active,
schedule: this.map('ScheduleResponse', source.schedule, 'Schedule'),
jobVariables: source.job_variables ?? {},
Expand Down
1 change: 1 addition & 0 deletions src/maps/deploymentScheduleCreate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { MapFunction } from '@/services/Mapper'

export const mapDeploymentScheduleCreateToDeploymentScheduleCreateRequest: MapFunction<DeploymentScheduleCreate, DeploymentScheduleCreateRequest> = function(source) {
return {
slug: source.slug ?? null,
active: source.active,
schedule: this.map('Schedule', source.schedule, 'ScheduleRequest'),
job_variables: source.jobVariables,
Expand Down
1 change: 1 addition & 0 deletions src/maps/deploymentScheduleUpdate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { MapFunction } from '@/services/Mapper'

export const mapDeploymentScheduleUpdateToDeploymentScheduleUpdateRequest: MapFunction<DeploymentScheduleUpdate, DeploymentScheduleUpdateRequest> = function(source) {
return {
slug: source.slug ?? null,
active: source.active,
schedule: this.map('Schedule', source.schedule, 'ScheduleRequest'),
job_variables: source.jobVariables,
Expand Down
1 change: 1 addition & 0 deletions src/mocks/deploymentSchedule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export const randomDeploymentSchedules: MockFunction<DeploymentSchedule[], [Part
id: this.create('id'),
created: this.create('date'),
updated: this.create('date'),
slug: random() > 0.25 ? this.create('string') : null,
active: random() > 0.25,
schedule: this.create('schedule'),
jobVariables: {},
Expand Down
3 changes: 3 additions & 0 deletions src/models/DeploymentSchedule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export interface IDeploymentSchedule {
id: string,
created: Date,
updated: Date,
slug: string | null,
active: boolean,
schedule: Schedule,
jobVariables: Record<string, unknown>,
Expand All @@ -15,6 +16,7 @@ export class DeploymentSchedule implements IDeploymentSchedule {
public readonly id: string
public created: Date
public updated: Date
public slug: string | null
public active: boolean
public schedule: Schedule
public jobVariables: Record<string, unknown>
Expand All @@ -24,6 +26,7 @@ export class DeploymentSchedule implements IDeploymentSchedule {
this.id = deploymentSchedule.id
this.created = deploymentSchedule.created
this.updated = deploymentSchedule.updated
this.slug = deploymentSchedule.slug
this.active = deploymentSchedule.active
this.schedule = deploymentSchedule.schedule
this.jobVariables = deploymentSchedule.jobVariables
Expand Down
1 change: 1 addition & 0 deletions src/models/DeploymentScheduleCompatible.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ export type DeploymentScheduleCompatible = {
schedule: Schedule | null,
jobVariables: Record<string, unknown> | undefined,
parameters?: SchemaValuesV2,
slug?: string | null,
}
1 change: 1 addition & 0 deletions src/models/DeploymentScheduleCreate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { Schedule } from '@/models/Schedule'
import type { SchemaValuesV2 } from '@/schemas'

export type DeploymentScheduleCreate = {
slug?: string | null,
active: boolean,
schedule: Schedule,
jobVariables?: Record<string, unknown>,
Expand Down
1 change: 1 addition & 0 deletions src/models/DeploymentScheduleUpdate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Schedule } from '@/models/Schedule'
import { SchemaValuesV2 } from '@/schemas'

export type DeploymentScheduleUpdate = {
slug?: string | null,
active?: boolean,
schedule?: Schedule,
jobVariables?: Record<string, unknown>,
Expand Down
1 change: 1 addition & 0 deletions src/models/api/DeploymentScheduleCreateRequest.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { ScheduleResponse } from '@/models'
import { SchemaValuesV2 } from '@/schemas'
export type DeploymentScheduleCreateRequest = {
slug?: string | null,
active: boolean,
schedule: ScheduleResponse,
job_variables?: Record<string, unknown>,
Expand Down
1 change: 1 addition & 0 deletions src/models/api/DeploymentScheduleResponse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export type DeploymentScheduleResponse = {
id: string,
created: DateString,
updated: DateString,
slug: string | null,
active: boolean,
schedule: ScheduleResponse,
job_variables?: Record<string, unknown> | null,
Expand Down
1 change: 1 addition & 0 deletions src/models/api/DeploymentScheduleUpdateRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { ScheduleResponse } from '@/models'
import { SchemaValuesV2 } from '@/schemas'

export type DeploymentScheduleUpdateRequest = {
slug?: string | null,
active?: boolean,
schedule?: ScheduleResponse,
job_variables?: Record<string, unknown>,
Expand Down
20 changes: 17 additions & 3 deletions src/utilities/validation.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { isDateAfter, isDateAfterOrEqual, isDateBefore, isDateBeforeOrEqual, isNotNullish } from '@prefecthq/prefect-design'
import { ValidationRule } from '@prefecthq/vue-compositions'
import { localization } from '@/localization'
import { isEmptyArray } from '@/utilities/arrays'
import { isDate, isInvalidDate, formatDate, formatDateTimeNumeric } from '@/utilities/dates'
import { formatDate, formatDateTimeNumeric, isDate, isInvalidDate } from '@/utilities/dates'
import { isEmptyString, isString, isValidEmailAddress } from '@/utilities/strings'
import { isNullish } from '@/utilities/variables'
import { isDateAfter, isDateAfterOrEqual, isDateBefore, isDateBeforeOrEqual, isNotNullish } from '@prefecthq/prefect-design'
import { ValidationRule } from '@prefecthq/vue-compositions'

export type ValidationMethod = (value: unknown) => true | string | Promise<true | string>
export type ValidationMethodFactory = (property: string) => ValidationMethod
Expand Down Expand Up @@ -276,3 +276,17 @@ const SNAKE_CASE_REGEX = /^[a-z0-9]+(_+[a-z0-9]+)*$/
export const isSnakeCase: ValidationRule<unknown> = (value, field) => {
return isNotNullish(value) && isString(value) && SNAKE_CASE_REGEX.test(value) || localization.error.mustBeSnakeCase(field)
}

const SLUG_REGEX = /^[a-z0-9]+([_-]+[a-z0-9]+)*$/

export const isSlug: ValidationRule<unknown> = (value, field) => {
if (isNullish(value) || isEmptyString(value)) {
return true
}

if (typeof value === 'string' && SLUG_REGEX.test(value)) {
return true
}

return localization.error.mustBeSlug(field)
}

0 comments on commit 10476ae

Please sign in to comment.