diff --git a/.circleci/config.yml b/.circleci/config.yml index f94131d7b..9a7eda562 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -597,7 +597,9 @@ jobs: POSTGRES_USER: speckle command: -c 'max_connections=1000' -c 'port=5433' -c 'wal_level=logical' - image: 'minio/minio' - command: server /data --console-address ":9001" + command: server /data --console-address ":9001" --address "0.0.0.0:9000" + - image: 'minio/minio' + command: server /data --console-address ":9021" --address "0.0.0.0:9020" environment: # Same as test-server: NODE_ENV: test diff --git a/.circleci/multiregion.test-ci.json b/.circleci/multiregion.test-ci.json index bbf5d3142..3d5a9ec1c 100644 --- a/.circleci/multiregion.test-ci.json +++ b/.circleci/multiregion.test-ci.json @@ -2,12 +2,28 @@ "main": { "postgres": { "connectionUri": "postgresql://speckle:speckle@127.0.0.1:5432/speckle2_test" + }, + "blobStorage": { + "accessKey": "minioadmin", + "secretKey": "minioadmin", + "bucket": "speckle-server", + "createBucketIfNotExists": true, + "endpoint": "http://127.0.0.1:9000", + "s3Region": "us-east-1" } }, "regions": { "region1": { "postgres": { "connectionUri": "postgresql://speckle:speckle@127.0.0.1:5433/speckle2_test" + }, + "blobStorage": { + "accessKey": "minioadmin", + "secretKey": "minioadmin", + "bucket": "speckle-server", + "createBucketIfNotExists": true, + "endpoint": "http://127.0.0.1:9020", + "s3Region": "us-east-1" } } } diff --git a/README.md b/README.md index 68bd40db6..0a9ef1fc4 100644 --- a/README.md +++ b/README.md @@ -99,6 +99,14 @@ EMAIL_PORT="1025" The web portal is available at `localhost:1080` and it's listening for mail on port `1025`. +### Minio (S3 storage) + +Default credentials are: `minioadmin:minioadmin` +Main storage Web UI: [http://localhost:9001/](http://localhost:9001/) +Region1 storage Web UI: [http://localhost:9021/](http://localhost:9021/) + +You can use the web UI to validate uploaded blobs + # Contributing Please make sure you read the [contribution guidelines](https://github.com/specklesystems/speckle-server/blob/main/CONTRIBUTING.md) for an overview of the best practices we try to follow. diff --git a/mise.toml b/mise.toml new file mode 100644 index 000000000..46aafd5e8 --- /dev/null +++ b/mise.toml @@ -0,0 +1,2 @@ +[tools] +node = '22' \ No newline at end of file diff --git a/packages/frontend-2/components/auth/sso/Login.vue b/packages/frontend-2/components/auth/sso/Login.vue index cf37b7da9..a53516201 100644 --- a/packages/frontend-2/components/auth/sso/Login.vue +++ b/packages/frontend-2/components/auth/sso/Login.vue @@ -68,7 +68,6 @@ graphql(` slug name logo - defaultLogoIndex } `) diff --git a/packages/frontend-2/components/auth/sso/Register.vue b/packages/frontend-2/components/auth/sso/Register.vue index 353be263e..14f3756b5 100644 --- a/packages/frontend-2/components/auth/sso/Register.vue +++ b/packages/frontend-2/components/auth/sso/Register.vue @@ -2,8 +2,8 @@
diff --git a/packages/frontend-2/components/auth/sso/WorkspaceSelect.vue b/packages/frontend-2/components/auth/sso/WorkspaceSelect.vue index 4e9a13b7f..fe81ba7c6 100644 --- a/packages/frontend-2/components/auth/sso/WorkspaceSelect.vue +++ b/packages/frontend-2/components/auth/sso/WorkspaceSelect.vue @@ -9,22 +9,14 @@ > diff --git a/packages/frontend-2/components/dashboard/ProjectCard.vue b/packages/frontend-2/components/dashboard/ProjectCard.vue index 7f3eb638c..36b4e2fdb 100644 --- a/packages/frontend-2/components/dashboard/ProjectCard.vue +++ b/packages/frontend-2/components/dashboard/ProjectCard.vue @@ -28,7 +28,7 @@ >

