Skip to content

Commit

Permalink
Create and manage automations in the UI (#13342)
Browse files Browse the repository at this point in the history
  • Loading branch information
pleek91 committed May 15, 2024
1 parent 060a223 commit 320d588
Show file tree
Hide file tree
Showing 27 changed files with 1,018 additions and 0 deletions.
1 change: 1 addition & 0 deletions ui/.vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"editor.insertSpaces": true,
"editor.tabSize": 2,
"cSpell.words": [
"automations",
"camelcase",
"Combobox",
"kubernetes",
Expand Down
28 changes: 28 additions & 0 deletions ui/src/components/AutomationActionTypeSelect.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<template>
<p-select v-model="type" :options="options" class="automation-action-type-select" />
</template>

<script lang="ts" setup>
import { SelectOption } from '@prefecthq/prefect-design'
import { AutomationActionType, automationActionTypeLabels, automationActionTypes } from '@prefecthq/prefect-ui-library'
import { computed } from 'vue'
const type = defineModel<AutomationActionType | null>('type', { required: true })
const options = computed<SelectOption[]>(() => {
const allOptions = automationActionTypes.map(type => {
const label = automationActionTypeLabels[type]
return {
label,
value: type,
}
})
if (type.value === 'do-nothing') {
return allOptions
}
return allOptions.filter(option => option.value !== 'do-nothing')
})
</script>
78 changes: 78 additions & 0 deletions ui/src/components/AutomationCard.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
<template>
<p-card class="automation-card">
<p-content>
<p-content secondary>
<div class="automation-card__header">
<p-link class="automation-card__name" :to="routes.automation(automation.id)">
{{ automation.name }}
</p-link>
<div class="automation-card__header-actions">
<AutomationToggle :automation="automation" @update="emit('update')" />
<AutomationMenu :automation="automation" @delete="emit('update')" />
</div>
</div>
<template v-if="automation.description">
<p class="automation-card__description">
{{ automation.description }}
</p>
</template>
</p-content>

<p-content secondary>
<span class="automation-card__label">Trigger</span>
<AutomationTriggerDescription :trigger="automation.trigger" />
</p-content>

<p-content secondary>
<span class="automation-card__label">{{ toPluralString('Action', automation.actions.length) }}</span>
<template v-for="action in automation.actions" :key="action.id">
<p-card><AutomationActionDescription :action="action" /></p-card>
</template>
</p-content>
</p-content>
</p-card>
</template>

<script lang="ts" setup>
import { toPluralString } from '@prefecthq/prefect-design'
import { AutomationMenu, AutomationToggle, AutomationTriggerDescription, AutomationActionDescription, useWorkspaceRoutes } from '@prefecthq/prefect-ui-library'
import { Automation } from '@/types/automation'
defineProps<{
automation: Automation,
}>()
const emit = defineEmits<{
(event: 'update'): void,
}>()
const routes = useWorkspaceRoutes()
</script>

<style>
.automation-card__header { @apply
flex
gap-2
items-center
justify-between
}
.automation-card__header-actions { @apply
flex
gap-2
items-center
}
.automation-card__name { @apply
text-lg
}
.automation-card__description { @apply
text-sm
}
.automation-card__label { @apply
font-medium
mr-2
}
</style>
59 changes: 59 additions & 0 deletions ui/src/components/AutomationTriggerJsonInput.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<template>
<p-message info>
Custom triggers allow advanced configuration of the conditions on which a trigger executes its actions.

<template #action>
<DocumentationButton :to="localization.docs.automationTriggers" small />
</template>
</p-message>

<p-label label="Trigger" :state="state" :message="error">
<JsonInput v-model="model" class="automation-trigger-json-input__json-input" show-format-button :state="state" />
</p-label>
</template>

<script setup lang="ts">
import { DocumentationButton, JsonInput, isEmptyArray, isEmptyString, isInvalidDate, isNullish, localization } from '@prefecthq/prefect-ui-library'
import { ValidationRule, useValidation } from '@prefecthq/vue-compositions'
import { mapper } from '@/services/mapper'
const model = defineModel<string>({ required: true })
const isMappableAutomationTriggerJson: ValidationRule<string> = (value) => {
try {
const json = JSON.parse(value)
mapper.map('AutomationTriggerResponse', json, 'AutomationTrigger')
} catch (error) {
return false
}
return true
}
const isRequired: ValidationRule<unknown> = (value, name) => {
if (isNullish(value) || isEmptyArray(value) || isEmptyString(value) || isInvalidDate(value)) {
return `${name} is required`
}
return true
}
const isJson: ValidationRule<string> = (value, name) => {
try {
JSON.parse(value)
} catch {
return `${name} must be valid JSON`
}
return true
}
const { state, error } = useValidation(model, 'Trigger', [isRequired, isJson, isMappableAutomationTriggerJson])
</script>

<style>
.automation-trigger-json-input__json-input {
min-height: 400px;
}
</style>
22 changes: 22 additions & 0 deletions ui/src/components/AutomationTriggerTemplateSelect.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<template>
<p-select v-model="template" empty-message="Select template" :options="options" class="automation-trigger-template-select" />
</template>

<script lang="ts" setup>
import { SelectOptionNormalized } from '@prefecthq/prefect-design'
import { AutomationTriggerTemplate, automationTriggerTemplates, getAutomationTriggerTemplateLabel } from '@prefecthq/prefect-ui-library'
import { computed } from 'vue'
const template = defineModel<AutomationTriggerTemplate | null>('template', { required: true })
/*
* Currently OSS doesn't have support for enabled/disabled trigger templates like cloud does.
* Only because it wasn't needed at the time of porting automations to OSS.
*/
const options = computed<SelectOptionNormalized[]>(() => automationTriggerTemplates.map(type => {
return {
label: getAutomationTriggerTemplateLabel(type),
value: type,
}
}))
</script>
68 changes: 68 additions & 0 deletions ui/src/components/AutomationWizard.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<template>
<p-wizard
ref="wizardRef"
class="automation-wizard"
:steps="steps"
:last-step-text="lastStepText"
show-cancel
:nonlinear="editing"
:show-save-and-exit="editing"
@cancel="cancel"
@submit="submit"
>
<template #trigger-step>
<AutomationWizardStepTrigger v-model:automation="automation" />
</template>
<template #actions-step>
<template v-if="isAutomationActionFormValues(automation)">
<AutomationWizardStepActions v-model:automation="automation" />
</template>
</template>
<template #details-step>
<AutomationWizardStepDetails v-model:automation="automation" />
</template>
</p-wizard>
</template>

<script lang="ts" setup>
import { PWizard, WizardStep } from '@prefecthq/prefect-design'
import { isAutomationTriggerEvent } from '@prefecthq/prefect-ui-library'
import { computed, ref } from 'vue'
import { useRouter } from 'vue-router'
import AutomationWizardStepActions from '@/components/AutomationWizardStepActions.vue'
import AutomationWizardStepDetails from '@/components/AutomationWizardStepDetails.vue'
import AutomationWizardStepTrigger from '@/components/AutomationWizardStepTrigger.vue'
import { mapper } from '@/services/mapper'
import { Automation, AutomationFormValues, IAutomation, isAutomationActionFormValues } from '@/types/automation'
const props = defineProps<{
automation?: Partial<Automation>,
editing?: boolean,
}>()
const automation = ref<AutomationFormValues>(props.automation ?? {})
const emit = defineEmits<{
(event: 'submit', value: Automation): void,
}>()
const router = useRouter()
const lastStepText = computed(() => props.automation ? 'Save' : 'Create')
const steps: WizardStep[] = [
{ title: 'Trigger', key: 'trigger-step' },
{ title: 'Actions', key: 'actions-step' },
{ title: 'Details', key: 'details-step' },
]
const wizardRef = ref<InstanceType<typeof PWizard>>()
function submit(): void {
emit('submit', new Automation(automation.value as IAutomation))
}
function cancel(): void {
router.back()
}
</script>
91 changes: 91 additions & 0 deletions ui/src/components/AutomationWizardAction.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
<template>
<div class="automation-wizard-action">
<div class="automation-wizard-action__header">
<span class="automation-wizard-action__heading">Action {{ index + 1 }}</span>
<p-button size="sm" icon="TrashIcon" @click="emit('delete')" />
</div>

<p-content>
<p-label label="Action Type" :state :message>
<template #default="{ id }">
<AutomationActionTypeSelect :id v-model:type="type" :state />
</template>
</p-label>


<template v-if="input">
<component :is="input.component" v-bind="input.props" @update:action="updateAction" />
</template>
</p-content>
</div>
</template>

<script lang="ts" setup>
import { withProps, AutomationActionInput, AutomationAction, isNullish, getAutomationTriggerTemplate, getDefaultValueForAction } from '@prefecthq/prefect-ui-library'
import { useValidation } from '@prefecthq/vue-compositions'
import { computed } from 'vue'
import AutomationActionTypeSelect from '@/components/AutomationActionTypeSelect.vue'
import { AutomationActionFormValues } from '@/types/automation'
const props = defineProps<{
index: number,
action: Partial<AutomationAction>,
automation: AutomationActionFormValues,
}>()
const emit = defineEmits<{
(event: 'delete'): void,
(event: 'update:action', value: Partial<AutomationAction>): void,
}>()
const type = computed({
get() {
return props.action.type ?? null
},
set(value) {
if (isNullish(value)) {
emit('update:action', {})
return
}
const template = getAutomationTriggerTemplate(props.automation.trigger)
const action = getDefaultValueForAction(value, template)
emit('update:action', action)
},
})
const { state, error: message } = useValidation(type, 'Action Type', value => !!value)
const input = computed(() => {
if (!props.action.type) {
return null
}
return withProps(AutomationActionInput, {
action: props.action,
'onUpdate:action': value => emit('update:action', value),
})
})
function updateAction(action: Partial<AutomationAction>): void {
emit('update:action', action)
}
</script>

<style>
.automation-wizard-action { @apply
grid
gap-1
}
.automation-wizard-action__header { @apply
flex
items-center
justify-between
}
.automation-wizard-action__heading { @apply
font-bold
}
</style>
Loading

0 comments on commit 320d588

Please sign in to comment.