Files
speckle-server/packages/frontend-2/components/automate/automation/CreateDialog.vue
T
andrewwallacespeckle fcb924d3a5 DO NOT MERGE - refactor: new design system implementation (#2537)
* refactor WIP

* Button design changes

* FE2 FormButton Updates

* ts composition api

* CommonTextLink Changes

* CommonTextLink prop updates

* Add disabled styles

* WIP

* Design system updates

* Colour Updates

* New Text Styles. Initial FE2 changes

* More fe2 styling classes

* Minor update

* Minor update

* Fix build

* More updates for discussion

* More styling updates

* Minor updates to inputs

* Revert change to size options

* More text updates

* More font class swapping

* Revert dui3 changes

* Confirmed Lineheights

* Add story files for new text styles

* Minor copy changes

* Minor typo

* Revert variant>color

* New Colours WIP

* andrew/web-1371-misalignment-in-account-dropdown

* andrew/web-1374-settings-text-styles-are-not-right

* andrew/web-1375-nav-texts-should-be-14px

* andrew/web-1376-decrease-size-of-versions-header

* andrew/web-1377-version-card-title

* Updates

* semibold>medium

* Colour updates

* Sizing updates

* Colour updates

* Colour updates

* Measure mode

* Updates

* Fix build

* Fix build

* WIP Updates

* Changes from PR

* Updated login, registration and reset password styling

* Make share dropdown bg white

* Updated viewer titles

* Fix: Resize panel highlight color in the viewer should be blue

* Fix: Blue + Add link in Models. And other blue links in Viewer

* Add labelPosition Prop. Fix Button stories

* Updated CommonLink to remove default underline

* Add Highlight Color

* Card updates from Michal

* Updated discussion icon on version card

* Small tweaks to version card

* Small tweaks to version card

* Fix: Ghost button doesn't have padding

* Fix: Write Delete...

* Fix: Version hover border color

* Updates to Project Card. Updates to PageTabs

* Fix: Adjust title in announcement modal

* Updates from Comments

* Select Background Colour

* Fix: Select dropdown color

* Improve list view. Improve discussions

* Fix: Minor tweaks to onboarding checklist

* Fix: Clean up nav

* Hide third item when not >md

* Change project heading size

* Add border to version card

* Adjust spacing in dropdowns

* Slight change

* Update button style in Version card

* Tweaked nav menu

* Tweaked nav menu

* Various styling tweaks

* Fix settings modal subheader

* Various styling tweaks and fixes

* Tweak settings dialog styling

* Tweak simple scrollbar

* Minor tweaks to model page

* Minor tweaks to model page

* Minor tweak to login

* Tweak discussion card

* Tweak settings page

* Tweak vertical tabs

* Tweak Dialog alignment

* Fix some paddings

* Change IconVersions to ClockIcon

* Tweak spacing between icons

* Updates to Card Icons

* Bold "connectors" in empty project message

* Remove padding in Profile field

* Update inline model create

* Remove icons from share menu

* Updated Delete dialog

* Wrong text positioning in alert

* Updated copy in dropdown

* Change bg to bg-foundation in select dropdown component

* Fix merge conflicy

* Selection Info title colour

* Wrong text class

* Update card colours based on call

* Update card colours

* Update empty state

* Input label font weight

* Updates to Embed

* Various styling fixes

* Fix; Viewer panel header styling

* Fix; Adjust BG in dev mode list items

* Fix; Fix button placement in video modal

* Fix: Share menu is not using LayoutMenu

* Fix: Buttons clash under filters

* Fix: Adjust spacing in selection info

* Fix: Adjust gray BG behind model preview images

* Fix: No hover cursor on model card

* Fix: Align text styling in dev mode and selection info panel

* Fix for menu width

* Fix mobile problems

* Fix Add spacing on new login screens

* Revert prose change. Add prose-sm

* Text - Use contain for bg image

* Fix onboarding screens

* Responsive fixes

* Fix hydration errors

* Added padding to Add Model Dialog

* Fix versions buttons

* Fix build problem

* Changes PRE PR

* Final Pre PR Changes

* Remove DUI3 change

* Fix small issue with dialog after merge conflict

* Remove label classes from Visibility Select

* Revert changes made in Controls.vue

* Remove old-webhooks

* Add highlight colours to Storybook

* Add v-keyboard-clickable

---------

Co-authored-by: Mike Tasset <mike.tasset@gmail.com>
2024-07-30 15:34:41 +01:00

510 lines
14 KiB
Vue

<template>
<LayoutDialog
v-model:open="open"
max-width="lg"
:title="title"
:buttons-wrapper-classes="buttonsWrapperClasses"
:buttons="buttons"
:on-submit="onDialogSubmit"
prevent-close-on-click-outside
@fully-closed="reset"
>
<template v-if="isTestAutomation" #header>
Create
<span class="font-extrabold text-fancy-gradient">test</span>
automation
</template>
<div class="flex flex-col gap-6">
<CommonStepsNumber
v-if="shouldShowStepsWidget"
v-model="stepsWidgetModel"
class="mb-2"
:steps="stepsWidgetSteps"
:go-vertical-below="TailwindBreakpoints.sm"
non-interactive
/>
<CommonAlert v-if="isTestAutomation" color="info">
<template #title>What is a "test automation"?</template>
<template #description>
<ul class="list-disc ml-4">
<li>
A test automation is a sandbox environment that allows you to connect your
local development environment for testing purposes. It enables you to run
your code against project data and submit results directly to the
connected test automation.
</li>
<li>
Unlike regular automations, test automations are not triggered by changes
to project data. They cannot be started by pushing a new version to a
model.
</li>
<li>Consequently, test automations do not execute published functions.</li>
</ul>
</template>
</CommonAlert>
<AutomateAutomationCreateDialogSelectFunctionStep
v-if="enumStep === AutomationCreateSteps.SelectFunction"
v-model:selected-function="selectedFunction"
:show-label="false"
:show-required="false"
:preselected-function="validatedPreselectedFunction"
/>
<AutomateAutomationCreateDialogFunctionParametersStep
v-else-if="
enumStep === AutomationCreateSteps.FunctionParameters && selectedFunction
"
ref="parametersStep"
v-model:parameters="functionParameters"
v-model:has-errors="hasParameterErrors"
:fn="selectedFunction"
/>
<template v-else-if="enumStep === AutomationCreateSteps.AutomationDetails">
<AutomateAutomationCreateDialogAutomationDetailsStep
v-model:project="selectedProject"
v-model:model="selectedModel"
v-model:automation-name="automationName"
:preselected-project="preselectedProject"
:is-test-automation="isTestAutomation"
/>
<AutomateAutomationCreateDialogSelectFunctionStep
v-if="isTestAutomation"
v-model:selected-function="selectedFunction"
:preselected-function="validatedPreselectedFunction"
:page-size="2"
/>
</template>
</div>
</LayoutDialog>
</template>
<script setup lang="ts">
import { useEnumSteps, useEnumStepsWidgetSetup } from '~/lib/form/composables/steps'
import {
CommonStepsNumber,
TailwindBreakpoints,
type LayoutDialogButton
} from '@speckle/ui-components'
import {
ChevronLeftIcon,
ChevronRightIcon,
CodeBracketIcon
} from '@heroicons/vue/24/outline'
import { graphql } from '~/lib/common/generated/gql'
import { Automate, type Optional } from '@speckle/shared'
import type { CreateAutomationSelectableFunction } from '~/lib/automate/helpers/automations'
import {
AutomateRunTriggerType,
type FormSelectModels_ModelFragment,
type FormSelectProjects_ProjectFragment
} from '~/lib/common/generated/gql/graphql'
import { useForm } from 'vee-validate'
import {
useCreateAutomation,
useCreateAutomationRevision,
useCreateTestAutomation,
useUpdateAutomation
} from '~/lib/projects/composables/automationManagement'
import { formatJsonFormSchemaInputs } from '~/lib/automate/helpers/jsonSchema'
import { projectAutomationRoute } from '~/lib/common/helpers/route'
import {
useAutomationInputEncryptor,
type AutomationInputEncryptor
} from '~/lib/automate/composables/automations'
import { useMixpanel } from '~/lib/core/composables/mp'
import { hasJsonFormErrors } from '~/lib/automate/composables/jsonSchema'
import type { JsonFormsChangeEvent } from '@jsonforms/vue'
enum AutomationCreateSteps {
SelectFunction,
FunctionParameters,
AutomationDetails
}
type DetailsFormValues = {
project: FormSelectProjects_ProjectFragment
model: FormSelectModels_ModelFragment
automationName: string
}
graphql(`
fragment AutomateAutomationCreateDialog_AutomateFunction on AutomateFunction {
id
...AutomationsFunctionsCard_AutomateFunction
...AutomateAutomationCreateDialogFunctionParametersStep_AutomateFunction
}
`)
const props = defineProps<{
preselectedFunction?: Optional<CreateAutomationSelectableFunction>
preselectedProject?: Optional<FormSelectProjects_ProjectFragment>
}>()
const open = defineModel<boolean>('open', { required: true })
const mixpanel = useMixpanel()
const { handleSubmit: handleDetailsSubmit } = useForm<DetailsFormValues>()
const stepsOrder = computed(() => [
AutomationCreateSteps.SelectFunction,
AutomationCreateSteps.FunctionParameters,
AutomationCreateSteps.AutomationDetails
])
const stepsWidgetData = computed(() => [
{
step: AutomationCreateSteps.SelectFunction,
title: 'Select function'
},
{
step: AutomationCreateSteps.FunctionParameters,
title: 'Set parameters'
},
{
step: AutomationCreateSteps.AutomationDetails,
title: 'Add details'
}
])
const router = useRouter()
const inputEncryption = useAutomationInputEncryptor({ ensureWhen: open })
const logger = useLogger()
const updateAutomation = useUpdateAutomation()
const createAutomation = useCreateAutomation()
const createRevision = useCreateAutomationRevision()
const createTestAutomation = useCreateTestAutomation()
const { enumStep, step } = useEnumSteps({ order: stepsOrder })
const {
items: stepsWidgetSteps,
model: stepsWidgetModel,
shouldShowWidget
} = useEnumStepsWidgetSetup({ enumStep, widgetStepsMap: stepsWidgetData })
const parametersStep = ref<{ submit: () => Promise<Optional<JsonFormsChangeEvent>> }>()
const creationLoading = ref(false)
const automationId = ref<string>()
const automationName = ref<string>()
const selectedProject = ref<FormSelectProjects_ProjectFragment>()
const selectedModel = ref<FormSelectModels_ModelFragment>()
const selectedFunction = ref<Optional<CreateAutomationSelectableFunction>>()
const functionParameters = ref<Record<string, unknown>>()
const hasParameterErrors = ref(false)
const isTestAutomation = ref(false)
const shouldShowStepsWidget = computed(() => {
return !!shouldShowWidget.value && !isTestAutomation.value
})
const enableSubmitTestAutomation = computed(() => {
const isValidInput =
!!automationName.value && !!selectedModel.value && !!selectedFunction.value
const isLoading = creationLoading.value
return isValidInput && !isLoading
})
const title = computed(() => {
return isTestAutomation.value ? undefined : 'Create Automation'
})
const buttons = computed((): LayoutDialogButton[] => {
switch (enumStep.value) {
case AutomationCreateSteps.SelectFunction:
return [
{
id: 'createTestAutomation',
text: 'Create test automation',
props: {
color: 'outline',
iconLeft: CodeBracketIcon
},
onClick: () => {
isTestAutomation.value = true
enumStep.value = AutomationCreateSteps.AutomationDetails
}
},
{
id: 'selectFnNext',
text: 'Next',
props: {
iconRight: ChevronRightIcon,
disabled: !selectedFunction.value
},
onClick: () => {
step.value++
}
}
]
case AutomationCreateSteps.FunctionParameters:
return [
{
id: 'fnParamsPrev',
text: 'Previous',
props: {
color: 'outline',
iconLeft: ChevronLeftIcon,
class: '!text-primary'
},
onClick: () => step.value--
},
{
id: 'fnParamsNext',
text: 'Next',
props: {
iconRight: ChevronRightIcon,
disabled: hasParameterErrors.value
},
submit: true
}
]
case AutomationCreateSteps.AutomationDetails: {
const automationButtons: LayoutDialogButton[] = [
{
id: 'detailsPrev',
text: 'Previous',
props: {
color: 'outline',
iconLeft: ChevronLeftIcon
},
onClick: () => step.value--
},
{
id: 'detailsCreate',
text: 'Create',
submit: true,
disabled: creationLoading.value
}
]
const testAutomationButtons: LayoutDialogButton[] = [
{
id: 'detailsPrev',
text: 'Back',
props: {
color: 'outline',
iconLeft: ChevronLeftIcon
},
onClick: reset
},
{
id: 'submitTestAutomation',
text: 'Create',
disabled: !enableSubmitTestAutomation.value,
submit: true
}
]
return isTestAutomation.value ? testAutomationButtons : automationButtons
}
default:
return []
}
})
const buttonsWrapperClasses = computed(() => {
switch (enumStep.value) {
case AutomationCreateSteps.SelectFunction:
return 'justify-between'
default:
return 'justify-between'
}
})
const validatedPreselectedFunction = computed(() => {
if (!(props.preselectedFunction?.releases.items || []).length) {
return undefined
}
return props.preselectedFunction
})
const goToNewAutomation = async () => {
if (!selectedProject.value || !automationId.value) {
logger.error('Missing required data for redirect', {
project: selectedProject.value,
automationId: automationId.value
})
return
}
await router.push(
projectAutomationRoute(selectedProject.value.id, automationId.value)
)
}
const reset = () => {
step.value = 0
selectedFunction.value = undefined
functionParameters.value = undefined
hasParameterErrors.value = false
selectedProject.value = undefined
selectedModel.value = undefined
automationName.value = undefined
automationId.value = undefined
isTestAutomation.value = false
}
const onDetailsSubmit = handleDetailsSubmit(async () => {
const fn = selectedFunction.value
const fnRelease = selectedFunction.value?.releases.items[0]
const project = selectedProject.value
const model = selectedModel.value
const parameters = functionParameters.value
const name = automationName.value
if (!fn || !project || !model || !name?.length || !fnRelease) {
logger.error('Missing required data', {
fn,
project,
model,
parameters,
name,
fnRelease
})
return
}
creationLoading.value = true
let aId: Optional<string> = undefined
let automationEncrypt: Optional<AutomationInputEncryptor> = undefined
try {
if (isTestAutomation.value) {
// Use simplified pathway
const testAutomationId = await createTestAutomation({
projectId: project.id,
input: {
name,
functionId: fn.id,
modelId: model.id
}
})
if (!testAutomationId) {
logger.error('Failed to create test automation')
return
}
automationId.value = testAutomationId
await goToNewAutomation()
return
}
const createRes = await createAutomation({
projectId: project.id,
input: {
name,
enabled: false
}
})
aId = automationId.value = createRes?.id
if (!aId) {
logger.error('Failed to create automation', { createRes })
return
}
automationEncrypt = await inputEncryption.forAutomation({
automationId: aId,
projectId: project.id
})
const cleanParams =
formatJsonFormSchemaInputs(parameters, fnRelease.inputSchema) || null
const encryptedParams = automationEncrypt.encryptInputs({
inputs: cleanParams
})
const revisionRes = await createRevision(
{
projectId: project.id,
input: {
automationId: aId,
functions: [
{
functionReleaseId: fnRelease.id,
functionId: fn.id,
parameters: encryptedParams
}
],
triggerDefinitions: <Automate.AutomateTypes.TriggerDefinitionsSchema>{
version: Automate.AutomateTypes.TRIGGER_DEFINITIONS_SCHEMA_VERSION,
definitions: [
{
type: AutomateRunTriggerType.VersionCreated,
modelId: model.id
}
]
}
}
},
{ hideSuccessToast: true }
)
if (!revisionRes?.id) {
logger.error('Failed to create revision', { revisionRes })
return
}
mixpanel.track('Automation created', {
automationId: aId,
name,
projectId: project.id,
functionName: fn.name,
functionId: fn.id,
functionReleaseId: fnRelease.id,
modelId: model.id
})
// Enable
await updateAutomation(
{
projectId: project.id,
input: {
id: aId,
enabled: true
}
},
{ hideSuccessToast: true }
)
await goToNewAutomation()
} finally {
creationLoading.value = false
automationEncrypt?.dispose()
}
})
const onDialogSubmit = async (e: SubmitEvent) => {
if (enumStep.value === AutomationCreateSteps.AutomationDetails) {
await onDetailsSubmit(e)
} else if (enumStep.value === AutomationCreateSteps.FunctionParameters) {
const validationResult = await parametersStep.value?.submit()
if (validationResult && !hasJsonFormErrors(validationResult)) {
step.value++
}
}
}
watch(open, (newVal, oldVal) => {
if (newVal && !oldVal) {
reset()
if (validatedPreselectedFunction.value) {
selectedFunction.value = validatedPreselectedFunction.value
enumStep.value = AutomationCreateSteps.FunctionParameters
}
if (props.preselectedProject) {
selectedProject.value = props.preselectedProject
}
}
})
watch(selectedFunction, (newVal, oldVal) => {
if (newVal?.id !== oldVal?.id) {
// Reset params
functionParameters.value = undefined
}
})
</script>