@@ -77,7 +77,7 @@ graphql(` id slug name - ...WorkspaceAvatar_Workspace + logo } } `) diff --git a/packages/frontend-2/components/dashboard/Sidebar.vue b/packages/frontend-2/components/dashboard/Sidebar.vue index cab1f5b01..b4a52aca7 100644 --- a/packages/frontend-2/components/dashboard/Sidebar.vue +++ b/packages/frontend-2/components/dashboard/Sidebar.vue @@ -90,11 +90,7 @@ class="!pl-1" > @@ -235,10 +231,10 @@ const workspacesItems = computed(() => workspaceResult.value?.activeUser ? workspaceResult.value.activeUser.workspaces.items.map((workspace) => ({ label: workspace.name, + name: workspace.name, id: workspace.id, to: workspaceRoute(workspace.slug), logo: workspace.logo, - defaultLogoIndex: workspace.defaultLogoIndex, plan: { status: workspace.plan?.status }, diff --git a/packages/frontend-2/components/invite/Banner.vue b/packages/frontend-2/components/invite/Banner.vue index 74a3b1d65..6b6ddb169 100644 --- a/packages/frontend-2/components/invite/Banner.vue +++ b/packages/frontend-2/components/invite/Banner.vue @@ -5,7 +5,7 @@

@@ -69,7 +69,7 @@ type GenericInviteItem = { workspace?: { id: string logo?: string - defaultLogoIndex: number + name: string } user?: MaybeNullOrUndefined<{ id: string diff --git a/packages/frontend-2/components/project/page/Header.vue b/packages/frontend-2/components/project/page/Header.vue index 399f3f436..3ad44b4e1 100644 --- a/packages/frontend-2/components/project/page/Header.vue +++ b/packages/frontend-2/components/project/page/Header.vue @@ -28,7 +28,7 @@ > @@ -59,7 +59,7 @@ graphql(` id slug name - ...WorkspaceAvatar_Workspace + logo } } `) diff --git a/packages/frontend-2/components/projects/HiddenProjectWarning.vue b/packages/frontend-2/components/projects/HiddenProjectWarning.vue index e3b73a7b8..406a742f5 100644 --- a/packages/frontend-2/components/projects/HiddenProjectWarning.vue +++ b/packages/frontend-2/components/projects/HiddenProjectWarning.vue @@ -17,11 +17,7 @@ color="outline" >
- + {{ session.name }}
@@ -42,7 +38,6 @@ graphql(` slug name logo - defaultLogoIndex } } `) diff --git a/packages/frontend-2/components/projects/MoveToWorkspaceDialog.vue b/packages/frontend-2/components/projects/MoveToWorkspaceDialog.vue index 2deb8a628..f0cd5ccb2 100644 --- a/packages/frontend-2/components/projects/MoveToWorkspaceDialog.vue +++ b/packages/frontend-2/components/projects/MoveToWorkspaceDialog.vue @@ -78,7 +78,6 @@ graphql(` id role name - defaultLogoIndex logo ...WorkspaceHasCustomDataResidency_Workspace ...ProjectsWorkspaceSelect_Workspace diff --git a/packages/frontend-2/components/projects/ProjectDashboardCard.vue b/packages/frontend-2/components/projects/ProjectDashboardCard.vue index ce7709c6f..c946acbde 100644 --- a/packages/frontend-2/components/projects/ProjectDashboardCard.vue +++ b/packages/frontend-2/components/projects/ProjectDashboardCard.vue @@ -33,7 +33,7 @@ >

diff --git a/packages/frontend-2/components/projects/WorkspaceSelect.vue b/packages/frontend-2/components/projects/WorkspaceSelect.vue index f1e8d5037..185cb11e5 100644 --- a/packages/frontend-2/components/projects/WorkspaceSelect.vue +++ b/packages/frontend-2/components/projects/WorkspaceSelect.vue @@ -22,7 +22,7 @@

@@ -32,11 +32,7 @@ @@ -54,7 +50,6 @@ graphql(` id role name - defaultLogoIndex logo } `) diff --git a/packages/frontend-2/components/settings/Dialog.vue b/packages/frontend-2/components/settings/Dialog.vue index eb8eaf620..1e60e20df 100644 --- a/packages/frontend-2/components/settings/Dialog.vue +++ b/packages/frontend-2/components/settings/Dialog.vue @@ -59,7 +59,7 @@ @@ -148,12 +148,12 @@ import { WorkspacePlanStatuses } from '~/lib/common/generated/gql/graphql' graphql(` fragment SettingsDialog_Workspace on Workspace { - ...WorkspaceAvatar_Workspace ...SettingsMenu_Workspace id slug role name + logo plan { status } diff --git a/packages/frontend-2/components/settings/workspaces/Billing.vue b/packages/frontend-2/components/settings/workspaces/Billing.vue index b24edbb8f..73074c6b6 100644 --- a/packages/frontend-2/components/settings/workspaces/Billing.vue +++ b/packages/frontend-2/components/settings/workspaces/Billing.vue @@ -58,32 +58,29 @@ : 'Monthly bill' }} - -
- -
+

+ {{ billValue }} per + {{ + subscription?.billingInterval === BillingInterval.Yearly + ? 'year' + : 'month' + }} +

+

+ {{ billDescription }} + +

{{ statusIsTrial && isPurchasablePlan ? 'Trial ends' + : statusIsCanceled + ? 'Cancels' : 'Next payment due' }}

@@ -134,6 +131,34 @@ />
+
+ Need help? + + Read the docs + + or + + contact support + +
+ currentPlan.value?.status === WorkspacePlanStatuses.Canceled +) const isActivePlan = computed( () => currentPlan.value && currentPlan.value?.status !== WorkspacePlanStatuses.Trial && currentPlan.value?.status !== WorkspacePlanStatuses.Canceled ) -const isPurchasablePlan = computed( - () => - currentPlan.value?.name === WorkspacePlans.Starter || - currentPlan.value?.name === WorkspacePlans.Plus || - currentPlan.value?.name === WorkspacePlans.Business || - !currentPlan.value?.name // no plan equals pro trial plan -) +const isPurchasablePlan = computed(() => isPaidPlan(currentPlan.value?.name)) const seatPrice = computed(() => currentPlan.value && subscription.value ? seatPrices.value[currentPlan.value.name as keyof typeof seatPrices.value][ @@ -252,19 +279,23 @@ const nextPaymentDue = computed(() => ) const isAdmin = computed(() => workspace.value?.role === Roles.Workspace.Admin) const guestSeatCount = computed(() => - workspace.value - ? workspace.value.team.items.filter((user) => user.role === Roles.Workspace.Guest) - .length - : 0 + isActivePlan.value + ? workspace.value?.subscription?.seats.guest ?? 0 + : workspace.value?.team.items.filter((user) => user.role === Roles.Workspace.Guest) + .length ?? 0 ) const memberSeatCount = computed(() => - workspace.value ? workspace.value.team.items.length - guestSeatCount.value : 0 + isActivePlan.value + ? workspace.value?.subscription?.seats.plan ?? 0 + : workspace.value + ? workspace.value.team.items.length - guestSeatCount.value + : 0 ) const billValue = computed(() => { const guestPrice = seatPrice.value[Roles.Workspace.Guest] * guestSeatCount.value const memberPrice = seatPrice.value[Roles.Workspace.Member] * memberSeatCount.value const totalPrice = guestPrice + memberPrice - if (statusIsTrial.value) return `£${totalPrice}` + if (isPurchasablePlan.value) return `£${totalPrice}` return `£0` }) const billDescription = computed(() => { diff --git a/packages/frontend-2/components/settings/workspaces/General/DeleteDialog.vue b/packages/frontend-2/components/settings/workspaces/General/DeleteDialog.vue index 0e7ea5664..e6d8a9e35 100644 --- a/packages/frontend-2/components/settings/workspaces/General/DeleteDialog.vue +++ b/packages/frontend-2/components/settings/workspaces/General/DeleteDialog.vue @@ -126,14 +126,12 @@ const onDelete = async () => { isDeleted: true }) - if (!import.meta.dev) { - await sendWebhook(defaultZapierWebhookUrl, { - userId: activeUser.value?.id ?? '', - feedback: feedback.value - ? `Action: Workspace Deleted(${props.workspace.name}) Feedback: ${feedback.value}` - : `Action: Workspace Deleted(${props.workspace.name}) - No feedback provided` - }) - } + await sendWebhook(defaultZapierWebhookUrl, { + userId: activeUser.value?.id ?? '', + feedback: feedback.value + ? `Action: Workspace Deleted(${props.workspace.name}) Feedback: ${feedback.value}` + : `Action: Workspace Deleted(${props.workspace.name}) - No feedback provided` + }) triggerNotification({ type: ToastNotificationType.Success, diff --git a/packages/frontend-2/components/settings/workspaces/General/EditAvatar.vue b/packages/frontend-2/components/settings/workspaces/General/EditAvatar.vue index 2b91445c8..122e48673 100644 --- a/packages/frontend-2/components/settings/workspaces/General/EditAvatar.vue +++ b/packages/frontend-2/components/settings/workspaces/General/EditAvatar.vue @@ -3,7 +3,6 @@ v-model:edit-mode="editMode" :model-value="workspace.logo" :placeholder="workspace.name" - :default-img="defaultAvatar" name="edit-avatar" :disabled="loading || disabled" :size="size" @@ -16,14 +15,12 @@ import type { SettingsWorkspacesGeneralEditAvatar_WorkspaceFragment } from '~~/l import type { MaybeNullOrUndefined } from '@speckle/shared' import type { UserAvatarSize } from '@speckle/ui-components/dist/composables/user/avatar' import { useUpdateWorkspace } from '~~/lib/settings/composables/management' -import { useWorkspacesAvatar } from '~~/lib/workspaces/composables/avatar' graphql(` fragment SettingsWorkspacesGeneralEditAvatar_Workspace on Workspace { id logo name - defaultLogoIndex } `) @@ -34,12 +31,9 @@ const props = defineProps<{ }>() const { mutate, loading } = useUpdateWorkspace() -const { getDefaultAvatar } = useWorkspacesAvatar() const editMode = ref(false) -const defaultAvatar = computed(() => getDefaultAvatar(props.workspace.defaultLogoIndex)) - const onSave = async (newVal: MaybeNullOrUndefined) => { if (props.workspace.logo === newVal) return if (loading.value) return diff --git a/packages/frontend-2/components/settings/workspaces/billing/PricingTable/Plan.vue b/packages/frontend-2/components/settings/workspaces/billing/PricingTable/Plan.vue index 397516639..f87bb2ada 100644 --- a/packages/frontend-2/components/settings/workspaces/billing/PricingTable/Plan.vue +++ b/packages/frontend-2/components/settings/workspaces/billing/PricingTable/Plan.vue @@ -67,7 +67,7 @@ v-if="plan.features.includes(feature.name as PlanFeaturesList)" class="w-4 h-4 text-foreground mx-2" /> - +
+ class="h-full w-full bg-cover bg-center bg-no-repeat flex items-center justify-center" + :style="{ backgroundImage: `url('${logo}')` }" + > + + {{ name[0] }} + +
diff --git a/packages/frontend-2/components/workspace/CreateDialog.vue b/packages/frontend-2/components/workspace/CreateDialog.vue index 7bee3956e..5e3c31f3b 100644 --- a/packages/frontend-2/components/workspace/CreateDialog.vue +++ b/packages/frontend-2/components/workspace/CreateDialog.vue @@ -33,7 +33,6 @@ v-model:edit-mode="editAvatarMode" :model-value="workspaceLogo" :placeholder="workspaceName" - :default-img="defaultAvatar" name="edit-avatar" size="xxl" @save="onLogoSave" @@ -47,7 +46,6 @@ import { useForm } from 'vee-validate' import type { MaybeNullOrUndefined } from '@speckle/shared' import type { LayoutDialogButton } from '@speckle/ui-components' import { useCreateWorkspace } from '~/lib/workspaces/composables/management' -import { useWorkspacesAvatar } from '~/lib/workspaces/composables/avatar' import { isRequired, isStringOfLength } from '~~/lib/common/helpers/validation' import { generateSlugFromName } from '@speckle/shared' import { debounce } from 'lodash' @@ -65,7 +63,6 @@ const props = defineProps<{ const isOpen = defineModel('open', { required: true }) const createWorkspace = useCreateWorkspace() -const { generateDefaultLogoIndex, getDefaultAvatar } = useWorkspacesAvatar() const { handleSubmit, resetForm } = useForm<{ name: string; slug: string }>() const workspaceName = ref('') @@ -73,7 +70,6 @@ const workspaceShortId = ref('') const debouncedWorkspaceShortId = ref('') const editAvatarMode = ref(false) const workspaceLogo = ref>() -const defaultLogoIndex = ref(generateDefaultLogoIndex()) const shortIdManuallyEdited = ref(false) const { error, loading } = useQuery( @@ -88,8 +84,6 @@ const { error, loading } = useQuery( const baseUrl = useRuntimeConfig().public.baseUrl -const defaultAvatar = computed(() => getDefaultAvatar(defaultLogoIndex.value)) - const getShortIdHelp = computed(() => workspaceShortId.value ? `${baseUrl}/workspaces/${workspaceShortId.value}` @@ -122,7 +116,6 @@ const handleCreateWorkspace = handleSubmit(async () => { { name: workspaceName.value, slug: workspaceShortId.value, - defaultLogoIndex: defaultLogoIndex.value, logo: workspaceLogo.value }, { navigateOnSuccess: props.navigateOnSuccess === true } @@ -140,7 +133,6 @@ const onLogoSave = (newVal: MaybeNullOrUndefined) => { } const reset = () => { - defaultLogoIndex.value = generateDefaultLogoIndex() debouncedWorkspaceShortId.value = '' workspaceLogo.value = null editAvatarMode.value = false diff --git a/packages/frontend-2/components/workspace/CreatePage.vue b/packages/frontend-2/components/workspace/CreatePage.vue index 9475668f4..e66b9c084 100644 --- a/packages/frontend-2/components/workspace/CreatePage.vue +++ b/packages/frontend-2/components/workspace/CreatePage.vue @@ -1,25 +1,24 @@ diff --git a/packages/frontend-2/components/workspace/ProjectList.vue b/packages/frontend-2/components/workspace/ProjectList.vue index 2e6802dc4..15f833c10 100644 --- a/packages/frontend-2/components/workspace/ProjectList.vue +++ b/packages/frontend-2/components/workspace/ProjectList.vue @@ -227,7 +227,7 @@ const { query, identifier, onInfiniteLoad } = usePaginatedQuery({ }), resolveCursorFromVariables: (vars) => vars.cursor }) -const { finalizeWizard, isLoading: wizardLoading } = useWorkspacesWizard() +const { finalizeWizard } = useWorkspacesWizard() const projects = computed(() => query.result.value?.workspaceBySlug?.projects) const workspaceInvite = computed(() => initialQueryResult.value?.workspaceInvite) @@ -306,7 +306,7 @@ onResult((queryResult) => { queryResult.data?.workspaceBySlug.creationState?.completed === false && queryResult.data.workspaceBySlug.creationState.state ) { - if (wizardLoading.value || import.meta.server) return + if (import.meta.server) return finalizeWizard( queryResult.data.workspaceBySlug.creationState.state as WorkspaceWizardState, queryResult.data.workspaceBySlug.id diff --git a/packages/frontend-2/components/workspace/RegionStaticDataDisclaimer.vue b/packages/frontend-2/components/workspace/RegionStaticDataDisclaimer.vue index 109411b74..9cb7f5b0d 100644 --- a/packages/frontend-2/components/workspace/RegionStaticDataDisclaimer.vue +++ b/packages/frontend-2/components/workspace/RegionStaticDataDisclaimer.vue @@ -44,7 +44,7 @@ const dialogButtons = computed((): LayoutDialogButton[] => [ } }, { - text: 'I Understand', + text: 'I understand', onClick: () => { open.value = false emit('confirm') diff --git a/packages/frontend-2/components/workspace/header/Header.vue b/packages/frontend-2/components/workspace/header/Header.vue index 27fcccf07..7e9faac47 100644 --- a/packages/frontend-2/components/workspace/header/Header.vue +++ b/packages/frontend-2/components/workspace/header/Header.vue @@ -15,8 +15,8 @@
@@ -134,7 +134,6 @@ import type { AlertAction } from '@speckle/ui-components' graphql(` fragment WorkspaceHeader_Workspace on Workspace { - ...WorkspaceAvatar_Workspace ...BillingAlert_Workspace id slug diff --git a/packages/frontend-2/components/workspace/invite/DiscoverableWorkspaceBanner.vue b/packages/frontend-2/components/workspace/invite/DiscoverableWorkspaceBanner.vue index 780a2929e..5c1d4b9f1 100644 --- a/packages/frontend-2/components/workspace/invite/DiscoverableWorkspaceBanner.vue +++ b/packages/frontend-2/components/workspace/invite/DiscoverableWorkspaceBanner.vue @@ -32,7 +32,6 @@ graphql(` slug description logo - defaultLogoIndex } fragment WorkspaceInviteDiscoverableWorkspaceBanner_Workspace on Workspace { id @@ -41,7 +40,6 @@ graphql(` createdAt updatedAt logo - defaultLogoIndex domainBasedMembershipProtectionEnabled discoverabilityEnabled } @@ -67,7 +65,7 @@ const invite = computed(() => ({ workspace: { id: props.workspace.id, logo: props.workspace.logo || undefined, - defaultLogoIndex: props.workspace.defaultLogoIndex + name: props.workspace.name } })) diff --git a/packages/frontend-2/components/workspace/wizard/Wizard.vue b/packages/frontend-2/components/workspace/wizard/Wizard.vue index 10ed450d2..70ffbf392 100644 --- a/packages/frontend-2/components/workspace/wizard/Wizard.vue +++ b/packages/frontend-2/components/workspace/wizard/Wizard.vue @@ -1,6 +1,6 @@