Skip to content

Commit

Permalink
Feat(dui3): automate (#3475)
Browse files Browse the repository at this point in the history
* WIP

* feat: implement doughnut and more

* Remove unused icon

* Add comment

* feat: WIP reworks a few things, adds subscriptions for results (#3487)

* feat: WIP reworks a few things, adds subscriptions for results

* feat: wip

* feat: real time automation runs ✅

* feat: cleanup

---------

Co-authored-by: oguzhankoral <[email protected]>
Co-authored-by: Dimitrie Stefanescu <[email protected]>
  • Loading branch information
3 people authored Nov 12, 2024
1 parent cf2cfa2 commit e9d1e63
Show file tree
Hide file tree
Showing 18 changed files with 1,180 additions and 12 deletions.
158 changes: 158 additions & 0 deletions packages/dui3/components/automate/CreateDialog.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
<!-- NOT WILL BE USED SINCE WE ENABLE AUTOMATION CREATION FROM DUI3 -->
<template>
<div class="p-0">
<slot name="activator" :toggle="toggleDialog"></slot>
<LayoutDialog
v-model:open="showAutomateDialog"
:title="`Settings`"
fullscreen="none"
>
<div v-if="hasFunctions">
<FormSelectBase
key="name"
v-model="selectedFunction"
clearable
label="Automate functions"
placeholder="Nothing selected"
name="Functions"
show-label
:items="functions"
mount-menu-on-body
>
<template #something-selected="{ value }">
<span>{{ value.name }}</span>
</template>
<template #option="{ item }">
<div class="flex items-center">
<span class="truncate">{{ item.name }}</span>
</div>
</template>
</FormSelectBase>
</div>
<div v-if="selectedFunction && finalParams && step === 0">
<FormJsonForm
ref="jsonForm"
:data="data"
:schema="finalParams"
class="space-y-4"
:validate-on-mount="false"
@change="handler"
/>
</div>
<div v-if="step === 1">
<FormTextInput
v-model="automationName"
name="automationName"
label="Automation name"
color="foundation"
show-label
help="Give your automation a name"
placeholder="Name"
show-required
validate-on-value-update
/>
</div>
<FormButton
v-if="selectedFunction && step === 0"
size="sm"
class="mt-4"
@click="step++"
>
Next
</FormButton>
<FormButton
v-if="selectedFunction && step === 1"
size="sm"
class="mt-4"
@click="createAutomationHandler"
>
Create
</FormButton>
</LayoutDialog>
</div>
</template>

<script setup lang="ts">
import { storeToRefs } from 'pinia'
import type { AutomateFunctionItemFragment } from '~/lib/common/generated/gql/graphql'
import {
automateFunctionsQuery,
createAutomationMutation
} from '~/lib/graphql/mutationsAndQueries'
import { provideApolloClient, useMutation, useQuery } from '@vue/apollo-composable'
import { useAccountStore } from '~/store/accounts'
import type { ApolloError } from '@apollo/client/errors'
import { formatVersionParams } from '~/lib/common/helpers/jsonSchema'
import { useJsonFormsChangeHandler } from '~/lib/core/composables/jsonSchema'

const props = defineProps<{
projectId: string
modelId: string
}>()

const step = ref<number>(0)

const automationName = ref<string>('')

const accountStore = useAccountStore()
const { activeAccount } = storeToRefs(accountStore)
const accountId = computed(() => activeAccount.value?.accountInfo.id) // NOTE: none of the tokens here has read, write access to automate, only frontend tokens have. Keep in mind after first pass!

const selectedFunction = ref<AutomateFunctionItemFragment>()

const showAutomateDialog = ref(false)

const toggleDialog = () => {
showAutomateDialog.value = !showAutomateDialog.value
}

const { mutate } = provideApolloClient(activeAccount.value.client)(() =>
useMutation(createAutomationMutation)
)

const createAutomationHandler = async () => {
const _res = await mutate({
projectId: props.projectId,
input: { name: automationName.value, enabled: false }
})
showAutomateDialog.value = false
}

const { result: functionsResult, onError } = useQuery(
automateFunctionsQuery,
() => ({}),
() => ({ clientId: accountId.value, debounce: 500, fetchPolicy: 'network-only' })
)

onError((err: ApolloError) => {
console.warn(err.message)
})

const functions = computed(() => functionsResult.value?.automateFunctions.items)
const hasFunctions = computed(() => functions.value?.length !== 0)

const release = computed(() =>
selectedFunction.value?.releases.items.length
? selectedFunction.value?.releases.items[0]
: undefined
)

const finalParams = computed(() => formatVersionParams(release.value?.inputSchema))

const { handler } = useJsonFormsChangeHandler({
schema: finalParams
})

console.log(finalParams)

type DataType = Record<string, unknown>
const data = computed(() => {
const kvp = {} as DataType
if (finalParams.value) {
Object.entries(finalParams.value).forEach((k, _) => {
kvp[k as unknown as string] = undefined
})
}
return kvp
})
</script>
39 changes: 39 additions & 0 deletions packages/dui3/components/automate/ResultDialog.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<template>
<div class="p-0">
<slot name="activator" :toggle="toggleDialog"></slot>
<LayoutDialog
v-model:open="showAutomateReportDialog"
:title="`Automation Report`"
fullscreen="none"
>
<div v-if="props.automationRuns" class="space-y-2">
<AutomateFunctionRunsRows
v-for="aRun in automationRuns"
:key="aRun.id"
:model-card="modelCard"
:automation-name="aRun.automation.name"
:runs="aRun.functionRuns"
:project-id="modelCard.projectId"
:model-id="modelId"
/>
</div>
</LayoutDialog>
</div>
</template>

<script setup lang="ts">
import type { IModelCard } from '~/lib/models/card'
import type { AutomationRunItemFragment } from '~/lib/common/generated/gql/graphql'
const props = defineProps<{
modelCard: IModelCard
modelId: string
automationRuns: AutomationRunItemFragment[] | undefined
}>()
const showAutomateReportDialog = ref(false)
const toggleDialog = () => {
showAutomateReportDialog.value = !showAutomateReportDialog.value
}
</script>
60 changes: 60 additions & 0 deletions packages/dui3/components/automate/function/Logo.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<template>
<div :class="classes">
<img v-if="finalLogo" :src="finalLogo" alt="Function logo" class="h-10 w-10" />
<span v-else :class="fallbackIconClasses">λ</span>
</div>
</template>
<script setup lang="ts">
import type { MaybeNullOrUndefined, Nullable } from '@speckle/shared'
type Size = 'base' | 'xs'
const props = withDefaults(
defineProps<{
logo?: MaybeNullOrUndefined<string>
size?: Size
}>(),
{
size: 'base'
}
)
const cleanFunctionLogo = (logo: MaybeNullOrUndefined<string>): Nullable<string> => {
if (!logo?.length) return null
if (logo.startsWith('data:')) return logo
if (logo.startsWith('http:')) return logo
if (logo.startsWith('https:')) return logo
return null
}
const finalLogo = computed(() => cleanFunctionLogo(props.logo))
const classes = computed(() => {
const classParts = [
'bg-foundation-focus text-primary font-medium rounded-full shrink-0 flex justify-center text-center items-center overflow-hidden select-none'
]
switch (props.size) {
case 'xs':
classParts.push('h-4 w-4')
break
case 'base':
default:
classParts.push('h-10 w-10')
break
}
return classParts.join(' ')
})
const fallbackIconClasses = computed(() => {
const classParts: string[] = []
switch (props.size) {
case 'xs':
classParts.push('text-xs')
break
}
return classParts.join(' ')
})
</script>
134 changes: 134 additions & 0 deletions packages/dui3/components/automate/function/RunRow.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
<template>
<div
:class="`border border-blue-500/10 rounded-md space-y-2 overflow-hidden ${
expanded ? 'shadow' : ''
}`"
>
<button
class="flex space-x-1 items-center max-w-full w-full px-1 py-1 h-8 transition hover:bg-primary-muted"
@click="expanded = !expanded"
>
<div>
<Component
:is="statusMetaData.icon"
v-tippy="functionRun.status"
:class="['h-4 w-4 outline-none', statusMetaData.iconColor]"
/>
</div>
<AutomateFunctionLogo :logo="functionRun.function?.logo" size="xs" />
<div class="font-medium text-xs truncate">
{{ automationName ? automationName + ' / ' : ''
}}{{ functionRun.function?.name || 'Unknown function' }}
</div>

<div class="h-full grow flex justify-end">
<button
class="hover:bg-primary-muted hover:text-primary flex h-full items-center justify-center rounded"
>
<ChevronDownIcon
:class="`h-3 w-3 transition ${!expanded ? '-rotate-90' : 'rotate-0'}`"
/>
</button>
</div>
</button>
<div v-if="expanded" class="px-2 pb-2 space-y-4">
<!-- Status message -->
<div class="space-y-1">
<div class="text-xs font-medium text-foreground-2">Status</div>
<div
v-if="
[
AutomateRunStatus.Initializing,
AutomateRunStatus.Running,
AutomateRunStatus.Pending
].includes(functionRun.status)
"
class="text-xs text-foreground-2 italic"
>
Function is {{ functionRun.status.toLowerCase() }}.
</div>
<div v-else class="text-xs text-foreground-2 italic">
{{ functionRun.statusMessage || 'No status message' }}
</div>
</div>

<!-- Attachments -->
<!-- <div
v-if="attachments.length !== 0"
class="border-t pt-2 border-foreground-2 space-y-1"
>
<div class="text-xs font-medium text-foreground-2">Attachments</div>
<div class="ml-[2px] justify-start">
<AutomateRunsAttachmentButton
v-for="id in attachments"
:key="id"
:blob-id="id"
:project-id="projectId"
size="xs"
link
class="mr-2"
/>
</div>
</div> -->
<!-- Results -->
<div
v-if="!!results?.values.objectResults.length"
class="border-t pt-2 border-foreground-2"
>
<div class="text-xs font-medium text-foreground-2 mb-2">Results</div>
<div class="space-y-1">
<AutomateFunctionRunRowObjectResult
v-for="(result, index) in results.values.objectResults.slice(
0,
pageRunLimit
)"
:key="index"
:model-card="modelCard"
:function-id="functionRun.function?.id"
:result="result"
/>
<FormButton
v-if="pageRunLimit < results.values.objectResults.length"
size="sm"
color="outline"
class="w-full"
@click="pageRunLimit += 10"
>
Load more ({{ results.values.objectResults.length - pageRunLimit }}
hidden results)
</FormButton>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ChevronDownIcon } from '@heroicons/vue/24/outline'
import { AutomateRunStatus } from '~/lib/common/generated/gql/graphql'
import type { AutomateFunctionRunItemFragment } from '~/lib/common/generated/gql/graphql'
import {
useRunStatusMetadata,
useAutomationFunctionRunResults
} from '~/lib/automate/runStatus'
import type { IModelCard } from '~/lib/models/card'
const props = defineProps<{
modelCard: IModelCard
functionRun: AutomateFunctionRunItemFragment
automationName: string
}>()
const results = useAutomationFunctionRunResults({
results: computed(() => props.functionRun.results)
})
const { metadata: statusMetaData } = useRunStatusMetadata({
status: computed(() => props.functionRun.status)
})
const pageRunLimit = ref(5)
const expanded = ref(false)
// const attachments = computed(() =>
// (results.value?.values.blobIds || []).filter((b) => !!b)
// )
</script>
Loading

0 comments on commit e9d1e63

Please sign in to comment.