feat: function update flow re-introduced (#2410)

This commit is contained in:
Kristaps Fabians Geikins
2024-06-20 12:24:15 +03:00
committed by GitHub
parent 04dd67aa4f
commit 689bb4e941
17 changed files with 186 additions and 75 deletions
@@ -69,7 +69,10 @@ const onSubmit = handleSubmit(async (values) => {
})
const reset = () => {
setValues(props.model)
// Temp hack while FormSelectBase has a bug where it rewrites form value with initialValue
nextTick(() => {
setValues(props.model)
})
}
watch(
@@ -17,11 +17,17 @@
Edit
</FormButton>
</div>
<div class="flex gap-2 shrink-0">
<div
v-tippy="
hasReleases ? undefined : 'Your function needs to have at least one release'
"
class="flex gap-2 shrink-0"
>
<FormButton
:icon-left="BoltIcon"
class="shrink-0"
full-width
:disabled="!hasReleases"
@click="$emit('createAutomation')"
>
Use in an Automation
@@ -54,11 +60,16 @@ graphql(`
owner
name
}
releases(limit: 1) {
totalCount
}
}
`)
defineProps<{
const props = defineProps<{
fn: AutomateFunctionPageHeader_FunctionFragment
isOwner: boolean
}>()
const hasReleases = computed(() => props.fn.releases.totalCount > 0)
</script>
@@ -76,13 +76,21 @@
Use this function to create an automation on your project.
</div>
</div>
<FormButton
:icon-left="BoltIcon"
<div
v-tippy="
hasReleases ? undefined : 'Your function needs to have at least one release'
"
class="shrink-0"
@click="$emit('createAutomation')"
>
Use in an Automation
</FormButton>
<FormButton
:icon-left="BoltIcon"
class="shrink-0"
:disabled="!hasReleases"
@click="$emit('createAutomation')"
>
Use in an Automation
</FormButton>
</div>
</div>
<AutomateFunctionPageParametersDialog
v-if="latestRelease"
@@ -163,6 +171,7 @@ const publishedAt = computed(() => dayjs(latestRelease.value?.createdAt).from(da
const description = computed(() =>
props.fn.description?.length ? props.fn.description : 'No description provided.'
)
const hasReleases = computed(() => !!latestRelease.value)
const onViewParameters = () => {
if (!latestRelease.value) return
@@ -25,7 +25,7 @@ const 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": types.AutomationsFunctionsCard_AutomateFunctionFragmentDoc,
"\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 }\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 }\n": types.AutomateFunctionPageHeader_FunctionFragmentDoc,
"\n fragment AutomateFunctionPageInfo_AutomateFunction on AutomateFunction {\n id\n repo {\n id\n url\n owner\n name\n }\n automationCount\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,
@@ -231,7 +231,7 @@ const documents = {
"\n query LegacyBranchRedirectMetadata($streamId: String!, $branchName: String!) {\n stream(id: $streamId) {\n branch(name: $branchName) {\n id\n }\n }\n }\n": types.LegacyBranchRedirectMetadataDocument,
"\n query LegacyViewerCommitRedirectMetadata($streamId: String!, $commitId: String!) {\n stream(id: $streamId) {\n commit(id: $commitId) {\n id\n branch {\n id\n }\n }\n }\n }\n": types.LegacyViewerCommitRedirectMetadataDocument,
"\n query ResolveCommentLink($commentId: String!, $projectId: String!) {\n comment(id: $commentId, streamId: $projectId) {\n ...LinkableComment\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 }\n": types.AutomateFunctionPage_AutomateFunctionFragmentDoc,
"\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 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 ...ProjectPageProjectHeader\n ...ProjectPageTeamDialog\n }\n": types.ProjectPageProjectFragmentDoc,
@@ -305,7 +305,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 }\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 }\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 }\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"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
@@ -1129,7 +1129,7 @@ export function graphql(source: "\n query ResolveCommentLink($commentId: String
/**
* 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 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 }\n"): (typeof documents)["\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 }\n"];
export function graphql(source: "\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"): (typeof documents)["\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"];
/**
* 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
+11 -6
View File
@@ -46,6 +46,9 @@ graphql(`
...AutomateFunctionPageHeader_Function
...AutomateFunctionPageInfo_AutomateFunction
...AutomateAutomationCreateDialog_AutomateFunction
creator {
id
}
}
`)
@@ -60,6 +63,8 @@ const pageQuery = graphql(`
definePageMeta({
middleware: ['auth', 'require-valid-function']
})
const { activeUser } = useActiveUser()
const pageFetchPolicy = usePageQueryStandardFetchPolicy()
const route = useRoute()
const functionId = computed(() => route.params.fid as string)
@@ -80,12 +85,12 @@ const showNewAutomationDialog = ref(false)
const fn = computed(() => result.value?.automateFunction)
const isOwner = computed(
() => false // TODO: Gergo rethinking function auth logic
// !!(
// activeUser.value?.id &&
// fn.value?.creator &&
// activeUser.value.id === fn.value.creator.id
// )
() =>
!!(
activeUser.value?.id &&
fn.value?.creator &&
activeUser.value.id === fn.value.creator.id
)
)
const { html: plaintextDescription } = useMarkdown(
@@ -144,6 +144,10 @@ type AutomateFunction {
"""
supportedSourceApps: [String!]!
tags: [String!]!
"""
Only returned if user is a part of this speckle server
"""
creator: LimitedUser
}
type AutomateFunctionRelease {
@@ -17,10 +17,7 @@ import {
VersionCreationTriggerType,
isVersionCreatedTriggerManifest
} from '@/modules/automate/helpers/types'
import type {
AuthCodePayload,
AuthCodePayloadWithOrigin
} from '@/modules/automate/services/authCode'
import type { AuthCodePayload } from '@/modules/automate/services/authCode'
import { MisconfiguredEnvironmentError } from '@/modules/shared/errors'
import { getServerOrigin, speckleAutomateUrl } from '@/modules/shared/helpers/envHelper'
import {
@@ -33,6 +30,13 @@ import {
} from '@speckle/shared'
import { has, isObjectLike } from 'lodash'
export type AuthCodePayloadWithOrigin = AuthCodePayload & { origin: string }
const addOrigin = (P: AuthCodePayload): AuthCodePayloadWithOrigin => {
const origin = getServerOrigin()
return { ...P, origin }
}
const isErrorResponse = (e: unknown): e is ExecutionEngineErrorResponse =>
isObjectLike(e) && has(e, 'statusCode') && has(e, 'statusMessage')
@@ -120,7 +124,14 @@ const invokeRequest = async (params: {
url,
body
}
const errorResponse = await response.json()
let errorResponse: unknown
try {
errorResponse = await response.json()
} catch (e) {
throw new ExecutionEngineBadResponseBodyError(errorReq)
}
if (!isErrorResponse(errorResponse)) {
throw new ExecutionEngineBadResponseBodyError(errorReq)
}
@@ -241,16 +252,17 @@ export enum ExecutionEngineFunctionTemplateId {
TypeScript = 'typescript'
}
export type CreateFunctionBody = {
speckleServerAuthenticationPayload: AuthCodePayloadWithOrigin
template: ExecutionEngineFunctionTemplateId
functionName: string
description: string
supportedSourceApps: SourceAppName[]
tags: string[]
logo: Nullable<string>
org: Nullable<string>
}
export type CreateFunctionBody<AP extends AuthCodePayload = AuthCodePayloadWithOrigin> =
{
speckleServerAuthenticationPayload: AP
template: ExecutionEngineFunctionTemplateId
functionName: string
description: string
supportedSourceApps: SourceAppName[]
tags: string[]
logo: Nullable<string>
org: Nullable<string>
}
export type CreateFunctionResponse = {
functionId: string
@@ -268,34 +280,55 @@ export type CreateFunctionResponse = {
export const createFunction = async ({
body
}: {
body: CreateFunctionBody
body: CreateFunctionBody<AuthCodePayload>
}): Promise<CreateFunctionResponse> => {
const url = getApiUrl('/api/v2/functions/from-template')
const formattedBody: CreateFunctionBody = {
...body,
speckleServerAuthenticationPayload: addOrigin(
body.speckleServerAuthenticationPayload
)
}
return invokeJsonRequest<CreateFunctionResponse>({
url,
method: 'post',
body,
body: formattedBody,
retry: false
})
}
export type UpdateFunctionBody = {
//TODO add speckleServerAuthenticationPayload
functionName?: string
description?: string
supportedSourceApps?: SourceAppName[]
tags?: string[]
logo?: string
}
export type UpdateFunctionBody<AP extends AuthCodePayload = AuthCodePayloadWithOrigin> =
{
speckleServerAuthenticationPayload: AP
functionName?: string
description?: string
supportedSourceApps?: SourceAppName[]
tags?: string[]
logo?: string
}
export type UpdateFunctionResponse = FunctionSchemaType
export const updateFunction = async (params: {
functionId: string
body: UpdateFunctionBody
body: UpdateFunctionBody<AuthCodePayload>
}): Promise<UpdateFunctionResponse> => {
throw new Error('Not implemented! Needs re-thinking by Gergo & Iain')
console.log(params)
const { functionId, body } = params
const url = getApiUrl(`/api/v2/functions/${functionId}`)
const formattedBody: UpdateFunctionBody = {
...body,
speckleServerAuthenticationPayload: addOrigin(
body.speckleServerAuthenticationPayload
)
}
return await invokeJsonRequest<UpdateFunctionResponse>({
url,
method: 'PATCH',
body: formattedBody,
retry: false
})
}
export type GetFunctionResponse = FunctionWithVersionsSchemaType & {
@@ -55,7 +55,7 @@ import { getUser } from '@/modules/core/repositories/users'
import { createAutomation as clientCreateAutomation } from '@/modules/automate/clients/executionEngine'
import { validateStreamAccess } from '@/modules/core/services/streams/streamAccessService'
import { Automate, Roles, isNullOrUndefined, isNonNullable } from '@speckle/shared'
import { getFeatureFlags } from '@/modules/shared/helpers/envHelper'
import { getFeatureFlags, getServerOrigin } from '@/modules/shared/helpers/envHelper'
import {
getBranchLatestCommits,
getBranchesByIds
@@ -404,6 +404,16 @@ export = (FF_AUTOMATE_MODULE_ENABLED
throw e
}
},
async creator(parent, _args, ctx) {
if (
!parent.functionCreator ||
parent.functionCreator.speckleServerOrigin !== getServerOrigin()
) {
return null
}
return ctx.loaders.users.getUser.load(parent.functionCreator.speckleUserId)
}
},
AutomateFunctionRelease: {
@@ -434,7 +444,8 @@ export = (FF_AUTOMATE_MODULE_ENABLED
async updateFunction(_parent, args, ctx) {
const update = updateFunction({
updateFunction: execEngineUpdateFunction,
getFunction
getFunction,
createStoredAuthCode: createStoredAuthCode({ redis: getGenericRedis() })
})
return await update({ input: args.input, userId: ctx.userId! })
}
@@ -16,6 +16,10 @@ export type FunctionSchemaType = {
createdAt: string
isFeatured: boolean
logo: Nullable<string>
functionCreator: Nullable<{
speckleUserId: string
speckleServerOrigin: string
}>
}
export type FunctionReleaseSchemaType = {
@@ -28,7 +28,12 @@ export type AutomateFunctionGraphQLReturn = Pick<
| 'logo'
| 'tags'
| 'supportedSourceApps'
>
> & {
functionCreator: Nullable<{
speckleUserId: string
speckleServerOrigin: string
}>
}
export type AutomateFunctionReleaseGraphQLReturn = Pick<
AutomateFunctionRelease,
@@ -8,7 +8,8 @@ export enum AuthCodePayloadAction {
CreateAutomation = 'createAutomation',
CreateFunction = 'createFunction',
BecomeFunctionAuthor = 'becomeFunctionAuthor',
GetAvailableGithubOrganizations = 'getAvailableGithubOrganizations'
GetAvailableGithubOrganizations = 'getAvailableGithubOrganizations',
UpdateFunction = 'updateFunction'
}
export type AuthCodePayload = {
@@ -17,8 +18,6 @@ export type AuthCodePayload = {
action: AuthCodePayloadAction
}
export type AuthCodePayloadWithOrigin = AuthCodePayload & { origin: string }
const isPayload = (payload: unknown): payload is AuthCodePayload =>
!!(
payload &&
@@ -35,11 +35,17 @@ import {
import { Request, Response } from 'express'
import { UnauthorizedError } from '@/modules/shared/errors'
import {
AuthCodePayload,
AuthCodePayloadAction,
createStoredAuthCode
} from '@/modules/automate/services/authCode'
import { getServerOrigin, speckleAutomateUrl } from '@/modules/shared/helpers/envHelper'
import {
getServerOrigin,
isDevEnv,
speckleAutomateUrl
} from '@/modules/shared/helpers/envHelper'
import { getFunctionsMarketplaceUrl } from '@/modules/core/helpers/routeHelper'
import { automateLogger } from '@/logging/logging'
const mapGqlTemplateIdToExecEngineTemplateId = (
id: AutomateFunctionTemplateLanguage
@@ -88,7 +94,8 @@ export const convertFunctionToGraphQLReturn = (
description: fn.description,
logo: cleanFunctionLogo(fn.logo),
tags: fn.tags,
supportedSourceApps: fn.supportedSourceApps
supportedSourceApps: fn.supportedSourceApps,
functionCreator: fn.functionCreator
}
return ret
@@ -131,12 +138,9 @@ export const createFunctionFromTemplate =
userId: user.id,
action: AuthCodePayloadAction.CreateFunction
})
const body: CreateFunctionBody = {
const body: CreateFunctionBody<AuthCodePayload> = {
...input,
speckleServerAuthenticationPayload: {
...authCode,
origin: new URL(getServerOrigin()).origin
},
speckleServerAuthenticationPayload: authCode,
functionName: input.name,
template: mapGqlTemplateIdToExecEngineTemplateId(input.template),
supportedSourceApps: input.supportedSourceApps as SourceAppName[],
@@ -146,6 +150,10 @@ export const createFunctionFromTemplate =
const created = await createExecutionEngineFn({ body })
if (isDevEnv() && created) {
automateLogger.info({ created }, `[dev] Created function #${created.functionId}`)
}
// Don't want to pull the function w/ another req, so we'll just return the input
const gqlReturn: AutomateFunctionGraphQLReturn = {
id: created.functionId,
@@ -160,7 +168,11 @@ export const createFunctionFromTemplate =
description: body.description,
logo: body.logo,
tags: body.tags,
supportedSourceApps: body.supportedSourceApps
supportedSourceApps: body.supportedSourceApps,
functionCreator: {
speckleServerOrigin: getServerOrigin(),
speckleUserId: user.id
}
}
return {
@@ -172,15 +184,14 @@ export const createFunctionFromTemplate =
export type UpdateFunctionDeps = {
updateFunction: typeof updateExecEngineFunction
getFunction: typeof getFunction
createStoredAuthCode: ReturnType<typeof createStoredAuthCode>
}
export const updateFunction =
(deps: UpdateFunctionDeps) =>
async (params: { input: UpdateAutomateFunctionInput; userId: string }) => {
throw new AutomateFunctionUpdateError('Function update not supported yet')
const { updateFunction } = deps
const { input } = params
const { updateFunction, createStoredAuthCode } = deps
const { input, userId } = params
const existingFn = await getFunction({ functionId: input.id })
if (!existingFn) {
@@ -200,11 +211,18 @@ export const updateFunction =
return existingFn
}
const authCode = await createStoredAuthCode({
userId,
action: AuthCodePayloadAction.UpdateFunction
})
const apiResult = await updateFunction({
functionId: updates.id,
body: {
...updates,
supportedSourceApps: updates.supportedSourceApps as Optional<SourceAppName[]>
functionName: updates.name,
supportedSourceApps: updates.supportedSourceApps as Optional<SourceAppName[]>,
speckleServerAuthenticationPayload: authCode
}
})
@@ -236,7 +254,7 @@ export const startAutomateFunctionCreatorAuth =
)
redirectUrl.searchParams.set(
'speckleServerAuthenticationPayload',
JSON.stringify({ ...authCode, origin: new URL(getServerOrigin()).origin })
JSON.stringify({ ...authCode, origin: getServerOrigin() })
)
return res.redirect(redirectUrl.toString())
@@ -210,6 +210,8 @@ export type AutomateAuthCodePayloadTest = {
export type AutomateFunction = {
__typename?: 'AutomateFunction';
automationCount: Scalars['Int']['output'];
/** Only returned if user is a part of this speckle server */
creator?: Maybe<LimitedUser>;
description: Scalars['String']['output'];
id: Scalars['ID']['output'];
isFeatured: Scalars['Boolean']['output'];
@@ -4157,6 +4159,7 @@ export type AuthStrategyResolvers<ContextType = GraphQLContext, ParentType exten
export type AutomateFunctionResolvers<ContextType = GraphQLContext, ParentType extends ResolversParentTypes['AutomateFunction'] = ResolversParentTypes['AutomateFunction']> = {
automationCount?: Resolver<ResolversTypes['Int'], ParentType, ContextType>;
creator?: Resolver<Maybe<ResolversTypes['LimitedUser']>, ParentType, ContextType>;
description?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
isFeatured?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType>;
@@ -199,6 +199,8 @@ export type AutomateAuthCodePayloadTest = {
export type AutomateFunction = {
__typename?: 'AutomateFunction';
automationCount: Scalars['Int']['output'];
/** Only returned if user is a part of this speckle server */
creator?: Maybe<LimitedUser>;
description: Scalars['String']['output'];
id: Scalars['ID']['output'];
isFeatured: Scalars['Boolean']['output'];
@@ -169,7 +169,7 @@ export function getServerOrigin() {
)
}
return trimEnd(process.env.CANONICAL_URL, '/')
return new URL(trimEnd(process.env.CANONICAL_URL, '/')).origin
}
/**
@@ -200,6 +200,8 @@ export type AutomateAuthCodePayloadTest = {
export type AutomateFunction = {
__typename?: 'AutomateFunction';
automationCount: Scalars['Int']['output'];
/** Only returned if user is a part of this speckle server */
creator?: Maybe<LimitedUser>;
description: Scalars['String']['output'];
id: Scalars['ID']['output'];
isFeatured: Scalars['Boolean']['output'];