Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create and manage automations in the UI #13293

Merged
merged 18 commits into from
May 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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>
21 changes: 21 additions & 0 deletions ui/src/components/AutomationTriggerTemplateSelect.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<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.
*/
pleek91 marked this conversation as resolved.
Show resolved Hide resolved
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>