Merge branch 'main' of github.com:specklesystems/speckle-server into alessandro/web-2803-downscale-workspace-subscription
This commit is contained in:
@@ -1,4 +1,3 @@
|
||||
version: '2.4'
|
||||
services:
|
||||
speckle-ingress:
|
||||
build:
|
||||
@@ -22,12 +21,16 @@ services:
|
||||
restart: always
|
||||
environment:
|
||||
NUXT_PUBLIC_SERVER_NAME: 'local'
|
||||
#TODO: Change this to the URL of your server. This is the URL of the server as accessed by users.
|
||||
NUXT_PUBLIC_API_ORIGIN: 'http://127.0.0.1'
|
||||
#TODO: Change this to the URL of your server. This is the URL of the server as accessed by users.
|
||||
NUXT_PUBLIC_BASE_URL: 'http://127.0.0.1'
|
||||
# This is the URL of the server as accessed via this docker compose network.
|
||||
NUXT_PUBLIC_BACKEND_API_ORIGIN: 'http://speckle-server:3000'
|
||||
NUXT_PUBLIC_LOG_LEVEL: 'warn'
|
||||
NUXT_REDIS_URL: 'redis://redis'
|
||||
LOG_LEVEL: 'info'
|
||||
LOG_PRETTY: 'true'
|
||||
|
||||
speckle-server:
|
||||
build:
|
||||
@@ -47,14 +50,20 @@ services:
|
||||
retries: 3
|
||||
start_period: 90s
|
||||
environment:
|
||||
# TODO: Change this to the URL of the speckle server, as accessed from the network
|
||||
# TODO. Change this to the url of your server. This is the URL of the server as accessed by users.
|
||||
CANONICAL_URL: 'http://127.0.0.1'
|
||||
# This is the URL of the server as accessed by other Speckle services within this docker compose network, such as preview-service.
|
||||
# This will be the same value as NUXT_PUBLIC_BACKEND_API_ORIGIN as defined in the frontend-2 service.
|
||||
PRIVATE_OBJECTS_SERVER_URL: 'http://speckle-server:3000'
|
||||
|
||||
# TODO: Change this to a unique secret for this server
|
||||
SESSION_SECRET: 'TODO:Replace'
|
||||
|
||||
# This is the authentication strategy to use. Local (i.e. username & password) is the default strategy.
|
||||
STRATEGY_LOCAL: 'true'
|
||||
|
||||
LOG_LEVEL: 'info'
|
||||
LOG_PRETTY: 'true'
|
||||
|
||||
POSTGRES_URL: 'postgres'
|
||||
POSTGRES_USER: 'speckle'
|
||||
@@ -62,6 +71,8 @@ services:
|
||||
POSTGRES_DB: 'speckle'
|
||||
|
||||
REDIS_URL: 'redis://redis'
|
||||
PREVIEW_SERVICE_USE_PRIVATE_OBJECTS_SERVER_URL: 'true'
|
||||
PREVIEW_SERVICE_REDIS_URL: 'redis://redis'
|
||||
|
||||
S3_ENDPOINT: 'http://minio:9000'
|
||||
S3_ACCESS_KEY: 'minioadmin'
|
||||
@@ -85,10 +96,11 @@ services:
|
||||
mem_limit: '3000m'
|
||||
memswap_limit: '3000m'
|
||||
environment:
|
||||
HOST: '127.0.0.1' # Only accept connections from localhost, as preview service does not need to be exposed outside the container.
|
||||
METRICS_HOST: '127.0.0.1' # Amend if you want to expose Prometheus metrics outside of the container
|
||||
HOST: '127.0.0.1' # The preview service does not need to be exposed outside the container.
|
||||
PORT: '3001'
|
||||
LOG_LEVEL: 'info'
|
||||
PG_CONNECTION_STRING: 'postgres://speckle:speckle@postgres/speckle'
|
||||
LOG_PRETTY: 'true'
|
||||
REDIS_URL: 'redis://redis'
|
||||
|
||||
webhook-service:
|
||||
build:
|
||||
@@ -99,6 +111,7 @@ services:
|
||||
restart: always
|
||||
environment:
|
||||
LOG_LEVEL: 'info'
|
||||
LOG_PRETTY: 'true'
|
||||
PG_CONNECTION_STRING: 'postgres://speckle:speckle@postgres/speckle'
|
||||
|
||||
fileimport-service:
|
||||
@@ -110,6 +123,7 @@ services:
|
||||
restart: always
|
||||
environment:
|
||||
LOG_LEVEL: 'info'
|
||||
LOG_PRETTY: 'true'
|
||||
PG_CONNECTION_STRING: 'postgres://speckle:speckle@postgres/speckle'
|
||||
SPECKLE_SERVER_URL: 'http://speckle-server:3000'
|
||||
FILE_IMPORT_TIME_LIMIT_MIN: 10
|
||||
|
||||
@@ -74,4 +74,5 @@ ENV IFC_DOTNET_DLL_PATH='/speckle-server/packages/fileimport-service/src/ifc-dot
|
||||
|
||||
WORKDIR /speckle-server/packages/fileimport-service
|
||||
|
||||
ENTRYPOINT [ "tini", "--", "node", "--loader=./dist/src/aliasLoader.js", "bin/www.js" ]
|
||||
ENTRYPOINT [ "tini", "--", "node", "--loader=./dist/src/aliasLoader.js" ]
|
||||
CMD ["bin/www.js"]
|
||||
|
||||
@@ -23,13 +23,17 @@
|
||||
@click="isOpenMobile = false"
|
||||
/>
|
||||
<div
|
||||
class="absolute z-40 lg:static h-full flex w-[17rem] shrink-0 transition-all"
|
||||
:class="isOpenMobile ? '' : '-translate-x-[17rem] lg:translate-x-0'"
|
||||
class="absolute z-40 lg:static h-full flex w-[13rem] shrink-0 transition-all"
|
||||
:class="isOpenMobile ? '' : '-translate-x-[13rem] lg:translate-x-0'"
|
||||
>
|
||||
<LayoutSidebar
|
||||
class="border-r border-outline-3 px-2 pt-3 pb-2 bg-foundation-page"
|
||||
>
|
||||
<LayoutSidebarMenu>
|
||||
<LayoutSidebarMenuGroup v-if="isWorkspacesEnabled && isMobile">
|
||||
<HeaderWorkspaceSwitcher />
|
||||
</LayoutSidebarMenuGroup>
|
||||
|
||||
<LayoutSidebarMenuGroup>
|
||||
<template v-if="!isWorkspacesEnabled">
|
||||
<NuxtLink :to="projectsRoute" @click="isOpenMobile = false">
|
||||
@@ -168,11 +172,15 @@ import { useRoute } from 'vue-router'
|
||||
import { useActiveUser } from '~~/lib/auth/composables/activeUser'
|
||||
import { HomeIcon } from '@heroicons/vue/24/outline'
|
||||
import { useNavigation } from '~~/lib/navigation/composables/navigation'
|
||||
import { TailwindBreakpoints } from '~~/lib/common/helpers/tailwind'
|
||||
import { useBreakpoints } from '@vueuse/core'
|
||||
|
||||
const { isLoggedIn } = useActiveUser()
|
||||
const isWorkspacesEnabled = useIsWorkspacesEnabled()
|
||||
const route = useRoute()
|
||||
const { activeWorkspaceSlug, isProjectsActive } = useNavigation()
|
||||
const breakpoints = useBreakpoints(TailwindBreakpoints)
|
||||
const isMobile = breakpoints.smaller('lg')
|
||||
|
||||
const isOpenMobile = ref(false)
|
||||
const showFeedbackDialog = ref(false)
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
<template>
|
||||
<div>
|
||||
<nav class="fixed z-40 top-0 h-12 bg-foundation border-b border-outline-2">
|
||||
<div
|
||||
class="flex gap-4 items-center justify-between h-full w-screen py-4 px-3 sm:px-4"
|
||||
>
|
||||
<div class="flex gap-4 items-center justify-between h-full w-screen py-4 px-3">
|
||||
<div class="hidden lg:block">
|
||||
<HeaderWorkspaceSwitcher v-if="showWorkspaceSwitcher" />
|
||||
<HeaderLogoBlock
|
||||
|
||||
+1
-3
@@ -1,13 +1,11 @@
|
||||
<template>
|
||||
<div class="flex items-center">
|
||||
<div class="w-6 flex-shrink-0">
|
||||
<IconCheck v-if="isActive" class="w-4 h-4 mx-1 text-foreground" />
|
||||
</div>
|
||||
<MenuItem class="min-w-0 w-full">
|
||||
<NuxtLink class="flex-1 min-w-0" @click="$emit('onClick')">
|
||||
<LayoutSidebarMenuGroupItem
|
||||
:label="name"
|
||||
:tag="tag"
|
||||
:active="isActive"
|
||||
color-classes="bg-foundation-2 text-foreground-2"
|
||||
>
|
||||
<template #icon>
|
||||
+99
-115
@@ -1,12 +1,16 @@
|
||||
<template>
|
||||
<div>
|
||||
<Menu as="div" class="flex items-center">
|
||||
<MenuButton :id="menuButtonId" v-slot="{ open: userOpen }">
|
||||
<MenuButton :id="menuButtonId" v-slot="{ open: userOpen }" class="w-full">
|
||||
<span class="sr-only">Open workspace menu</span>
|
||||
<div class="flex items-center gap-2 p-0.5 pr-1.5 hover:bg-highlight-2 rounded">
|
||||
<template v-if="activeWorkspaceSlug || isProjectsActive">
|
||||
<div class="relative">
|
||||
<WorkspaceAvatar :name="displayName" :logo="displayLogo" />
|
||||
<WorkspaceAvatar
|
||||
:size="isMobile ? 'sm' : 'base'"
|
||||
:name="displayName || ''"
|
||||
:logo="displayLogo"
|
||||
/>
|
||||
<div
|
||||
v-if="hasDiscoverableWorkspaces"
|
||||
class="absolute -top-[4px] -right-[4px] size-3 border-[2px] border-foundation-page bg-primary rounded-full"
|
||||
@@ -32,93 +36,40 @@
|
||||
leave-to-class="transform opacity-0 scale-95"
|
||||
>
|
||||
<MenuItems
|
||||
class="absolute left-4 top-14 w-64 origin-top-right bg-foundation outline outline-1 outline-primary-muted rounded-md shadow-lg overflow-hidden divide-y divide-outline-2"
|
||||
class="absolute left-2 lg:left-3 top-12 lg:top-14 w-full lg:w-[17rem] origin-top-right bg-foundation outline outline-1 outline-primary-muted rounded-md shadow-lg overflow-hidden divide-y divide-outline-2"
|
||||
>
|
||||
<HeaderWorkspaceSwitcherHeaderSsoExpired
|
||||
v-if="activeWorkspaceHasExpiredSsoSession"
|
||||
:workspace="expiredSsoWorkspaceData"
|
||||
/>
|
||||
<HeaderWorkspaceSwitcherHeaderProjects v-else-if="isProjectsActive" />
|
||||
<HeaderWorkspaceSwitcherHeaderWorkspace
|
||||
v-else-if="!!activeWorkspace"
|
||||
:workspace="activeWorkspace"
|
||||
/>
|
||||
<div
|
||||
v-if="activeWorkspaceSlug || isProjectsActive"
|
||||
class="p-2 pb-3 flex flex-col gap-y-4"
|
||||
class="p-2 pt-1 max-h-[60vh] lg:max-h-96 overflow-y-auto simple-scrollbar"
|
||||
>
|
||||
<div class="flex gap-x-2 items-center">
|
||||
<MenuItem>
|
||||
<NuxtLink
|
||||
:to="
|
||||
activeWorkspaceSlug
|
||||
? workspaceRoute(activeWorkspaceSlug)
|
||||
: projectsRoute
|
||||
"
|
||||
>
|
||||
<WorkspaceAvatar
|
||||
:name="displayName"
|
||||
:logo="displayLogo"
|
||||
size="lg"
|
||||
class="flex-shrink-0"
|
||||
/>
|
||||
</NuxtLink>
|
||||
</MenuItem>
|
||||
<div class="flex flex-col space-between min-w-0">
|
||||
<p class="text-body-xs text-foreground truncate">
|
||||
{{ displayName }}
|
||||
</p>
|
||||
<p
|
||||
v-if="activeWorkspace"
|
||||
class="text-body-2xs text-foreground-2 capitalize truncate"
|
||||
>
|
||||
{{ activeWorkspace?.plan?.name }} ·
|
||||
{{ activeWorkspace?.team?.totalCount }} member{{
|
||||
activeWorkspace?.team?.totalCount > 1 ? 's' : ''
|
||||
}}
|
||||
</p>
|
||||
<p v-else class="text-body-2xs text-foreground-2 truncate">
|
||||
2 projects to move
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="activeWorkspaceSlug" class="flex gap-x-2">
|
||||
<MenuItem>
|
||||
<FormButton
|
||||
color="outline"
|
||||
full-width
|
||||
size="sm"
|
||||
@click="goToSettingsRoute"
|
||||
>
|
||||
Settings
|
||||
</FormButton>
|
||||
</MenuItem>
|
||||
<MenuItem>
|
||||
<FormButton
|
||||
full-width
|
||||
color="outline"
|
||||
size="sm"
|
||||
:disabled="activeWorkspace?.role !== Roles.Workspace.Admin"
|
||||
@click="showInviteDialog = true"
|
||||
>
|
||||
Invite members
|
||||
</FormButton>
|
||||
</MenuItem>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-2 pt-1 max-h-96 overflow-y-auto simple-scrollbar">
|
||||
<LayoutSidebarMenuGroup
|
||||
title="Workspaces"
|
||||
:icon-click="isGuest ? undefined : handlePlusClick"
|
||||
icon-text="Create workspace"
|
||||
>
|
||||
<div v-if="hasWorkspaces" class="w-full">
|
||||
<template v-for="item in workspaces" :key="`menu-item-${item.id}`">
|
||||
<DashboardSidebarWorkspaceItem
|
||||
:is-active="item.slug === activeWorkspaceSlug"
|
||||
:name="item.name"
|
||||
:logo="item.logo"
|
||||
@on-click="onWorkspaceSelect(item.slug)"
|
||||
/>
|
||||
</template>
|
||||
<DashboardSidebarWorkspaceItem
|
||||
:is-active="route.path === projectsRoute"
|
||||
name="Personal projects"
|
||||
tag="LEGACY"
|
||||
@on-click="onProjectsSelect"
|
||||
/>
|
||||
</div>
|
||||
<HeaderWorkspaceSwitcherItem
|
||||
v-for="item in workspaces"
|
||||
:key="`menu-item-${item.id}`"
|
||||
:is-active="item.slug === activeWorkspaceSlug"
|
||||
:name="item.name"
|
||||
:logo="item.logo"
|
||||
:tag="getWorkspaceTag(item)"
|
||||
@on-click="onWorkspaceSelect(item.slug)"
|
||||
/>
|
||||
<HeaderWorkspaceSwitcherItem
|
||||
:is-active="route.path === projectsRoute"
|
||||
name="Personal projects"
|
||||
tag="LEGACY"
|
||||
@on-click="onProjectsSelect"
|
||||
/>
|
||||
</LayoutSidebarMenuGroup>
|
||||
</div>
|
||||
<MenuItem v-if="hasDiscoverableWorkspacesOrJoinRequests">
|
||||
@@ -138,11 +89,6 @@
|
||||
</Transition>
|
||||
</Menu>
|
||||
|
||||
<InviteDialogWorkspace
|
||||
v-model:open="showInviteDialog"
|
||||
:workspace="activeWorkspace"
|
||||
/>
|
||||
|
||||
<WorkspaceDiscoverableWorkspacesModal
|
||||
v-model:open="showDiscoverableWorkspacesModal"
|
||||
/>
|
||||
@@ -155,28 +101,54 @@ import { useActiveUser } from '~~/lib/auth/composables/activeUser'
|
||||
import {
|
||||
workspaceCreateRoute,
|
||||
workspaceRoute,
|
||||
settingsWorkspaceRoutes,
|
||||
projectsRoute
|
||||
} from '~/lib/common/helpers/route'
|
||||
import { useMixpanel } from '~~/lib/core/composables/mp'
|
||||
import { useUserWorkspaces } from '~/lib/user/composables/workspaces'
|
||||
import { useDiscoverableWorkspaces } from '~/lib/workspaces/composables/discoverableWorkspaces'
|
||||
import { graphql } from '~/lib/common/generated/gql'
|
||||
import { useNavigation } from '~~/lib/navigation/composables/navigation'
|
||||
import { Roles } from '@speckle/shared'
|
||||
import { Roles, WorkspacePlans } from '@speckle/shared'
|
||||
import type { HeaderWorkspaceSwitcherWorkspaceList_WorkspaceFragment } from '~/lib/common/generated/gql/graphql'
|
||||
import { TailwindBreakpoints } from '~~/lib/common/helpers/tailwind'
|
||||
import { useBreakpoints } from '@vueuse/core'
|
||||
|
||||
graphql(`
|
||||
fragment HeaderWorkspaceSwitcher_Workspace on Workspace {
|
||||
...InviteDialogWorkspace_Workspace
|
||||
fragment HeaderWorkspaceSwitcherActiveWorkspace_Workspace on Workspace {
|
||||
id
|
||||
name
|
||||
logo
|
||||
...HeaderWorkspaceSwitcherHeaderWorkspace_Workspace
|
||||
}
|
||||
`)
|
||||
|
||||
graphql(`
|
||||
fragment HeaderWorkspaceSwitcherWorkspaceList_Workspace on Workspace {
|
||||
id
|
||||
name
|
||||
logo
|
||||
role
|
||||
slug
|
||||
creationState {
|
||||
completed
|
||||
}
|
||||
plan {
|
||||
name
|
||||
}
|
||||
team {
|
||||
totalCount
|
||||
}
|
||||
`)
|
||||
|
||||
graphql(`
|
||||
fragment HeaderWorkspaceSwitcherWorkspaceList_User on User {
|
||||
id
|
||||
expiredSsoSessions {
|
||||
id
|
||||
...HeaderWorkspaceSwitcherHeaderExpiredSso_LimitedWorkspace
|
||||
}
|
||||
workspaces {
|
||||
items {
|
||||
id
|
||||
...HeaderWorkspaceSwitcherWorkspaceList_Workspace
|
||||
}
|
||||
}
|
||||
}
|
||||
`)
|
||||
@@ -189,30 +161,40 @@ const {
|
||||
isProjectsActive,
|
||||
mutateActiveWorkspaceSlug,
|
||||
mutateIsProjectsActive,
|
||||
workspaceData
|
||||
activeWorkspaceData,
|
||||
workspaceList: workspaces,
|
||||
activeWorkspaceHasExpiredSsoSession,
|
||||
expiredSsoWorkspaceData
|
||||
} = useNavigation()
|
||||
|
||||
const showInviteDialog = ref(false)
|
||||
const showDiscoverableWorkspacesModal = ref(false)
|
||||
|
||||
const activeWorkspace = computed(() => {
|
||||
return workspaceData.value
|
||||
})
|
||||
|
||||
const displayName = computed(() => activeWorkspace.value?.name || 'Personal projects')
|
||||
|
||||
const displayLogo = computed(() => {
|
||||
if (isProjectsActive.value) return null
|
||||
return activeWorkspace.value?.logo
|
||||
})
|
||||
|
||||
const route = useRoute()
|
||||
const { workspaces, hasWorkspaces } = useUserWorkspaces()
|
||||
const {
|
||||
hasDiscoverableWorkspaces,
|
||||
discoverableWorkspacesCount,
|
||||
hasDiscoverableWorkspacesOrJoinRequests
|
||||
} = useDiscoverableWorkspaces()
|
||||
const breakpoints = useBreakpoints(TailwindBreakpoints)
|
||||
const isMobile = breakpoints.smaller('lg')
|
||||
|
||||
const showDiscoverableWorkspacesModal = ref(false)
|
||||
|
||||
const activeWorkspace = computed(() => {
|
||||
return activeWorkspaceData.value
|
||||
})
|
||||
|
||||
const displayName = computed(() =>
|
||||
isProjectsActive.value
|
||||
? 'Personal projects'
|
||||
: activeWorkspaceHasExpiredSsoSession.value
|
||||
? expiredSsoWorkspaceData.value?.name
|
||||
: activeWorkspace.value?.name
|
||||
)
|
||||
|
||||
const displayLogo = computed(() => {
|
||||
if (isProjectsActive.value) return null
|
||||
return activeWorkspaceHasExpiredSsoSession.value
|
||||
? expiredSsoWorkspaceData.value?.logo
|
||||
: activeWorkspace.value?.logo
|
||||
})
|
||||
|
||||
const onWorkspaceSelect = (slug: string) => {
|
||||
navigateTo(workspaceRoute(slug))
|
||||
@@ -224,15 +206,17 @@ const onProjectsSelect = () => {
|
||||
navigateTo(projectsRoute)
|
||||
}
|
||||
|
||||
const goToSettingsRoute = () => {
|
||||
if (!activeWorkspaceSlug.value) return
|
||||
navigateTo(settingsWorkspaceRoutes.general.route(activeWorkspaceSlug.value))
|
||||
}
|
||||
|
||||
const handlePlusClick = () => {
|
||||
navigateTo(workspaceCreateRoute())
|
||||
mixpanel.track('Create Workspace Button Clicked', {
|
||||
source: 'navigation'
|
||||
})
|
||||
}
|
||||
|
||||
const getWorkspaceTag = (
|
||||
workspace: HeaderWorkspaceSwitcherWorkspaceList_WorkspaceFragment
|
||||
) => {
|
||||
if (workspace.role === Roles.Workspace.Guest) return 'GUEST'
|
||||
if (workspace.plan?.name === WorkspacePlans.Free) return 'FREE'
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,33 @@
|
||||
<template>
|
||||
<div class="p-2 pb-3 flex flex-col gap-y-4">
|
||||
<div class="flex gap-x-2 items-center">
|
||||
<MenuItem>
|
||||
<NuxtLink :to="to">
|
||||
<WorkspaceAvatar
|
||||
:name="name || ''"
|
||||
:logo="logo"
|
||||
size="lg"
|
||||
class="flex-shrink-0"
|
||||
/>
|
||||
</NuxtLink>
|
||||
</MenuItem>
|
||||
<div class="flex flex-col space-between min-w-0">
|
||||
<p class="text-body-xs text-foreground truncate">
|
||||
{{ name }}
|
||||
</p>
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
<slot name="actions" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { MaybeNullOrUndefined } from '@speckle/shared'
|
||||
|
||||
defineProps<{
|
||||
name: MaybeNullOrUndefined<string>
|
||||
logo?: MaybeNullOrUndefined<string>
|
||||
to: string
|
||||
}>()
|
||||
</script>
|
||||
@@ -0,0 +1,9 @@
|
||||
<template>
|
||||
<HeaderWorkspaceSwitcherHeader name="Projects" :to="projectsRoute">
|
||||
<p class="text-body-2xs text-foreground-2 truncate">2 projects to move</p>
|
||||
</HeaderWorkspaceSwitcherHeader>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { projectsRoute } from '~/lib/common/helpers/route'
|
||||
</script>
|
||||
@@ -0,0 +1,52 @@
|
||||
<template>
|
||||
<HeaderWorkspaceSwitcherHeader
|
||||
:name="workspace?.name"
|
||||
:logo="workspace?.logo"
|
||||
:to="workspaceRoute(activeWorkspaceSlug || '')"
|
||||
>
|
||||
<p class="text-body-2xs text-foreground-2 truncate">Requires SSO authentication</p>
|
||||
<template #actions>
|
||||
<MenuItem>
|
||||
<FormButton color="outline" full-width size="sm" @click="handleSsoLogin">
|
||||
Sign in with SSO
|
||||
</FormButton>
|
||||
</MenuItem>
|
||||
</template>
|
||||
</HeaderWorkspaceSwitcherHeader>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { MenuItem } from '@headlessui/vue'
|
||||
import { graphql } from '~/lib/common/generated/gql'
|
||||
import type { HeaderWorkspaceSwitcherHeaderExpiredSso_LimitedWorkspaceFragment } from '~/lib/common/generated/gql/graphql'
|
||||
import type { MaybeNullOrUndefined } from '@speckle/shared'
|
||||
import { workspaceRoute } from '~/lib/common/helpers/route'
|
||||
import { useNavigation } from '~~/lib/navigation/composables/navigation'
|
||||
import { useAuthManager, useLoginOrRegisterUtils } from '~/lib/auth/composables/auth'
|
||||
|
||||
graphql(`
|
||||
fragment HeaderWorkspaceSwitcherHeaderExpiredSso_LimitedWorkspace on LimitedWorkspace {
|
||||
id
|
||||
slug
|
||||
name
|
||||
logo
|
||||
}
|
||||
`)
|
||||
|
||||
defineProps<{
|
||||
workspace: MaybeNullOrUndefined<HeaderWorkspaceSwitcherHeaderExpiredSso_LimitedWorkspaceFragment>
|
||||
}>()
|
||||
|
||||
const { activeWorkspaceSlug } = useNavigation()
|
||||
const { signInOrSignUpWithSso } = useAuthManager()
|
||||
const { challenge } = useLoginOrRegisterUtils()
|
||||
|
||||
const handleSsoLogin = () => {
|
||||
if (!activeWorkspaceSlug.value) return
|
||||
|
||||
signInOrSignUpWithSso({
|
||||
workspaceSlug: activeWorkspaceSlug.value,
|
||||
challenge: challenge.value
|
||||
})
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,75 @@
|
||||
<template>
|
||||
<div>
|
||||
<HeaderWorkspaceSwitcherHeader
|
||||
:name="workspace?.name"
|
||||
:logo="workspace?.logo"
|
||||
:to="workspaceRoute(activeWorkspaceSlug || '')"
|
||||
>
|
||||
<p class="text-body-2xs text-foreground-2 capitalize truncate">
|
||||
{{ workspace?.plan?.name }} · {{ workspace?.team?.totalCount ?? 0 }} member{{
|
||||
(workspace?.team?.totalCount ?? 0) > 1 ? 's' : ''
|
||||
}}
|
||||
</p>
|
||||
<template #actions>
|
||||
<div class="flex gap-x-2 w-full">
|
||||
<MenuItem>
|
||||
<FormButton
|
||||
color="outline"
|
||||
full-width
|
||||
size="sm"
|
||||
:to="settingsWorkspaceRoutes.general.route(activeWorkspaceSlug || '')"
|
||||
>
|
||||
Settings
|
||||
</FormButton>
|
||||
</MenuItem>
|
||||
<MenuItem>
|
||||
<FormButton
|
||||
full-width
|
||||
color="outline"
|
||||
size="sm"
|
||||
:disabled="workspace?.role !== Roles.Workspace.Admin"
|
||||
@click="showInviteDialog = true"
|
||||
>
|
||||
Invite members
|
||||
</FormButton>
|
||||
</MenuItem>
|
||||
</div>
|
||||
</template>
|
||||
</HeaderWorkspaceSwitcherHeader>
|
||||
|
||||
<InviteDialogWorkspace v-model:open="showInviteDialog" :workspace="workspace" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { MenuItem } from '@headlessui/vue'
|
||||
import { graphql } from '~/lib/common/generated/gql'
|
||||
import type { HeaderWorkspaceSwitcherHeaderWorkspace_WorkspaceFragment } from '~/lib/common/generated/gql/graphql'
|
||||
import { Roles, type MaybeNullOrUndefined } from '@speckle/shared'
|
||||
import { workspaceRoute, settingsWorkspaceRoutes } from '~/lib/common/helpers/route'
|
||||
import { useNavigation } from '~~/lib/navigation/composables/navigation'
|
||||
|
||||
graphql(`
|
||||
fragment HeaderWorkspaceSwitcherHeaderWorkspace_Workspace on Workspace {
|
||||
...InviteDialogWorkspace_Workspace
|
||||
id
|
||||
name
|
||||
logo
|
||||
role
|
||||
plan {
|
||||
name
|
||||
}
|
||||
team {
|
||||
totalCount
|
||||
}
|
||||
}
|
||||
`)
|
||||
|
||||
defineProps<{
|
||||
workspace: MaybeNullOrUndefined<HeaderWorkspaceSwitcherHeaderWorkspace_WorkspaceFragment>
|
||||
}>()
|
||||
|
||||
const { activeWorkspaceSlug } = useNavigation()
|
||||
|
||||
const showInviteDialog = ref(false)
|
||||
</script>
|
||||
@@ -3,7 +3,9 @@
|
||||
<Portal to="navigation">
|
||||
<HeaderNavLink
|
||||
v-if="project.workspace && isWorkspacesEnabled"
|
||||
:to="workspaceRoute(project.workspace.slug)"
|
||||
:to="
|
||||
isWorkspaceNewPlansEnabled ? 'Home' : workspaceRoute(project.workspace.slug)
|
||||
"
|
||||
:name="project.workspace.name"
|
||||
:separator="false"
|
||||
/>
|
||||
@@ -55,4 +57,5 @@ const props = defineProps<{
|
||||
}>()
|
||||
|
||||
const isWorkspacesEnabled = useIsWorkspacesEnabled()
|
||||
const isWorkspaceNewPlansEnabled = useWorkspaceNewPlansEnabled()
|
||||
</script>
|
||||
|
||||
@@ -4,21 +4,13 @@
|
||||
<template v-if="project.workspace && isWorkspacesEnabled">
|
||||
<HeaderNavLink
|
||||
:to="workspaceRoute(project.workspace.slug)"
|
||||
:name="project.workspace.name"
|
||||
:name="isWorkspaceNewPlansEnabled ? 'Home' : project.workspace.name"
|
||||
:separator="false"
|
||||
></HeaderNavLink>
|
||||
/>
|
||||
</template>
|
||||
<HeaderNavLink
|
||||
v-else
|
||||
:to="projectsRoute"
|
||||
name="Projects"
|
||||
:separator="false"
|
||||
></HeaderNavLink>
|
||||
<HeaderNavLink v-else :to="projectsRoute" name="Projects" :separator="false" />
|
||||
|
||||
<HeaderNavLink
|
||||
:to="projectRoute(project.id)"
|
||||
:name="project.name"
|
||||
></HeaderNavLink>
|
||||
<HeaderNavLink :to="projectRoute(project.id)" :name="project.name" />
|
||||
</Portal>
|
||||
|
||||
<div class="flex gap-x-3">
|
||||
@@ -69,4 +61,5 @@ defineProps<{
|
||||
}>()
|
||||
|
||||
const isWorkspacesEnabled = useIsWorkspacesEnabled()
|
||||
const isWorkspaceNewPlansEnabled = useWorkspaceNewPlansEnabled()
|
||||
</script>
|
||||
|
||||
@@ -19,10 +19,7 @@
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</Portal>
|
||||
<div
|
||||
class="absolute z-40 lg:static h-full flex w-[17rem] shrink-0 transition-all"
|
||||
:class="isOpenMobile ? '' : '-translate-x-[17rem] lg:translate-x-0'"
|
||||
>
|
||||
<div :class="wrapperClasses">
|
||||
<LayoutSidebar
|
||||
class="border-r border-outline-3 px-2 pt-3 pb-2 bg-foundation-page"
|
||||
>
|
||||
@@ -254,6 +251,15 @@ const workspaceItems = computed(
|
||||
const activeWorkspaceItem = computed(() =>
|
||||
workspaceItems.value.find((item) => item.slug === activeWorkspaceSlug.value)
|
||||
)
|
||||
const wrapperClasses = computed(() => {
|
||||
// TODO: Simplify post WS migration
|
||||
const width = isWorkspaceNewPlansEnabled.value ? '13' : '17'
|
||||
return [
|
||||
'absolute z-40 lg:static h-full flex shrink-0 transition-all',
|
||||
`w-[${width}rem]`,
|
||||
isOpenMobile.value ? '' : `-translate-x-[${width}rem] lg:translate-x-0`
|
||||
]
|
||||
})
|
||||
|
||||
const needsSsoSession = (
|
||||
workspace: SettingsMenu_WorkspaceFragment,
|
||||
|
||||
@@ -13,7 +13,16 @@
|
||||
</div>
|
||||
<p class="text-body mt-1">
|
||||
<span class="font-medium">
|
||||
{{ formatPrice(finalPlanPrice) }}
|
||||
{{
|
||||
formatPrice(
|
||||
props.yearlyIntervalSelected && planPrice?.['workspace:member']
|
||||
? {
|
||||
...planPrice['workspace:member'],
|
||||
amount: planPrice['workspace:member'].amount * 0.8
|
||||
}
|
||||
: planPrice?.['workspace:member']
|
||||
)
|
||||
}}
|
||||
</span>
|
||||
per seat/month
|
||||
</p>
|
||||
@@ -136,19 +145,7 @@ const isUpgradeDialogOpen = ref(false)
|
||||
const isYearlyIntervalSelected = ref(props.yearlyIntervalSelected)
|
||||
|
||||
const planFeatures = computed(() => WorkspacePlanConfigs[props.plan].features)
|
||||
const planPrice = computed(
|
||||
() =>
|
||||
prices.value?.[props.plan]?.[props.yearlyIntervalSelected ? 'yearly' : 'monthly']
|
||||
)
|
||||
|
||||
const finalPlanPrice = computed(() => {
|
||||
const basePrice = prices.value?.[props.plan].monthly?.['workspace:member']
|
||||
if (!basePrice) return undefined
|
||||
return {
|
||||
...basePrice,
|
||||
amount: props.yearlyIntervalSelected ? basePrice.amount * 0.8 : basePrice.amount
|
||||
}
|
||||
})
|
||||
const planPrice = computed(() => prices.value?.[props.plan]?.monthly)
|
||||
|
||||
const hasCta = computed(() => !!slots.cta)
|
||||
const canUpgradeToPlan = computed(() => {
|
||||
|
||||
@@ -8,20 +8,17 @@
|
||||
<template v-if="project?.workspace && isWorkspacesEnabled">
|
||||
<HeaderNavLink
|
||||
:to="workspaceRoute(project?.workspace.slug)"
|
||||
:name="project?.workspace.name"
|
||||
:name="isWorkspaceNewPlansEnabled ? 'Home' : project?.workspace.name"
|
||||
:separator="false"
|
||||
></HeaderNavLink>
|
||||
/>
|
||||
</template>
|
||||
<HeaderNavLink
|
||||
v-else
|
||||
:to="projectsRoute"
|
||||
name="Projects"
|
||||
:separator="false"
|
||||
></HeaderNavLink>
|
||||
<HeaderNavLink
|
||||
:to="`/projects/${project?.id}`"
|
||||
:name="project?.name"
|
||||
></HeaderNavLink>
|
||||
/>
|
||||
<HeaderNavLink :to="`/projects/${project?.id}`" :name="project?.name" />
|
||||
<ViewerExplorerNavbarLink />
|
||||
</ViewerScope>
|
||||
</Portal>
|
||||
@@ -140,6 +137,7 @@ const projectId = writableAsyncComputed({
|
||||
asyncRead: false
|
||||
})
|
||||
|
||||
const isWorkspaceNewPlansEnabled = useWorkspaceNewPlansEnabled()
|
||||
const state = useSetupViewer({
|
||||
projectId
|
||||
})
|
||||
|
||||
@@ -48,7 +48,11 @@ type Documents = {
|
||||
"\n fragment FormSelectProjects_Project on Project {\n id\n name\n }\n": typeof types.FormSelectProjects_ProjectFragmentDoc,
|
||||
"\n fragment FormUsersSelectItem on LimitedUser {\n id\n name\n avatar\n }\n": typeof types.FormUsersSelectItemFragmentDoc,
|
||||
"\n fragment HeaderNavShare_Project on Project {\n id\n visibility\n ...ProjectsModelPageEmbed_Project\n }\n": typeof types.HeaderNavShare_ProjectFragmentDoc,
|
||||
"\n fragment HeaderWorkspaceSwitcher_Workspace on Workspace {\n ...InviteDialogWorkspace_Workspace\n id\n name\n logo\n role\n plan {\n name\n }\n team {\n totalCount\n }\n }\n": typeof types.HeaderWorkspaceSwitcher_WorkspaceFragmentDoc,
|
||||
"\n fragment HeaderWorkspaceSwitcherActiveWorkspace_Workspace on Workspace {\n id\n name\n logo\n ...HeaderWorkspaceSwitcherHeaderWorkspace_Workspace\n }\n": typeof types.HeaderWorkspaceSwitcherActiveWorkspace_WorkspaceFragmentDoc,
|
||||
"\n fragment HeaderWorkspaceSwitcherWorkspaceList_Workspace on Workspace {\n id\n name\n logo\n role\n slug\n creationState {\n completed\n }\n plan {\n name\n }\n }\n": typeof types.HeaderWorkspaceSwitcherWorkspaceList_WorkspaceFragmentDoc,
|
||||
"\n fragment HeaderWorkspaceSwitcherWorkspaceList_User on User {\n id\n expiredSsoSessions {\n id\n ...HeaderWorkspaceSwitcherHeaderExpiredSso_LimitedWorkspace\n }\n workspaces {\n items {\n id\n ...HeaderWorkspaceSwitcherWorkspaceList_Workspace\n }\n }\n }\n": typeof types.HeaderWorkspaceSwitcherWorkspaceList_UserFragmentDoc,
|
||||
"\n fragment HeaderWorkspaceSwitcherHeaderExpiredSso_LimitedWorkspace on LimitedWorkspace {\n id\n slug\n name\n logo\n }\n": typeof types.HeaderWorkspaceSwitcherHeaderExpiredSso_LimitedWorkspaceFragmentDoc,
|
||||
"\n fragment HeaderWorkspaceSwitcherHeaderWorkspace_Workspace on Workspace {\n ...InviteDialogWorkspace_Workspace\n id\n name\n logo\n role\n plan {\n name\n }\n team {\n totalCount\n }\n }\n": typeof types.HeaderWorkspaceSwitcherHeaderWorkspace_WorkspaceFragmentDoc,
|
||||
"\n fragment InviteDialogWorkspace_Workspace on Workspace {\n id\n domainBasedMembershipProtectionEnabled\n domains {\n domain\n id\n }\n plan {\n status\n name\n }\n subscription {\n seats {\n guest\n plan\n }\n }\n }\n": typeof types.InviteDialogWorkspace_WorkspaceFragmentDoc,
|
||||
"\n fragment InviteDialogProject_Project on Project {\n id\n name\n ...InviteDialogProjectWorkspaceMembers_Project\n workspace {\n id\n name\n defaultProjectRole\n role\n domainBasedMembershipProtectionEnabled\n domains {\n domain\n id\n }\n plan {\n status\n name\n }\n subscription {\n seats {\n guest\n plan\n }\n }\n }\n }\n": typeof types.InviteDialogProject_ProjectFragmentDoc,
|
||||
"\n fragment InviteDialogProjectWorkspaceMembersRow_WorkspaceCollaborator on WorkspaceCollaborator {\n role\n id\n user {\n id\n name\n bio\n company\n avatar\n verified\n role\n }\n }\n": typeof types.InviteDialogProjectWorkspaceMembersRow_WorkspaceCollaboratorFragmentDoc,
|
||||
@@ -208,9 +212,11 @@ type Documents = {
|
||||
"\n query InviteUserSearch($input: UsersRetrievalInput!) {\n users(input: $input) {\n items {\n id\n name\n avatar\n }\n }\n }\n": typeof types.InviteUserSearchDocument,
|
||||
"\n mutation CreateNewRegion($input: CreateServerRegionInput!) {\n serverInfoMutations {\n multiRegion {\n create(input: $input) {\n id\n ...SettingsServerRegionsAddEditDialog_ServerRegionItem\n ...SettingsServerRegionsTable_ServerRegionItem\n }\n }\n }\n }\n": typeof types.CreateNewRegionDocument,
|
||||
"\n mutation UpdateRegion($input: UpdateServerRegionInput!) {\n serverInfoMutations {\n multiRegion {\n update(input: $input) {\n id\n ...SettingsServerRegionsAddEditDialog_ServerRegionItem\n ...SettingsServerRegionsTable_ServerRegionItem\n }\n }\n }\n }\n": typeof types.UpdateRegionDocument,
|
||||
"\n fragment UseNavigation_Workspace on Workspace {\n ...HeaderWorkspaceSwitcher_Workspace\n id\n }\n": typeof types.UseNavigation_WorkspaceFragmentDoc,
|
||||
"\n fragment UseNavigationActiveWorkspace_Workspace on Workspace {\n ...HeaderWorkspaceSwitcherActiveWorkspace_Workspace\n id\n }\n": typeof types.UseNavigationActiveWorkspace_WorkspaceFragmentDoc,
|
||||
"\n fragment UseNavigationWorkspaceList_User on User {\n id\n ...HeaderWorkspaceSwitcherWorkspaceList_User\n }\n": typeof types.UseNavigationWorkspaceList_UserFragmentDoc,
|
||||
"\n mutation SetActiveWorkspace($slug: String, $isProjectsActive: Boolean) {\n activeUserMutations {\n setActiveWorkspace(slug: $slug, isProjectsActive: $isProjectsActive)\n }\n }\n": typeof types.SetActiveWorkspaceDocument,
|
||||
"\n query HeaderWorkspaceSwitcher($slug: String!) {\n workspaceBySlug(slug: $slug) {\n ...HeaderWorkspaceSwitcher_Workspace\n }\n }\n": typeof types.HeaderWorkspaceSwitcherDocument,
|
||||
"\n query NavigationActiveWorkspace($slug: String!) {\n workspaceBySlug(slug: $slug) {\n ...UseNavigationActiveWorkspace_Workspace\n }\n }\n": typeof types.NavigationActiveWorkspaceDocument,
|
||||
"\n query NavigationWorkspaceList {\n activeUser {\n id\n ...UseNavigationWorkspaceList_User\n }\n }\n": typeof types.NavigationWorkspaceListDocument,
|
||||
"\n fragment ProjectPageTeamInternals_Project on Project {\n id\n role\n invitedTeam {\n id\n title\n role\n inviteId\n user {\n role\n ...LimitedUserAvatar\n }\n }\n team {\n role\n user {\n id\n role\n ...LimitedUserAvatar\n }\n }\n }\n": typeof types.ProjectPageTeamInternals_ProjectFragmentDoc,
|
||||
"\n fragment ProjectPageTeamInternals_Workspace on Workspace {\n id\n team {\n items {\n id\n role\n user {\n id\n }\n }\n }\n }\n": typeof types.ProjectPageTeamInternals_WorkspaceFragmentDoc,
|
||||
"\n fragment ProjectDashboardItemNoModels on Project {\n id\n name\n createdAt\n updatedAt\n role\n team {\n id\n user {\n id\n name\n avatar\n }\n }\n ...ProjectPageModelsCardProject\n }\n": typeof types.ProjectDashboardItemNoModelsFragmentDoc,
|
||||
@@ -448,7 +454,11 @@ const documents: Documents = {
|
||||
"\n fragment FormSelectProjects_Project on Project {\n id\n name\n }\n": types.FormSelectProjects_ProjectFragmentDoc,
|
||||
"\n fragment FormUsersSelectItem on LimitedUser {\n id\n name\n avatar\n }\n": types.FormUsersSelectItemFragmentDoc,
|
||||
"\n fragment HeaderNavShare_Project on Project {\n id\n visibility\n ...ProjectsModelPageEmbed_Project\n }\n": types.HeaderNavShare_ProjectFragmentDoc,
|
||||
"\n fragment HeaderWorkspaceSwitcher_Workspace on Workspace {\n ...InviteDialogWorkspace_Workspace\n id\n name\n logo\n role\n plan {\n name\n }\n team {\n totalCount\n }\n }\n": types.HeaderWorkspaceSwitcher_WorkspaceFragmentDoc,
|
||||
"\n fragment HeaderWorkspaceSwitcherActiveWorkspace_Workspace on Workspace {\n id\n name\n logo\n ...HeaderWorkspaceSwitcherHeaderWorkspace_Workspace\n }\n": types.HeaderWorkspaceSwitcherActiveWorkspace_WorkspaceFragmentDoc,
|
||||
"\n fragment HeaderWorkspaceSwitcherWorkspaceList_Workspace on Workspace {\n id\n name\n logo\n role\n slug\n creationState {\n completed\n }\n plan {\n name\n }\n }\n": types.HeaderWorkspaceSwitcherWorkspaceList_WorkspaceFragmentDoc,
|
||||
"\n fragment HeaderWorkspaceSwitcherWorkspaceList_User on User {\n id\n expiredSsoSessions {\n id\n ...HeaderWorkspaceSwitcherHeaderExpiredSso_LimitedWorkspace\n }\n workspaces {\n items {\n id\n ...HeaderWorkspaceSwitcherWorkspaceList_Workspace\n }\n }\n }\n": types.HeaderWorkspaceSwitcherWorkspaceList_UserFragmentDoc,
|
||||
"\n fragment HeaderWorkspaceSwitcherHeaderExpiredSso_LimitedWorkspace on LimitedWorkspace {\n id\n slug\n name\n logo\n }\n": types.HeaderWorkspaceSwitcherHeaderExpiredSso_LimitedWorkspaceFragmentDoc,
|
||||
"\n fragment HeaderWorkspaceSwitcherHeaderWorkspace_Workspace on Workspace {\n ...InviteDialogWorkspace_Workspace\n id\n name\n logo\n role\n plan {\n name\n }\n team {\n totalCount\n }\n }\n": types.HeaderWorkspaceSwitcherHeaderWorkspace_WorkspaceFragmentDoc,
|
||||
"\n fragment InviteDialogWorkspace_Workspace on Workspace {\n id\n domainBasedMembershipProtectionEnabled\n domains {\n domain\n id\n }\n plan {\n status\n name\n }\n subscription {\n seats {\n guest\n plan\n }\n }\n }\n": types.InviteDialogWorkspace_WorkspaceFragmentDoc,
|
||||
"\n fragment InviteDialogProject_Project on Project {\n id\n name\n ...InviteDialogProjectWorkspaceMembers_Project\n workspace {\n id\n name\n defaultProjectRole\n role\n domainBasedMembershipProtectionEnabled\n domains {\n domain\n id\n }\n plan {\n status\n name\n }\n subscription {\n seats {\n guest\n plan\n }\n }\n }\n }\n": types.InviteDialogProject_ProjectFragmentDoc,
|
||||
"\n fragment InviteDialogProjectWorkspaceMembersRow_WorkspaceCollaborator on WorkspaceCollaborator {\n role\n id\n user {\n id\n name\n bio\n company\n avatar\n verified\n role\n }\n }\n": types.InviteDialogProjectWorkspaceMembersRow_WorkspaceCollaboratorFragmentDoc,
|
||||
@@ -608,9 +618,11 @@ const documents: Documents = {
|
||||
"\n query InviteUserSearch($input: UsersRetrievalInput!) {\n users(input: $input) {\n items {\n id\n name\n avatar\n }\n }\n }\n": types.InviteUserSearchDocument,
|
||||
"\n mutation CreateNewRegion($input: CreateServerRegionInput!) {\n serverInfoMutations {\n multiRegion {\n create(input: $input) {\n id\n ...SettingsServerRegionsAddEditDialog_ServerRegionItem\n ...SettingsServerRegionsTable_ServerRegionItem\n }\n }\n }\n }\n": types.CreateNewRegionDocument,
|
||||
"\n mutation UpdateRegion($input: UpdateServerRegionInput!) {\n serverInfoMutations {\n multiRegion {\n update(input: $input) {\n id\n ...SettingsServerRegionsAddEditDialog_ServerRegionItem\n ...SettingsServerRegionsTable_ServerRegionItem\n }\n }\n }\n }\n": types.UpdateRegionDocument,
|
||||
"\n fragment UseNavigation_Workspace on Workspace {\n ...HeaderWorkspaceSwitcher_Workspace\n id\n }\n": types.UseNavigation_WorkspaceFragmentDoc,
|
||||
"\n fragment UseNavigationActiveWorkspace_Workspace on Workspace {\n ...HeaderWorkspaceSwitcherActiveWorkspace_Workspace\n id\n }\n": types.UseNavigationActiveWorkspace_WorkspaceFragmentDoc,
|
||||
"\n fragment UseNavigationWorkspaceList_User on User {\n id\n ...HeaderWorkspaceSwitcherWorkspaceList_User\n }\n": types.UseNavigationWorkspaceList_UserFragmentDoc,
|
||||
"\n mutation SetActiveWorkspace($slug: String, $isProjectsActive: Boolean) {\n activeUserMutations {\n setActiveWorkspace(slug: $slug, isProjectsActive: $isProjectsActive)\n }\n }\n": types.SetActiveWorkspaceDocument,
|
||||
"\n query HeaderWorkspaceSwitcher($slug: String!) {\n workspaceBySlug(slug: $slug) {\n ...HeaderWorkspaceSwitcher_Workspace\n }\n }\n": types.HeaderWorkspaceSwitcherDocument,
|
||||
"\n query NavigationActiveWorkspace($slug: String!) {\n workspaceBySlug(slug: $slug) {\n ...UseNavigationActiveWorkspace_Workspace\n }\n }\n": types.NavigationActiveWorkspaceDocument,
|
||||
"\n query NavigationWorkspaceList {\n activeUser {\n id\n ...UseNavigationWorkspaceList_User\n }\n }\n": types.NavigationWorkspaceListDocument,
|
||||
"\n fragment ProjectPageTeamInternals_Project on Project {\n id\n role\n invitedTeam {\n id\n title\n role\n inviteId\n user {\n role\n ...LimitedUserAvatar\n }\n }\n team {\n role\n user {\n id\n role\n ...LimitedUserAvatar\n }\n }\n }\n": types.ProjectPageTeamInternals_ProjectFragmentDoc,
|
||||
"\n fragment ProjectPageTeamInternals_Workspace on Workspace {\n id\n team {\n items {\n id\n role\n user {\n id\n }\n }\n }\n }\n": types.ProjectPageTeamInternals_WorkspaceFragmentDoc,
|
||||
"\n fragment ProjectDashboardItemNoModels on Project {\n id\n name\n createdAt\n updatedAt\n role\n team {\n id\n user {\n id\n name\n avatar\n }\n }\n ...ProjectPageModelsCardProject\n }\n": types.ProjectDashboardItemNoModelsFragmentDoc,
|
||||
@@ -967,7 +979,23 @@ export function graphql(source: "\n fragment HeaderNavShare_Project on Project
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n fragment HeaderWorkspaceSwitcher_Workspace on Workspace {\n ...InviteDialogWorkspace_Workspace\n id\n name\n logo\n role\n plan {\n name\n }\n team {\n totalCount\n }\n }\n"): (typeof documents)["\n fragment HeaderWorkspaceSwitcher_Workspace on Workspace {\n ...InviteDialogWorkspace_Workspace\n id\n name\n logo\n role\n plan {\n name\n }\n team {\n totalCount\n }\n }\n"];
|
||||
export function graphql(source: "\n fragment HeaderWorkspaceSwitcherActiveWorkspace_Workspace on Workspace {\n id\n name\n logo\n ...HeaderWorkspaceSwitcherHeaderWorkspace_Workspace\n }\n"): (typeof documents)["\n fragment HeaderWorkspaceSwitcherActiveWorkspace_Workspace on Workspace {\n id\n name\n logo\n ...HeaderWorkspaceSwitcherHeaderWorkspace_Workspace\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n fragment HeaderWorkspaceSwitcherWorkspaceList_Workspace on Workspace {\n id\n name\n logo\n role\n slug\n creationState {\n completed\n }\n plan {\n name\n }\n }\n"): (typeof documents)["\n fragment HeaderWorkspaceSwitcherWorkspaceList_Workspace on Workspace {\n id\n name\n logo\n role\n slug\n creationState {\n completed\n }\n plan {\n name\n }\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n fragment HeaderWorkspaceSwitcherWorkspaceList_User on User {\n id\n expiredSsoSessions {\n id\n ...HeaderWorkspaceSwitcherHeaderExpiredSso_LimitedWorkspace\n }\n workspaces {\n items {\n id\n ...HeaderWorkspaceSwitcherWorkspaceList_Workspace\n }\n }\n }\n"): (typeof documents)["\n fragment HeaderWorkspaceSwitcherWorkspaceList_User on User {\n id\n expiredSsoSessions {\n id\n ...HeaderWorkspaceSwitcherHeaderExpiredSso_LimitedWorkspace\n }\n workspaces {\n items {\n id\n ...HeaderWorkspaceSwitcherWorkspaceList_Workspace\n }\n }\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n fragment HeaderWorkspaceSwitcherHeaderExpiredSso_LimitedWorkspace on LimitedWorkspace {\n id\n slug\n name\n logo\n }\n"): (typeof documents)["\n fragment HeaderWorkspaceSwitcherHeaderExpiredSso_LimitedWorkspace on LimitedWorkspace {\n id\n slug\n name\n logo\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n fragment HeaderWorkspaceSwitcherHeaderWorkspace_Workspace on Workspace {\n ...InviteDialogWorkspace_Workspace\n id\n name\n logo\n role\n plan {\n name\n }\n team {\n totalCount\n }\n }\n"): (typeof documents)["\n fragment HeaderWorkspaceSwitcherHeaderWorkspace_Workspace on Workspace {\n ...InviteDialogWorkspace_Workspace\n id\n name\n logo\n role\n plan {\n name\n }\n team {\n totalCount\n }\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
@@ -1607,7 +1635,11 @@ export function graphql(source: "\n mutation UpdateRegion($input: UpdateServerR
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n fragment UseNavigation_Workspace on Workspace {\n ...HeaderWorkspaceSwitcher_Workspace\n id\n }\n"): (typeof documents)["\n fragment UseNavigation_Workspace on Workspace {\n ...HeaderWorkspaceSwitcher_Workspace\n id\n }\n"];
|
||||
export function graphql(source: "\n fragment UseNavigationActiveWorkspace_Workspace on Workspace {\n ...HeaderWorkspaceSwitcherActiveWorkspace_Workspace\n id\n }\n"): (typeof documents)["\n fragment UseNavigationActiveWorkspace_Workspace on Workspace {\n ...HeaderWorkspaceSwitcherActiveWorkspace_Workspace\n id\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n fragment UseNavigationWorkspaceList_User on User {\n id\n ...HeaderWorkspaceSwitcherWorkspaceList_User\n }\n"): (typeof documents)["\n fragment UseNavigationWorkspaceList_User on User {\n id\n ...HeaderWorkspaceSwitcherWorkspaceList_User\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
@@ -1615,7 +1647,11 @@ export function graphql(source: "\n mutation SetActiveWorkspace($slug: String,
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n query HeaderWorkspaceSwitcher($slug: String!) {\n workspaceBySlug(slug: $slug) {\n ...HeaderWorkspaceSwitcher_Workspace\n }\n }\n"): (typeof documents)["\n query HeaderWorkspaceSwitcher($slug: String!) {\n workspaceBySlug(slug: $slug) {\n ...HeaderWorkspaceSwitcher_Workspace\n }\n }\n"];
|
||||
export function graphql(source: "\n query NavigationActiveWorkspace($slug: String!) {\n workspaceBySlug(slug: $slug) {\n ...UseNavigationActiveWorkspace_Workspace\n }\n }\n"): (typeof documents)["\n query NavigationActiveWorkspace($slug: String!) {\n workspaceBySlug(slug: $slug) {\n ...UseNavigationActiveWorkspace_Workspace\n }\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n query NavigationWorkspaceList {\n activeUser {\n id\n ...UseNavigationWorkspaceList_User\n }\n }\n"): (typeof documents)["\n query NavigationWorkspaceList {\n activeUser {\n id\n ...UseNavigationWorkspaceList_User\n }\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -18,7 +18,7 @@ export const connectorItems: ConnectorItem[] = [
|
||||
title: 'Revit',
|
||||
slug: 'revit',
|
||||
description:
|
||||
'Publish and load models to boost design coordination and business intelligence workflows.',
|
||||
'Publish and load Revit models to boost design coordination and business intelligence workflows.',
|
||||
url: 'https://www.speckle.systems/connectors/revit',
|
||||
image: '/images/connectors/revit.png',
|
||||
categories: [ConnectorCategory.NextGen, ConnectorCategory.BIM]
|
||||
@@ -36,7 +36,7 @@ export const connectorItems: ConnectorItem[] = [
|
||||
title: 'Power BI',
|
||||
slug: 'powerbi',
|
||||
description:
|
||||
'Load Power BI models to boost design coordination and business intelligence workflows.',
|
||||
'Load your models into Power BI to boost design coordination and business intelligence workflows.',
|
||||
url: 'https://www.speckle.systems/connectors/power-bi',
|
||||
image: '/images/connectors/powerbi.png',
|
||||
categories: [ConnectorCategory.BusinessIntelligence]
|
||||
@@ -139,7 +139,7 @@ export const connectorItems: ConnectorItem[] = [
|
||||
{
|
||||
title: 'Blender',
|
||||
slug: 'blender',
|
||||
description: 'Load Blender models to boost design coordination workflows.',
|
||||
description: 'Load models into Blender to boost design coordination workflows.',
|
||||
image: '/images/connectors/blender.png',
|
||||
categories: [ConnectorCategory.Visualisation, ConnectorCategory.CADAndModeling],
|
||||
isComingSoon: true
|
||||
|
||||
@@ -1,21 +1,31 @@
|
||||
import { setActiveWorkspaceMutation } from '~/lib/navigation/graphql/mutations'
|
||||
import { useMutation, useQuery } from '@vue/apollo-composable'
|
||||
import { headerWorkspaceSwitcherQuery } from '~/lib/navigation/graphql/queries'
|
||||
import {
|
||||
navigationActiveWorkspaceQuery,
|
||||
navigationWorkspaceListQuery
|
||||
} from '~/lib/navigation/graphql/queries'
|
||||
import { graphql } from '~/lib/common/generated/gql'
|
||||
import type { UseNavigation_WorkspaceFragment } from '~/lib/common/generated/gql/graphql'
|
||||
import type { UseNavigationActiveWorkspace_WorkspaceFragment } from '~/lib/common/generated/gql/graphql'
|
||||
|
||||
graphql(`
|
||||
fragment UseNavigation_Workspace on Workspace {
|
||||
...HeaderWorkspaceSwitcher_Workspace
|
||||
fragment UseNavigationActiveWorkspace_Workspace on Workspace {
|
||||
...HeaderWorkspaceSwitcherActiveWorkspace_Workspace
|
||||
id
|
||||
}
|
||||
`)
|
||||
|
||||
graphql(`
|
||||
fragment UseNavigationWorkspaceList_User on User {
|
||||
id
|
||||
...HeaderWorkspaceSwitcherWorkspaceList_User
|
||||
}
|
||||
`)
|
||||
|
||||
export const useNavigationState = () =>
|
||||
useState<{
|
||||
activeWorkspaceSlug: string | null
|
||||
isProjectsActive: boolean
|
||||
cachedWorkspaceData: UseNavigation_WorkspaceFragment | null
|
||||
cachedWorkspaceData: UseNavigationActiveWorkspace_WorkspaceFragment | null
|
||||
}>('navigation-state', () => ({
|
||||
activeWorkspaceSlug: null,
|
||||
isProjectsActive: false,
|
||||
@@ -25,31 +35,48 @@ export const useNavigationState = () =>
|
||||
export const useNavigation = () => {
|
||||
const state = useNavigationState()
|
||||
const { mutate } = useMutation(setActiveWorkspaceMutation)
|
||||
const isWorkspacesEnabled = useIsWorkspacesEnabled()
|
||||
|
||||
const activeWorkspaceSlug = computed({
|
||||
get: () => state.value.activeWorkspaceSlug,
|
||||
set: (newVal) => (state.value.activeWorkspaceSlug = newVal)
|
||||
})
|
||||
|
||||
const {
|
||||
result,
|
||||
loading: workspaceLoading,
|
||||
onResult
|
||||
} = useQuery(
|
||||
headerWorkspaceSwitcherQuery,
|
||||
() => ({
|
||||
slug: activeWorkspaceSlug.value || ''
|
||||
}),
|
||||
() => ({
|
||||
enabled: !!activeWorkspaceSlug.value
|
||||
})
|
||||
)
|
||||
|
||||
const isProjectsActive = computed({
|
||||
get: () => state.value.isProjectsActive,
|
||||
set: (newVal) => (state.value.isProjectsActive = newVal)
|
||||
})
|
||||
|
||||
const { result: workspacesResult } = useQuery(navigationWorkspaceListQuery, null, {
|
||||
enabled: isWorkspacesEnabled.value
|
||||
})
|
||||
|
||||
// Check for expired SSO sessions
|
||||
const expiredSsoSessions = computed(
|
||||
() => workspacesResult.value?.activeUser?.expiredSsoSessions || []
|
||||
)
|
||||
|
||||
// Check if the current active workspace has an expired SSO session
|
||||
const activeWorkspaceHasExpiredSsoSession = computed(
|
||||
() =>
|
||||
!!expiredSsoSessions.value.find(
|
||||
(session) => session.slug === activeWorkspaceSlug.value
|
||||
)
|
||||
)
|
||||
|
||||
const { result: activeWorkspaceResult, onResult } = useQuery(
|
||||
navigationActiveWorkspaceQuery,
|
||||
() => ({
|
||||
slug: activeWorkspaceSlug.value || ''
|
||||
}),
|
||||
() => ({
|
||||
enabled:
|
||||
!!activeWorkspaceSlug.value &&
|
||||
isWorkspacesEnabled.value &&
|
||||
!activeWorkspaceHasExpiredSsoSession.value
|
||||
})
|
||||
)
|
||||
|
||||
// Set state and mutate
|
||||
const mutateActiveWorkspaceSlug = async (newVal: string) => {
|
||||
state.value.activeWorkspaceSlug = newVal
|
||||
@@ -64,11 +91,28 @@ export const useNavigation = () => {
|
||||
await mutate({ isProjectsActive: state.value.isProjectsActive, slug: null })
|
||||
}
|
||||
|
||||
// Active workspace where SSO session is expired
|
||||
const expiredSsoWorkspaceData = computed(() =>
|
||||
expiredSsoSessions.value.find(
|
||||
(session) => session.slug === activeWorkspaceSlug.value
|
||||
)
|
||||
)
|
||||
|
||||
// Use the cached data or the current result
|
||||
const workspaceData = computed(() => {
|
||||
return result.value?.workspaceBySlug || state.value.cachedWorkspaceData
|
||||
const activeWorkspaceData = computed(() => {
|
||||
return (
|
||||
activeWorkspaceResult.value?.workspaceBySlug || state.value.cachedWorkspaceData
|
||||
)
|
||||
})
|
||||
|
||||
const workspaceList = computed(() =>
|
||||
workspacesResult.value?.activeUser
|
||||
? workspacesResult.value.activeUser.workspaces.items.filter(
|
||||
(workspace) => workspace.creationState?.completed !== false
|
||||
)
|
||||
: []
|
||||
)
|
||||
|
||||
// Save data in the state, the prevent flickering when the component remount in between navigation
|
||||
onResult((result) => {
|
||||
const workspace = result.data?.workspaceBySlug
|
||||
@@ -82,7 +126,9 @@ export const useNavigation = () => {
|
||||
isProjectsActive,
|
||||
mutateActiveWorkspaceSlug,
|
||||
mutateIsProjectsActive,
|
||||
workspaceData,
|
||||
workspaceLoading
|
||||
activeWorkspaceData,
|
||||
workspaceList,
|
||||
activeWorkspaceHasExpiredSsoSession,
|
||||
expiredSsoWorkspaceData
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,18 @@
|
||||
import { graphql } from '~/lib/common/generated/gql'
|
||||
|
||||
export const headerWorkspaceSwitcherQuery = graphql(`
|
||||
query HeaderWorkspaceSwitcher($slug: String!) {
|
||||
export const navigationActiveWorkspaceQuery = graphql(`
|
||||
query NavigationActiveWorkspace($slug: String!) {
|
||||
workspaceBySlug(slug: $slug) {
|
||||
...HeaderWorkspaceSwitcher_Workspace
|
||||
...UseNavigationActiveWorkspace_Workspace
|
||||
}
|
||||
}
|
||||
`)
|
||||
|
||||
export const navigationWorkspaceListQuery = graphql(`
|
||||
query NavigationWorkspaceList {
|
||||
activeUser {
|
||||
id
|
||||
...UseNavigationWorkspaceList_User
|
||||
}
|
||||
}
|
||||
`)
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
import { useQuery } from '@vue/apollo-composable'
|
||||
import { settingsSidebarQuery } from '~/lib/settings/graphql/queries'
|
||||
|
||||
export const useUserWorkspaces = () => {
|
||||
const isWorkspacesEnabled = useIsWorkspacesEnabled()
|
||||
const { result } = useQuery(settingsSidebarQuery, null, {
|
||||
enabled: isWorkspacesEnabled.value
|
||||
})
|
||||
|
||||
const workspaces = computed(() =>
|
||||
result.value?.activeUser
|
||||
? result.value.activeUser.workspaces.items.filter(
|
||||
(workspace) => workspace.creationState?.completed !== false
|
||||
)
|
||||
: []
|
||||
)
|
||||
|
||||
const hasWorkspaces = computed(() => workspaces.value.length > 0)
|
||||
|
||||
return {
|
||||
workspaces,
|
||||
hasWorkspaces
|
||||
}
|
||||
}
|
||||
@@ -192,7 +192,6 @@ export const useWorkspacesWizard = () => {
|
||||
|
||||
const finalizeWizard = async (state: WorkspaceWizardState, workspaceId: string) => {
|
||||
isLoading.value = true
|
||||
mutateActiveWorkspaceSlug(workspaceId)
|
||||
|
||||
if (state.region?.key && state.plan === PaidWorkspacePlans.Business) {
|
||||
await updateWorkspaceDefaultRegion({
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import { emailLogger as logger } from '@/observability/logging'
|
||||
import { emailLogger } from '@/observability/logging'
|
||||
import { SendEmail, SendEmailParams } from '@/modules/emails/domain/operations'
|
||||
import { getTransporter } from '@/modules/emails/utils/transporter'
|
||||
import { getEmailFromAddress } from '@/modules/shared/helpers/envHelper'
|
||||
import { resolveMixpanelUserId } from '@speckle/shared'
|
||||
import {
|
||||
getRequestLogger,
|
||||
loggerWithMaybeContext
|
||||
} from '@/observability/components/express/requestContext'
|
||||
|
||||
export type { SendEmailParams } from '@/modules/emails/domain/operations'
|
||||
/**
|
||||
@@ -15,6 +19,7 @@ export const sendEmail: SendEmail = async ({
|
||||
text,
|
||||
html
|
||||
}: SendEmailParams): Promise<boolean> => {
|
||||
const logger = getRequestLogger() || loggerWithMaybeContext({ logger: emailLogger })
|
||||
const transporter = getTransporter()
|
||||
if (!transporter) {
|
||||
logger.warn('No email transport present. Cannot send emails. Skipping send...')
|
||||
|
||||
@@ -37,7 +37,11 @@ import {
|
||||
storeTokenScopesFactory,
|
||||
storeUserServerAppTokenFactory
|
||||
} from '@/modules/core/repositories/tokens'
|
||||
import { getServerOrigin } from '@/modules/shared/helpers/envHelper'
|
||||
import {
|
||||
getPrivateObjectsServerOrigin,
|
||||
getServerOrigin,
|
||||
previewServiceShouldUsePrivateObjectsServerUrl
|
||||
} from '@/modules/shared/helpers/envHelper'
|
||||
import { requestObjectPreviewFactory } from '@/modules/previews/queues/previews'
|
||||
import type { Queue } from 'bull'
|
||||
import type { Knex } from 'knex'
|
||||
@@ -61,7 +65,10 @@ const buildCreateObjectPreviewFunction = ({
|
||||
queue: previewRequestQueue,
|
||||
responseQueue: responseQueueName
|
||||
}),
|
||||
serverOrigin: getServerOrigin(),
|
||||
// use the private server origin if defined, otherwise use the public server origin
|
||||
serverOrigin: previewServiceShouldUsePrivateObjectsServerUrl()
|
||||
? getPrivateObjectsServerOrigin()
|
||||
: getServerOrigin(),
|
||||
storeObjectPreview: storeObjectPreviewFactory({ db: projectDb }),
|
||||
getStreamCollaborators: getStreamCollaboratorsFactory({ db }),
|
||||
createAppToken: createAppTokenFactory({
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { MisconfiguredEnvironmentError } from '@/modules/shared/errors'
|
||||
import { trimEnd } from 'lodash'
|
||||
import * as Environment from '@speckle/shared/dist/commonjs/environment/index.js'
|
||||
import { ensureError } from '@speckle/shared'
|
||||
import { ensureError, Nullable } from '@speckle/shared'
|
||||
|
||||
export function getStringFromEnv(
|
||||
envVarKey: string,
|
||||
@@ -28,6 +28,32 @@ export function getBooleanFromEnv(envVarKey: string, aDefault = false): boolean
|
||||
return ['1', 'true', true].includes(process.env[envVarKey] || aDefault.toString())
|
||||
}
|
||||
|
||||
function mustGetUrlFromEnv(name: string, trimTrailingSlash: boolean = false): URL {
|
||||
const url = getUrlFromEnv(name, trimTrailingSlash)
|
||||
if (!url) throw new MisconfiguredEnvironmentError(`${name} env var not configured`)
|
||||
return url
|
||||
}
|
||||
|
||||
function getUrlFromEnv(
|
||||
name: string,
|
||||
trimTrailingSlash: boolean = false
|
||||
): Nullable<URL> {
|
||||
const value = process.env[name]
|
||||
if (!value) {
|
||||
return null
|
||||
}
|
||||
try {
|
||||
return new URL(trimTrailingSlash ? trimEnd(value, '/') : value)
|
||||
} catch (e: unknown) {
|
||||
const err = ensureError(e, 'Unknown error parsing URL')
|
||||
if (err instanceof TypeError && err.message === 'Invalid URL')
|
||||
throw new MisconfiguredEnvironmentError(`${name} has to be a valid URL`, {
|
||||
cause: err
|
||||
})
|
||||
throw new MisconfiguredEnvironmentError(`Error parsing ${name} URL`, { cause: err })
|
||||
}
|
||||
}
|
||||
|
||||
export function getSessionSecret() {
|
||||
if (!process.env.SESSION_SECRET) {
|
||||
throw new MisconfiguredEnvironmentError('SESSION_SECRET env var not configured')
|
||||
@@ -86,6 +112,10 @@ export function getRedisUrl() {
|
||||
return getStringFromEnv('REDIS_URL')
|
||||
}
|
||||
|
||||
export const previewServiceShouldUsePrivateObjectsServerUrl = (): boolean => {
|
||||
return getBooleanFromEnv('PREVIEW_SERVICE_USE_PRIVATE_OBJECTS_SERVER_URL')
|
||||
}
|
||||
|
||||
export const getPreviewServiceRedisUrl = (): string | undefined => {
|
||||
return process.env['PREVIEW_SERVICE_REDIS_URL']
|
||||
}
|
||||
@@ -195,33 +225,19 @@ export function getFrontendOrigin() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get server app origin/base URL
|
||||
* Get server app origin/base URL.
|
||||
* This is the public server URL, i.e. 'canonical url', used for external communication.
|
||||
*/
|
||||
export function getServerOrigin() {
|
||||
if (!process.env.CANONICAL_URL) {
|
||||
throw new MisconfiguredEnvironmentError(
|
||||
'Server origin environment variable (CANONICAL_URL) not configured'
|
||||
)
|
||||
}
|
||||
return mustGetUrlFromEnv('CANONICAL_URL', true).origin
|
||||
}
|
||||
|
||||
try {
|
||||
return new URL(trimEnd(process.env.CANONICAL_URL, '/')).origin
|
||||
} catch (e) {
|
||||
const err = ensureError(e)
|
||||
if (e instanceof TypeError && e.message === 'Invalid URL') {
|
||||
throw new MisconfiguredEnvironmentError(
|
||||
`Server origin environment variable (CANONICAL_URL) is not a valid URL: ${process.env.CANONICAL_URL} ${err.message}`,
|
||||
{
|
||||
cause: e,
|
||||
info: {
|
||||
value: process.env.CANONICAL_URL
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
throw err
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @returns the private server origin, which is used for internal communication between services
|
||||
*/
|
||||
export function getPrivateObjectsServerOrigin() {
|
||||
return mustGetUrlFromEnv('PRIVATE_OBJECTS_SERVER_URL', true).origin
|
||||
}
|
||||
|
||||
export function getBindAddress(aDefault: string = '127.0.0.1') {
|
||||
@@ -239,26 +255,12 @@ export function isSSLServer() {
|
||||
return /^https:\/\//.test(getServerOrigin())
|
||||
}
|
||||
|
||||
function parseUrlVar(value: string, name: string) {
|
||||
try {
|
||||
return new URL(value)
|
||||
} catch (err: unknown) {
|
||||
if (err instanceof TypeError && err.message === 'Invalid URL')
|
||||
throw new MisconfiguredEnvironmentError(`${name} has to be a valid URL`)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
export function getServerMovedFrom() {
|
||||
const value = process.env.MIGRATION_SERVER_MOVED_FROM
|
||||
if (!value) return value
|
||||
return parseUrlVar(value, 'MIGRATION_SERVER_MOVED_FROM')
|
||||
return getUrlFromEnv('MIGRATION_SERVER_MOVED_FROM')
|
||||
}
|
||||
|
||||
export function getServerMovedTo() {
|
||||
const value = process.env.MIGRATION_SERVER_MOVED_TO
|
||||
if (!value) return value
|
||||
return parseUrlVar(value, 'MIGRATION_SERVER_MOVED_TO')
|
||||
return getUrlFromEnv('MIGRATION_SERVER_MOVED_TO')
|
||||
}
|
||||
|
||||
export function adminOverrideEnabled() {
|
||||
|
||||
@@ -528,7 +528,6 @@ Retrieve the s3 parameters from ConfigMap if enabled, or default to retrieving t
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
|
||||
{{/*
|
||||
Generate the environment variables for Speckle server and Speckle objects deployments
|
||||
*/}}
|
||||
@@ -542,6 +541,10 @@ Generate the environment variables for Speckle server and Speckle objects deploy
|
||||
|
||||
- name: PORT
|
||||
value: {{ include "server.port" $ | quote }}
|
||||
|
||||
- name: PRIVATE_OBJECTS_SERVER_URL
|
||||
value: {{ printf "http://%s:%s" ( include "objects.service.fqdn" $ ) ( include "objects.port" $ ) }}
|
||||
|
||||
- name: LOG_LEVEL
|
||||
value: {{ .Values.server.logLevel }}
|
||||
- name: LOG_PRETTY
|
||||
@@ -799,6 +802,12 @@ Generate the environment variables for Speckle server and Speckle objects deploy
|
||||
value: {{ .Values.server.gendoAI.ratelimiting.burstRenderRequestPeriodSeconds | quote }}
|
||||
{{- end }}
|
||||
|
||||
# *** Preview service ***
|
||||
{{- if .Values.preview_service.deployInCluster }}
|
||||
- name: PREVIEW_SERVICE_USE_PRIVATE_OBJECTS_SERVER_URL
|
||||
value: "true"
|
||||
{{- end }}
|
||||
|
||||
# *** Redis ***
|
||||
- name: REDIS_URL
|
||||
valueFrom:
|
||||
|
||||
@@ -23,6 +23,11 @@ spec:
|
||||
- name: main
|
||||
image: {{ default (printf "speckle/speckle-fileimport-service:%s" .Values.docker_image_tag) .Values.fileimport_service.image }}
|
||||
imagePullPolicy: {{ .Values.imagePullPolicy }}
|
||||
args: #overwrites the Dockerfile CMD statement
|
||||
{{- if .Values.fileimport_service.inspect.enabled }}
|
||||
- {{ printf "--inspect=%s" .Values.fileimport_service.inspect.port }}
|
||||
{{- end }}
|
||||
- "bin/www.js"
|
||||
|
||||
ports:
|
||||
- name: metrics
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{{- if .Values.preview_service.deployInCluster }}
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
@@ -121,4 +122,4 @@ spec:
|
||||
|
||||
# Should be > preview generation time ( 1 hour for good measure )
|
||||
terminationGracePeriodSeconds: 3600
|
||||
|
||||
{{- end }}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{{- if .Values.preview_service.deployInCluster }}
|
||||
{{- if (and (.Values.preview_service.networkPolicy.enabled) (eq .Values.networkPlugin.type "cilium")) -}}
|
||||
apiVersion: cilium.io/v2
|
||||
kind: CiliumNetworkPolicy
|
||||
@@ -38,3 +39,4 @@ spec:
|
||||
# postgres
|
||||
{{ include "speckle.networkpolicy.egress.postgres.cilium" $ | indent 4 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{{- if .Values.preview_service.deployInCluster }}
|
||||
{{- if (and (.Values.preview_service.networkPolicy.enabled) (eq .Values.networkPlugin.type "kubernetes")) -}}
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
@@ -38,4 +39,5 @@ spec:
|
||||
protocol: UDP
|
||||
# postgres
|
||||
{{ include "speckle.networkpolicy.egress.postgres" $ | indent 4 }}
|
||||
{{- end -}}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{{- if .Values.preview_service.deployInCluster }}
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
@@ -14,3 +15,4 @@ spec:
|
||||
name: web
|
||||
port: {{ .Values.preview_service.port }}
|
||||
targetPort: metrics
|
||||
{{- end }}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{{- if .Values.preview_service.deployInCluster }}
|
||||
{{- if .Values.preview_service.serviceAccount.create -}}
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
@@ -21,3 +22,4 @@ secrets:
|
||||
- name: {{ default .Values.secretName .Values.redis.previewServiceConnectionString.secretName }}
|
||||
{{- end }}
|
||||
{{- end -}}
|
||||
{{- end }}
|
||||
|
||||
@@ -1884,6 +1884,11 @@
|
||||
"preview_service": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"deployInCluster": {
|
||||
"type": "boolean",
|
||||
"description": "If enabled, the Preview Service will be deployed within the cluster and speckle-server will be configured to send the kubernetes service url of the objects server to the Preview Service.",
|
||||
"default": true
|
||||
},
|
||||
"dedicatedPreviewsQueue": {
|
||||
"type": "boolean",
|
||||
"description": "Allows using a dedicated redis url for the preview service job queue",
|
||||
@@ -2138,6 +2143,21 @@
|
||||
"description": "The maximum number of connections that the File Import Service postgres client will make to the Postgres database.",
|
||||
"default": 1
|
||||
},
|
||||
"inspect": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": "boolean",
|
||||
"description": "If enabled, indicates that the File Import service should be deployed with the nodejs inspect feature enabled",
|
||||
"default": false
|
||||
},
|
||||
"port": {
|
||||
"type": "string",
|
||||
"description": "The port on which the nodejs inspect feature should be exposed",
|
||||
"default": "7000"
|
||||
}
|
||||
}
|
||||
},
|
||||
"requests": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@@ -1101,6 +1101,8 @@ frontend_2:
|
||||
## @descriptionEnd
|
||||
##
|
||||
preview_service:
|
||||
## @param preview_service.deployInCluster If enabled, the Preview Service will be deployed within the cluster and speckle-server will be configured to send the kubernetes service url of the objects server to the Preview Service.
|
||||
deployInCluster: true
|
||||
## @param preview_service.dedicatedPreviewsQueue Allows using a dedicated redis url for the preview service job queue
|
||||
##
|
||||
dedicatedPreviewsQueue: false
|
||||
@@ -1279,6 +1281,12 @@ fileimport_service:
|
||||
##
|
||||
postgresMaxConnections: 1
|
||||
|
||||
inspect:
|
||||
## @param fileimport_service.inspect.enabled If enabled, indicates that the File Import service should be deployed with the nodejs inspect feature enabled
|
||||
enabled: false
|
||||
## @param fileimport_service.inspect.port The port on which the nodejs inspect feature should be exposed
|
||||
port: '7000'
|
||||
|
||||
requests:
|
||||
## @param fileimport_service.requests.cpu The CPU that should be available on a node when scheduling this pod.
|
||||
## ref: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/
|
||||
|
||||
Reference in New Issue
Block a user