Automate Public Beta (#3472)

* 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 <gergo@jedlicska.com>
This commit is contained in:
Chuck Driesler
2024-11-29 16:33:14 +00:00
committed by GitHub
parent 0f13b8d2ca
commit e312110933
45 changed files with 1006 additions and 190 deletions
@@ -43,6 +43,7 @@
:show-label="false"
:show-required="false"
:preselected-function="validatedPreselectedFunction"
:workspace-id="workspaceId"
/>
<AutomateAutomationCreateDialogFunctionParametersStep
v-else-if="
@@ -66,6 +67,7 @@
v-model:selected-function="selectedFunction"
:preselected-function="validatedPreselectedFunction"
:page-size="2"
:workspace-id="workspaceId"
/>
</template>
</div>
@@ -124,6 +126,7 @@ graphql(`
`)
const props = defineProps<{
workspaceId?: string
preselectedFunction?: Optional<CreateAutomationSelectableFunction>
preselectedProject?: Optional<FormSelectProjects_ProjectFragment>
}>()
@@ -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<CreateAutomationSelectableFunction>
pageSize?: Optional<number>
showLabel?: Optional<boolean>
@@ -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
@@ -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<boolean>('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<DetailsFormValues>()
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(() => {
@@ -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<Workspace, 'id' | 'name'>[]
}>()
const open = defineModel<boolean>('open', { required: true })
const { handleSubmit, setValues } = useForm<FunctionDetailsFormValues>()
@@ -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 || [],
@@ -33,6 +33,30 @@
:rules="descriptionRules"
validate-on-value-update
/>
<FormSelectBase
v-if="workspaces?.length"
name="workspace"
label="Workspace"
placeholder="Select a workspace"
show-label
allow-unset
clearable
help="Allow automations in one of your workspaces to use this function."
:items="workspaces"
>
<template #something-selected="{ value }">
<div class="label label--light">
{{ isArray(value) ? value[0].name : value.name }}
</div>
</template>
<template #option="{ item, selected }">
<div class="flex flex-col">
<div :class="['label label--light', selected ? 'text-primary' : '']">
{{ item.name }}
</div>
</div>
</template>
</FormSelectBase>
<FormSelectSourceApps
name="allowedSourceApps"
label="Supported source apps"
@@ -85,9 +109,19 @@
<script setup lang="ts">
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<Workspace, 'id' | 'name'>[]
}>()
const avatarEditMode = ref(false)
@@ -11,23 +11,10 @@
<div class="flex items-center gap-4">
<AutomateFunctionLogo :logo="fn.logo" />
<h1 class="text-heading-lg">{{ fn.name }}</h1>
<FormButton v-if="isOwner" size="sm" text class="mt-1" @click="$emit('edit')">
Edit
</FormButton>
</div>
<div
v-tippy="
hasReleases ? undefined : 'Your function needs to have at least one release'
"
class="flex gap-2 shrink-0"
>
<FormButton
class="shrink-0"
full-width
:disabled="!hasReleases"
@click="$emit('createAutomation')"
>
Use in automation
<div class="flex items-center align-center gap-2">
<FormButton v-if="isOwner" color="outline" @click="$emit('edit')">
Edit
</FormButton>
</div>
</div>
@@ -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: []
}>()
</script>
@@ -1,27 +1,30 @@
<template>
<div>
<Portal to="navigation">
<template v-if="!!workspace?.slug && !!workspace?.name">
<HeaderNavLink
:to="workspaceRoute(workspace.slug)"
:name="workspace.name"
:separator="false"
/>
</template>
<HeaderNavLink
:separator="false"
:to="automationFunctionsRoute"
name="Automate functions"
:seperator="false"
:to="workspaceFunctionsRoute(workspace?.slug!)"
name="Functions"
:separator="!!workspace"
/>
</Portal>
<div class="pt-4 flex flex-col md:flex-row gap-y-2 md:gap-x-4 md:justify-between">
<h1 class="text-heading-lg">Automate functions</h1>
<div class="flex flex-row gap-2">
<div class="flex-1">
<FormTextInput
name="search"
placeholder="Search functions..."
show-clear
color="foundation"
class="grow"
v-bind="bind"
v-on="on"
/>
</div>
<div class="flex flex-col md:flex-row gap-y-2 md:gap-x-4 md:justify-between">
<div class="w-full flex flex-row justify-between gap-2">
<FormTextInput
name="search"
placeholder="Search..."
show-clear
color="foundation"
class="grow"
v-bind="bind"
v-on="on"
/>
<FormButton :disabled="!canCreateFunction" @click="createDialogOpen = true">
New function
</FormButton>
@@ -32,20 +35,26 @@
:is-authorized="!!activeUser?.automateInfo.hasAutomateGithubApp"
:github-orgs="activeUser?.automateInfo.availableGithubOrgs || []"
:templates="availableTemplates"
:workspace-id="workspace?.id"
/>
</div>
</template>
<script setup lang="ts">
import type { Nullable, Optional } from '@speckle/shared'
import { Roles, type Nullable, type Optional } from '@speckle/shared'
import { useDebouncedTextInput } from '@speckle/ui-components'
import { graphql } from '~/lib/common/generated/gql'
import type { AutomateFunctionsPageHeader_QueryFragment } from '~/lib/common/generated/gql/graphql'
import { automationFunctionsRoute } from '~/lib/common/helpers/route'
import type {
AutomateFunctionsPageHeader_QueryFragment,
Workspace
} from '~/lib/common/generated/gql/graphql'
import { workspaceFunctionsRoute, workspaceRoute } from '~/lib/common/helpers/route'
graphql(`
fragment AutomateFunctionsPageHeader_Query on Query {
activeUser {
id
role
automateInfo {
hasAutomateGithubApp
availableGithubOrgs
@@ -64,6 +73,7 @@ graphql(`
const props = defineProps<{
activeUser: Optional<AutomateFunctionsPageHeader_QueryFragment['activeUser']>
serverInfo: Optional<AutomateFunctionsPageHeader_QueryFragment['serverInfo']>
workspace?: Pick<Workspace, 'id' | 'slug' | 'name'>
}>()
const search = defineModel<string>('search')
@@ -77,9 +87,11 @@ const createDialogOpen = ref(false)
const availableTemplates = computed(
() => props.serverInfo?.automate.availableFunctionTemplates || []
)
const canCreateFunction = computed(
() => !!props.activeUser?.id && !!availableTemplates.value.length
)
const canCreateFunction = computed(() => {
return props.workspace
? !!props.activeUser?.id && !!availableTemplates.value.length
: props.activeUser?.role === Roles.Server.Admin
})
if (import.meta.client) {
watch(
@@ -18,7 +18,10 @@
</template>
<script setup lang="ts">
import { graphql } from '~/lib/common/generated/gql'
import type { AutomateFunctionsPageItems_QueryFragment } from '~/lib/common/generated/gql/graphql'
import type {
AutomateAutomationCreateDialog_AutomateFunctionFragment,
AutomationsFunctionsCard_AutomateFunctionFragment
} from '~/lib/common/generated/gql/graphql'
import type { CreateAutomationSelectableFunction } from '~/lib/automate/helpers/automations'
defineEmits<{
@@ -28,7 +31,7 @@ defineEmits<{
graphql(`
fragment AutomateFunctionsPageItems_Query on Query {
automateFunctions(limit: 20, filter: { search: $search }, cursor: $cursor) {
automateFunctions(limit: 6, filter: { search: $search }, cursor: $cursor) {
totalCount
items {
id
@@ -41,10 +44,11 @@ graphql(`
`)
const props = defineProps<{
functions?: AutomateFunctionsPageItems_QueryFragment
functions?: (AutomationsFunctionsCard_AutomateFunctionFragment &
AutomateAutomationCreateDialog_AutomateFunctionFragment)[]
search?: boolean
loading?: boolean
}>()
const fns = computed(() => props.functions?.automateFunctions.items || [])
const fns = computed(() => props.functions || [])
</script>
@@ -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()
@@ -0,0 +1,18 @@
<template>
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="m3.75 13.5 10.5-11.25L12 10.5h8.25L9.75 21.75 12 13.5H3.75Z"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
vector-effect="non-scaling-stroke"
/>
</svg>
</template>
@@ -7,7 +7,7 @@
:key="fn.fn.id"
:fn="fn.fn"
:is-outdated="isOutdated(fn)"
show-edit
:show-edit="isEditable"
@edit="onEdit(fn.fn)"
/>
</AutomateFunctionCardView>
@@ -65,6 +65,7 @@ graphql(`
const props = defineProps<{
projectId: string
automation: ProjectPageAutomationFunctions_AutomationFragment
isEditable: boolean
}>()
const dialogOpen = ref(false)
@@ -8,7 +8,7 @@
<div class="flex flow-row justify-start items-center z-20">
<CommonEditableTitle
v-model="name"
:disabled="loading"
:disabled="loading || !isEditable"
:custom-classes="{
input: 'h4',
pencil: 'ml-2 mt-2 w-4 h-4'
@@ -24,7 +24,7 @@
</div>
</div>
<FormSwitch
v-if="!automation.isTestAutomation"
v-if="!automation.isTestAutomation && isEditable"
:id="switchId"
v-model="enabled"
name="enable"
@@ -69,6 +69,7 @@ graphql(`
graphql(`
fragment ProjectPageAutomationHeader_Project on Project {
id
role
...ProjectPageModelsCardProject
}
`)
@@ -76,6 +77,7 @@ graphql(`
const props = defineProps<{
project: ProjectPageAutomationHeader_ProjectFragment
automation: ProjectPageAutomationHeader_AutomationFragment
isEditable: boolean
}>()
const switchId = useId()
@@ -44,7 +44,7 @@ graphql(`
`)
graphql(`
fragment ProjectPageAutomationHeader_Project on Project {
fragment ProjectPageAutomationModels_Project on Project {
id
...ProjectPageModelsCardProject
}
@@ -3,7 +3,7 @@
<div class="flex items-center justify-between h-6 mb-6">
<h2 class="h6 font-medium">Runs</h2>
<FormButton
v-if="!automation.isTestAutomation"
v-if="!automation.isTestAutomation && isEditable"
:disabled="!automation.enabled"
@click="onTrigger"
>
@@ -47,6 +47,7 @@ graphql(`
const props = defineProps<{
automation: ProjectPageAutomationRuns_AutomationFragment
projectId: string
isEditable: boolean
}>()
const { identifier, onInfiniteLoad } = usePaginatedQuery({
@@ -9,9 +9,9 @@
faults, and effortlessly creating delivery artifacts.
</div>
<div class="flex gap-x-4">
<div v-if="isAutomateEnabled" v-tippy="creationDisabledReason">
<div v-if="isAutomateEnabled" v-tippy="creationDisabledMessage">
<FormButton
:disabled="!!creationDisabledReason"
:disabled="!!creationDisabledMessage"
@click="$emit('new-automation')"
>
New automation
@@ -38,9 +38,9 @@
</div>
<div v-if="isAutomateEnabled" class="flex flex-col gap-6">
<div class="flex gap-2 flex-row justify-between items-center">
<h2 class="text-heading-lg text-foreground">Featured functions</h2>
<FormButton color="outline" class="shrink-0" :to="automationFunctionsRoute">
Explore all
<h2 class="text-heading-lg text-foreground">Public functions</h2>
<FormButton color="outline" class="shrink-0" :to="functionsGalleryRoute">
{{ functionGalleryLabel }}
</FormButton>
</div>
<AutomateFunctionCardView v-if="functions.length">
@@ -58,12 +58,15 @@
<script setup lang="ts">
import { graphql } from '~/lib/common/generated/gql'
import type { ProjectPageAutomationsEmptyState_QueryFragment } from '~/lib/common/generated/gql/graphql'
import { automationFunctionsRoute } from '~/lib/common/helpers/route'
import {
automationFunctionsRoute,
workspaceFunctionsRoute
} from '~/lib/common/helpers/route'
import type { CreateAutomationSelectableFunction } from '~/lib/automate/helpers/automations'
graphql(`
fragment ProjectPageAutomationsEmptyState_Query on Query {
automateFunctions(limit: 9, filter: { featuredFunctionsOnly: true }) {
automateFunctions(limit: 9) {
items {
...AutomationsFunctionsCard_AutomateFunction
...AutomateAutomationCreateDialog_AutomateFunction
@@ -78,9 +81,19 @@ defineEmits<{
const props = defineProps<{
functions?: ProjectPageAutomationsEmptyState_QueryFragment
workspaceSlug?: string
isAutomateEnabled: boolean
creationDisabledReason?: string
creationDisabledMessage?: string
}>()
const functions = computed(() => props.functions?.automateFunctions.items || [])
const functionGalleryLabel = computed(() =>
props.workspaceSlug ? 'View workspace functions' : 'Explore all'
)
const functionsGalleryRoute = computed(() =>
props.workspaceSlug
? workspaceFunctionsRoute(props.workspaceSlug)
: automationFunctionsRoute
)
</script>
@@ -13,34 +13,47 @@
v-bind="bind"
v-on="on"
/>
<div v-tippy="creationDisabledReason" class="shrink-0">
<FormButton color="outline" class="shrink-0" :to="exploreFunctionsRoute">
{{ exploreFunctionsMessage }}
</FormButton>
<div v-tippy="creationDisabledMessage" class="shrink-0">
<FormButton
class="shrink-0"
:disabled="!!creationDisabledReason"
:disabled="!!creationDisabledMessage"
@click="$emit('new-automation')"
>
New automation
</FormButton>
</div>
<FormButton color="outline" class="shrink-0" :to="automationFunctionsRoute">
Explore functions
</FormButton>
</div>
</div>
</template>
<script setup lang="ts">
import { useDebouncedTextInput } from '@speckle/ui-components'
import { automationFunctionsRoute } from '~/lib/common/helpers/route'
import {
automationFunctionsRoute,
workspaceFunctionsRoute
} from '~/lib/common/helpers/route'
defineEmits<{
'new-automation': []
}>()
defineProps<{
const props = defineProps<{
workspaceSlug?: string
showEmptyState?: boolean
creationDisabledReason?: string
creationDisabledMessage?: string
}>()
const exploreFunctionsMessage = computed(() =>
props.workspaceSlug ? 'View functions' : 'Explore functions'
)
const exploreFunctionsRoute = computed(() =>
props.workspaceSlug
? workspaceFunctionsRoute(props.workspaceSlug)
: automationFunctionsRoute
)
const search = defineModel<string>('search')
const { on, bind } = useDebouncedTextInput({ model: search })
</script>
@@ -2,10 +2,9 @@
<div class="flex flex-col gap-y-4 md:gap-y-6">
<ProjectPageAutomationsHeader
v-model:search="search"
:workspace-slug="workspaceSlug"
:show-empty-state="shouldShowEmptyState"
:creation-disabled-reason="
allowNewCreation !== true ? allowNewCreation : undefined
"
:creation-disabled-message="disableCreateMessage"
@new-automation="onNewAutomation"
/>
<template v-if="loading">
@@ -14,11 +13,10 @@
<template v-else>
<ProjectPageAutomationsEmptyState
v-if="shouldShowEmptyState"
:workspace-slug="workspaceSlug"
:functions="result"
:is-automate-enabled="isAutomateEnabled"
:creation-disabled-reason="
allowNewCreation !== true ? allowNewCreation : undefined
"
:creation-disabled-message="disableCreateMessage"
@new-automation="onNewAutomation"
/>
<template v-else>
@@ -39,7 +37,9 @@
</template>
</template>
<AutomateAutomationCreateDialog
v-if="workspaceId"
v-model:open="showNewAutomationDialog"
:workspace-id="workspaceId"
:preselected-project="project"
:preselected-function="newAutomationTargetFn"
/>
@@ -53,6 +53,7 @@ import {
} from '~/lib/projects/graphql/queries'
import type { CreateAutomationSelectableFunction } from '~/lib/automate/helpers/automations'
import { usePaginatedQuery } from '~/lib/common/composables/graphql'
import { Roles } from '@speckle/shared'
const route = useRoute()
const projectId = computed(() => route.params.id as string)
@@ -74,6 +75,9 @@ const { result, loading } = useQuery(
})
)
const workspaceId = computed(() => result.value?.project?.workspace?.id)
const workspaceSlug = computed(() => result.value?.project?.workspace?.slug)
// Pagination query
const {
identifier,
@@ -114,10 +118,18 @@ const shouldShowEmptyState = computed(() => {
return false
})
const allowNewCreation = computed(() => {
return (result.value?.project?.models?.items.length || 0) > 0
? true
: 'Your project should have at least 1 model before you can create an automation.'
const disableCreateMessage = computed(() => {
const allowedRoles: string[] = [Roles.Stream.Owner]
if (!allowedRoles.includes(result.value?.project?.role ?? '')) {
return 'You must be a project owner to create automations.'
}
if ((result.value?.project?.models?.items.length || 0) === 0) {
return 'Your project should have at least 1 model before you can create an automation.'
}
return undefined
})
const onNewAutomation = (fn?: CreateAutomationSelectableFunction) => {
@@ -48,3 +48,16 @@ export const automateFunctionsPagePaginationQuery = graphql(`
...AutomateFunctionsPageItems_Query
}
`)
export const activeUserFunctionsQuery = graphql(`
query ActiveUserFunctions {
activeUser {
automateFunctions(limit: 2) {
items {
id
...AutomationsFunctionsCard_AutomateFunction
}
}
}
}
`)
@@ -3,7 +3,10 @@ import type {
Nullable,
SourceAppDefinition
} from '@speckle/shared'
import type { AutomateFunctionCreateDialogTemplateStep_AutomateFunctionTemplateFragment } from '~/lib/common/generated/gql/graphql'
import type {
AutomateFunctionCreateDialogTemplateStep_AutomateFunctionTemplateFragment,
Workspace
} from '~/lib/common/generated/gql/graphql'
export type CreatableFunctionTemplate =
AutomateFunctionCreateDialogTemplateStep_AutomateFunctionTemplateFragment
@@ -15,6 +18,7 @@ export type FunctionDetailsFormValues = {
allowedSourceApps?: SourceAppDefinition[]
tags?: string[]
org?: string
workspace?: Pick<Workspace, 'id' | 'name'>
}
export const cleanFunctionLogo = (
@@ -25,15 +25,16 @@ const documents = {
"\n fragment AuthThirdPartyLoginButtonOIDC_ServerInfo on ServerInfo {\n authStrategies {\n id\n name\n }\n }\n": types.AuthThirdPartyLoginButtonOidc_ServerInfoFragmentDoc,
"\n fragment AutomateAutomationCreateDialog_AutomateFunction on AutomateFunction {\n id\n ...AutomationsFunctionsCard_AutomateFunction\n ...AutomateAutomationCreateDialogFunctionParametersStep_AutomateFunction\n }\n": types.AutomateAutomationCreateDialog_AutomateFunctionFragmentDoc,
"\n fragment AutomateAutomationCreateDialogFunctionParametersStep_AutomateFunction on AutomateFunction {\n id\n releases(limit: 1) {\n items {\n id\n inputSchema\n }\n }\n }\n": types.AutomateAutomationCreateDialogFunctionParametersStep_AutomateFunctionFragmentDoc,
"\n query AutomationCreateDialogFunctionsSearch($search: String, $cursor: String = null) {\n automateFunctions(limit: 20, filter: { search: $search }, cursor: $cursor) {\n cursor\n totalCount\n items {\n id\n ...AutomateAutomationCreateDialog_AutomateFunction\n }\n }\n }\n": types.AutomationCreateDialogFunctionsSearchDocument,
"\n query AutomationCreateDialogFunctionsSearch(\n $workspaceId: String!\n $search: String\n $cursor: String = null\n ) {\n workspace(id: $workspaceId) {\n automateFunctions(limit: 20, filter: { search: $search }, cursor: $cursor) {\n cursor\n totalCount\n items {\n id\n ...AutomateAutomationCreateDialog_AutomateFunction\n }\n }\n }\n }\n": types.AutomationCreateDialogFunctionsSearchDocument,
"\n fragment AutomationsFunctionsCard_AutomateFunction on AutomateFunction {\n id\n name\n isFeatured\n description\n logo\n repo {\n id\n url\n owner\n name\n }\n }\n": types.AutomationsFunctionsCard_AutomateFunctionFragmentDoc,
"\n fragment AutomateFunctionCreateDialog_Workspace on Workspace {\n id\n name\n }\n": types.AutomateFunctionCreateDialog_WorkspaceFragmentDoc,
"\n fragment AutomateFunctionCreateDialogDoneStep_AutomateFunction on AutomateFunction {\n id\n repo {\n id\n url\n owner\n name\n }\n ...AutomationsFunctionsCard_AutomateFunction\n }\n": types.AutomateFunctionCreateDialogDoneStep_AutomateFunctionFragmentDoc,
"\n fragment AutomateFunctionCreateDialogTemplateStep_AutomateFunctionTemplate on AutomateFunctionTemplate {\n id\n title\n logo\n url\n }\n": types.AutomateFunctionCreateDialogTemplateStep_AutomateFunctionTemplateFragmentDoc,
"\n fragment AutomateFunctionPageHeader_Function on AutomateFunction {\n id\n name\n logo\n repo {\n id\n url\n owner\n name\n }\n releases(limit: 1) {\n totalCount\n }\n }\n": types.AutomateFunctionPageHeader_FunctionFragmentDoc,
"\n fragment AutomateFunctionPageHeader_Function on AutomateFunction {\n id\n name\n logo\n repo {\n id\n url\n owner\n name\n }\n releases(limit: 1) {\n totalCount\n }\n workspaceIds\n }\n": types.AutomateFunctionPageHeader_FunctionFragmentDoc,
"\n fragment AutomateFunctionPageInfo_AutomateFunction on AutomateFunction {\n id\n repo {\n id\n url\n owner\n name\n }\n description\n releases(limit: 1) {\n items {\n id\n inputSchema\n createdAt\n commitId\n ...AutomateFunctionPageParametersDialog_AutomateFunctionRelease\n }\n }\n }\n": types.AutomateFunctionPageInfo_AutomateFunctionFragmentDoc,
"\n fragment AutomateFunctionPageParametersDialog_AutomateFunctionRelease on AutomateFunctionRelease {\n id\n inputSchema\n }\n": types.AutomateFunctionPageParametersDialog_AutomateFunctionReleaseFragmentDoc,
"\n fragment AutomateFunctionsPageHeader_Query on Query {\n activeUser {\n id\n automateInfo {\n hasAutomateGithubApp\n availableGithubOrgs\n }\n }\n serverInfo {\n automate {\n availableFunctionTemplates {\n ...AutomateFunctionCreateDialogTemplateStep_AutomateFunctionTemplate\n }\n }\n }\n }\n": types.AutomateFunctionsPageHeader_QueryFragmentDoc,
"\n fragment AutomateFunctionsPageItems_Query on Query {\n automateFunctions(limit: 20, filter: { search: $search }, cursor: $cursor) {\n totalCount\n items {\n id\n ...AutomationsFunctionsCard_AutomateFunction\n ...AutomateAutomationCreateDialog_AutomateFunction\n }\n cursor\n }\n }\n": types.AutomateFunctionsPageItems_QueryFragmentDoc,
"\n fragment AutomateFunctionsPageHeader_Query on Query {\n activeUser {\n id\n role\n automateInfo {\n hasAutomateGithubApp\n availableGithubOrgs\n }\n }\n serverInfo {\n automate {\n availableFunctionTemplates {\n ...AutomateFunctionCreateDialogTemplateStep_AutomateFunctionTemplate\n }\n }\n }\n }\n": types.AutomateFunctionsPageHeader_QueryFragmentDoc,
"\n fragment AutomateFunctionsPageItems_Query on Query {\n automateFunctions(limit: 6, filter: { search: $search }, cursor: $cursor) {\n totalCount\n items {\n id\n ...AutomationsFunctionsCard_AutomateFunction\n ...AutomateAutomationCreateDialog_AutomateFunction\n }\n cursor\n }\n }\n": types.AutomateFunctionsPageItems_QueryFragmentDoc,
"\n fragment AutomateRunsTriggerStatus_TriggeredAutomationsStatus on TriggeredAutomationsStatus {\n id\n ...TriggeredAutomationsStatusSummary\n ...AutomateRunsTriggerStatusDialog_TriggeredAutomationsStatus\n }\n": types.AutomateRunsTriggerStatus_TriggeredAutomationsStatusFragmentDoc,
"\n fragment AutomateRunsTriggerStatusDialog_TriggeredAutomationsStatus on TriggeredAutomationsStatus {\n id\n automationRuns {\n id\n ...AutomateRunsTriggerStatusDialogRunsRows_AutomateRun\n }\n }\n": types.AutomateRunsTriggerStatusDialog_TriggeredAutomationsStatusFragmentDoc,
"\n fragment AutomateRunsTriggerStatusDialogFunctionRun_AutomateFunctionRun on AutomateFunctionRun {\n id\n results\n status\n statusMessage\n contextView\n function {\n id\n logo\n name\n }\n createdAt\n updatedAt\n }\n": types.AutomateRunsTriggerStatusDialogFunctionRun_AutomateFunctionRunFragmentDoc,
@@ -43,6 +44,7 @@ const documents = {
"\n fragment BillingAlert_Workspace on Workspace {\n id\n plan {\n name\n status\n createdAt\n }\n subscription {\n billingInterval\n currentBillingCycleEnd\n }\n }\n": types.BillingAlert_WorkspaceFragmentDoc,
"\n fragment CommonModelSelectorModel on Model {\n id\n name\n }\n": types.CommonModelSelectorModelFragmentDoc,
"\n fragment DashboardProjectCard_Project on Project {\n id\n name\n role\n updatedAt\n models {\n totalCount\n }\n team {\n user {\n ...LimitedUserAvatar\n }\n }\n workspace {\n id\n slug\n name\n ...WorkspaceAvatar_Workspace\n }\n }\n": types.DashboardProjectCard_ProjectFragmentDoc,
"\n fragment Sidebar_User on User {\n id\n automateFunctions {\n items {\n id\n name\n description\n logo\n }\n }\n }\n": types.Sidebar_UserFragmentDoc,
"\n fragment FormSelectModels_Model on Model {\n id\n name\n }\n": types.FormSelectModels_ModelFragmentDoc,
"\n fragment FormSelectProjects_Project on Project {\n id\n name\n }\n": types.FormSelectProjects_ProjectFragmentDoc,
"\n fragment FormUsersSelectItem on LimitedUser {\n id\n name\n avatar\n }\n": types.FormUsersSelectItemFragmentDoc,
@@ -61,9 +63,10 @@ const documents = {
"\n fragment ProjectPageAutomationFunctionSettingsDialog_AutomationRevision on AutomationRevision {\n id\n triggerDefinitions {\n ... on VersionCreatedTriggerDefinition {\n type\n model {\n id\n ...CommonModelSelectorModel\n }\n }\n }\n }\n": types.ProjectPageAutomationFunctionSettingsDialog_AutomationRevisionFragmentDoc,
"\n fragment ProjectPageAutomationFunctions_Automation on Automation {\n id\n currentRevision {\n id\n ...ProjectPageAutomationFunctionSettingsDialog_AutomationRevision\n functions {\n release {\n id\n function {\n id\n ...AutomationsFunctionsCard_AutomateFunction\n releases(limit: 1) {\n items {\n id\n }\n }\n }\n }\n ...ProjectPageAutomationFunctionSettingsDialog_AutomationRevisionFunction\n }\n }\n }\n": types.ProjectPageAutomationFunctions_AutomationFragmentDoc,
"\n fragment ProjectPageAutomationHeader_Automation on Automation {\n id\n name\n enabled\n isTestAutomation\n currentRevision {\n id\n triggerDefinitions {\n ... on VersionCreatedTriggerDefinition {\n model {\n ...ProjectPageLatestItemsModelItem\n }\n }\n }\n }\n }\n": types.ProjectPageAutomationHeader_AutomationFragmentDoc,
"\n fragment ProjectPageAutomationHeader_Project on Project {\n id\n ...ProjectPageModelsCardProject\n }\n": types.ProjectPageAutomationHeader_ProjectFragmentDoc,
"\n fragment ProjectPageAutomationHeader_Project on Project {\n id\n role\n ...ProjectPageModelsCardProject\n }\n": types.ProjectPageAutomationHeader_ProjectFragmentDoc,
"\n fragment ProjectPageAutomationModels_Project on Project {\n id\n ...ProjectPageModelsCardProject\n }\n": types.ProjectPageAutomationModels_ProjectFragmentDoc,
"\n fragment ProjectPageAutomationRuns_Automation on Automation {\n id\n name\n enabled\n isTestAutomation\n runs(limit: 10) {\n items {\n ...AutomationRunDetails\n }\n totalCount\n cursor\n }\n }\n": types.ProjectPageAutomationRuns_AutomationFragmentDoc,
"\n fragment ProjectPageAutomationsEmptyState_Query on Query {\n automateFunctions(limit: 9, filter: { featuredFunctionsOnly: true }) {\n items {\n ...AutomationsFunctionsCard_AutomateFunction\n ...AutomateAutomationCreateDialog_AutomateFunction\n }\n }\n }\n": types.ProjectPageAutomationsEmptyState_QueryFragmentDoc,
"\n fragment ProjectPageAutomationsEmptyState_Query on Query {\n automateFunctions(limit: 9) {\n items {\n ...AutomationsFunctionsCard_AutomateFunction\n ...AutomateAutomationCreateDialog_AutomateFunction\n }\n }\n }\n": types.ProjectPageAutomationsEmptyState_QueryFragmentDoc,
"\n fragment ProjectPageAutomationsRow_Automation on Automation {\n id\n name\n enabled\n isTestAutomation\n currentRevision {\n id\n triggerDefinitions {\n ... on VersionCreatedTriggerDefinition {\n model {\n id\n name\n }\n }\n }\n }\n runs(limit: 10) {\n totalCount\n items {\n ...AutomationRunDetails\n }\n cursor\n }\n }\n": types.ProjectPageAutomationsRow_AutomationFragmentDoc,
"\n fragment ProjectDiscussionsPageHeader_Project on Project {\n id\n name\n }\n": types.ProjectDiscussionsPageHeader_ProjectFragmentDoc,
"\n fragment ProjectDiscussionsPageResults_Project on Project {\n id\n }\n": types.ProjectDiscussionsPageResults_ProjectFragmentDoc,
@@ -168,6 +171,7 @@ const documents = {
"\n query FunctionAccessCheck($id: ID!) {\n automateFunction(id: $id) {\n id\n }\n }\n": types.FunctionAccessCheckDocument,
"\n query ProjectAutomationCreationPublicKeys(\n $projectId: String!\n $automationId: String!\n ) {\n project(id: $projectId) {\n id\n automation(id: $automationId) {\n id\n creationPublicKeys\n }\n }\n }\n": types.ProjectAutomationCreationPublicKeysDocument,
"\n query AutomateFunctionsPagePagination($search: String, $cursor: String) {\n ...AutomateFunctionsPageItems_Query\n }\n": types.AutomateFunctionsPagePaginationDocument,
"\n query ActiveUserFunctions {\n activeUser {\n automateFunctions(limit: 2) {\n items {\n id\n ...AutomationsFunctionsCard_AutomateFunction\n }\n }\n }\n }\n": types.ActiveUserFunctionsDocument,
"\n mutation BillingCreateCheckoutSession($input: CheckoutSessionInput!) {\n workspaceMutations {\n billing {\n createCheckoutSession(input: $input) {\n url\n id\n }\n }\n }\n }\n": types.BillingCreateCheckoutSessionDocument,
"\n mutation BillingUpgradePlan($input: UpgradePlanInput!) {\n workspaceMutations {\n billing {\n upgradePlan(input: $input)\n }\n }\n }\n": types.BillingUpgradePlanDocument,
"\n query MentionsUserSearch($query: String!, $emailOnly: Boolean = false) {\n userSearch(\n query: $query\n limit: 5\n cursor: null\n archived: false\n emailOnly: $emailOnly\n ) {\n items {\n id\n name\n company\n }\n }\n }\n": types.MentionsUserSearchDocument,
@@ -251,7 +255,7 @@ const documents = {
"\n query ProjectModelVersions(\n $projectId: String!\n $modelId: String!\n $versionsCursor: String\n ) {\n project(id: $projectId) {\n id\n ...ProjectModelPageVersionsPagination\n }\n }\n": types.ProjectModelVersionsDocument,
"\n query ProjectModelsPage($projectId: String!) {\n project(id: $projectId) {\n id\n ...ProjectModelsPageHeader_Project\n ...ProjectModelsPageResults_Project\n }\n }\n": types.ProjectModelsPageDocument,
"\n query ProjectDiscussionsPage($projectId: String!) {\n project(id: $projectId) {\n id\n ...ProjectDiscussionsPageHeader_Project\n ...ProjectDiscussionsPageResults_Project\n }\n }\n": types.ProjectDiscussionsPageDocument,
"\n query ProjectAutomationsTab($projectId: String!) {\n project(id: $projectId) {\n id\n models(limit: 1) {\n items {\n id\n }\n }\n automations(filter: null, cursor: null, limit: 5) {\n totalCount\n items {\n id\n ...ProjectPageAutomationsRow_Automation\n }\n cursor\n }\n ...FormSelectProjects_Project\n }\n ...ProjectPageAutomationsEmptyState_Query\n }\n": types.ProjectAutomationsTabDocument,
"\n query ProjectAutomationsTab($projectId: String!) {\n project(id: $projectId) {\n id\n role\n models(limit: 1) {\n items {\n id\n }\n }\n automations(filter: null, cursor: null, limit: 5) {\n totalCount\n items {\n id\n ...ProjectPageAutomationsRow_Automation\n }\n cursor\n }\n workspace {\n id\n slug\n }\n ...FormSelectProjects_Project\n }\n ...ProjectPageAutomationsEmptyState_Query\n }\n": types.ProjectAutomationsTabDocument,
"\n query ProjectAutomationsTabAutomationsPagination(\n $projectId: String!\n $search: String = null\n $cursor: String = null\n ) {\n project(id: $projectId) {\n id\n automations(filter: $search, cursor: $cursor, limit: 5) {\n totalCount\n cursor\n items {\n id\n ...ProjectPageAutomationsRow_Automation\n }\n }\n }\n }\n": types.ProjectAutomationsTabAutomationsPaginationDocument,
"\n query ProjectAutomationPage($projectId: String!, $automationId: String!) {\n project(id: $projectId) {\n id\n ...ProjectPageAutomationPage_Project\n automation(id: $automationId) {\n id\n ...ProjectPageAutomationPage_Automation\n }\n }\n }\n": types.ProjectAutomationPageDocument,
"\n query ProjectAutomationPagePaginatedRuns(\n $projectId: String!\n $automationId: String!\n $cursor: String = null\n ) {\n project(id: $projectId) {\n id\n automation(id: $automationId) {\n id\n runs(cursor: $cursor, limit: 10) {\n totalCount\n cursor\n items {\n id\n ...AutomationRunDetails\n }\n }\n }\n }\n }\n": types.ProjectAutomationPagePaginatedRunsDocument,
@@ -296,6 +300,7 @@ const documents = {
"\n mutation SettingsLeaveWorkspace($leaveId: ID!) {\n workspaceMutations {\n leave(id: $leaveId)\n }\n }\n": types.SettingsLeaveWorkspaceDocument,
"\n mutation SettingsBillingCancelCheckoutSession($input: CancelCheckoutSessionInput!) {\n workspaceMutations {\n billing {\n cancelCheckoutSession(input: $input)\n }\n }\n }\n": types.SettingsBillingCancelCheckoutSessionDocument,
"\n query SettingsSidebar {\n activeUser {\n ...SettingsDialog_User\n }\n }\n": types.SettingsSidebarDocument,
"\n query SettingsSidebarAutomateFunctions {\n activeUser {\n ...Sidebar_User\n }\n }\n": types.SettingsSidebarAutomateFunctionsDocument,
"\n query SettingsWorkspaceGeneral($id: String!) {\n workspace(id: $id) {\n ...SettingsWorkspacesGeneral_Workspace\n }\n }\n": types.SettingsWorkspaceGeneralDocument,
"\n query SettingsWorkspaceBilling($workspaceId: String!) {\n workspace(id: $workspaceId) {\n id\n ...SettingsWorkspacesBilling_Workspace\n }\n }\n": types.SettingsWorkspaceBillingDocument,
"\n query SettingsWorkspaceBillingCustomerPortal($workspaceId: String!) {\n workspace(id: $workspaceId) {\n customerPortalUrl\n }\n }\n": types.SettingsWorkspaceBillingCustomerPortalDocument,
@@ -345,6 +350,7 @@ const documents = {
"\n query WorkspaceAccessCheck($slug: String!) {\n workspaceBySlug(slug: $slug) {\n id\n }\n }\n": types.WorkspaceAccessCheckDocument,
"\n query WorkspacePageQuery(\n $workspaceSlug: String!\n $invitesFilter: PendingWorkspaceCollaboratorsFilter\n $token: String\n ) {\n workspaceBySlug(slug: $workspaceSlug) {\n ...WorkspaceProjectList_Workspace\n }\n workspaceInvite(\n workspaceId: $workspaceSlug\n token: $token\n options: { useSlug: true }\n ) {\n id\n ...WorkspaceInviteBanner_PendingWorkspaceCollaborator\n ...WorkspaceInviteBlock_PendingWorkspaceCollaborator\n }\n }\n": types.WorkspacePageQueryDocument,
"\n query WorkspaceProjectsQuery(\n $workspaceSlug: String!\n $filter: WorkspaceProjectsFilter\n $cursor: String\n ) {\n workspaceBySlug(slug: $workspaceSlug) {\n id\n projects(filter: $filter, cursor: $cursor, limit: 10) {\n ...WorkspaceProjectList_ProjectCollection\n }\n }\n }\n": types.WorkspaceProjectsQueryDocument,
"\n query WorkspaceFunctionsQuery($workspaceSlug: String!) {\n ...AutomateFunctionsPageHeader_Query\n workspaceBySlug(slug: $workspaceSlug) {\n id\n name\n automateFunctions {\n items {\n id\n ...AutomationsFunctionsCard_AutomateFunction\n ...AutomateAutomationCreateDialog_AutomateFunction\n }\n }\n }\n }\n": types.WorkspaceFunctionsQueryDocument,
"\n query WorkspaceInvite(\n $workspaceId: String\n $token: String\n $options: WorkspaceInviteLookupOptions\n ) {\n workspaceInvite(workspaceId: $workspaceId, token: $token, options: $options) {\n ...WorkspaceInviteBanner_PendingWorkspaceCollaborator\n ...WorkspaceInviteBlock_PendingWorkspaceCollaborator\n }\n }\n": types.WorkspaceInviteDocument,
"\n query MoveProjectsDialog {\n activeUser {\n ...MoveProjectsDialog_User\n }\n }\n": types.MoveProjectsDialogDocument,
"\n query ValidateWorkspaceSlug($slug: String!) {\n validateWorkspaceSlug(slug: $slug)\n }\n": types.ValidateWorkspaceSlugDocument,
@@ -357,7 +363,7 @@ const documents = {
"\n query AutoAcceptableWorkspaceInvite(\n $token: String!\n $workspaceId: String!\n $options: WorkspaceInviteLookupOptions\n ) {\n workspaceInvite(token: $token, workspaceId: $workspaceId, options: $options) {\n id\n ...UseWorkspaceInviteManager_PendingWorkspaceCollaborator\n }\n }\n": types.AutoAcceptableWorkspaceInviteDocument,
"\n query ResolveCommentLink($commentId: String!, $projectId: String!) {\n project(id: $projectId) {\n comment(id: $commentId) {\n id\n ...LinkableComment\n }\n }\n }\n": types.ResolveCommentLinkDocument,
"\n fragment AutomateFunctionPage_AutomateFunction on AutomateFunction {\n id\n name\n description\n logo\n supportedSourceApps\n tags\n ...AutomateFunctionPageHeader_Function\n ...AutomateFunctionPageInfo_AutomateFunction\n ...AutomateAutomationCreateDialog_AutomateFunction\n creator {\n id\n }\n }\n": types.AutomateFunctionPage_AutomateFunctionFragmentDoc,
"\n query AutomateFunctionPage($functionId: ID!) {\n automateFunction(id: $functionId) {\n ...AutomateFunctionPage_AutomateFunction\n }\n }\n": types.AutomateFunctionPageDocument,
"\n query AutomateFunctionPage($functionId: ID!) {\n automateFunction(id: $functionId) {\n ...AutomateFunctionPage_AutomateFunction\n }\n activeUser {\n workspaces {\n items {\n ...AutomateFunctionCreateDialog_Workspace\n }\n }\n }\n }\n": types.AutomateFunctionPageDocument,
"\n query AutomateFunctionsPage($search: String, $cursor: String = null) {\n ...AutomateFunctionsPageItems_Query\n ...AutomateFunctionsPageHeader_Query\n }\n": types.AutomateFunctionsPageDocument,
"\n fragment ProjectPageProject on Project {\n id\n createdAt\n modelCount: models(limit: 0) {\n totalCount\n }\n commentThreadCount: commentThreads(limit: 0) {\n totalCount\n }\n workspace {\n id\n }\n ...ProjectPageTeamInternals_Project\n ...ProjectPageProjectHeader\n ...ProjectPageTeamDialog\n ...ProjectsMoveToWorkspaceDialog_Project\n }\n": types.ProjectPageProjectFragmentDoc,
"\n fragment ProjectPageAutomationPage_Automation on Automation {\n id\n ...ProjectPageAutomationHeader_Automation\n ...ProjectPageAutomationFunctions_Automation\n ...ProjectPageAutomationRuns_Automation\n }\n": types.ProjectPageAutomationPage_AutomationFragmentDoc,
@@ -426,11 +432,15 @@ export function graphql(source: "\n fragment AutomateAutomationCreateDialogFunc
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n query AutomationCreateDialogFunctionsSearch($search: String, $cursor: String = null) {\n automateFunctions(limit: 20, filter: { search: $search }, cursor: $cursor) {\n cursor\n totalCount\n items {\n id\n ...AutomateAutomationCreateDialog_AutomateFunction\n }\n }\n }\n"): (typeof documents)["\n query AutomationCreateDialogFunctionsSearch($search: String, $cursor: String = null) {\n automateFunctions(limit: 20, filter: { search: $search }, cursor: $cursor) {\n cursor\n totalCount\n items {\n id\n ...AutomateAutomationCreateDialog_AutomateFunction\n }\n }\n }\n"];
export function graphql(source: "\n query AutomationCreateDialogFunctionsSearch(\n $workspaceId: String!\n $search: String\n $cursor: String = null\n ) {\n workspace(id: $workspaceId) {\n automateFunctions(limit: 20, filter: { search: $search }, cursor: $cursor) {\n cursor\n totalCount\n items {\n id\n ...AutomateAutomationCreateDialog_AutomateFunction\n }\n }\n }\n }\n"): (typeof documents)["\n query AutomationCreateDialogFunctionsSearch(\n $workspaceId: String!\n $search: String\n $cursor: String = null\n ) {\n workspace(id: $workspaceId) {\n automateFunctions(limit: 20, filter: { search: $search }, cursor: $cursor) {\n cursor\n totalCount\n items {\n id\n ...AutomateAutomationCreateDialog_AutomateFunction\n }\n }\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n fragment AutomationsFunctionsCard_AutomateFunction on AutomateFunction {\n id\n name\n isFeatured\n description\n logo\n repo {\n id\n url\n owner\n name\n }\n }\n"): (typeof documents)["\n fragment AutomationsFunctionsCard_AutomateFunction on AutomateFunction {\n id\n name\n isFeatured\n description\n logo\n repo {\n id\n url\n owner\n name\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n fragment AutomateFunctionCreateDialog_Workspace on Workspace {\n id\n name\n }\n"): (typeof documents)["\n fragment AutomateFunctionCreateDialog_Workspace on Workspace {\n id\n name\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
@@ -442,7 +452,7 @@ export function graphql(source: "\n fragment AutomateFunctionCreateDialogTempla
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n fragment AutomateFunctionPageHeader_Function on AutomateFunction {\n id\n name\n logo\n repo {\n id\n url\n owner\n name\n }\n releases(limit: 1) {\n totalCount\n }\n }\n"): (typeof documents)["\n fragment AutomateFunctionPageHeader_Function on AutomateFunction {\n id\n name\n logo\n repo {\n id\n url\n owner\n name\n }\n releases(limit: 1) {\n totalCount\n }\n }\n"];
export function graphql(source: "\n fragment AutomateFunctionPageHeader_Function on AutomateFunction {\n id\n name\n logo\n repo {\n id\n url\n owner\n name\n }\n releases(limit: 1) {\n totalCount\n }\n workspaceIds\n }\n"): (typeof documents)["\n fragment AutomateFunctionPageHeader_Function on AutomateFunction {\n id\n name\n logo\n repo {\n id\n url\n owner\n name\n }\n releases(limit: 1) {\n totalCount\n }\n workspaceIds\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
@@ -454,11 +464,11 @@ export function graphql(source: "\n fragment AutomateFunctionPageParametersDial
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n fragment AutomateFunctionsPageHeader_Query on Query {\n activeUser {\n id\n automateInfo {\n hasAutomateGithubApp\n availableGithubOrgs\n }\n }\n serverInfo {\n automate {\n availableFunctionTemplates {\n ...AutomateFunctionCreateDialogTemplateStep_AutomateFunctionTemplate\n }\n }\n }\n }\n"): (typeof documents)["\n fragment AutomateFunctionsPageHeader_Query on Query {\n activeUser {\n id\n automateInfo {\n hasAutomateGithubApp\n availableGithubOrgs\n }\n }\n serverInfo {\n automate {\n availableFunctionTemplates {\n ...AutomateFunctionCreateDialogTemplateStep_AutomateFunctionTemplate\n }\n }\n }\n }\n"];
export function graphql(source: "\n fragment AutomateFunctionsPageHeader_Query on Query {\n activeUser {\n id\n role\n automateInfo {\n hasAutomateGithubApp\n availableGithubOrgs\n }\n }\n serverInfo {\n automate {\n availableFunctionTemplates {\n ...AutomateFunctionCreateDialogTemplateStep_AutomateFunctionTemplate\n }\n }\n }\n }\n"): (typeof documents)["\n fragment AutomateFunctionsPageHeader_Query on Query {\n activeUser {\n id\n role\n automateInfo {\n hasAutomateGithubApp\n availableGithubOrgs\n }\n }\n serverInfo {\n automate {\n availableFunctionTemplates {\n ...AutomateFunctionCreateDialogTemplateStep_AutomateFunctionTemplate\n }\n }\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n fragment AutomateFunctionsPageItems_Query on Query {\n automateFunctions(limit: 20, filter: { search: $search }, cursor: $cursor) {\n totalCount\n items {\n id\n ...AutomationsFunctionsCard_AutomateFunction\n ...AutomateAutomationCreateDialog_AutomateFunction\n }\n cursor\n }\n }\n"): (typeof documents)["\n fragment AutomateFunctionsPageItems_Query on Query {\n automateFunctions(limit: 20, filter: { search: $search }, cursor: $cursor) {\n totalCount\n items {\n id\n ...AutomationsFunctionsCard_AutomateFunction\n ...AutomateAutomationCreateDialog_AutomateFunction\n }\n cursor\n }\n }\n"];
export function graphql(source: "\n fragment AutomateFunctionsPageItems_Query on Query {\n automateFunctions(limit: 6, filter: { search: $search }, cursor: $cursor) {\n totalCount\n items {\n id\n ...AutomationsFunctionsCard_AutomateFunction\n ...AutomateAutomationCreateDialog_AutomateFunction\n }\n cursor\n }\n }\n"): (typeof documents)["\n fragment AutomateFunctionsPageItems_Query on Query {\n automateFunctions(limit: 6, filter: { search: $search }, cursor: $cursor) {\n totalCount\n items {\n id\n ...AutomationsFunctionsCard_AutomateFunction\n ...AutomateAutomationCreateDialog_AutomateFunction\n }\n cursor\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
@@ -495,6 +505,10 @@ export function graphql(source: "\n fragment CommonModelSelectorModel on Model
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n fragment DashboardProjectCard_Project on Project {\n id\n name\n role\n updatedAt\n models {\n totalCount\n }\n team {\n user {\n ...LimitedUserAvatar\n }\n }\n workspace {\n id\n slug\n name\n ...WorkspaceAvatar_Workspace\n }\n }\n"): (typeof documents)["\n fragment DashboardProjectCard_Project on Project {\n id\n name\n role\n updatedAt\n models {\n totalCount\n }\n team {\n user {\n ...LimitedUserAvatar\n }\n }\n workspace {\n id\n slug\n name\n ...WorkspaceAvatar_Workspace\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n fragment Sidebar_User on User {\n id\n automateFunctions {\n items {\n id\n name\n description\n logo\n }\n }\n }\n"): (typeof documents)["\n fragment Sidebar_User on User {\n id\n automateFunctions {\n items {\n id\n name\n description\n logo\n }\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
@@ -570,7 +584,11 @@ export function graphql(source: "\n fragment ProjectPageAutomationHeader_Automa
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n fragment ProjectPageAutomationHeader_Project on Project {\n id\n ...ProjectPageModelsCardProject\n }\n"): (typeof documents)["\n fragment ProjectPageAutomationHeader_Project on Project {\n id\n ...ProjectPageModelsCardProject\n }\n"];
export function graphql(source: "\n fragment ProjectPageAutomationHeader_Project on Project {\n id\n role\n ...ProjectPageModelsCardProject\n }\n"): (typeof documents)["\n fragment ProjectPageAutomationHeader_Project on Project {\n id\n role\n ...ProjectPageModelsCardProject\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n fragment ProjectPageAutomationModels_Project on Project {\n id\n ...ProjectPageModelsCardProject\n }\n"): (typeof documents)["\n fragment ProjectPageAutomationModels_Project on Project {\n id\n ...ProjectPageModelsCardProject\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
@@ -578,7 +596,7 @@ export function graphql(source: "\n fragment ProjectPageAutomationRuns_Automati
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n fragment ProjectPageAutomationsEmptyState_Query on Query {\n automateFunctions(limit: 9, filter: { featuredFunctionsOnly: true }) {\n items {\n ...AutomationsFunctionsCard_AutomateFunction\n ...AutomateAutomationCreateDialog_AutomateFunction\n }\n }\n }\n"): (typeof documents)["\n fragment ProjectPageAutomationsEmptyState_Query on Query {\n automateFunctions(limit: 9, filter: { featuredFunctionsOnly: true }) {\n items {\n ...AutomationsFunctionsCard_AutomateFunction\n ...AutomateAutomationCreateDialog_AutomateFunction\n }\n }\n }\n"];
export function graphql(source: "\n fragment ProjectPageAutomationsEmptyState_Query on Query {\n automateFunctions(limit: 9) {\n items {\n ...AutomationsFunctionsCard_AutomateFunction\n ...AutomateAutomationCreateDialog_AutomateFunction\n }\n }\n }\n"): (typeof documents)["\n fragment ProjectPageAutomationsEmptyState_Query on Query {\n automateFunctions(limit: 9) {\n items {\n ...AutomationsFunctionsCard_AutomateFunction\n ...AutomateAutomationCreateDialog_AutomateFunction\n }\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
@@ -995,6 +1013,10 @@ export function graphql(source: "\n query ProjectAutomationCreationPublicKeys(\
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n query AutomateFunctionsPagePagination($search: String, $cursor: String) {\n ...AutomateFunctionsPageItems_Query\n }\n"): (typeof documents)["\n query AutomateFunctionsPagePagination($search: String, $cursor: String) {\n ...AutomateFunctionsPageItems_Query\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n query ActiveUserFunctions {\n activeUser {\n automateFunctions(limit: 2) {\n items {\n id\n ...AutomationsFunctionsCard_AutomateFunction\n }\n }\n }\n }\n"): (typeof documents)["\n query ActiveUserFunctions {\n activeUser {\n automateFunctions(limit: 2) {\n items {\n id\n ...AutomationsFunctionsCard_AutomateFunction\n }\n }\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
@@ -1330,7 +1352,7 @@ export function graphql(source: "\n query ProjectDiscussionsPage($projectId: St
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n query ProjectAutomationsTab($projectId: String!) {\n project(id: $projectId) {\n id\n models(limit: 1) {\n items {\n id\n }\n }\n automations(filter: null, cursor: null, limit: 5) {\n totalCount\n items {\n id\n ...ProjectPageAutomationsRow_Automation\n }\n cursor\n }\n ...FormSelectProjects_Project\n }\n ...ProjectPageAutomationsEmptyState_Query\n }\n"): (typeof documents)["\n query ProjectAutomationsTab($projectId: String!) {\n project(id: $projectId) {\n id\n models(limit: 1) {\n items {\n id\n }\n }\n automations(filter: null, cursor: null, limit: 5) {\n totalCount\n items {\n id\n ...ProjectPageAutomationsRow_Automation\n }\n cursor\n }\n ...FormSelectProjects_Project\n }\n ...ProjectPageAutomationsEmptyState_Query\n }\n"];
export function graphql(source: "\n query ProjectAutomationsTab($projectId: String!) {\n project(id: $projectId) {\n id\n role\n models(limit: 1) {\n items {\n id\n }\n }\n automations(filter: null, cursor: null, limit: 5) {\n totalCount\n items {\n id\n ...ProjectPageAutomationsRow_Automation\n }\n cursor\n }\n workspace {\n id\n slug\n }\n ...FormSelectProjects_Project\n }\n ...ProjectPageAutomationsEmptyState_Query\n }\n"): (typeof documents)["\n query ProjectAutomationsTab($projectId: String!) {\n project(id: $projectId) {\n id\n role\n models(limit: 1) {\n items {\n id\n }\n }\n automations(filter: null, cursor: null, limit: 5) {\n totalCount\n items {\n id\n ...ProjectPageAutomationsRow_Automation\n }\n cursor\n }\n workspace {\n id\n slug\n }\n ...FormSelectProjects_Project\n }\n ...ProjectPageAutomationsEmptyState_Query\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
@@ -1507,6 +1529,10 @@ export function graphql(source: "\n mutation SettingsBillingCancelCheckoutSessi
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n query SettingsSidebar {\n activeUser {\n ...SettingsDialog_User\n }\n }\n"): (typeof documents)["\n query SettingsSidebar {\n activeUser {\n ...SettingsDialog_User\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n query SettingsSidebarAutomateFunctions {\n activeUser {\n ...Sidebar_User\n }\n }\n"): (typeof documents)["\n query SettingsSidebarAutomateFunctions {\n activeUser {\n ...Sidebar_User\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
@@ -1703,6 +1729,10 @@ export function graphql(source: "\n query WorkspacePageQuery(\n $workspaceSl
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n query WorkspaceProjectsQuery(\n $workspaceSlug: String!\n $filter: WorkspaceProjectsFilter\n $cursor: String\n ) {\n workspaceBySlug(slug: $workspaceSlug) {\n id\n projects(filter: $filter, cursor: $cursor, limit: 10) {\n ...WorkspaceProjectList_ProjectCollection\n }\n }\n }\n"): (typeof documents)["\n query WorkspaceProjectsQuery(\n $workspaceSlug: String!\n $filter: WorkspaceProjectsFilter\n $cursor: String\n ) {\n workspaceBySlug(slug: $workspaceSlug) {\n id\n projects(filter: $filter, cursor: $cursor, limit: 10) {\n ...WorkspaceProjectList_ProjectCollection\n }\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n query WorkspaceFunctionsQuery($workspaceSlug: String!) {\n ...AutomateFunctionsPageHeader_Query\n workspaceBySlug(slug: $workspaceSlug) {\n id\n name\n automateFunctions {\n items {\n id\n ...AutomationsFunctionsCard_AutomateFunction\n ...AutomateAutomationCreateDialog_AutomateFunction\n }\n }\n }\n }\n"): (typeof documents)["\n query WorkspaceFunctionsQuery($workspaceSlug: String!) {\n ...AutomateFunctionsPageHeader_Query\n workspaceBySlug(slug: $workspaceSlug) {\n id\n name\n automateFunctions {\n items {\n id\n ...AutomationsFunctionsCard_AutomateFunction\n ...AutomateAutomationCreateDialog_AutomateFunction\n }\n }\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
@@ -1754,7 +1784,7 @@ export function graphql(source: "\n fragment AutomateFunctionPage_AutomateFunct
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n query AutomateFunctionPage($functionId: ID!) {\n automateFunction(id: $functionId) {\n ...AutomateFunctionPage_AutomateFunction\n }\n }\n"): (typeof documents)["\n query AutomateFunctionPage($functionId: ID!) {\n automateFunction(id: $functionId) {\n ...AutomateFunctionPage_AutomateFunction\n }\n }\n"];
export function graphql(source: "\n query AutomateFunctionPage($functionId: ID!) {\n automateFunction(id: $functionId) {\n ...AutomateFunctionPage_AutomateFunction\n }\n activeUser {\n workspaces {\n items {\n ...AutomateFunctionCreateDialog_Workspace\n }\n }\n }\n }\n"): (typeof documents)["\n query AutomateFunctionPage($functionId: ID!) {\n automateFunction(id: $functionId) {\n ...AutomateFunctionPage_AutomateFunction\n }\n activeUser {\n workspaces {\n items {\n ...AutomateFunctionCreateDialog_Workspace\n }\n }\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
File diff suppressed because one or more lines are too long
@@ -71,6 +71,8 @@ export const automationFunctionRoute = (functionId: string) =>
export const workspaceRoute = (slug: string) => `/workspaces/${slug}`
export const workspaceFunctionsRoute = (slug: string) => `/workspaces/${slug}/functions`
const buildNavigationComposable = (route: string) => () => {
const router = useRouter()
return (params?: { query?: LocationQueryRaw }) => {
@@ -235,6 +235,7 @@ export const projectAutomationsTabQuery = graphql(`
query ProjectAutomationsTab($projectId: String!) {
project(id: $projectId) {
id
role
models(limit: 1) {
items {
id
@@ -248,6 +249,10 @@ export const projectAutomationsTabQuery = graphql(`
}
cursor
}
workspace {
id
slug
}
...FormSelectProjects_Project
}
...ProjectPageAutomationsEmptyState_Query
@@ -8,6 +8,14 @@ export const settingsSidebarQuery = graphql(`
}
`)
export const settingsSidebarAutomateFunctionsQuery = graphql(`
query SettingsSidebarAutomateFunctions {
activeUser {
...Sidebar_User
}
}
`)
export const settingsWorkspaceGeneralQuery = graphql(`
query SettingsWorkspaceGeneral($id: String!) {
workspace(id: $id) {
@@ -44,6 +44,23 @@ export const workspaceProjectsQuery = graphql(`
}
`)
export const workspaceFunctionsQuery = graphql(`
query WorkspaceFunctionsQuery($workspaceSlug: String!) {
...AutomateFunctionsPageHeader_Query
workspaceBySlug(slug: $workspaceSlug) {
id
name
automateFunctions {
items {
id
...AutomationsFunctionsCard_AutomateFunction
...AutomateAutomationCreateDialog_AutomateFunction
}
}
}
}
`)
export const workspaceInviteQuery = graphql(`
query WorkspaceInvite(
$workspaceId: String
+17 -1
View File
@@ -21,6 +21,7 @@
v-if="editModel"
v-model:open="showEditDialog"
:model="editModel"
:workspaces="activeUserWorkspaces"
:fn-id="fn.id"
/>
</template>
@@ -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<FunctionDetailsFormValues> => {
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<FunctionDetailsFormValues> => {
allowedSourceApps: SourceApps.filter((app) =>
func.supportedSourceApps.includes(app.name)
),
tags: func.tags
tags: func.tags,
workspace: activeUserWorkspaces.value.find(
(workspace) => workspace.id === workspaceId
)
}
})
+26 -9
View File
@@ -1,12 +1,24 @@
<template>
<div>
<AutomateFunctionsPageHeader
v-model:search="search"
:active-user="result?.activeUser"
:server-info="result?.serverInfo"
class="mb-6"
/>
<CommonLoadingBar :loading="pageQueryLoading" client-only class="mb-2" />
<Portal to="navigation">
<HeaderNavLink
:to="automationFunctionsRoute"
name="Functions"
:separator="false"
/>
</Portal>
<div class="flex flex-col gap-4">
<div class="flex items-center gap-2 mb-2">
<IconBolt class="h-5 w-5" />
<h1 class="text-heading-lg">Functions</h1>
</div>
<AutomateFunctionsPageHeader
v-model:search="search"
:active-user="result?.activeUser"
:server-info="result?.serverInfo"
class="mb-6"
/>
</div>
<AutomateFunctionsPageItems
:functions="finalResult"
:search="!!search"
@@ -14,8 +26,8 @@
@create-automation-from="openCreateNewAutomation"
@clear-search="search = ''"
/>
<CommonLoadingBar :loading="pageQueryLoading" client-only class="mb-2" />
<InfiniteLoading :settings="{ identifier }" @infinite="onInfiniteLoad" />
<AutomateAutomationCreateDialog
v-model:open="showNewAutomationDialog"
:preselected-function="newAutomationTargetFn"
@@ -32,6 +44,7 @@ import {
usePaginatedQuery
} from '~/lib/common/composables/graphql'
import { graphql } from '~/lib/common/generated/gql'
import { automationFunctionsRoute } from '~/lib/common/helpers/route'
definePageMeta({
middleware: ['auth', 'requires-automate-enabled']
@@ -81,7 +94,11 @@ const {
const showNewAutomationDialog = ref(false)
const newAutomationTargetFn = ref<CreateAutomationSelectableFunction>()
const finalResult = computed(() => paginatedResult.value || result.value)
const finalResult = computed(
() =>
paginatedResult.value?.automateFunctions.items ||
result.value?.automateFunctions.items
)
const openCreateNewAutomation = (fn: CreateAutomationSelectableFunction) => {
newAutomationTargetFn.value = fn
@@ -212,7 +212,7 @@ const pageTabItems = computed((): LayoutPageTabItem[] => {
}
]
if (isOwner.value && isAutomateEnabled.value) {
if (isAutomateEnabled.value && project.value?.workspace) {
items.push({
title: 'Automations',
id: 'automations',
@@ -1,6 +1,10 @@
<template>
<div v-if="automation && project" class="flex flex-col gap-8 items-start">
<ProjectPageAutomationHeader :automation="automation" :project="project" />
<ProjectPageAutomationHeader
:automation="automation"
:project="project"
:is-editable="isEditable"
/>
<div class="grid grid-cols-1 xl:grid-cols-4 gap-6 w-full">
<div
@@ -9,6 +13,7 @@
<ProjectPageAutomationFunctions
:automation="automation"
:project-id="projectId"
:is-editable="isEditable"
/>
<ProjectPageAutomationModels :automation="automation" :project="project" />
</div>
@@ -16,6 +21,7 @@
class="col-span-1 xl:col-span-3"
:project-id="projectId"
:automation="automation"
:is-editable="isEditable"
/>
</div>
</div>
@@ -23,6 +29,7 @@
<div v-else />
</template>
<script setup lang="ts">
import { Roles } from '@speckle/shared'
import { useQuery } from '@vue/apollo-composable'
import { graphql } from '~/lib/common/generated/gql'
import { projectAutomationPageQuery } from '~/lib/projects/graphql/queries'
@@ -60,4 +67,8 @@ const { result, loading } = useQuery(
)
const automation = computed(() => result.value?.project.automation || null)
const project = computed(() => result.value?.project)
const isEditable = computed(() => {
const allowedRoles: string[] = [Roles.Stream.Owner]
return allowedRoles.includes(result.value?.project.role ?? '')
})
</script>
@@ -0,0 +1,82 @@
<template>
<div>
<div class="flex flex-col gap-4">
<div class="flex items-center gap-2 mb-2">
<IconBolt class="h-5 w-5" />
<h1 class="text-heading-lg">Workspace functions</h1>
</div>
<AutomateFunctionsPageHeader
v-model:search="search"
:active-user="workspaceFunctionsResult?.activeUser"
:server-info="workspaceFunctionsResult?.serverInfo"
:workspace="workspace"
class="mb-6"
/>
</div>
<AutomateFunctionsPageItems
:functions="workspaceFunctions"
:search="!!search"
:loading="false"
@create-automation-from="openCreateNewAutomation"
@clear-search="search = ''"
/>
<CommonLoadingBar :loading="workspaceFunctionsLoading" client-only class="mb-2" />
<AutomateAutomationCreateDialog
v-model:open="showNewAutomationDialog"
:workspace-id="workspace?.id"
:preselected-function="newAutomationTargetFn"
/>
</div>
</template>
<script setup lang="ts">
import { useQuery } from '@vue/apollo-composable'
import type { CreateAutomationSelectableFunction } from '~/lib/automate/helpers/automations'
import { usePageQueryStandardFetchPolicy } from '~/lib/common/composables/graphql'
import { workspaceFunctionsQuery } from '~/lib/workspaces/graphql/queries'
definePageMeta({
middleware: ['auth', 'requires-automate-enabled']
})
const route = useRoute()
const workspaceSlug = computed(() => route.params.slug as string)
const pageFetchPolicy = usePageQueryStandardFetchPolicy()
const { result: workspaceFunctionsResult, loading: workspaceFunctionsLoading } =
useQuery(
workspaceFunctionsQuery,
() => ({
workspaceSlug: workspaceSlug.value
}),
() => ({
fetchPolicy: pageFetchPolicy.value
})
)
const workspace = computed(() => {
const workspaceData = workspaceFunctionsResult.value?.workspaceBySlug
return workspaceData
? {
id: workspaceData.id,
name: workspaceData.name,
slug: workspaceSlug.value
}
: undefined
})
const workspaceFunctions = computed(
() => workspaceFunctionsResult.value?.workspaceBySlug?.automateFunctions?.items ?? []
)
const search = ref('')
const showNewAutomationDialog = ref(false)
const newAutomationTargetFn = ref<CreateAutomationSelectableFunction>()
const openCreateNewAutomation = (fn: CreateAutomationSelectableFunction) => {
newAutomationTargetFn.value = fn
showNewAutomationDialog.value = true
}
</script>
@@ -147,6 +147,12 @@ type AutomateFunction {
Only returned if user is a part of this speckle server
"""
creator: LimitedUser
workspaceIds: [String!]
}
type AutomateFunctionToken {
functionId: String!
functionToken: String!
}
type AutomateFunctionRelease {
@@ -212,7 +218,6 @@ input AutomateFunctionsFilter {
By default we skip functions without releases. Set this to true to include them.
"""
functionsWithoutReleases: Boolean
featuredFunctionsOnly: Boolean
}
input AutomateFunctionRunStatusReportInput {
@@ -246,6 +251,11 @@ input CreateAutomateFunctionInput {
org: String
}
input CreateAutomateFunctionWithoutVersionInput {
name: String!
description: String!
}
"""
Any null values will be ignored
"""
@@ -259,6 +269,7 @@ input UpdateAutomateFunctionInput {
supportedSourceApps: [String!]
tags: [String!]
logo: String
workspaceIds: [String!]
}
type UserAutomateInfo {
@@ -268,6 +279,11 @@ type UserAutomateInfo {
extend type User {
automateInfo: UserAutomateInfo! @isOwner
automateFunctions(
filter: AutomateFunctionsFilter
cursor: String
limit: Int
): AutomateFunctionCollection!
}
enum AutomateFunctionTemplateLanguage {
@@ -313,22 +329,28 @@ extend type ProjectMutations {
type AutomateMutations {
createFunction(input: CreateAutomateFunctionInput!): AutomateFunction!
@hasScope(scope: "automate-functions:write")
createFunctionWithoutVersion(
input: CreateAutomateFunctionWithoutVersionInput!
): AutomateFunctionToken!
@hasScope(scope: "automate-functions:write")
@hasServerRole(role: SERVER_ADMIN)
updateFunction(input: UpdateAutomateFunctionInput!): AutomateFunction!
@hasScope(scope: "automate-functions:write")
}
extend type Project {
automations(filter: String, cursor: String, limit: Int): AutomationCollection!
@hasStreamRole(role: STREAM_OWNER)
@hasStreamRole(role: STREAM_REVIEWER)
"""
Get a single automation by id. Error will be thrown if automation is not found or inaccessible.
"""
automation(id: String!): Automation! @hasStreamRole(role: STREAM_OWNER)
automation(id: String!): Automation! @hasStreamRole(role: STREAM_REVIEWER)
}
input AutomateAuthCodePayloadTest {
code: String!
userId: String!
workspaceId: String
action: String!
}
@@ -276,6 +276,11 @@ type Workspace {
cursor: String
filter: WorkspaceProjectsFilter
): ProjectCollection!
automateFunctions(
limit: Int! = 25
cursor: String
filter: AutomateFunctionsFilter
): AutomateFunctionCollection!
"""
Information about the workspace's SSO configuration and the current user's SSO session, if present
"""
@@ -63,7 +63,11 @@ const getApiUrl = (
if (options?.query) {
Object.entries(options.query).forEach(([key, val]) => {
if (isNullOrUndefined(val)) return
url.searchParams.append(key, val.toString())
try {
url.searchParams.append(key, val.toString())
} catch (e) {
console.log({ val })
}
})
}
@@ -300,6 +304,31 @@ export const createFunction = async ({
})
}
type CreateFunctionWithoutVersionBody = {
speckleServerAuthenticationPayload: AuthCodePayloadWithOrigin
functionName: string
description: string
}
type CreateFunctionWithoutVersionResponse = {
functionId: string
functionToken: string
}
export const createFunctionWithoutVersion = async ({
body
}: {
body: CreateFunctionWithoutVersionBody
}): Promise<CreateFunctionWithoutVersionResponse> => {
const url = getApiUrl('/api/v2/functions')
return await invokeJsonRequest<CreateFunctionWithoutVersionResponse>({
url,
method: 'post',
body,
retry: false
})
}
export type UpdateFunctionBody<AP extends AuthCodePayload = AuthCodePayloadWithOrigin> =
{
speckleServerAuthenticationPayload: AP
@@ -417,18 +446,21 @@ export type GetFunctionsResponse = {
items: FunctionWithVersionsSchemaType[]
}
export const getFunctions = async (params: {
export const getPublicFunctions = async (params: {
query?: {
query?: string
cursor?: string
limit?: number
functionsWithoutVersions?: boolean
featuredFunctionsOnly?: boolean
}
}) => {
const { query } = params
const url = getApiUrl(`/api/v1/functions`, { query })
const url = getApiUrl(`/api/v1/functions`, {
query: {
...query,
featuredFunctionsOnly: true
}
})
const result = await invokeJsonRequest<GetFunctionsResponse>({
url,
method: 'get'
@@ -437,6 +469,58 @@ export const getFunctions = async (params: {
return result
}
type GetUserFunctionsResponse = {
functions: FunctionWithVersionsSchemaType[]
}
export const getUserFunctions = async (params: {
userId: string
query?: {
query?: string
cursor?: string
limit?: number
}
body: {
speckleServerAuthenticationPayload: AuthCodePayloadWithOrigin
}
}): Promise<GetUserFunctionsResponse> => {
const { userId, query, body } = params
const url = getApiUrl(`/api/v2/users/${userId}/functions`, { query })
return await invokeJsonRequest({
url,
method: 'POST',
body,
retry: false
})
}
type GetWorkspaceFunctionsResponse = {
functions: FunctionWithVersionsSchemaType[]
}
export const getWorkspaceFunctions = async (params: {
workspaceId: string
query?: {
query?: string
cursor?: string
limit?: number
}
body: {
speckleServerAuthenticationPayload: AuthCodePayloadWithOrigin
}
}): Promise<GetWorkspaceFunctionsResponse> => {
const { workspaceId, query, body } = params
const url = getApiUrl(`/api/v2/workspaces/${workspaceId}/functions`, { query })
return await invokeJsonRequest({
url,
method: 'POST',
body,
retry: false
})
}
type UserGithubAuthStateResponse = {
userHasAuthorizedGitHubApp: boolean
}
@@ -66,16 +66,14 @@ const mocks: SpeckleModuleMocksConfig = FF_AUTOMATE_MODULE_ENABLED
version: store.get('Version') as any
},
Query: {
automateFunctions: (_parent, args) => {
automateFunctions: () => {
const forceZero = false
const count = forceZero ? 0 : faker.number.int({ min: 0, max: 20 })
const isFeatured = args.filter?.featuredFunctionsOnly
return {
cursor: null,
totalCount: count,
items: times(count, () => store.get('AutomateFunction', { isFeatured }))
items: times(count, () => store.get('AutomateFunction'))
} as any
},
automateFunction: (_parent, args) => {
@@ -1,13 +1,15 @@
import {
createFunction,
createFunctionWithoutVersion,
triggerAutomationRun,
updateFunction as execEngineUpdateFunction,
getFunction,
getFunctionRelease,
getFunctions,
getPublicFunctions,
getFunctionReleases,
getUserGithubAuthState,
getUserGithubOrganizations
getUserGithubOrganizations,
getUserFunctions
} from '@/modules/automate/clients/executionEngine'
import {
GetProjectAutomationsParams,
@@ -54,7 +56,13 @@ import {
} from '@/modules/core/graph/generated/graphql'
import { getGenericRedis } from '@/modules/shared/redis/redis'
import { createAutomation as clientCreateAutomation } from '@/modules/automate/clients/executionEngine'
import { Automate, Roles, isNullOrUndefined, isNonNullable } from '@speckle/shared'
import {
Automate,
Roles,
isNullOrUndefined,
isNonNullable,
removeNullOrUndefinedKeys
} from '@speckle/shared'
import { getFeatureFlags, getServerOrigin } from '@/modules/shared/helpers/envHelper'
import {
getBranchesByIdsFactory,
@@ -116,6 +124,7 @@ import {
storeTokenScopesFactory,
storeUserServerAppTokenFactory
} from '@/modules/core/repositories/tokens'
import { getEventBus } from '@/modules/shared/services/eventBus'
import { getProjectDbClient } from '@/modules/multiregion/dbSelector'
const { FF_AUTOMATE_MODULE_ENABLED } = getFeatureFlags()
@@ -326,7 +335,7 @@ export = (FF_AUTOMATE_MODULE_ENABLED
await authorizeResolver(
ctx.userId,
parent.projectId,
Roles.Stream.Owner,
Roles.Stream.Contributor,
ctx.resourceAccessRules
)
@@ -526,6 +535,24 @@ export = (FF_AUTOMATE_MODULE_ENABLED
return (await create({ input: args.input, userId: ctx.userId! }))
.graphqlReturn
},
async createFunctionWithoutVersion(_parent, args, ctx) {
const authCode = await createStoredAuthCodeFactory({
redis: getGenericRedis()
})({
userId: ctx.userId!,
action: AuthCodePayloadAction.CreateFunction
})
return await createFunctionWithoutVersion({
body: {
speckleServerAuthenticationPayload: {
...authCode,
origin: getServerOrigin()
},
functionName: args.input.name,
description: args.input.description
}
})
},
async updateFunction(_parent, args, ctx) {
const update = updateFunctionFactory({
updateFunction: execEngineUpdateFunction,
@@ -693,10 +720,12 @@ export = (FF_AUTOMATE_MODULE_ENABLED
Query: {
async automateValidateAuthCode(_parent, args) {
const validate = validateStoredAuthCodeFactory({
redis: getGenericRedis()
redis: getGenericRedis(),
emit: getEventBus().emit
})
const payload = removeNullOrUndefinedKeys(args.payload)
return await validate({
...args.payload,
...payload,
action: args.payload.action as AuthCodePayloadAction
})
},
@@ -712,14 +741,13 @@ export = (FF_AUTOMATE_MODULE_ENABLED
},
async automateFunctions(_parent, args) {
try {
const res = await getFunctions({
const res = await getPublicFunctions({
query: {
query: args.filter?.search || undefined,
cursor: args.cursor || undefined,
limit: isNullOrUndefined(args.limit) ? undefined : args.limit,
functionsWithoutVersions:
args.filter?.functionsWithoutReleases || undefined,
featuredFunctionsOnly: args.filter?.featuredFunctionsOnly || undefined
args.filter?.functionsWithoutReleases || undefined
}
})
@@ -747,7 +775,53 @@ export = (FF_AUTOMATE_MODULE_ENABLED
}
},
User: {
automateInfo: (parent) => ({ userId: parent.id })
automateInfo: (parent) => ({ userId: parent.id }),
automateFunctions: async (_parent, args, context) => {
try {
const authCode = await createStoredAuthCodeFactory({
redis: getGenericRedis()
})({
userId: context.userId!,
action: AuthCodePayloadAction.ListUserFunctions
})
const res = await getUserFunctions({
userId: context.userId!,
query: {
query: args.filter?.search || undefined,
cursor: args.cursor || undefined,
limit: isNullOrUndefined(args.limit) ? undefined : args.limit
},
body: {
speckleServerAuthenticationPayload: {
...authCode,
origin: getServerOrigin()
}
}
})
const items = res.functions.map(convertFunctionToGraphQLReturn)
return {
cursor: undefined,
totalCount: res.functions.length,
items
}
} catch (e) {
const isNotFound =
e instanceof ExecutionEngineFailedResponseError &&
e.response.statusMessage === 'FunctionNotFound'
if (e instanceof ExecutionEngineNetworkError || isNotFound) {
return {
cursor: null,
totalCount: 0,
items: []
}
}
throw e
}
}
},
UserAutomateInfo: {
hasAutomateGithubApp: async (parent, _args, ctx) => {
@@ -807,7 +881,7 @@ export = (FF_AUTOMATE_MODULE_ENABLED
await validateStreamAccess(
ctx.userId,
projectId,
Roles.Stream.Owner,
Roles.Stream.Contributor,
ctx.resourceAccessRules
)
return { projectId }
@@ -886,9 +960,6 @@ export = (FF_AUTOMATE_MODULE_ENABLED
Project: {
automation: () => {
throw new AutomateApiDisabledError()
},
automations: () => {
throw new AutomateApiDisabledError()
}
},
AutomateMutations: {
@@ -20,6 +20,7 @@ export type FunctionSchemaType = {
speckleUserId: string
speckleServerOrigin: string
}>
workspaceIds: string[]
}
export type FunctionReleaseSchemaType = {
@@ -28,6 +28,7 @@ export type AutomateFunctionGraphQLReturn = Pick<
| 'logo'
| 'tags'
| 'supportedSourceApps'
| 'workspaceIds'
> & {
functionCreator: Nullable<{
speckleUserId: string
@@ -30,7 +30,7 @@ export default (app: Application) => {
getStream: getStreamFactory({ db })
}),
validateStreamRoleBuilderFactory({ getRoles: getRolesFactory({ db }) })({
requiredRole: Roles.Stream.Owner
requiredRole: Roles.Stream.Reviewer
}),
validateResourceAccess
]),
@@ -1,6 +1,7 @@
import { automateLogger } from '@/logging/logging'
import { CreateStoredAuthCode } from '@/modules/automate/domain/operations'
import { AutomateAuthCodeHandshakeError } from '@/modules/automate/errors/management'
import { EventBus } from '@/modules/shared/services/eventBus'
import cryptoRandomString from 'crypto-random-string'
import Redis from 'ioredis'
import { get, has, isObjectLike } from 'lodash'
@@ -8,6 +9,8 @@ import { get, has, isObjectLike } from 'lodash'
export enum AuthCodePayloadAction {
CreateAutomation = 'createAutomation',
CreateFunction = 'createFunction',
ListWorkspaceFunctions = 'listWorkspaceFunctions',
ListUserFunctions = 'listUserFunctions',
BecomeFunctionAuthor = 'becomeFunctionAuthor',
GetAvailableGithubOrganizations = 'getAvailableGithubOrganizations',
UpdateFunction = 'updateFunction'
@@ -16,6 +19,7 @@ export enum AuthCodePayloadAction {
export type AuthCodePayload = {
code: string
userId: string
workspaceId?: string
action: AuthCodePayloadAction
}
@@ -44,8 +48,9 @@ export const createStoredAuthCodeFactory =
}
export const validateStoredAuthCodeFactory =
(deps: { redis: Redis }) => async (payload: AuthCodePayload) => {
const { redis } = deps
(deps: { redis: Redis; emit: EventBus['emit'] }) =>
async (payload: AuthCodePayload) => {
const { redis, emit } = deps
const potentialPayloadString = await redis.get(payload.code)
const potentialPayload: unknown = potentialPayloadString
@@ -62,6 +67,13 @@ export const validateStoredAuthCodeFactory =
throw new AutomateAuthCodeHandshakeError('Invalid automate auth payload')
}
if (payload.workspaceId) {
emit({
eventName: 'workspace.authorized',
payload: { userId: payload.userId, workspaceId: payload.workspaceId }
})
}
try {
await redis.del(payload.code)
} catch (e) {
@@ -99,7 +99,8 @@ export const convertFunctionToGraphQLReturn = (
logo: cleanFunctionLogo(fn.logo),
tags: fn.tags,
supportedSourceApps: fn.supportedSourceApps,
functionCreator: fn.functionCreator
functionCreator: fn.functionCreator,
workspaceIds: fn.workspaceIds
}
return ret
@@ -230,6 +231,8 @@ export const updateFunctionFactory =
}
})
console.log(JSON.stringify(apiResult, null, 2))
return convertFunctionToGraphQLReturn(apiResult)
}
@@ -232,6 +232,7 @@ export type AutomateAuthCodePayloadTest = {
action: Scalars['String']['input'];
code: Scalars['String']['input'];
userId: Scalars['String']['input'];
workspaceId?: InputMaybe<Scalars['String']['input']>;
};
export type AutomateFunction = {
@@ -248,6 +249,7 @@ export type AutomateFunction = {
/** SourceAppNames values from @speckle/shared. Empty array means - all of them */
supportedSourceApps: Array<Scalars['String']['output']>;
tags: Array<Scalars['String']['output']>;
workspaceIds?: Maybe<Array<Scalars['String']['output']>>;
};
@@ -329,8 +331,13 @@ export enum AutomateFunctionTemplateLanguage {
Typescript = 'TYPESCRIPT'
}
export type AutomateFunctionToken = {
__typename?: 'AutomateFunctionToken';
functionId: Scalars['String']['output'];
functionToken: Scalars['String']['output'];
};
export type AutomateFunctionsFilter = {
featuredFunctionsOnly?: InputMaybe<Scalars['Boolean']['input']>;
/** By default we skip functions without releases. Set this to true to include them. */
functionsWithoutReleases?: InputMaybe<Scalars['Boolean']['input']>;
search?: InputMaybe<Scalars['String']['input']>;
@@ -339,6 +346,7 @@ export type AutomateFunctionsFilter = {
export type AutomateMutations = {
__typename?: 'AutomateMutations';
createFunction: AutomateFunction;
createFunctionWithoutVersion: AutomateFunctionToken;
updateFunction: AutomateFunction;
};
@@ -348,6 +356,11 @@ export type AutomateMutationsCreateFunctionArgs = {
};
export type AutomateMutationsCreateFunctionWithoutVersionArgs = {
input: CreateAutomateFunctionWithoutVersionInput;
};
export type AutomateMutationsUpdateFunctionArgs = {
input: UpdateAutomateFunctionInput;
};
@@ -838,6 +851,11 @@ export type CreateAutomateFunctionInput = {
template: AutomateFunctionTemplateLanguage;
};
export type CreateAutomateFunctionWithoutVersionInput = {
description: Scalars['String']['input'];
name: Scalars['String']['input'];
};
export type CreateCommentInput = {
content: CommentContentInput;
projectId: Scalars['String']['input'];
@@ -3533,6 +3551,7 @@ export type UpdateAutomateFunctionInput = {
/** SourceAppNames values from @speckle/shared */
supportedSourceApps?: InputMaybe<Array<Scalars['String']['input']>>;
tags?: InputMaybe<Array<Scalars['String']['input']>>;
workspaceIds?: InputMaybe<Array<Scalars['String']['input']>>;
};
export type UpdateModelInput = {
@@ -3576,6 +3595,7 @@ export type User = {
apiTokens: Array<ApiToken>;
/** Returns the apps you have authorized. */
authorizedApps?: Maybe<Array<ServerAppListItem>>;
automateFunctions: AutomateFunctionCollection;
automateInfo: UserAutomateInfo;
avatar?: Maybe<Scalars['String']['output']>;
bio?: Maybe<Scalars['String']['output']>;
@@ -3667,6 +3687,17 @@ export type UserActivityArgs = {
};
/**
* Full user type, should only be used in the context of admin operations or
* when a user is reading/writing info about himself
*/
export type UserAutomateFunctionsArgs = {
cursor?: InputMaybe<Scalars['String']['input']>;
filter?: InputMaybe<AutomateFunctionsFilter>;
limit?: InputMaybe<Scalars['Int']['input']>;
};
/**
* Full user type, should only be used in the context of admin operations or
* when a user is reading/writing info about himself
@@ -4061,6 +4092,7 @@ export type WebhookUpdateInput = {
export type Workspace = {
__typename?: 'Workspace';
automateFunctions: AutomateFunctionCollection;
createdAt: Scalars['DateTime']['output'];
/** Info about the workspace creation state */
creationState?: Maybe<WorkspaceCreationState>;
@@ -4101,6 +4133,13 @@ export type Workspace = {
};
export type WorkspaceAutomateFunctionsArgs = {
cursor?: InputMaybe<Scalars['String']['input']>;
filter?: InputMaybe<AutomateFunctionsFilter>;
limit?: Scalars['Int']['input'];
};
export type WorkspaceHasAccessToFeatureArgs = {
featureName: WorkspaceFeatureName;
};
@@ -4613,6 +4652,7 @@ export type ResolversTypes = {
AutomateFunctionRunStatusReportInput: AutomateFunctionRunStatusReportInput;
AutomateFunctionTemplate: ResolverTypeWrapper<AutomateFunctionTemplate>;
AutomateFunctionTemplateLanguage: AutomateFunctionTemplateLanguage;
AutomateFunctionToken: ResolverTypeWrapper<AutomateFunctionToken>;
AutomateFunctionsFilter: AutomateFunctionsFilter;
AutomateMutations: ResolverTypeWrapper<MutationsObjectGraphQLReturn>;
AutomateRun: ResolverTypeWrapper<AutomateRunGraphQLReturn>;
@@ -4661,6 +4701,7 @@ export type ResolversTypes = {
CommitsMoveInput: CommitsMoveInput;
CountOnlyCollection: ResolverTypeWrapper<CountOnlyCollection>;
CreateAutomateFunctionInput: CreateAutomateFunctionInput;
CreateAutomateFunctionWithoutVersionInput: CreateAutomateFunctionWithoutVersionInput;
CreateCommentInput: CreateCommentInput;
CreateCommentReplyInput: CreateCommentReplyInput;
CreateModelInput: CreateModelInput;
@@ -4899,6 +4940,7 @@ export type ResolversParentTypes = {
AutomateFunctionRun: AutomateFunctionRunGraphQLReturn;
AutomateFunctionRunStatusReportInput: AutomateFunctionRunStatusReportInput;
AutomateFunctionTemplate: AutomateFunctionTemplate;
AutomateFunctionToken: AutomateFunctionToken;
AutomateFunctionsFilter: AutomateFunctionsFilter;
AutomateMutations: MutationsObjectGraphQLReturn;
AutomateRun: AutomateRunGraphQLReturn;
@@ -4944,6 +4986,7 @@ export type ResolversParentTypes = {
CommitsMoveInput: CommitsMoveInput;
CountOnlyCollection: CountOnlyCollection;
CreateAutomateFunctionInput: CreateAutomateFunctionInput;
CreateAutomateFunctionWithoutVersionInput: CreateAutomateFunctionWithoutVersionInput;
CreateCommentInput: CreateCommentInput;
CreateCommentReplyInput: CreateCommentReplyInput;
CreateModelInput: CreateModelInput;
@@ -5274,6 +5317,7 @@ export type AutomateFunctionResolvers<ContextType = GraphQLContext, ParentType e
repo?: Resolver<ResolversTypes['BasicGitRepositoryMetadata'], ParentType, ContextType>;
supportedSourceApps?: Resolver<Array<ResolversTypes['String']>, ParentType, ContextType>;
tags?: Resolver<Array<ResolversTypes['String']>, ParentType, ContextType>;
workspaceIds?: Resolver<Maybe<Array<ResolversTypes['String']>>, ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
@@ -5325,8 +5369,15 @@ export type AutomateFunctionTemplateResolvers<ContextType = GraphQLContext, Pare
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
export type AutomateFunctionTokenResolvers<ContextType = GraphQLContext, ParentType extends ResolversParentTypes['AutomateFunctionToken'] = ResolversParentTypes['AutomateFunctionToken']> = {
functionId?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
functionToken?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
export type AutomateMutationsResolvers<ContextType = GraphQLContext, ParentType extends ResolversParentTypes['AutomateMutations'] = ResolversParentTypes['AutomateMutations']> = {
createFunction?: Resolver<ResolversTypes['AutomateFunction'], ParentType, ContextType, RequireFields<AutomateMutationsCreateFunctionArgs, 'input'>>;
createFunctionWithoutVersion?: Resolver<ResolversTypes['AutomateFunctionToken'], ParentType, ContextType, RequireFields<AutomateMutationsCreateFunctionWithoutVersionArgs, 'input'>>;
updateFunction?: Resolver<ResolversTypes['AutomateFunction'], ParentType, ContextType, RequireFields<AutomateMutationsUpdateFunctionArgs, 'input'>>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
@@ -6334,6 +6385,7 @@ export type UserResolvers<ContextType = GraphQLContext, ParentType extends Resol
activity?: Resolver<Maybe<ResolversTypes['ActivityCollection']>, ParentType, ContextType, RequireFields<UserActivityArgs, 'limit'>>;
apiTokens?: Resolver<Array<ResolversTypes['ApiToken']>, ParentType, ContextType>;
authorizedApps?: Resolver<Maybe<Array<ResolversTypes['ServerAppListItem']>>, ParentType, ContextType>;
automateFunctions?: Resolver<ResolversTypes['AutomateFunctionCollection'], ParentType, ContextType, Partial<UserAutomateFunctionsArgs>>;
automateInfo?: Resolver<ResolversTypes['UserAutomateInfo'], ParentType, ContextType>;
avatar?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
bio?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
@@ -6518,6 +6570,7 @@ export type WebhookEventCollectionResolvers<ContextType = GraphQLContext, Parent
};
export type WorkspaceResolvers<ContextType = GraphQLContext, ParentType extends ResolversParentTypes['Workspace'] = ResolversParentTypes['Workspace']> = {
automateFunctions?: Resolver<ResolversTypes['AutomateFunctionCollection'], ParentType, ContextType, RequireFields<WorkspaceAutomateFunctionsArgs, 'limit'>>;
createdAt?: Resolver<ResolversTypes['DateTime'], ParentType, ContextType>;
creationState?: Resolver<Maybe<ResolversTypes['WorkspaceCreationState']>, ParentType, ContextType>;
customerPortalUrl?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
@@ -6687,6 +6740,7 @@ export type Resolvers<ContextType = GraphQLContext> = {
AutomateFunctionReleaseCollection?: AutomateFunctionReleaseCollectionResolvers<ContextType>;
AutomateFunctionRun?: AutomateFunctionRunResolvers<ContextType>;
AutomateFunctionTemplate?: AutomateFunctionTemplateResolvers<ContextType>;
AutomateFunctionToken?: AutomateFunctionTokenResolvers<ContextType>;
AutomateMutations?: AutomateMutationsResolvers<ContextType>;
AutomateRun?: AutomateRunResolvers<ContextType>;
AutomateRunCollection?: AutomateRunCollectionResolvers<ContextType>;
@@ -213,6 +213,7 @@ export type AutomateAuthCodePayloadTest = {
action: Scalars['String']['input'];
code: Scalars['String']['input'];
userId: Scalars['String']['input'];
workspaceId?: InputMaybe<Scalars['String']['input']>;
};
export type AutomateFunction = {
@@ -229,6 +230,7 @@ export type AutomateFunction = {
/** SourceAppNames values from @speckle/shared. Empty array means - all of them */
supportedSourceApps: Array<Scalars['String']['output']>;
tags: Array<Scalars['String']['output']>;
workspaceIds?: Maybe<Array<Scalars['String']['output']>>;
};
@@ -310,8 +312,13 @@ export enum AutomateFunctionTemplateLanguage {
Typescript = 'TYPESCRIPT'
}
export type AutomateFunctionToken = {
__typename?: 'AutomateFunctionToken';
functionId: Scalars['String']['output'];
functionToken: Scalars['String']['output'];
};
export type AutomateFunctionsFilter = {
featuredFunctionsOnly?: InputMaybe<Scalars['Boolean']['input']>;
/** By default we skip functions without releases. Set this to true to include them. */
functionsWithoutReleases?: InputMaybe<Scalars['Boolean']['input']>;
search?: InputMaybe<Scalars['String']['input']>;
@@ -320,6 +327,7 @@ export type AutomateFunctionsFilter = {
export type AutomateMutations = {
__typename?: 'AutomateMutations';
createFunction: AutomateFunction;
createFunctionWithoutVersion: AutomateFunctionToken;
updateFunction: AutomateFunction;
};
@@ -329,6 +337,11 @@ export type AutomateMutationsCreateFunctionArgs = {
};
export type AutomateMutationsCreateFunctionWithoutVersionArgs = {
input: CreateAutomateFunctionWithoutVersionInput;
};
export type AutomateMutationsUpdateFunctionArgs = {
input: UpdateAutomateFunctionInput;
};
@@ -819,6 +832,11 @@ export type CreateAutomateFunctionInput = {
template: AutomateFunctionTemplateLanguage;
};
export type CreateAutomateFunctionWithoutVersionInput = {
description: Scalars['String']['input'];
name: Scalars['String']['input'];
};
export type CreateCommentInput = {
content: CommentContentInput;
projectId: Scalars['String']['input'];
@@ -3514,6 +3532,7 @@ export type UpdateAutomateFunctionInput = {
/** SourceAppNames values from @speckle/shared */
supportedSourceApps?: InputMaybe<Array<Scalars['String']['input']>>;
tags?: InputMaybe<Array<Scalars['String']['input']>>;
workspaceIds?: InputMaybe<Array<Scalars['String']['input']>>;
};
export type UpdateModelInput = {
@@ -3557,6 +3576,7 @@ export type User = {
apiTokens: Array<ApiToken>;
/** Returns the apps you have authorized. */
authorizedApps?: Maybe<Array<ServerAppListItem>>;
automateFunctions: AutomateFunctionCollection;
automateInfo: UserAutomateInfo;
avatar?: Maybe<Scalars['String']['output']>;
bio?: Maybe<Scalars['String']['output']>;
@@ -3648,6 +3668,17 @@ export type UserActivityArgs = {
};
/**
* Full user type, should only be used in the context of admin operations or
* when a user is reading/writing info about himself
*/
export type UserAutomateFunctionsArgs = {
cursor?: InputMaybe<Scalars['String']['input']>;
filter?: InputMaybe<AutomateFunctionsFilter>;
limit?: InputMaybe<Scalars['Int']['input']>;
};
/**
* Full user type, should only be used in the context of admin operations or
* when a user is reading/writing info about himself
@@ -4042,6 +4073,7 @@ export type WebhookUpdateInput = {
export type Workspace = {
__typename?: 'Workspace';
automateFunctions: AutomateFunctionCollection;
createdAt: Scalars['DateTime']['output'];
/** Info about the workspace creation state */
creationState?: Maybe<WorkspaceCreationState>;
@@ -4082,6 +4114,13 @@ export type Workspace = {
};
export type WorkspaceAutomateFunctionsArgs = {
cursor?: InputMaybe<Scalars['String']['input']>;
filter?: InputMaybe<AutomateFunctionsFilter>;
limit?: Scalars['Int']['input'];
};
export type WorkspaceHasAccessToFeatureArgs = {
featureName: WorkspaceFeatureName;
};
@@ -163,6 +163,7 @@ export const onWorkspaceAuthorizedFactory =
// Guests cannot use (and are not restricted by) SSO
const workspaceRole = await getWorkspaceRoleForUser({ userId, workspaceId })
if (!workspaceRole) throw new WorkspacesNotAuthorizedError()
if (workspaceRole?.role === Roles.Workspace.Guest) return
const provider = await getWorkspaceSsoProviderRecord({ workspaceId })
@@ -42,7 +42,7 @@ import {
import { createProjectInviteFactory } from '@/modules/serverinvites/services/projectInviteManagement'
import { getInvitationTargetUsersFactory } from '@/modules/serverinvites/services/retrieval'
import { authorizeResolver } from '@/modules/shared'
import { getFeatureFlags } from '@/modules/shared/helpers/envHelper'
import { getFeatureFlags, getServerOrigin } from '@/modules/shared/helpers/envHelper'
import { getEventBus } from '@/modules/shared/services/eventBus'
import { WorkspaceInviteResourceType } from '@/modules/workspaces/domain/constants'
import {
@@ -177,7 +177,18 @@ import {
listWorkspaceSsoMembershipsFactory
} from '@/modules/workspaces/repositories/sso'
import { getDecryptor } from '@/modules/workspaces/helpers/sso'
import { getWorkspaceFunctions } from '@/modules/automate/clients/executionEngine'
import {
ExecutionEngineFailedResponseError,
ExecutionEngineNetworkError
} from '@/modules/automate/errors/executionEngine'
import { getDefaultRegionFactory } from '@/modules/workspaces/repositories/regions'
import {
AuthCodePayloadAction,
createStoredAuthCodeFactory
} from '@/modules/automate/services/authCode'
import { getGenericRedis } from '@/modules/shared/redis/redis'
import { convertFunctionToGraphQLReturn } from '@/modules/automate/services/functionManagement'
import { getWorkspacePlanFactory } from '@/modules/gatekeeper/repositories/billing'
import { Knex } from 'knex'
@@ -561,7 +572,10 @@ export = FF_WORKSPACES_MODULE_ENABLED
await updateWorkspaceRole({ userId, workspaceId, role })
}
return await getWorkspaceFactory({ db })({ workspaceId })
return await getWorkspaceFactory({ db })({
workspaceId: args.input.workspaceId,
userId: context.userId
})
},
addDomain: async (_parent, args, context) => {
await authorizeResolver(
@@ -982,6 +996,48 @@ export = FF_WORKSPACES_MODULE_ENABLED
})
}
},
automateFunctions: async (parent, args, context) => {
try {
const authCode = await createStoredAuthCodeFactory({
redis: getGenericRedis()
})({
userId: context.userId!,
action: AuthCodePayloadAction.ListWorkspaceFunctions
})
const res = await getWorkspaceFunctions({
workspaceId: parent.id,
query: removeNullOrUndefinedKeys(args),
body: {
speckleServerAuthenticationPayload: {
...authCode,
origin: getServerOrigin()
}
}
})
const items = res.functions.map(convertFunctionToGraphQLReturn)
return {
cursor: undefined,
totalCount: res.functions.length,
items
}
} catch (e) {
const isNotFound =
e instanceof ExecutionEngineFailedResponseError &&
e.response.statusMessage === 'FunctionNotFound'
if (e instanceof ExecutionEngineNetworkError || isNotFound) {
return {
cursor: null,
totalCount: 0,
items: []
}
}
throw e
}
},
domains: async (parent) => {
return await getWorkspaceDomainsFactory({ db })({ workspaceIds: [parent.id] })
},
@@ -214,6 +214,7 @@ export type AutomateAuthCodePayloadTest = {
action: Scalars['String']['input'];
code: Scalars['String']['input'];
userId: Scalars['String']['input'];
workspaceId?: InputMaybe<Scalars['String']['input']>;
};
export type AutomateFunction = {
@@ -230,6 +231,7 @@ export type AutomateFunction = {
/** SourceAppNames values from @speckle/shared. Empty array means - all of them */
supportedSourceApps: Array<Scalars['String']['output']>;
tags: Array<Scalars['String']['output']>;
workspaceIds?: Maybe<Array<Scalars['String']['output']>>;
};
@@ -311,8 +313,13 @@ export enum AutomateFunctionTemplateLanguage {
Typescript = 'TYPESCRIPT'
}
export type AutomateFunctionToken = {
__typename?: 'AutomateFunctionToken';
functionId: Scalars['String']['output'];
functionToken: Scalars['String']['output'];
};
export type AutomateFunctionsFilter = {
featuredFunctionsOnly?: InputMaybe<Scalars['Boolean']['input']>;
/** By default we skip functions without releases. Set this to true to include them. */
functionsWithoutReleases?: InputMaybe<Scalars['Boolean']['input']>;
search?: InputMaybe<Scalars['String']['input']>;
@@ -321,6 +328,7 @@ export type AutomateFunctionsFilter = {
export type AutomateMutations = {
__typename?: 'AutomateMutations';
createFunction: AutomateFunction;
createFunctionWithoutVersion: AutomateFunctionToken;
updateFunction: AutomateFunction;
};
@@ -330,6 +338,11 @@ export type AutomateMutationsCreateFunctionArgs = {
};
export type AutomateMutationsCreateFunctionWithoutVersionArgs = {
input: CreateAutomateFunctionWithoutVersionInput;
};
export type AutomateMutationsUpdateFunctionArgs = {
input: UpdateAutomateFunctionInput;
};
@@ -820,6 +833,11 @@ export type CreateAutomateFunctionInput = {
template: AutomateFunctionTemplateLanguage;
};
export type CreateAutomateFunctionWithoutVersionInput = {
description: Scalars['String']['input'];
name: Scalars['String']['input'];
};
export type CreateCommentInput = {
content: CommentContentInput;
projectId: Scalars['String']['input'];
@@ -3515,6 +3533,7 @@ export type UpdateAutomateFunctionInput = {
/** SourceAppNames values from @speckle/shared */
supportedSourceApps?: InputMaybe<Array<Scalars['String']['input']>>;
tags?: InputMaybe<Array<Scalars['String']['input']>>;
workspaceIds?: InputMaybe<Array<Scalars['String']['input']>>;
};
export type UpdateModelInput = {
@@ -3558,6 +3577,7 @@ export type User = {
apiTokens: Array<ApiToken>;
/** Returns the apps you have authorized. */
authorizedApps?: Maybe<Array<ServerAppListItem>>;
automateFunctions: AutomateFunctionCollection;
automateInfo: UserAutomateInfo;
avatar?: Maybe<Scalars['String']['output']>;
bio?: Maybe<Scalars['String']['output']>;
@@ -3649,6 +3669,17 @@ export type UserActivityArgs = {
};
/**
* Full user type, should only be used in the context of admin operations or
* when a user is reading/writing info about himself
*/
export type UserAutomateFunctionsArgs = {
cursor?: InputMaybe<Scalars['String']['input']>;
filter?: InputMaybe<AutomateFunctionsFilter>;
limit?: InputMaybe<Scalars['Int']['input']>;
};
/**
* Full user type, should only be used in the context of admin operations or
* when a user is reading/writing info about himself
@@ -4043,6 +4074,7 @@ export type WebhookUpdateInput = {
export type Workspace = {
__typename?: 'Workspace';
automateFunctions: AutomateFunctionCollection;
createdAt: Scalars['DateTime']['output'];
/** Info about the workspace creation state */
creationState?: Maybe<WorkspaceCreationState>;
@@ -4083,6 +4115,13 @@ export type Workspace = {
};
export type WorkspaceAutomateFunctionsArgs = {
cursor?: InputMaybe<Scalars['String']['input']>;
filter?: InputMaybe<AutomateFunctionsFilter>;
limit?: Scalars['Int']['input'];
};
export type WorkspaceHasAccessToFeatureArgs = {
featureName: WorkspaceFeatureName;
};