Merge branch 'main' into andrew/usage-limits-plan-composables

This commit is contained in:
andrewwallacespeckle
2025-04-04 14:55:53 +01:00
16 changed files with 114 additions and 173 deletions
@@ -27,39 +27,8 @@
<h3 class="label mb-2">Access permissions</h3>
<ProjectVisibilitySelect v-model="visibility" mount-menu-on-body />
</div>
<template v-if="isWorkspacesEnabled && !workspaceId">
<div class="flex gap-y-2 flex-col">
<p class="text-body-xs text-foreground font-medium">Workspace</p>
<div class="flex gap-x-2 items-center">
<ProjectsWorkspaceSelect
v-model="selectedWorkspace"
:items="workspaces"
:disabled-roles="[Roles.Workspace.Guest]"
:disabled="!hasWorkspaces"
disabled-item-tooltip="You dont have rights to create projects in this workspace"
class="flex-1"
/>
<div v-tippy="'Create workspace'" class="flex">
<FormButton
:icon-left="PlusIcon"
hide-text
class="flex"
color="outline"
@click="navigateToWorkspaceExplainer"
/>
</div>
</div>
<p class="text-foreground-2 text-body-2xs">
Workspaces offer better project management and higher data security.
</p>
</div>
</template>
</div>
</form>
<CommonConfirmDialog
v-model:open="showConfirmDialog"
@confirm="handleConfirmAction"
/>
</LayoutDialog>
</template>
<script setup lang="ts">
@@ -69,31 +38,7 @@ import { SimpleProjectVisibility } from '~~/lib/common/generated/gql/graphql'
import { isRequired, isStringOfLength } from '~~/lib/common/helpers/validation'
import { useMixpanel } from '~~/lib/core/composables/mp'
import { useCreateProject } from '~~/lib/projects/composables/projectManagement'
import { useIsWorkspacesEnabled } from '~/composables/globals'
import { PlusIcon } from '@heroicons/vue/24/outline'
import type { ProjectsAddDialog_WorkspaceFragment } from '~/lib/common/generated/gql/graphql'
import { graphql } from '~~/lib/common/generated/gql'
import { projectWorkspaceSelectQuery } from '~/lib/projects/graphql/queries'
import { useQuery } from '@vue/apollo-composable'
import { Roles } from '@speckle/shared'
import { workspacesRoute } from '~/lib/common/helpers/route'
graphql(`
fragment ProjectsAddDialog_Workspace on Workspace {
id
...ProjectsWorkspaceSelect_Workspace
}
`)
graphql(`
fragment ProjectsAddDialog_User on User {
workspaces {
items {
...ProjectsAddDialog_Workspace
}
}
}
`)
type FormValues = {
name: string
@@ -108,37 +53,29 @@ const emit = defineEmits<{
(e: 'created'): void
}>()
const isWorkspacesEnabled = useIsWorkspacesEnabled()
const createProject = useCreateProject()
const router = useRouter()
const logger = useLogger()
const { handleSubmit, meta, isSubmitting } = useForm<FormValues>()
const { result: workspaceResult } = useQuery(projectWorkspaceSelectQuery, null, () => ({
enabled: isWorkspacesEnabled.value
}))
const { handleSubmit, isSubmitting } = useForm<FormValues>()
const visibility = ref(SimpleProjectVisibility.Unlisted)
const selectedWorkspace = ref<ProjectsAddDialog_WorkspaceFragment>()
const showConfirmDialog = ref(false)
const confirmActionType = ref<'navigate' | 'close' | null>(null)
const isClosing = ref(false)
const isLoading = ref(false)
const open = defineModel<boolean>('open', { required: true })
const mp = useMixpanel()
const onSubmit = handleSubmit(async (values) => {
if (isClosing.value) return // Prevent submission while closing
if (isLoading.value) return // Prevent submission while closing
try {
isClosing.value = true
const workspaceId = props.workspaceId || selectedWorkspace.value?.id
isLoading.value = true
await createProject({
name: values.name,
description: values.description,
visibility: visibility.value,
...(workspaceId ? { workspaceId } : {})
...(props.workspaceId ? { workspaceId: props.workspaceId } : {})
})
emit('created')
mp.track('Stream Action', {
@@ -149,18 +86,13 @@ const onSubmit = handleSubmit(async (values) => {
})
open.value = false
} catch (error) {
isClosing.value = false
isLoading.value = false
logger.error('Failed to create project:', error)
}
})
const workspaces = computed(
() => workspaceResult.value?.activeUser?.workspaces.items ?? []
)
const hasWorkspaces = computed(() => workspaces.value.length > 0)
const dialogButtons = computed((): LayoutDialogButton[] => {
const isDisabled = isSubmitting.value || isClosing.value
const isDisabled = isSubmitting.value || isLoading.value
return [
{
@@ -169,7 +101,7 @@ const dialogButtons = computed((): LayoutDialogButton[] => {
color: 'outline',
disabled: isDisabled
},
onClick: confirmCancel
onClick: () => (open.value = false)
},
{
text: 'Create',
@@ -183,41 +115,10 @@ const dialogButtons = computed((): LayoutDialogButton[] => {
]
})
const formIsDirty = computed(() => {
return meta.value.dirty
})
const navigateToWorkspaceExplainer = () => {
if (formIsDirty.value) {
confirmActionType.value = 'navigate'
showConfirmDialog.value = true
} else {
router.push(workspacesRoute)
}
}
const confirmCancel = () => {
if (formIsDirty.value) {
confirmActionType.value = 'close'
showConfirmDialog.value = true
} else {
open.value = false
}
}
const handleConfirmAction = () => {
if (confirmActionType.value === 'navigate') {
router.push(workspacesRoute)
} else if (confirmActionType.value === 'close') {
open.value = false
}
confirmActionType.value = null
}
watch(open, (newVal, oldVal) => {
if (newVal && !oldVal) {
selectedWorkspace.value = undefined
isClosing.value = false
isLoading.value = false
}
})
</script>
@@ -18,7 +18,7 @@
<p class="text-body-xs text-foreground font-medium">
You're not a member of any workspaces.
</p>
<FormButton :to="workspacesRoute">Learn about workspaces</FormButton>
<FormButton :to="workspaceCreateRoute">Learn about workspaces</FormButton>
</div>
</template>
@@ -75,7 +75,7 @@ import { useMutationLoading, useQuery } from '@vue/apollo-composable'
import type { LayoutDialogButton } from '@speckle/ui-components'
import { useMoveProjectToWorkspace } from '~/lib/projects/composables/projectManagement'
import { Roles } from '@speckle/shared'
import { workspacesRoute } from '~/lib/common/helpers/route'
import { workspaceCreateRoute } from '~/lib/common/helpers/route'
import {
useWorkspaceCustomDataResidencyDisclaimer,
RegionStaticDataDisclaimerVariant
@@ -45,7 +45,7 @@
</template>
<script setup lang="ts">
import { workspacesRoute } from '~~/lib/common/helpers/route'
import { workspaceCreateRoute } from '~~/lib/common/helpers/route'
import { WizardSteps } from '~/lib/workspaces/helpers/types'
import { useWorkspacesWizard } from '~/lib/workspaces/composables/wizard'
import { useMixpanel } from '~/lib/core/composables/mp'
@@ -81,7 +81,7 @@ const requiresWorkspaceCreation = computed(() => {
const onCancelClick = () => {
if (isFirstStep.value) {
navigateTo(workspacesRoute)
navigateTo(workspaceCreateRoute())
resetWizardState()
mixpanel.stop_session_recording()
} else {
@@ -16,7 +16,7 @@
<script setup lang="ts">
import type { LayoutDialogButton } from '@speckle/ui-components'
import { useMixpanel } from '~/lib/core/composables/mp'
import { workspacesRoute } from '~/lib/common/helpers/route'
import { homeRoute } from '~/lib/common/helpers/route'
import {
convertThrowIntoFetchResult,
modifyObjectFields
@@ -93,7 +93,7 @@ const onConfirm = async () => {
}
}
router.push(workspacesRoute)
router.push(homeRoute)
isOpen.value = false
resetWizardState()
mixpanel.track('Workspace Creation Canceled')
@@ -7,7 +7,6 @@ export const profileRoute = '/profile'
export const authBlockedDueToVerificationRoute = '/error-email-verify'
export const homeRoute = '/'
export const projectsRoute = '/projects'
export const workspacesRoute = '/workspaces'
export const loginRoute = '/authn/login'
export const registerRoute = '/authn/register'
export const ssoLoginRoute = '/authn/sso'
@@ -197,7 +197,11 @@ export = FF_GATEKEEPER_MODULE_ENABLED
const projectModelCount =
await getPaginatedProjectModelsTotalCountFactory({ db: regionDb })(
project.id,
{}
{
filter: {
onlyWithVersions: true
}
}
)
modelCount = modelCount + projectModelCount
}
@@ -29,7 +29,14 @@ import {
TestApolloServer
} from '@/test/graphqlHelper'
import { beforeEachContext } from '@/test/hooks'
import { createTestBranches } from '@/test/speckle-helpers/branchHelper'
import {
BasicTestBranch,
createTestBranches
} from '@/test/speckle-helpers/branchHelper'
import {
createTestCommits,
createTestObject
} from '@/test/speckle-helpers/commitHelper'
import { BasicTestStream, createTestStream } from '@/test/speckle-helpers/streamHelper'
import { Roles } from '@speckle/shared'
import { expect } from 'chai'
@@ -283,36 +290,49 @@ describe('Workspaces Billing', () => {
workspaceId: workspace.id
}
await createTestStream(project, user)
await createTestBranches([
const models: BasicTestBranch[] = [
{
owner: user,
stream: project,
branch: {
id: createRandomString(),
streamId: project.id,
authorId: user.id,
name: createRandomString()
}
id: '',
streamId: project.id,
authorId: user.id,
name: createRandomString()
},
{
owner: user,
stream: project,
branch: {
id: createRandomString(),
streamId: project.id,
authorId: user.id,
name: createRandomString()
}
id: '',
streamId: project.id,
authorId: user.id,
name: createRandomString()
},
{
id: '',
streamId: project.id,
authorId: user.id,
name: createRandomString()
}
]
await createTestBranches(
models.map((branch) => ({
owner: user,
stream: project,
branch: {
id: createRandomString(),
streamId: project.id,
authorId: user.id,
name: createRandomString()
}
branch
}))
)
const objectId = await createTestObject({ projectId: project.id })
await createTestCommits([
{
id: '',
authorId: user.id,
objectId,
streamId: project.id,
branchName: models[0].name
},
{
id: '',
authorId: user.id,
objectId,
streamId: project.id,
branchName: models[1].name
}
])
@@ -324,7 +344,7 @@ describe('Workspaces Billing', () => {
expect(res).to.not.haveGraphQLErrors()
expect(res?.data?.workspace?.plan?.usage?.projectCount).to.equal(1)
expect(res?.data?.workspace?.plan?.usage?.modelCount).to.equal(3)
expect(res?.data?.workspace?.plan?.usage?.modelCount).to.equal(2)
})
}
)
@@ -45,12 +45,14 @@ spec:
- "process.exit((Date.now() - require('fs').readFileSync('/tmp/last_successful_query', 'utf8') > 25 * 60 * 1000) ? 1 : 0)"
resources:
{{- with .Values.fileimport_service.requests }}
requests:
cpu: {{ .Values.fileimport_service.requests.cpu }}
memory: {{ .Values.fileimport_service.requests.memory }}
{{- toYaml . | nindent 12 }}
{{- end }}
{{- with .Values.fileimport_service.limits }}
limits:
cpu: {{ .Values.fileimport_service.limits.cpu }}
memory: {{ .Values.fileimport_service.limits.memory }}
{{- toYaml . | nindent 12 }}
{{- end }}
securityContext:
allowPrivilegeEscalation: false
@@ -35,12 +35,14 @@ spec:
protocol: TCP
resources:
{{- with .Values.frontend_2.requests }}
requests:
cpu: {{ .Values.frontend_2.requests.cpu }}
memory: {{ .Values.frontend_2.requests.memory }}
{{- toYaml . | nindent 12 }}
{{- end }}
{{- with .Values.frontend_2.limits }}
limits:
cpu: {{ .Values.frontend_2.limits.cpu }}
memory: {{ .Values.frontend_2.limits.memory }}
{{- toYaml . | nindent 12 }}
{{- end }}
# Allow for k8s to remove the pod from the service endpoints to stop receive traffic
lifecycle:
@@ -30,12 +30,14 @@ spec:
protocol: TCP
resources:
{{- with .Values.monitoring.requests }}
requests:
cpu: {{ .Values.monitoring.requests.cpu }}
memory: {{ .Values.monitoring.requests.memory }}
{{- toYaml . | nindent 12 }}
{{- end }}
{{- with .Values.monitoring.limits }}
limits:
cpu: {{ .Values.monitoring.limits.cpu }}
memory: {{ .Values.monitoring.limits.memory }}
{{- toYaml . | nindent 12 }}
{{- end }}
securityContext:
allowPrivilegeEscalation: false
@@ -34,12 +34,14 @@ spec:
protocol: TCP
resources:
{{- with .Values.objects.requests }}
requests:
cpu: {{ .Values.objects.requests.cpu }}
memory: {{ .Values.objects.requests.memory }}
{{- toYaml . | nindent 12 }}
{{- end }}
{{- with .Values.objects.limits }}
limits:
cpu: {{ .Values.objects.limits.cpu }}
memory: {{ .Values.objects.limits.memory }}
{{- toYaml . | nindent 12 }}
{{- end }}
securityContext:
allowPrivilegeEscalation: false
@@ -42,12 +42,14 @@ spec:
port: metrics
resources:
{{- with .Values.preview_service.requests }}
requests:
cpu: {{ .Values.preview_service.requests.cpu }}
memory: {{ .Values.preview_service.requests.memory }}
{{- toYaml . | nindent 12 }}
{{- end }}
{{- with .Values.preview_service.limits }}
limits:
cpu: {{ .Values.preview_service.limits.cpu }}
memory: {{ .Values.preview_service.limits.memory }}
{{- toYaml . | nindent 12 }}
{{- end }}
securityContext:
allowPrivilegeEscalation: false
@@ -34,12 +34,14 @@ spec:
protocol: TCP
resources:
{{- with .Values.server.requests }}
requests:
cpu: {{ .Values.server.requests.cpu }}
memory: {{ .Values.server.requests.memory }}
{{- toYaml . | nindent 12 }}
{{- end }}
{{- with .Values.server.limits }}
limits:
cpu: {{ .Values.server.limits.cpu }}
memory: {{ .Values.server.limits.memory }}
{{- toYaml . | nindent 12 }}
{{- end }}
securityContext:
allowPrivilegeEscalation: false
@@ -32,12 +32,14 @@ spec:
- name: LOG_PRETTY
value: {{ .Values.test.logPretty | quote }}
resources:
{{- with .Values.test.requests }}
requests:
cpu: {{ .Values.test.requests.cpu }}
memory: {{ .Values.test.requests.memory }}
{{- toYaml . | nindent 14 }}
{{- end }}
{{- with .Values.test.limits }}
limits:
cpu: {{ .Values.test.limits.cpu }}
memory: {{ .Values.test.limits.memory }}
{{- toYaml . | nindent 14 }}
{{- end }}
securityContext:
allowPrivilegeEscalation: false
capabilities:
@@ -39,12 +39,14 @@ spec:
- process.exit(Date.now() - require('fs').readFileSync('/tmp/last_successful_query', 'utf8') > 30 * 1000)
resources:
{{- with .Values.webhook_service.requests }}
requests:
cpu: {{ .Values.webhook_service.requests.cpu }}
memory: {{ .Values.webhook_service.requests.memory }}
{{- toYaml . | nindent 12 }}
{{- end }}
{{- with .Values.webhook_service.limits }}
limits:
cpu: {{ .Values.webhook_service.limits.cpu }}
memory: {{ .Values.webhook_service.limits.memory }}
{{- toYaml . | nindent 12 }}
{{- end }}
securityContext:
allowPrivilegeEscalation: false
+1
View File
@@ -1123,6 +1123,7 @@ preview_service:
## @param preview_service.dedicatedPreviewsQueue Allows using a dedicated redis url for the preview service job queue
##
dedicatedPreviewsQueue: false
## @param preview_service.replicas The number of instances of the Preview Service pod to be deployed within the cluster.
##
replicas: 1