Merge branch 'main' into andrew/web-3448-enable-auto-joining-a-workspace-as-a-setting

This commit is contained in:
andrewwallacespeckle
2025-06-04 14:54:52 +02:00
18 changed files with 540 additions and 84 deletions
@@ -0,0 +1,176 @@
<template>
<section class="flex flex-col space-y-3">
<div class="flex flex-col sm:flex-row gap-y-3 sm:items-center">
<div class="flex-1 flex-col pr-6 gap-y-1">
<p class="text-body-xs font-medium text-foreground">
Default seat for new members
</p>
<p class="text-body-2xs text-foreground-2 leading-5 max-w-[250px]">
Set the default seat type assigned to new workspace members.
</p>
</div>
<FormSelectBase
v-model="seatTypeModel"
v-tippy="!isWorkspaceAdmin ? 'You must be a workspace admin' : undefined"
:items="defaultSeatTypeOptions"
:disabled="!isWorkspaceAdmin"
name="defaultSeatType"
label="Default seat type"
class="min-w-[240px]"
:allow-unset="false"
:show-label="false"
fully-control-value
>
<template #nothing-selected>Select default</template>
<template #something-selected="{ value }">
<div class="text-foreground font-medium capitalize">
{{ Array.isArray(value) ? value[0] : value }}
</div>
</template>
<template #option="{ item }">
<div class="flex flex-col space-y-0.5">
<span class="capitalize">{{ item }}</span>
<span class="text-body-3xs text-foreground-2">
{{ WorkspaceSeatTypeDescription[Roles.Workspace.Member][item] }}
</span>
</div>
</template>
</FormSelectBase>
</div>
<SettingsConfirmDialog
v-model:open="showConfirmSeatTypeDialog"
title="Confirm change"
@confirm="handleSeatTypeConfirm"
@cancel="handleSeatTypeCancel"
>
<p class="text-body-xs text-foreground mb-2">
You have
<span class="font-medium">Join without admin approval</span>
enabled.
</p>
<p class="text-body-xs text-foreground mb-2">
Setting the default seat type to
<span class="font-medium">Editor</span>
means each user who joins will consume a paid seat and possibly incur charges.
</p>
<p class="text-body-xs text-foreground">Are you sure you want to enable this?</p>
</SettingsConfirmDialog>
</section>
</template>
<script setup lang="ts">
import { useMutation } from '@vue/apollo-composable'
import type {
WorkspaceSeatType,
SettingsWorkspacesSecurity_WorkspaceFragment
} from '~/lib/common/generated/gql/graphql'
import { Roles, SeatTypes } from '@speckle/shared'
import { workspaceUpdateDefaultSeatTypeMutation } from '~/lib/workspaces/graphql/mutations'
import { useMixpanel } from '~/lib/core/composables/mp'
import { useWorkspacePlan } from '~/lib/workspaces/composables/plan'
import { WorkspaceSeatTypeDescription } from '~/lib/settings/helpers/constants'
import {
getFirstErrorMessage,
convertThrowIntoFetchResult
} from '~/lib/common/helpers/graphql'
const props = defineProps<{
workspace: SettingsWorkspacesSecurity_WorkspaceFragment
}>()
const mixpanel = useMixpanel()
const { mutate: updateDefaultSeatType } = useMutation(
workspaceUpdateDefaultSeatTypeMutation
)
const { triggerNotification } = useGlobalToast()
const { isSelfServePlan } = useWorkspacePlan(props.workspace.slug)
const currentSeatType = ref<WorkspaceSeatType>(props.workspace.defaultSeatType)
const showConfirmSeatTypeDialog = ref(false)
const pendingNewSeatType = ref<WorkspaceSeatType>()
const isWorkspaceAdmin = computed(() => {
return props.workspace.role === Roles.Workspace.Admin
})
const seatTypeModel = computed({
get: () => currentSeatType.value,
set: (newValue: WorkspaceSeatType) => {
handleSeatTypeChange(newValue)
}
})
const handleSeatTypeChange = (newValue: WorkspaceSeatType) => {
if (newValue === currentSeatType.value) return
// If setting to Editor with auto-join enabled on paid plan, show confirmation
if (
newValue === SeatTypes.Editor &&
props.workspace.discoverabilityAutoJoinEnabled &&
isSelfServePlan
) {
pendingNewSeatType.value = newValue
showConfirmSeatTypeDialog.value = true
return
}
// Otherwise, apply the change directly
applySeatTypeChange(newValue)
}
const applySeatTypeChange = async (seatTypeValue: WorkspaceSeatType) => {
const result = await updateDefaultSeatType({
input: {
id: props.workspace.id,
defaultSeatType: seatTypeValue
}
}).catch(convertThrowIntoFetchResult)
if (result?.data) {
currentSeatType.value = seatTypeValue
triggerNotification({
type: ToastNotificationType.Success,
title: 'Default seat type updated',
description: `New members will now be assigned ${
seatTypeValue.charAt(0).toUpperCase() + seatTypeValue.slice(1)
} seats by default`
})
mixpanel.track('Workspace Default Seat Type Updated', {
value: seatTypeValue,
// eslint-disable-next-line camelcase
workspace_id: props.workspace.id
})
} else {
triggerNotification({
type: ToastNotificationType.Danger,
title: 'Failed to update default seat type',
description: getFirstErrorMessage(result?.errors)
})
}
}
const handleSeatTypeConfirm = async () => {
if (!pendingNewSeatType.value) return
await applySeatTypeChange(pendingNewSeatType.value)
pendingNewSeatType.value = undefined
}
const handleSeatTypeCancel = () => {
pendingNewSeatType.value = undefined
showConfirmSeatTypeDialog.value = false
}
const defaultSeatTypeOptions: WorkspaceSeatType[] = Object.values(SeatTypes)
watch(
() => props.workspace.defaultSeatType,
(newVal) => {
if (newVal) {
currentSeatType.value = newVal
}
}
)
</script>
@@ -13,8 +13,7 @@ import type {
WorkspaceUpdateInput,
AddDomainToWorkspaceInput
} from '~~/lib/common/generated/gql/graphql'
import type { WorkspaceDomain, Workspace } from '~/lib/common/generated/gql/graphql'
import { gql } from '@apollo/client'
import type { Workspace } from '~/lib/common/generated/gql/graphql'
export function useUpdateWorkspace() {
const { mutate, loading } = useMutation(settingsUpdateWorkspaceMutation)
@@ -49,14 +48,7 @@ export function useAddWorkspaceDomain() {
const { triggerNotification } = useGlobalToast()
return {
mutate: async (
input: AddDomainToWorkspaceInput,
domains: WorkspaceDomain[],
discoverabilityEnabled: boolean,
domainBasedMembershipProtectionEnabled: boolean,
hasAccessToSSO: boolean,
hasAccessToDomainBasedSecurityPolicies: boolean
) => {
mutate: async (input: AddDomainToWorkspaceInput) => {
const result = await apollo
.mutate({
mutation: settingsAddWorkspaceDomainMutation,
@@ -66,37 +58,6 @@ export function useAddWorkspaceDomain() {
workspaceId: input.workspaceId
}
},
optimisticResponse: {
workspaceMutations: {
addDomain: {
__typename: 'Workspace',
id: input.workspaceId,
slug: (
apollo.readFragment({
id: getCacheId('Workspace', input.workspaceId),
fragment: gql`
fragment AddDomainWorkspace on Workspace {
slug
}
`
}) as Workspace
).slug,
domains: [
...domains,
{
__typename: 'WorkspaceDomain',
id: '',
domain: input.domain
}
],
discoverabilityEnabled,
domainBasedMembershipProtectionEnabled,
hasAccessToSSO,
hasAccessToDomainBasedSecurityPolicies,
discoverabilityAutoJoinEnabled: false
}
}
},
update: (cache, res) => {
const { data } = res
if (!data?.workspaceMutations) return
@@ -163,3 +163,14 @@ export const workspaceUpdateAutoJoinMutation = graphql(`
}
}
`)
export const workspaceUpdateDefaultSeatTypeMutation = graphql(`
mutation WorkspaceUpdateDefaultSeatTypeMutation($input: WorkspaceUpdateInput!) {
workspaceMutations {
update(input: $input) {
id
defaultSeatType
}
}
}
`)
@@ -92,7 +92,7 @@ import {
CommitWithStreamBranchMetadata
} from '@/modules/core/domain/commits/types'
import { logger } from '@/observability/logging'
import { getLastVersionByProjectIdFactory } from '@/modules/core/repositories/versions'
import { getLastVersionsByProjectIdFactory } from '@/modules/core/repositories/versions'
import { StreamRoles } from '@speckle/shared'
declare module '@/modules/core/loaders' {
@@ -137,7 +137,7 @@ const dataLoadersDefinition = defineRequestDataloaders(
const getStreamsSourceApps = getStreamsSourceAppsFactory({ db })
const getUsers = getUsersFactory({ db })
const getStreamsCollaborators = getStreamsCollaboratorsFactory({ db })
const getLastVersionByProjectId = getLastVersionByProjectIdFactory({ db })
const getLastestVersionsByProjectId = getLastVersionsByProjectIdFactory({ db })
const getStreamsCollaboratorCounts = getStreamsCollaboratorCountsFactory({
db
})
@@ -371,11 +371,11 @@ const dataLoadersDefinition = defineRequestDataloaders(
}
}
})(),
getLastVersion: createLoader<string, Nullable<CommitRecord>>(
getLatestVersions: createLoader<string, Array<CommitRecord>>(
async (projectIds) => {
const results = keyBy(
await getLastVersionByProjectId({ projectIds }),
(c) => c.projectId
await getLastestVersionsByProjectId({ projectIds }),
(c) => c[0].projectId
)
return projectIds.map((projectId) => results[projectId] || null)
}
@@ -134,18 +134,23 @@ export = {
project
})
let lastVersion: Version | null
let latestVersion: Version | null = null
let latestVersions: Array<Version> | null = null
if (getTypeFromPath(info) === 'Model') {
lastVersion = await ctx.loaders
latestVersion = await ctx.loaders
.forRegion({ db: projectDB })
.branches.getLatestCommit.load(parent.branchId)
} else {
lastVersion = await ctx.loaders
latestVersions = await ctx.loaders
.forRegion({ db: projectDB })
.streams.getLastVersion.load(parent.streamId)
.streams.getLatestVersions.load(parent.streamId)
}
if (lastVersion?.id === parent.id) return parent.referencedObject
if (
latestVersion?.id === parent.id ||
latestVersions?.find((lv) => lv.id === parent.id)
)
return parent.referencedObject
if (isBeyondLimit) return null
return parent.referencedObject
}
@@ -1,31 +1,26 @@
import { Commits, knex, StreamCommits } from '@/modules/core/dbSchema'
import { BranchCommits, knex, Branches, Commits } from '@/modules/core/dbSchema'
import { Version } from '@/modules/core/domain/commits/types'
import { Knex } from 'knex'
import { groupBy } from 'lodash'
const tables = {
versions: (db: Knex) => db<Version>(Commits.name)
}
export const getLastVersionByProjectIdFactory =
export const getLastVersionsByProjectIdFactory =
({ db }: { db: Knex }) =>
async ({
projectIds
}: {
projectIds: readonly string[]
}): Promise<Record<string, Version & { projectId: string }>> => {
const results = await tables
.versions(db)
.join(StreamCommits.name, StreamCommits.col.commitId, Commits.col.id)
.whereIn(StreamCommits.col.streamId, projectIds)
.distinctOn(StreamCommits.col.streamId)
.select([...Commits.cols, knex.raw(`stream_commits."streamId" as "projectId"`)])
}): Promise<Record<string, Array<Version & { projectId: string }>>> => {
const res = await db(Branches.name)
.whereIn(Branches.col.streamId, projectIds)
.join(BranchCommits.name, BranchCommits.col.branchId, Branches.col.id)
.join(Commits.name, Commits.col.id, BranchCommits.col.commitId)
.distinctOn(Branches.col.id)
.select([...Commits.cols, knex.raw(`branches."streamId" as "projectId"`)])
.orderBy([
{ column: StreamCommits.col.streamId, order: 'desc' },
{ column: Commits.col.createdAt, order: 'desc' }
{ column: Branches.col.id, order: 'desc' },
{ column: Commits.col.createdAt, order: 'desc' },
{ column: Commits.col.id, order: 'desc' }
])
return results.reduce<Record<string, Version & { projectId: string }>>(
(acc, curr) => ({ ...acc, [curr.projectId]: curr }),
{}
)
return groupBy(res, 'projectId')
}
@@ -2,8 +2,11 @@ import cryptoRandomString from 'crypto-random-string'
import { Project } from '@/modules/core/domain/streams/types'
import { ProjectRecordVisibility } from '@/modules/core/helpers/types'
import { assign } from 'lodash'
import { BasicTestCommit } from '@/test/speckle-helpers/commitHelper'
import { BasicTestBranch } from '@/test/speckle-helpers/branchHelper'
import { BasicTestStream } from '@/test/speckle-helpers/streamHelper'
export const buildBasicTestProject = (overrides?: Partial<Project>): Project =>
export const buildTestProject = (overrides?: Partial<Project>): Project =>
assign(
{
id: cryptoRandomString({ length: 10 }),
@@ -19,3 +22,47 @@ export const buildBasicTestProject = (overrides?: Partial<Project>): Project =>
},
overrides
)
export const buildBasicTestProject = (
overrides?: Partial<BasicTestStream>
): BasicTestStream =>
assign(
{
name: cryptoRandomString({ length: 10 }),
isPublic: true,
ownerId: cryptoRandomString({ length: 10 }),
id: cryptoRandomString({ length: 10 })
},
overrides
)
export const buildBasicTestModel = (
overrides?: Partial<BasicTestBranch>
): BasicTestBranch =>
assign(
{
name: cryptoRandomString({ length: 10 }),
description: cryptoRandomString({ length: 10 }),
streamId: cryptoRandomString({ length: 10 }),
authorId: cryptoRandomString({ length: 10 }),
id: cryptoRandomString({ length: 10 })
},
overrides
)
export const buildBasicTestVersion = (
overrides?: Partial<BasicTestCommit>
): BasicTestCommit =>
assign(
{
id: cryptoRandomString({ length: 10 }),
objectId: cryptoRandomString({ length: 10 }),
streamId: cryptoRandomString({ length: 10 }),
authorId: cryptoRandomString({ length: 10 }),
branchId: cryptoRandomString({ length: 10 }),
branchName: cryptoRandomString({ length: 10 }),
message: cryptoRandomString({ length: 10 }),
createdAt: new Date()
},
overrides
)
@@ -3,7 +3,7 @@ import {
createRandomEmail,
createRandomString
} from '@/modules/core/helpers/testHelpers'
import { getLastVersionByProjectIdFactory } from '@/modules/core/repositories/versions'
import { getLastVersionsByProjectIdFactory } from '@/modules/core/repositories/versions'
import { createTestUser } from '@/test/authHelper'
import { BasicTestCommit, createTestCommit } from '@/test/speckle-helpers/commitHelper'
import { createTestStream } from '@/test/speckle-helpers/streamHelper'
@@ -11,7 +11,7 @@ import { expect } from 'chai'
describe('Versions repositories @core', () => {
describe('getLastVersionByProjectIdFactory returns a function that, ', () => {
const getLastVersionByProjectId = getLastVersionByProjectIdFactory({ db })
const getLastVersionsByProjectId = getLastVersionsByProjectIdFactory({ db })
it('should return the last version for each projectId', async () => {
const user = await createTestUser({
name: createRandomString(),
@@ -57,13 +57,13 @@ describe('Versions repositories @core', () => {
owner: user
})
const result = await getLastVersionByProjectId({
const result = await getLastVersionsByProjectId({
projectIds: [project1.id, project2.id]
})
const lastVersionProject1 = result[project1.id]
const lastVersionProject2 = result[project2.id]
expect(lastVersionProject1.projectId).to.eq(project1.id)
expect(lastVersionProject2.projectId).to.eq(project2.id)
expect(lastVersionProject1[0].projectId).to.eq(project1.id)
expect(lastVersionProject2[0].projectId).to.eq(project2.id)
})
})
})
@@ -17,6 +17,7 @@ import {
CreateProjectVersionDocument,
CreateWorkspaceDocument,
CreateWorkspaceProjectDocument,
GetProjectVersionsDocument,
GetProjectWithModelVersionsDocument,
GetProjectWithVersionsDocument
} from '@/test/graphql/generated/graphql'
@@ -42,13 +43,26 @@ import { WorkspaceReadOnlyError } from '@/modules/gatekeeper/errors/billing'
import { CreateVersionInput } from '@/modules/core/graph/generated/graphql'
import { getFeatureFlags } from '@/modules/shared/helpers/envHelper'
import { getEventBus } from '@/modules/shared/services/eventBus'
import { createTestUser, login } from '@/test/authHelper'
import { buildBasicTestUser, createTestUser, login } from '@/test/authHelper'
import { BasicTestStream, createTestStream } from '@/test/speckle-helpers/streamHelper'
import { BasicTestCommit, createTestCommit } from '@/test/speckle-helpers/commitHelper'
import {
BasicTestCommit,
createTestCommit,
createTestObject
} from '@/test/speckle-helpers/commitHelper'
import { BranchCommits, Commits, StreamCommits } from '@/modules/core/dbSchema'
import { BasicTestBranch, createTestBranch } from '@/test/speckle-helpers/branchHelper'
import dayjs from 'dayjs'
import { createTestWorkspace } from '@/modules/workspaces/tests/helpers/creation'
import {
buildBasicTestWorkspace,
createTestWorkspace
} from '@/modules/workspaces/tests/helpers/creation'
import {
buildBasicTestModel,
buildBasicTestProject,
buildBasicTestVersion
} from '@/modules/core/tests/helpers/creation'
import { Optional } from '@speckle/shared'
const getServerInfo = getServerInfoFactory({ db })
const getUser = legacyGetUserFactory({ db })
@@ -136,6 +150,156 @@ describe('Versions graphql @core', () => {
}
)
})
;(FF_BILLING_INTEGRATION_ENABLED ? describe : describe.skip)(
'version limit query @new-versions',
() => {
const updateCommitCreatedAtDate = async (
id: string,
createdAt: Date // Make the project read-only
) => await db('commits').update({ createdAt }).where({ id })
const user = buildBasicTestUser()
const workspace = buildBasicTestWorkspace()
const model1 = buildBasicTestModel()
const model2 = buildBasicTestModel()
const project = buildBasicTestProject()
const version1 = buildBasicTestVersion()
const version2 = buildBasicTestVersion()
const version3 = buildBasicTestVersion()
let objectId1: Optional<string> = undefined
let objectId2: Optional<string> = undefined
let objectId3: Optional<string> = undefined
before(async () => {
user.id = await createUser(user)
await createTestWorkspace(workspace, user, {
addPlan: { name: 'free', status: 'valid' }
})
project.workspaceId = workspace.id
await createTestStream(project, user)
await createTestBranch({
branch: model1,
stream: project,
owner: user
})
await createTestBranch({
branch: model2,
stream: project,
owner: user
})
objectId1 = await createTestObject({
projectId: project.id,
object: { test: 'a' }
})
objectId2 = await createTestObject({
projectId: project.id,
object: { test: 'b' }
})
objectId3 = await createTestObject({
projectId: project.id,
object: { test: 'c' }
})
version1.objectId = objectId1
version1.authorId = user.id
version1.branchName = model1.name
version1.branchId = model1.id
version1.streamId = project.id
version2.objectId = objectId2
version2.authorId = user.id
version2.branchName = model2.name
version2.branchId = model2.id
version2.streamId = project.id
version3.objectId = objectId3
version3.authorId = user.id
version3.branchName = model2.name // model 2 has 2 versions
version3.branchId = model2.id
version3.streamId = project.id
await createTestCommit(version1, { owner: user })
await createTestCommit(version2, { owner: user })
await createTestCommit(version3, { owner: user })
})
it('shows the referencedObject of all user versions', async () => {
const apollo = await testApolloServer({ authUserId: user.id })
await updateCommitCreatedAtDate(version1.id, new Date())
await updateCommitCreatedAtDate(version2.id, new Date())
await updateCommitCreatedAtDate(version3.id, new Date())
const res = await apollo.execute(GetProjectVersionsDocument, {
projectId: project.id
})
const versions = res.data?.project?.versions?.items
expect(res).to.not.haveGraphQLErrors()
expect(versions)
.to.be.a('array')
.and.to.have.lengthOf(3)
.and.to.deep.contain({
id: version1.id,
referencedObject: objectId1
})
.and.to.deep.contain({
id: version2.id,
referencedObject: objectId2
})
.and.to.deep.contain({
id: version3.id,
referencedObject: objectId3
})
})
it('hides those ones that are more than 7 day old', async () => {
const apollo = await testApolloServer({ authUserId: user.id })
const tenDaysAgo = dayjs().subtract(10, 'day').toDate()
await updateCommitCreatedAtDate(version1.id, new Date())
await updateCommitCreatedAtDate(version2.id, new Date())
await updateCommitCreatedAtDate(version3.id, tenDaysAgo)
const res = await apollo.execute(GetProjectVersionsDocument, {
projectId: project.id
})
const versions = res.data?.project?.versions?.items
expect(res).to.not.haveGraphQLErrors()
expect(versions).to.be.a('array').and.to.have.lengthOf(3).and.to.deep.contain({
id: version3.id,
referencedObject: null
})
})
it('does not hide the latest commit of a model even if its +7 days old', async () => {
const apollo = await testApolloServer({ authUserId: user.id })
const tenDaysAgo = dayjs().subtract(10, 'day').toDate()
const elevenDaysAgo = dayjs().subtract(11, 'day').toDate()
await updateCommitCreatedAtDate(version1.id, new Date())
await updateCommitCreatedAtDate(version2.id, tenDaysAgo)
await updateCommitCreatedAtDate(version3.id, elevenDaysAgo)
const res = await apollo.execute(GetProjectVersionsDocument, {
projectId: project.id
})
const versions = res.data?.project?.versions?.items
expect(res).to.not.haveGraphQLErrors()
expect(versions)
.to.be.a('array')
.and.to.have.lengthOf(3)
.and.to.deep.contain({
id: version2.id,
referencedObject: objectId2
})
.and.to.deep.contain({
id: version3.id,
referencedObject: null
})
})
}
)
;(FF_PERSONAL_PROJECTS_LIMITS_ENABLED ? describe : describe.skip)(
'Version.referencedObject',
() => {
@@ -1,4 +1,4 @@
import { buildBasicTestProject } from '@/modules/core/tests/helpers/creation'
import { buildTestProject } from '@/modules/core/tests/helpers/creation'
import {
buildMixpanelFake,
MixpanelFakeEventRecord
@@ -11,7 +11,7 @@ import { expect } from 'chai'
describe('fileuploadsTrackingFactory creates a function, that @fileuploads', () => {
const workspaceId = 'some_workspace_id'
const project = buildBasicTestProject({ workspaceId })
const project = buildTestProject({ workspaceId })
const user = buildTestUserWithOptionalRole()
const getProject = async () => project
const getUser = async () => user
@@ -45,7 +45,7 @@ describe('fileuploadsTrackingFactory creates a function, that @fileuploads', ()
})
it('does not include workspace_id if project does not belong to a workspace', async () => {
const projectWithoutWorkspace = buildBasicTestProject({ workspaceId: null })
const projectWithoutWorkspace = buildTestProject({ workspaceId: null })
const events: MixpanelFakeEventRecord = []
const workspaceTracking = fileuploadTrackingFactory({
getProject: async () => projectWithoutWorkspace,
@@ -29,6 +29,7 @@ export const getProjectLimitDateFactory = (deps: {
ctx: GraphQLContext
}): GetProjectLimitDate => {
const getProjectLimitDate = getProjectLimitDateFactoryBase({
// this one
getWorkspaceLimits: async ({ workspaceId }) =>
(await deps.ctx.loaders.gatekeeper?.getWorkspaceLimits.load(workspaceId)) || null,
getPersonalProjectLimits
@@ -6012,6 +6012,13 @@ export type GetProjectCollaboratorsQueryVariables = Exact<{
export type GetProjectCollaboratorsQuery = { __typename?: 'Query', project: { __typename?: 'Project', id: string, team: Array<{ __typename?: 'ProjectCollaborator', id: string, role: string }> } };
export type GetProjectVersionsQueryVariables = Exact<{
projectId: Scalars['String']['input'];
}>;
export type GetProjectVersionsQuery = { __typename?: 'Query', project: { __typename?: 'Project', versions: { __typename?: 'VersionCollection', items: Array<{ __typename?: 'Version', id: string, referencedObject?: string | null }> } } };
export type CreateServerInviteMutationVariables = Exact<{
input: ServerInviteCreateInput;
}>;
@@ -6546,6 +6553,7 @@ export const CreateProjectDocument = {"kind":"Document","definitions":[{"kind":"
export const BatchDeleteProjectsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"BatchDeleteProjects"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"ids"}},"type":{"kind":"NonNullType","type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"projectMutations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"batchDelete"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"ids"},"value":{"kind":"Variable","name":{"kind":"Name","value":"ids"}}}]}]}}]}}]} as unknown as DocumentNode<BatchDeleteProjectsMutation, BatchDeleteProjectsMutationVariables>;
export const UpdateProjectRoleDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateProjectRole"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ProjectUpdateRoleInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"projectMutations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateRole"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"BasicProjectFields"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"BasicProjectFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Project"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"visibility"}},{"kind":"Field","name":{"kind":"Name","value":"allowPublicComments"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]} as unknown as DocumentNode<UpdateProjectRoleMutation, UpdateProjectRoleMutationVariables>;
export const GetProjectCollaboratorsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetProjectCollaborators"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"projectId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"project"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"projectId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"team"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"role"}}]}}]}}]}}]} as unknown as DocumentNode<GetProjectCollaboratorsQuery, GetProjectCollaboratorsQueryVariables>;
export const GetProjectVersionsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetProjectVersions"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"projectId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"project"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"projectId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"versions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"referencedObject"}}]}}]}}]}}]}}]} as unknown as DocumentNode<GetProjectVersionsQuery, GetProjectVersionsQueryVariables>;
export const CreateServerInviteDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateServerInvite"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ServerInviteCreateInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"serverInviteCreate"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}]}]}}]} as unknown as DocumentNode<CreateServerInviteMutation, CreateServerInviteMutationVariables>;
export const CreateStreamInviteDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateStreamInvite"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"StreamInviteCreateInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"streamInviteCreate"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}]}]}}]} as unknown as DocumentNode<CreateStreamInviteMutation, CreateStreamInviteMutationVariables>;
export const ResendInviteDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"ResendInvite"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"inviteId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"inviteResend"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"inviteId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"inviteId"}}}]}]}}]} as unknown as DocumentNode<ResendInviteMutation, ResendInviteMutationVariables>;
+13
View File
@@ -109,3 +109,16 @@ export const getProjectCollaboratorsQuery = gql`
}
}
`
export const getProjectVersionsQuery = gql`
query GetProjectVersions($projectId: String!) {
project(id: $projectId) {
versions {
items {
id
referencedObject
}
}
}
}
`
@@ -65,7 +65,10 @@ export type BasicTestCommit = {
createdAt?: Date
}
export async function createTestObject(params: { projectId: string }) {
export async function createTestObject(params: {
projectId: string
object?: Record<string, unknown>
}) {
const projectDb = await getProjectDbClient(params)
const createObject = createObjectFactory({
storeSingleObjectIfNotFoundFactory: storeSingleObjectIfNotFoundFactory({
@@ -75,7 +78,7 @@ export async function createTestObject(params: { projectId: string }) {
return await createObject({
streamId: params.projectId,
object: { foo: 'bar' }
object: params.object ?? { foo: 'bar' }
})
}
@@ -25,4 +25,18 @@ spec:
name: speckle-objects
port:
name: web
- pathType: Prefix
path: "/api/stream/"
backend:
service:
name: speckle-objects
port:
name: web
- pathType: Prefix
path: "/api/thirdparty/gendo"
backend:
service:
name: speckle-objects
port:
name: web
{{- end }}
@@ -0,0 +1,49 @@
{{- if .Values.ingress.enabled }}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: speckle-server-minion-api-objects
namespace: {{ .Values.namespace }}
labels:
{{ include "speckle.labels" . | indent 4 }}
annotations:
nginx.org/mergeable-ingress-type: "minion"
{{- if .Values.cert_manager_issuer }}
cert-manager.io/cluster-issuer: {{ .Values.cert_manager_issuer }}
{{- end }}
nginx.ingress.kubernetes.io/proxy-body-size: {{ (printf "%dm" (int .Values.objects_size_limit_mb)) | quote }}
spec:
ingressClassName: nginx
rules:
- host: {{ .Values.domain }}
http:
paths:
- pathType: Prefix
path: "/api/getobjects/"
backend:
service:
name: speckle-objects
port:
name: web
- pathType: Prefix
path: "/api/objects/"
backend:
service:
name: speckle-objects
port:
name: web
- pathType: Prefix
path: "/api/diff/"
backend:
service:
name: speckle-objects
port:
name: web
- pathType: Prefix
path: "/objects/"
backend:
service:
name: speckle-objects
port:
name: web
{{- end }}
@@ -187,6 +187,11 @@
"description": "The maximum time (unit is minutes) that a file import can take before it is considered to have failed",
"default": 10
},
"objects_size_limit_mb": {
"type": "number",
"description": "This maximum size of the POST request body (unit is Megabytes) that can be sent to the Speckle Objects REST APIs.",
"default": 100
},
"enable_prometheus_monitoring": {
"type": "boolean",
"description": "If enabled, Speckle deploys a Prometheus ServiceMonitor resource",
+4
View File
@@ -132,6 +132,10 @@ file_size_limit_mb: 100
##
file_import_time_limit_min: 10
## @param objects_size_limit_mb This maximum size of the POST request body (unit is Megabytes) that can be sent to the Speckle Objects REST APIs.
##
objects_size_limit_mb: 100
## @section Monitoring
## @descriptionStart
## This enables metrics generated by Speckle to be ingested by Prometheus: https://prometheus.io/