From e312110933d42bf1c2ae4fff25cd1ec9e6d9712f Mon Sep 17 00:00:00 2001 From: Chuck Driesler Date: Fri, 29 Nov 2024 16:33:14 +0000 Subject: [PATCH] Automate Public Beta (#3472) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(automate): query active user functions * fix(automate): show automations to non-stream-owners * feat(automate): associate function with workspace * fix(automate): split functions page between user and example functions * fix(automate): ugh * fix(functions): use correct query type in different places * fix(automate): workspace functions page * feat(automate): query specific categories of functions * fix(automate): checkpoint * fix(workspaces): successful queries w local env * fix(automate): createFunctionWithoutVersion * fix(automate): successful associate function with workspace * fix(automate): query and return workspaces on functions * fix(automate): show current function workspace * fix(automate): query functions in automation create dialog * fix(automate): audit non-owner automation access * refactor(automate): logs api can get the projectId from the path * fix(automate): multiregion gql resolvers * fix(automate): multiregion event listeners * fix(automate): drop automationCount * fix(automate): multiregion run status * fix(automate): correctness * fix(automate): successful usage of multiregion results * fix(automate): actually finish event listeners * chore(automate): fix tests fix tests * fix(automate): fix tests but make it multiregion flavor * fix(automate): logs endpoint * fix(automate): inject projectid correctly * fix(automate): drop user-source functions * fix(automate): owners edit, others can view * fix(automate): simplify queries, auto workspace association * chore(automate): appease * chore(automate): fix function types * fix(automate): get to workspace functions from empty state * chore(automate): death to all slugs * fix(automate): no create automation from function * fix(automate): hide workspace change, tweak role access --------- Co-authored-by: Gergő Jedlicska --- .../automate/automation/CreateDialog.vue | 3 + .../create-dialog/SelectFunctionStep.vue | 30 ++-- .../automate/function/CreateDialog.vue | 37 +++-- .../automate/function/EditDialog.vue | 3 + .../function/create-dialog/DetailsStep.vue | 34 +++++ .../automate/function/page/Header.vue | 32 ++--- .../automate/functions/page/Header.vue | 60 ++++---- .../automate/functions/page/Items.vue | 12 +- .../components/dashboard/Sidebar.vue | 15 ++ .../components/global/icon/Bolt.vue | 18 +++ .../project/page/automation/Functions.vue | 3 +- .../project/page/automation/Header.vue | 6 +- .../project/page/automation/Models.vue | 2 +- .../project/page/automation/Runs.vue | 3 +- .../project/page/automations/EmptyState.vue | 29 ++-- .../project/page/automations/Header.vue | 29 ++-- .../project/page/automations/Tab.vue | 32 +++-- .../lib/automate/graphql/queries.ts | 13 ++ .../lib/automate/helpers/functions.ts | 6 +- .../lib/common/generated/gql/gql.ts | 62 ++++++--- .../lib/common/generated/gql/graphql.ts | 129 ++++++++++++++---- .../frontend-2/lib/common/helpers/route.ts | 2 + .../lib/projects/graphql/queries.ts | 5 + .../lib/settings/graphql/queries.ts | 8 ++ .../lib/workspaces/graphql/queries.ts | 17 +++ packages/frontend-2/pages/functions/[fid].vue | 18 ++- packages/frontend-2/pages/functions/index.vue | 35 +++-- .../frontend-2/pages/projects/[id]/index.vue | 2 +- .../projects/[id]/index/automations/[aid].vue | 13 +- .../workspaces/[slug]/functions/index.vue | 82 +++++++++++ .../assets/automate/typedefs/automate.graphql | 28 +++- .../typedefs/workspaces.graphql | 5 + .../automate/clients/executionEngine.ts | 94 ++++++++++++- .../modules/automate/graph/mocks/automate.ts | 6 +- .../automate/graph/resolvers/automate.ts | 99 ++++++++++++-- .../automate/helpers/executionEngine.ts | 1 + .../modules/automate/helpers/graphTypes.ts | 1 + .../server/modules/automate/rest/logStream.ts | 2 +- .../modules/automate/services/authCode.ts | 16 ++- .../automate/services/functionManagement.ts | 5 +- .../modules/core/graph/generated/graphql.ts | 56 +++++++- .../graph/generated/graphql.ts | 41 +++++- .../workspaces/events/eventListener.ts | 1 + .../workspaces/graph/resolvers/workspaces.ts | 60 +++++++- .../server/test/graphql/generated/graphql.ts | 41 +++++- 45 files changed, 1006 insertions(+), 190 deletions(-) create mode 100644 packages/frontend-2/components/global/icon/Bolt.vue create mode 100644 packages/frontend-2/pages/workspaces/[slug]/functions/index.vue diff --git a/packages/frontend-2/components/automate/automation/CreateDialog.vue b/packages/frontend-2/components/automate/automation/CreateDialog.vue index 758f103da..8fffa059a 100644 --- a/packages/frontend-2/components/automate/automation/CreateDialog.vue +++ b/packages/frontend-2/components/automate/automation/CreateDialog.vue @@ -43,6 +43,7 @@ :show-label="false" :show-required="false" :preselected-function="validatedPreselectedFunction" + :workspace-id="workspaceId" /> @@ -124,6 +126,7 @@ graphql(` `) const props = defineProps<{ + workspaceId?: string preselectedFunction?: Optional preselectedProject?: Optional }>() diff --git a/packages/frontend-2/components/automate/automation/create-dialog/SelectFunctionStep.vue b/packages/frontend-2/components/automate/automation/create-dialog/SelectFunctionStep.vue index f6d8254cf..46c327ae7 100644 --- a/packages/frontend-2/components/automate/automation/create-dialog/SelectFunctionStep.vue +++ b/packages/frontend-2/components/automate/automation/create-dialog/SelectFunctionStep.vue @@ -43,13 +43,19 @@ import type { Optional } from '@speckle/shared' import { usePaginatedQuery } from '~/lib/common/composables/graphql' const searchQuery = graphql(` - query AutomationCreateDialogFunctionsSearch($search: String, $cursor: String = null) { - automateFunctions(limit: 20, filter: { search: $search }, cursor: $cursor) { - cursor - totalCount - items { - id - ...AutomateAutomationCreateDialog_AutomateFunction + query AutomationCreateDialogFunctionsSearch( + $workspaceId: String! + $search: String + $cursor: String = null + ) { + workspace(id: $workspaceId) { + automateFunctions(limit: 20, filter: { search: $search }, cursor: $cursor) { + cursor + totalCount + items { + id + ...AutomateAutomationCreateDialog_AutomateFunction + } } } } @@ -57,6 +63,7 @@ const searchQuery = graphql(` const props = withDefaults( defineProps<{ + workspaceId?: string preselectedFunction: Optional pageSize?: Optional showLabel?: Optional @@ -83,10 +90,11 @@ const { } = usePaginatedQuery({ query: searchQuery, baseVariables: computed(() => ({ - search: search.value?.length ? search.value : null + workspaceId: props.workspaceId ?? '', + search: search.value?.length ? search.value : '' })), resolveKey: (vars) => [vars.search || ''], - resolveCurrentResult: (res) => res?.automateFunctions, + resolveCurrentResult: (res) => res?.workspace?.automateFunctions, resolveNextPageVariables: (baseVars, cursor) => ({ ...baseVars, cursor @@ -94,7 +102,9 @@ const { resolveCursorFromVariables: (vars) => vars.cursor }) -const queryItems = computed(() => result.value?.automateFunctions.items) +const queryItems = computed(() => { + return result.value?.workspace?.automateFunctions.items +}) const items = computed(() => { const baseItems = (queryItems.value || []).slice(0, props.pageSize) const preselectedFn = props.preselectedFunction diff --git a/packages/frontend-2/components/automate/function/CreateDialog.vue b/packages/frontend-2/components/automate/function/CreateDialog.vue index d47ed751e..de18f06c1 100644 --- a/packages/frontend-2/components/automate/function/CreateDialog.vue +++ b/packages/frontend-2/components/automate/function/CreateDialog.vue @@ -56,7 +56,10 @@ import { } from '~/lib/common/helpers/route' import { useEnumSteps, useEnumStepsWidgetSetup } from '~/lib/form/composables/steps' import { useForm } from 'vee-validate' -import { useCreateAutomateFunction } from '~/lib/automate/composables/management' +import { + useCreateAutomateFunction, + useUpdateAutomateFunction +} from '~/lib/automate/composables/management' import { useMutationLoading } from '@vue/apollo-composable' import type { AutomateFunctionCreateDialogDoneStep_AutomateFunctionFragment } from '~~/lib/common/generated/gql/graphql' import { useMixpanel } from '~/lib/core/composables/mp' @@ -74,6 +77,7 @@ const props = defineProps<{ isAuthorized: boolean templates: CreatableFunctionTemplate[] githubOrgs: string[] + workspaceId?: string }>() const open = defineModel('open', { required: true }) @@ -81,6 +85,7 @@ const mixpanel = useMixpanel() const logger = useLogger() const mutationLoading = useMutationLoading() const createFunction = useCreateAutomateFunction() +const updateFunction = useUpdateAutomateFunction() const { handleSubmit: handleDetailsSubmit } = useForm() const onDetailsSubmit = handleDetailsSubmit(async (values) => { if (!selectedTemplate.value) { @@ -99,15 +104,29 @@ const onDetailsSubmit = handleDetailsSubmit(async (values) => { } }) - if (res?.id) { - mixpanel.track('Automate Function Created', { - functionId: res.id, - templateId: selectedTemplate.value.id, - name: values.name - }) - createdFunction.value = res - step.value++ + if (!res?.id) { + // TODO: Error toast with butter + return } + + mixpanel.track('Automate Function Created', { + functionId: res.id, + templateId: selectedTemplate.value.id, + name: values.name + }) + createdFunction.value = res + step.value++ + + if (!props.workspaceId) { + return + } + + await updateFunction({ + input: { + id: res.id, + workspaceIds: [props.workspaceId] + } + }) }) const onSubmit = computed(() => { diff --git a/packages/frontend-2/components/automate/function/EditDialog.vue b/packages/frontend-2/components/automate/function/EditDialog.vue index d903c18ce..d4faaf47f 100644 --- a/packages/frontend-2/components/automate/function/EditDialog.vue +++ b/packages/frontend-2/components/automate/function/EditDialog.vue @@ -18,10 +18,12 @@ import { difference, differenceBy } from 'lodash-es' import { useForm } from 'vee-validate' import { useUpdateAutomateFunction } from '~/lib/automate/composables/management' import type { FunctionDetailsFormValues } from '~/lib/automate/helpers/functions' +import type { Workspace } from '~/lib/common/generated/gql/graphql' const props = defineProps<{ model: FunctionDetailsFormValues fnId: string + workspaces?: Pick[] }>() const open = defineModel('open', { required: true }) const { handleSubmit, setValues } = useForm() @@ -53,6 +55,7 @@ const onSubmit = handleSubmit(async (values) => { values.description !== props.model.description ? values.description : null, logo: values.image !== props.model.image ? values.image : null, tags: difference(values.tags, props.model.tags || []).length ? values.tags : null, + workspaceIds: values.workspace ? [values.workspace.id] : [], supportedSourceApps: differenceBy( values.allowedSourceApps, props.model.allowedSourceApps || [], diff --git a/packages/frontend-2/components/automate/function/create-dialog/DetailsStep.vue b/packages/frontend-2/components/automate/function/create-dialog/DetailsStep.vue index f9d9c538f..bf0aa467e 100644 --- a/packages/frontend-2/components/automate/function/create-dialog/DetailsStep.vue +++ b/packages/frontend-2/components/automate/function/create-dialog/DetailsStep.vue @@ -33,6 +33,30 @@ :rules="descriptionRules" validate-on-value-update /> + + + + import { ValidationHelpers } from '@speckle/ui-components' import { isArray } from 'lodash-es' +import { graphql } from '~/lib/common/generated/gql' +import type { Workspace } from '~/lib/common/generated/gql/graphql' + +graphql(` + fragment AutomateFunctionCreateDialog_Workspace on Workspace { + id + name + } +`) defineProps<{ githubOrgs?: string[] + workspaces?: Pick[] }>() const avatarEditMode = ref(false) diff --git a/packages/frontend-2/components/automate/function/page/Header.vue b/packages/frontend-2/components/automate/function/page/Header.vue index f0336c0ca..5c03b9f6a 100644 --- a/packages/frontend-2/components/automate/function/page/Header.vue +++ b/packages/frontend-2/components/automate/function/page/Header.vue @@ -11,23 +11,10 @@

{{ fn.name }}

- - Edit -
-
- - Use in automation +
+ + Edit
@@ -40,11 +27,6 @@ import { automationFunctionsRoute } from '~/lib/common/helpers/route' -defineEmits<{ - createAutomation: [] - edit: [] -}>() - graphql(` fragment AutomateFunctionPageHeader_Function on AutomateFunction { id @@ -59,13 +41,17 @@ graphql(` releases(limit: 1) { totalCount } + workspaceIds } `) -const props = defineProps<{ +defineProps<{ fn: AutomateFunctionPageHeader_FunctionFragment isOwner: boolean }>() -const hasReleases = computed(() => props.fn.releases.totalCount > 0) +defineEmits<{ + createAutomation: [] + edit: [] +}>() diff --git a/packages/frontend-2/components/automate/functions/page/Header.vue b/packages/frontend-2/components/automate/functions/page/Header.vue index 86d6e471a..1cded9174 100644 --- a/packages/frontend-2/components/automate/functions/page/Header.vue +++ b/packages/frontend-2/components/automate/functions/page/Header.vue @@ -1,27 +1,30 @@ + diff --git a/packages/frontend-2/components/dashboard/Sidebar.vue b/packages/frontend-2/components/dashboard/Sidebar.vue index 90a4de260..c098b0a86 100644 --- a/packages/frontend-2/components/dashboard/Sidebar.vue +++ b/packages/frontend-2/components/dashboard/Sidebar.vue @@ -193,8 +193,23 @@ import { useActiveUser } from '~~/lib/auth/composables/activeUser' import { HomeIcon } from '@heroicons/vue/24/outline' import { useMixpanel } from '~~/lib/core/composables/mp' import { Roles } from '@speckle/shared' +import { graphql } from '~/lib/common/generated/gql' import { WorkspacePlanStatuses } from '~/lib/common/generated/gql/graphql' +graphql(` + fragment Sidebar_User on User { + id + automateFunctions { + items { + id + name + description + logo + } + } + } +`) + const { isLoggedIn } = useActiveUser() const isWorkspacesEnabled = useIsWorkspacesEnabled() const route = useRoute() diff --git a/packages/frontend-2/components/global/icon/Bolt.vue b/packages/frontend-2/components/global/icon/Bolt.vue new file mode 100644 index 000000000..35be1f097 --- /dev/null +++ b/packages/frontend-2/components/global/icon/Bolt.vue @@ -0,0 +1,18 @@ + diff --git a/packages/frontend-2/components/project/page/automation/Functions.vue b/packages/frontend-2/components/project/page/automation/Functions.vue index 3a5a274c8..0b0678b72 100644 --- a/packages/frontend-2/components/project/page/automation/Functions.vue +++ b/packages/frontend-2/components/project/page/automation/Functions.vue @@ -7,7 +7,7 @@ :key="fn.fn.id" :fn="fn.fn" :is-outdated="isOutdated(fn)" - show-edit + :show-edit="isEditable" @edit="onEdit(fn.fn)" /> @@ -65,6 +65,7 @@ graphql(` const props = defineProps<{ projectId: string automation: ProjectPageAutomationFunctions_AutomationFragment + isEditable: boolean }>() const dialogOpen = ref(false) diff --git a/packages/frontend-2/components/project/page/automation/Header.vue b/packages/frontend-2/components/project/page/automation/Header.vue index a7e9a4e69..67c1e158c 100644 --- a/packages/frontend-2/components/project/page/automation/Header.vue +++ b/packages/frontend-2/components/project/page/automation/Header.vue @@ -8,7 +8,7 @@
() const switchId = useId() diff --git a/packages/frontend-2/components/project/page/automation/Models.vue b/packages/frontend-2/components/project/page/automation/Models.vue index 15c0afcdb..e466940dc 100644 --- a/packages/frontend-2/components/project/page/automation/Models.vue +++ b/packages/frontend-2/components/project/page/automation/Models.vue @@ -44,7 +44,7 @@ graphql(` `) graphql(` - fragment ProjectPageAutomationHeader_Project on Project { + fragment ProjectPageAutomationModels_Project on Project { id ...ProjectPageModelsCardProject } diff --git a/packages/frontend-2/components/project/page/automation/Runs.vue b/packages/frontend-2/components/project/page/automation/Runs.vue index 5ea42de33..d86186cdb 100644 --- a/packages/frontend-2/components/project/page/automation/Runs.vue +++ b/packages/frontend-2/components/project/page/automation/Runs.vue @@ -3,7 +3,7 @@

Runs

@@ -47,6 +47,7 @@ graphql(` const props = defineProps<{ automation: ProjectPageAutomationRuns_AutomationFragment projectId: string + isEditable: boolean }>() const { identifier, onInfiniteLoad } = usePaginatedQuery({ diff --git a/packages/frontend-2/components/project/page/automations/EmptyState.vue b/packages/frontend-2/components/project/page/automations/EmptyState.vue index cc9f38cc2..f5b339897 100644 --- a/packages/frontend-2/components/project/page/automations/EmptyState.vue +++ b/packages/frontend-2/components/project/page/automations/EmptyState.vue @@ -9,9 +9,9 @@ faults, and effortlessly creating delivery artifacts.
-
+
New automation @@ -38,9 +38,9 @@
-

Featured functions

- - Explore all +

Public functions

+ + {{ functionGalleryLabel }}
@@ -58,12 +58,15 @@ diff --git a/packages/frontend-2/components/project/page/automations/Header.vue b/packages/frontend-2/components/project/page/automations/Header.vue index 3a52949dd..410789fa5 100644 --- a/packages/frontend-2/components/project/page/automations/Header.vue +++ b/packages/frontend-2/components/project/page/automations/Header.vue @@ -13,34 +13,47 @@ v-bind="bind" v-on="on" /> -
+ + {{ exploreFunctionsMessage }} + +
New automation
- - Explore functions -
diff --git a/packages/frontend-2/components/project/page/automations/Tab.vue b/packages/frontend-2/components/project/page/automations/Tab.vue index 854219802..4d867651f 100644 --- a/packages/frontend-2/components/project/page/automations/Tab.vue +++ b/packages/frontend-2/components/project/page/automations/Tab.vue @@ -2,10 +2,9 @@
@@ -57,6 +58,13 @@ const pageQuery = graphql(` automateFunction(id: $functionId) { ...AutomateFunctionPage_AutomateFunction } + activeUser { + workspaces { + items { + ...AutomateFunctionCreateDialog_Workspace + } + } + } } `) @@ -92,6 +100,9 @@ const isOwner = computed( activeUser.value.id === fn.value.creator.id ) ) +const activeUserWorkspaces = computed( + () => result.value?.activeUser?.workspaces.items ?? [] +) const { html: plaintextDescription } = useMarkdown( computed(() => fn.value?.description || ''), @@ -102,6 +113,8 @@ const editModel = computed((): Optional => { const func = fn.value if (!func) return undefined + const workspaceId = func.workspaceIds?.at(0) + return { name: func.name, description: func.description, @@ -109,7 +122,10 @@ const editModel = computed((): Optional => { allowedSourceApps: SourceApps.filter((app) => func.supportedSourceApps.includes(app.name) ), - tags: func.tags + tags: func.tags, + workspace: activeUserWorkspaces.value.find( + (workspace) => workspace.id === workspaceId + ) } }) diff --git a/packages/frontend-2/pages/functions/index.vue b/packages/frontend-2/pages/functions/index.vue index b3840e9de..6266092df 100644 --- a/packages/frontend-2/pages/functions/index.vue +++ b/packages/frontend-2/pages/functions/index.vue @@ -1,12 +1,24 @@