Merge branch 'main' into andrew/web-3448-enable-auto-joining-a-workspace-as-a-setting
This commit is contained in:
@@ -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>;
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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/
|
||||
|
||||
Reference in New Issue
Block a user