Merge branch 'main' into andrew/placeholder-updates-to-workspace-settings
This commit is contained in:
+1
-2
@@ -2,7 +2,7 @@
|
||||
# shellcheck disable=SC1091
|
||||
set -e
|
||||
|
||||
if [ -n "$CI" ]
|
||||
if [ -n "$CI" ]
|
||||
then
|
||||
echo "running eslint"
|
||||
yarn eslint:projectwide
|
||||
@@ -16,7 +16,6 @@ else
|
||||
yarn lint-staged
|
||||
fi
|
||||
|
||||
|
||||
echo "🔍 looking for additional linter dependencies"
|
||||
|
||||
check_dependencies_available() {
|
||||
|
||||
+7
-7
@@ -9,9 +9,9 @@
|
||||
"node": "^22.6.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "yarn workspaces foreach -ptvW run build",
|
||||
"build:public": "yarn workspaces foreach -ptvW --no-private run build",
|
||||
"build:tailwind-deps": "yarn workspaces foreach -ivW -j unlimited --include '{@speckle/shared,@speckle/tailwind-theme,@speckle/ui-components}' run build",
|
||||
"build": "yarn workspaces foreach --parallel --topological --verbose --worktree run build",
|
||||
"build:public": "yarn ensure:tailwind-deps && yarn workspace @speckle/frontend-2 build:postinstall && yarn workspaces foreach --parallel --topological --verbose --worktree --no-private run build",
|
||||
"build:tailwind-deps": "yarn workspaces foreach --interlaced --verbose --worktree --jobs unlimited --include '{@speckle/shared,@speckle/tailwind-theme,@speckle/ui-components}' run build",
|
||||
"ensure:tailwind-deps": "node ./utils/ensure-tailwind-deps.mjs",
|
||||
"helm:readme:generate": "./utils/helm/update-schema-json.sh",
|
||||
"prettier:check": "prettier --check .",
|
||||
@@ -28,10 +28,10 @@
|
||||
"dev:kind:helm:up": "yarn dev:kind:up && tilt up --file ./.circleci/deployment/Tiltfile.helm --context kind-speckle-server",
|
||||
"dev:kind:helm:down": "tilt down --file ./.circleci/deployment/Tiltfile.helm --context kind-speckle-server",
|
||||
"dev:kind:helm:ci": "tilt ci --file ./.circleci/deployment/Tiltfile.helm --context kind-speckle-server --timeout 10m",
|
||||
"dev": "yarn workspaces foreach -pivW -j unlimited run dev",
|
||||
"dev:no-server": "yarn workspaces foreach --exclude @speckle/server -pivW -j unlimited run dev",
|
||||
"dev:minimal": "yarn workspaces foreach -pivW -j unlimited --include '{@speckle/server,@speckle/frontend-2}' run dev",
|
||||
"gqlgen": "yarn workspaces foreach -pivW -j unlimited --include '{@speckle/server,@speckle/frontend,@speckle/frontend-2}' run gqlgen",
|
||||
"dev": "yarn workspaces foreach --parallel --interlaced --verbose --worktree --jobs unlimited run dev",
|
||||
"dev:no-server": "yarn workspaces foreach --exclude @speckle/server --parallel --interlaced --verbose --worktree --jobs unlimited run dev",
|
||||
"dev:minimal": "yarn workspaces foreach --parallel --interlaced --verbose --worktree --jobs unlimited --include '{@speckle/server,@speckle/frontend-2}' run dev",
|
||||
"gqlgen": "yarn workspaces foreach --parallel --interlaced --verbose --worktree --jobs unlimited --include '{@speckle/server,@speckle/frontend-2}' run gqlgen",
|
||||
"dev:server": "yarn workspace @speckle/server dev",
|
||||
"dev:frontend-2": "yarn workspace @speckle/frontend-2 dev",
|
||||
"dev:shared": "yarn workspace @speckle/shared dev",
|
||||
|
||||
@@ -65,6 +65,59 @@
|
||||
<span>{{ option.label }}</span>
|
||||
</label>
|
||||
</div>
|
||||
<div v-if="isWorkspacesEnabled">
|
||||
<label
|
||||
:for="`option-hide-logo`"
|
||||
class="flex items-center gap-1 cursor-pointer max-w-max"
|
||||
>
|
||||
<FormCheckbox
|
||||
id="option-hide-logo"
|
||||
v-model="hideSpeckleBranding"
|
||||
name="Hide Speckle logo"
|
||||
hide-label
|
||||
class="cursor-pointer"
|
||||
:disabled="
|
||||
workspaceHideSpeckleBrandingEnabled ||
|
||||
!canEditEmbedOptions?.authorized
|
||||
"
|
||||
/>
|
||||
<div class="flex flex-col gap-0.5">
|
||||
<span
|
||||
:key="`hide-branding-tooltip-${workspaceHideSpeckleBrandingEnabled}`"
|
||||
v-tippy="hideSpeckleBrandingTooltip"
|
||||
>
|
||||
Hide Speckle logo
|
||||
</span>
|
||||
<span
|
||||
v-if="
|
||||
!canEditEmbedOptions?.authorized &&
|
||||
canEditEmbedOptions?.code === 'WorkspaceNoFeatureAccess'
|
||||
"
|
||||
class="text-body-2xs text-foreground-2"
|
||||
>
|
||||
This feature is only available on the business plan
|
||||
<NuxtLink
|
||||
:to="settingsWorkspaceRoutes.billing.route(workspaceSlug)"
|
||||
class="underline"
|
||||
>
|
||||
upgrade now
|
||||
</NuxtLink>
|
||||
</span>
|
||||
<span
|
||||
v-if="hideSpeckleBranding && !workspaceHideSpeckleBrandingEnabled"
|
||||
class="text-body-2xs text-foreground-2"
|
||||
>
|
||||
Tip: You can also hide the logo for all embeds in
|
||||
<NuxtLink
|
||||
:to="settingsWorkspaceRoutes.billing.route(workspaceSlug)"
|
||||
class="underline"
|
||||
>
|
||||
workspace settings.
|
||||
</NuxtLink>
|
||||
</span>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</LayoutDialogSection>
|
||||
<LayoutDialogSection
|
||||
@@ -100,11 +153,24 @@ import {
|
||||
castToSupportedVisibility,
|
||||
SupportedProjectVisibility
|
||||
} from '~/lib/projects/helpers/visibility'
|
||||
import { settingsWorkspaceRoutes } from '~/lib/common/helpers/route'
|
||||
|
||||
graphql(`
|
||||
fragment ProjectsModelPageEmbed_Project on Project {
|
||||
id
|
||||
...ProjectsPageTeamDialogManagePermissions_Project
|
||||
workspace {
|
||||
id
|
||||
slug
|
||||
embedOptions {
|
||||
hideSpeckleBranding
|
||||
}
|
||||
permissions {
|
||||
canEditEmbedOptions {
|
||||
...FullPermissionCheckResult
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`)
|
||||
|
||||
@@ -123,6 +189,7 @@ const {
|
||||
public: { baseUrl }
|
||||
} = useRuntimeConfig()
|
||||
|
||||
const isWorkspacesEnabled = useIsWorkspacesEnabled()
|
||||
const { isSmallerOrEqualSm } = useIsSmallerOrEqualThanBreakpoint()
|
||||
const updateProject = useUpdateProject()
|
||||
const mp = useMixpanel()
|
||||
@@ -134,6 +201,7 @@ const disableModelLink = ref(false)
|
||||
const preventScrolling = ref(false)
|
||||
const manuallyLoadModel = ref(false)
|
||||
const projectVisibility = ref(props.project.visibility)
|
||||
const hideSpeckleBranding = ref(false)
|
||||
|
||||
const routeModelId = computed(() => route.params.modelId as string)
|
||||
|
||||
@@ -176,6 +244,14 @@ const updatedUrl = computed(() => {
|
||||
}
|
||||
})
|
||||
|
||||
if (
|
||||
hideSpeckleBranding.value &&
|
||||
isWorkspacesEnabled.value &&
|
||||
!workspaceHideSpeckleBrandingEnabled.value
|
||||
) {
|
||||
embedOptions['hideSpeckleBranding'] = true
|
||||
}
|
||||
|
||||
// Serialize the embedOptions into a hash fragment
|
||||
const hashFragment = encodeURIComponent(JSON.stringify(embedOptions))
|
||||
url.hash = `embed=${hashFragment}`
|
||||
@@ -228,6 +304,25 @@ const nonDiscoverableButtons = computed((): LayoutDialogButton[] => [
|
||||
}
|
||||
])
|
||||
|
||||
const workspaceSlug = computed(() => {
|
||||
return props.project.workspace?.slug
|
||||
})
|
||||
const canEditEmbedOptions = computed(() => {
|
||||
return props.project.workspace?.permissions?.canEditEmbedOptions
|
||||
})
|
||||
const workspaceHideSpeckleBrandingEnabled = computed(() => {
|
||||
if (!isWorkspacesEnabled.value) return false
|
||||
return props.project.workspace?.embedOptions?.hideSpeckleBranding
|
||||
})
|
||||
|
||||
const hideSpeckleBrandingTooltip = computed(() => {
|
||||
if (!isWorkspacesEnabled.value) return ''
|
||||
if (workspaceHideSpeckleBrandingEnabled.value) {
|
||||
return 'Speckle branding is disabled for all embeds in this workspace'
|
||||
}
|
||||
return ''
|
||||
})
|
||||
|
||||
const handleEmbedCodeCopy = async (value: string) => {
|
||||
await copy(value, {
|
||||
successMessage: 'Embed code copied to clipboard',
|
||||
@@ -292,4 +387,15 @@ const embedDialogOptions = [
|
||||
value: manuallyLoadModel
|
||||
}
|
||||
]
|
||||
|
||||
watch(
|
||||
() => props.project.workspace?.embedOptions?.hideSpeckleBranding,
|
||||
() => {
|
||||
if (isWorkspacesEnabled.value) {
|
||||
hideSpeckleBranding.value =
|
||||
props.project.workspace?.embedOptions?.hideSpeckleBranding ?? false
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
</script>
|
||||
|
||||
@@ -102,7 +102,7 @@
|
||||
:name="modelName || 'Loading...'"
|
||||
:date="lastUpdate"
|
||||
:url="route.path"
|
||||
:hide-speckle-branding="hideSpeckleBranding"
|
||||
:hide-speckle-branding="hideSpeckleLogo"
|
||||
:disable-model-link="disableModelLink"
|
||||
/>
|
||||
<Portal to="primary-actions">
|
||||
@@ -142,6 +142,7 @@ graphql(`
|
||||
embedOptions {
|
||||
hideSpeckleBranding
|
||||
}
|
||||
hasAccessToFeature(featureName: hideSpeckleBranding)
|
||||
}
|
||||
`)
|
||||
|
||||
@@ -177,7 +178,8 @@ const {
|
||||
hideSelectionInfo,
|
||||
isTransparent,
|
||||
showControls,
|
||||
disableModelLink
|
||||
disableModelLink,
|
||||
hideSpeckleBranding
|
||||
} = useEmbed()
|
||||
const mp = useMixpanel()
|
||||
|
||||
@@ -262,8 +264,15 @@ const lastUpdate = computed(() => {
|
||||
} else return undefined
|
||||
})
|
||||
|
||||
const hideSpeckleBranding = computed(() => {
|
||||
return project.value ? project.value?.embedOptions?.hideSpeckleBranding : true
|
||||
const canEditEmbedOptions = computed(() => {
|
||||
return project.value?.hasAccessToFeature
|
||||
})
|
||||
|
||||
const hideSpeckleLogo = computed(() => {
|
||||
if (!project.value?.workspace) return true
|
||||
if (!canEditEmbedOptions.value) return false
|
||||
if (project.value?.embedOptions?.hideSpeckleBranding) return true
|
||||
else return hideSpeckleBranding.value
|
||||
})
|
||||
|
||||
useHead({ title })
|
||||
|
||||
@@ -48,7 +48,7 @@ defineProps<{
|
||||
date?: string
|
||||
name?: string
|
||||
url?: string
|
||||
hideSpeckleBranding?: boolean
|
||||
hideSpeckleBranding?: MaybeNullOrUndefined<boolean>
|
||||
disableModelLink?: MaybeNullOrUndefined<boolean>
|
||||
}>()
|
||||
|
||||
|
||||
@@ -61,7 +61,7 @@ type Documents = {
|
||||
"\n fragment ProjectModelPageDialogDeleteVersion on Version {\n id\n message\n }\n": typeof types.ProjectModelPageDialogDeleteVersionFragmentDoc,
|
||||
"\n fragment ProjectModelPageDialogEditMessageVersion on Version {\n id\n message\n }\n": typeof types.ProjectModelPageDialogEditMessageVersionFragmentDoc,
|
||||
"\n fragment ProjectModelPageDialogMoveToVersion on Version {\n id\n message\n }\n": typeof types.ProjectModelPageDialogMoveToVersionFragmentDoc,
|
||||
"\n fragment ProjectsModelPageEmbed_Project on Project {\n id\n ...ProjectsPageTeamDialogManagePermissions_Project\n }\n": typeof types.ProjectsModelPageEmbed_ProjectFragmentDoc,
|
||||
"\n fragment ProjectsModelPageEmbed_Project on Project {\n id\n ...ProjectsPageTeamDialogManagePermissions_Project\n workspace {\n id\n slug\n embedOptions {\n hideSpeckleBranding\n }\n permissions {\n canEditEmbedOptions {\n ...FullPermissionCheckResult\n }\n }\n }\n }\n": typeof types.ProjectsModelPageEmbed_ProjectFragmentDoc,
|
||||
"\n fragment ProjectModelPageVersionsCardVersion on Version {\n id\n message\n authorUser {\n ...LimitedUserAvatar\n }\n createdAt\n previewUrl\n referencedObject\n sourceApplication\n commentThreadCount: commentThreads(limit: 0) {\n totalCount\n }\n ...ProjectModelPageDialogDeleteVersion\n ...ProjectModelPageDialogMoveToVersion\n automationsStatus {\n ...AutomateRunsTriggerStatus_TriggeredAutomationsStatus\n }\n permissions {\n canUpdate {\n ...FullPermissionCheckResult\n }\n }\n }\n": typeof types.ProjectModelPageVersionsCardVersionFragmentDoc,
|
||||
"\n fragment ProjectPageProjectHeader on Project {\n id\n name\n description\n workspace {\n id\n slug\n name\n logo\n role\n }\n }\n": typeof types.ProjectPageProjectHeaderFragmentDoc,
|
||||
"\n fragment ProjectPageAutomationDeleteDialog_Project on Project {\n id\n name\n workspaceId\n }\n": typeof types.ProjectPageAutomationDeleteDialog_ProjectFragmentDoc,
|
||||
@@ -129,7 +129,7 @@ type Documents = {
|
||||
"\n fragment SettingsWorkspacesSecurityDomainRemoveDialog_WorkspaceDomain on WorkspaceDomain {\n id\n domain\n }\n": typeof types.SettingsWorkspacesSecurityDomainRemoveDialog_WorkspaceDomainFragmentDoc,
|
||||
"\n fragment SettingsWorkspacesSecurityDomainRemoveDialog_Workspace on Workspace {\n id\n domains {\n ...SettingsWorkspacesSecurityDomainRemoveDialog_WorkspaceDomain\n }\n }\n": typeof types.SettingsWorkspacesSecurityDomainRemoveDialog_WorkspaceFragmentDoc,
|
||||
"\n fragment SettingsWorkspacesSecuritySsoWrapper_Workspace on Workspace {\n id\n role\n slug\n sso {\n provider {\n id\n name\n clientId\n issuerUrl\n }\n }\n hasAccessToSSO: hasAccessToFeature(featureName: oidcSso)\n }\n": typeof types.SettingsWorkspacesSecuritySsoWrapper_WorkspaceFragmentDoc,
|
||||
"\n fragment ModelPageProject on Project {\n id\n createdAt\n name\n visibility\n workspace {\n id\n slug\n name\n role\n }\n embedOptions {\n hideSpeckleBranding\n }\n }\n": typeof types.ModelPageProjectFragmentDoc,
|
||||
"\n fragment ModelPageProject on Project {\n id\n createdAt\n name\n visibility\n workspace {\n id\n slug\n name\n role\n }\n embedOptions {\n hideSpeckleBranding\n }\n hasAccessToFeature(featureName: hideSpeckleBranding)\n }\n": typeof types.ModelPageProjectFragmentDoc,
|
||||
"\n fragment ViewerCommentThreadData on Comment {\n id\n permissions {\n canArchive {\n ...FullPermissionCheckResult\n }\n }\n }\n": typeof types.ViewerCommentThreadDataFragmentDoc,
|
||||
"\n fragment ThreadCommentAttachment on Comment {\n text {\n attachments {\n id\n fileName\n fileType\n fileSize\n }\n }\n }\n": typeof types.ThreadCommentAttachmentFragmentDoc,
|
||||
"\n fragment ViewerCommentsListItem on Comment {\n id\n rawText\n archived\n author {\n ...LimitedUserAvatar\n }\n createdAt\n viewedAt\n replies {\n totalCount\n cursor\n items {\n ...ViewerCommentsReplyItem\n }\n }\n replyAuthors(limit: 4) {\n totalCount\n items {\n ...FormUsersSelectItem\n }\n }\n resources {\n resourceId\n resourceType\n }\n }\n": typeof types.ViewerCommentsListItemFragmentDoc,
|
||||
@@ -481,7 +481,7 @@ const documents: Documents = {
|
||||
"\n fragment ProjectModelPageDialogDeleteVersion on Version {\n id\n message\n }\n": types.ProjectModelPageDialogDeleteVersionFragmentDoc,
|
||||
"\n fragment ProjectModelPageDialogEditMessageVersion on Version {\n id\n message\n }\n": types.ProjectModelPageDialogEditMessageVersionFragmentDoc,
|
||||
"\n fragment ProjectModelPageDialogMoveToVersion on Version {\n id\n message\n }\n": types.ProjectModelPageDialogMoveToVersionFragmentDoc,
|
||||
"\n fragment ProjectsModelPageEmbed_Project on Project {\n id\n ...ProjectsPageTeamDialogManagePermissions_Project\n }\n": types.ProjectsModelPageEmbed_ProjectFragmentDoc,
|
||||
"\n fragment ProjectsModelPageEmbed_Project on Project {\n id\n ...ProjectsPageTeamDialogManagePermissions_Project\n workspace {\n id\n slug\n embedOptions {\n hideSpeckleBranding\n }\n permissions {\n canEditEmbedOptions {\n ...FullPermissionCheckResult\n }\n }\n }\n }\n": types.ProjectsModelPageEmbed_ProjectFragmentDoc,
|
||||
"\n fragment ProjectModelPageVersionsCardVersion on Version {\n id\n message\n authorUser {\n ...LimitedUserAvatar\n }\n createdAt\n previewUrl\n referencedObject\n sourceApplication\n commentThreadCount: commentThreads(limit: 0) {\n totalCount\n }\n ...ProjectModelPageDialogDeleteVersion\n ...ProjectModelPageDialogMoveToVersion\n automationsStatus {\n ...AutomateRunsTriggerStatus_TriggeredAutomationsStatus\n }\n permissions {\n canUpdate {\n ...FullPermissionCheckResult\n }\n }\n }\n": types.ProjectModelPageVersionsCardVersionFragmentDoc,
|
||||
"\n fragment ProjectPageProjectHeader on Project {\n id\n name\n description\n workspace {\n id\n slug\n name\n logo\n role\n }\n }\n": types.ProjectPageProjectHeaderFragmentDoc,
|
||||
"\n fragment ProjectPageAutomationDeleteDialog_Project on Project {\n id\n name\n workspaceId\n }\n": types.ProjectPageAutomationDeleteDialog_ProjectFragmentDoc,
|
||||
@@ -549,7 +549,7 @@ const documents: Documents = {
|
||||
"\n fragment SettingsWorkspacesSecurityDomainRemoveDialog_WorkspaceDomain on WorkspaceDomain {\n id\n domain\n }\n": types.SettingsWorkspacesSecurityDomainRemoveDialog_WorkspaceDomainFragmentDoc,
|
||||
"\n fragment SettingsWorkspacesSecurityDomainRemoveDialog_Workspace on Workspace {\n id\n domains {\n ...SettingsWorkspacesSecurityDomainRemoveDialog_WorkspaceDomain\n }\n }\n": types.SettingsWorkspacesSecurityDomainRemoveDialog_WorkspaceFragmentDoc,
|
||||
"\n fragment SettingsWorkspacesSecuritySsoWrapper_Workspace on Workspace {\n id\n role\n slug\n sso {\n provider {\n id\n name\n clientId\n issuerUrl\n }\n }\n hasAccessToSSO: hasAccessToFeature(featureName: oidcSso)\n }\n": types.SettingsWorkspacesSecuritySsoWrapper_WorkspaceFragmentDoc,
|
||||
"\n fragment ModelPageProject on Project {\n id\n createdAt\n name\n visibility\n workspace {\n id\n slug\n name\n role\n }\n embedOptions {\n hideSpeckleBranding\n }\n }\n": types.ModelPageProjectFragmentDoc,
|
||||
"\n fragment ModelPageProject on Project {\n id\n createdAt\n name\n visibility\n workspace {\n id\n slug\n name\n role\n }\n embedOptions {\n hideSpeckleBranding\n }\n hasAccessToFeature(featureName: hideSpeckleBranding)\n }\n": types.ModelPageProjectFragmentDoc,
|
||||
"\n fragment ViewerCommentThreadData on Comment {\n id\n permissions {\n canArchive {\n ...FullPermissionCheckResult\n }\n }\n }\n": types.ViewerCommentThreadDataFragmentDoc,
|
||||
"\n fragment ThreadCommentAttachment on Comment {\n text {\n attachments {\n id\n fileName\n fileType\n fileSize\n }\n }\n }\n": types.ThreadCommentAttachmentFragmentDoc,
|
||||
"\n fragment ViewerCommentsListItem on Comment {\n id\n rawText\n archived\n author {\n ...LimitedUserAvatar\n }\n createdAt\n viewedAt\n replies {\n totalCount\n cursor\n items {\n ...ViewerCommentsReplyItem\n }\n }\n replyAuthors(limit: 4) {\n totalCount\n items {\n ...FormUsersSelectItem\n }\n }\n resources {\n resourceId\n resourceType\n }\n }\n": types.ViewerCommentsListItemFragmentDoc,
|
||||
@@ -1059,7 +1059,7 @@ export function graphql(source: "\n fragment ProjectModelPageDialogMoveToVersio
|
||||
/**
|
||||
* 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 ProjectsModelPageEmbed_Project on Project {\n id\n ...ProjectsPageTeamDialogManagePermissions_Project\n }\n"): (typeof documents)["\n fragment ProjectsModelPageEmbed_Project on Project {\n id\n ...ProjectsPageTeamDialogManagePermissions_Project\n }\n"];
|
||||
export function graphql(source: "\n fragment ProjectsModelPageEmbed_Project on Project {\n id\n ...ProjectsPageTeamDialogManagePermissions_Project\n workspace {\n id\n slug\n embedOptions {\n hideSpeckleBranding\n }\n permissions {\n canEditEmbedOptions {\n ...FullPermissionCheckResult\n }\n }\n }\n }\n"): (typeof documents)["\n fragment ProjectsModelPageEmbed_Project on Project {\n id\n ...ProjectsPageTeamDialogManagePermissions_Project\n workspace {\n id\n slug\n embedOptions {\n hideSpeckleBranding\n }\n permissions {\n canEditEmbedOptions {\n ...FullPermissionCheckResult\n }\n }\n }\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
@@ -1331,7 +1331,7 @@ export function graphql(source: "\n fragment SettingsWorkspacesSecuritySsoWrapp
|
||||
/**
|
||||
* 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 ModelPageProject on Project {\n id\n createdAt\n name\n visibility\n workspace {\n id\n slug\n name\n role\n }\n embedOptions {\n hideSpeckleBranding\n }\n }\n"): (typeof documents)["\n fragment ModelPageProject on Project {\n id\n createdAt\n name\n visibility\n workspace {\n id\n slug\n name\n role\n }\n embedOptions {\n hideSpeckleBranding\n }\n }\n"];
|
||||
export function graphql(source: "\n fragment ModelPageProject on Project {\n id\n createdAt\n name\n visibility\n workspace {\n id\n slug\n name\n role\n }\n embedOptions {\n hideSpeckleBranding\n }\n hasAccessToFeature(featureName: hideSpeckleBranding)\n }\n"): (typeof documents)["\n fragment ModelPageProject on Project {\n id\n createdAt\n name\n visibility\n workspace {\n id\n slug\n name\n role\n }\n embedOptions {\n hideSpeckleBranding\n }\n hasAccessToFeature(featureName: hideSpeckleBranding)\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
@@ -12,6 +12,7 @@ export type EmbedOptions = {
|
||||
disableModelLink?: boolean
|
||||
noScroll?: boolean
|
||||
manualLoad?: boolean
|
||||
hideSpeckleBranding?: boolean
|
||||
}
|
||||
|
||||
export function isEmbedOptions(obj: unknown): obj is EmbedOptions {
|
||||
@@ -26,7 +27,8 @@ export function isEmbedOptions(obj: unknown): obj is EmbedOptions {
|
||||
'hideSelectionInfo',
|
||||
'disableModelLink',
|
||||
'noScroll',
|
||||
'manualLoad'
|
||||
'manualLoad',
|
||||
'hideSpeckleBranding'
|
||||
].includes(key) &&
|
||||
typeof possibleOptions[key as keyof EmbedOptions] === 'boolean'
|
||||
)
|
||||
@@ -101,6 +103,7 @@ export function useEmbed() {
|
||||
const isEnabled = createComputed('isEnabled')
|
||||
const isTransparent = createComputed('isTransparent')
|
||||
const disableModelLink = createComputed('disableModelLink')
|
||||
const hideSpeckleBranding = createComputed('hideSpeckleBranding')
|
||||
const hideSelectionInfo = createComputed('hideSelectionInfo')
|
||||
const noScroll = createComputed('noScroll')
|
||||
const manualLoad = createComputed('manualLoad')
|
||||
@@ -125,6 +128,7 @@ export function useEmbed() {
|
||||
showControls: showControlsNew,
|
||||
hideSelectionInfo,
|
||||
disableModelLink,
|
||||
hideSpeckleBranding,
|
||||
noScroll,
|
||||
manualLoad
|
||||
}
|
||||
|
||||
@@ -5,13 +5,13 @@
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "NODE_OPTIONS=--max-old-space-size=8192 nuxt build",
|
||||
"build:postinstall": "nuxt prepare",
|
||||
"build:sourcemaps": "BUILD_SOURCEMAPS=true yarn build",
|
||||
"dev:nuxt": "nuxt dev",
|
||||
"dev:app": "concurrently \"nuxt dev\" \"yarn gqlgen:watch\"",
|
||||
"dev": "yarn dev:app",
|
||||
"preview": "nuxt preview",
|
||||
"analyze": "nuxt analyze",
|
||||
"postinstall": "yarn ensure:tailwind-deps && nuxt prepare",
|
||||
"lint:js": "eslint .",
|
||||
"lint:tsc": "vue-tsc --noEmit",
|
||||
"lint:prettier": "prettier --config ../../.prettierrc --ignore-path ../../.prettierignore --check .",
|
||||
|
||||
@@ -6,7 +6,7 @@ export default class BatchedPool<T> {
|
||||
#baseInterval: number
|
||||
|
||||
#processingLoop: Promise<void>
|
||||
#finished = false
|
||||
#disposed = false
|
||||
|
||||
constructor(params: {
|
||||
concurrencyAndSizes: number[]
|
||||
@@ -28,7 +28,7 @@ export default class BatchedPool<T> {
|
||||
}
|
||||
|
||||
async #runWorker(batchSize: number): Promise<void> {
|
||||
while (!this.#finished || this.#queue.length > 0) {
|
||||
while (!this.#disposed || this.#queue.length > 0) {
|
||||
if (this.#queue.length > 0) {
|
||||
const batch = this.getBatch(batchSize)
|
||||
await this.#processFunction(batch)
|
||||
@@ -38,7 +38,7 @@ export default class BatchedPool<T> {
|
||||
}
|
||||
|
||||
async disposeAsync(): Promise<void> {
|
||||
this.#finished = true
|
||||
this.#disposed = true
|
||||
await this.#processingLoop
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ export default class BatchingQueue<T> {
|
||||
#maxInterval: number
|
||||
|
||||
#processingLoop: Promise<void>
|
||||
#finished = false
|
||||
#disposed = false
|
||||
|
||||
constructor(params: {
|
||||
batchSize: number
|
||||
@@ -26,7 +26,7 @@ export default class BatchingQueue<T> {
|
||||
}
|
||||
|
||||
async disposeAsync(): Promise<void> {
|
||||
this.#finished = true
|
||||
this.#disposed = true
|
||||
await this.#processingLoop
|
||||
}
|
||||
|
||||
@@ -42,13 +42,17 @@ export default class BatchingQueue<T> {
|
||||
return this.#queue.size
|
||||
}
|
||||
|
||||
isDisposed(): boolean {
|
||||
return this.#disposed
|
||||
}
|
||||
|
||||
#getBatch(batchSize: number): T[] {
|
||||
return this.#queue.spliceValues(0, Math.min(batchSize, this.#queue.size))
|
||||
}
|
||||
|
||||
async #loop(): Promise<void> {
|
||||
let interval = this.#baseInterval
|
||||
while (!this.#finished || this.#queue.size > 0) {
|
||||
while (!this.#disposed || this.#queue.size > 0) {
|
||||
const startTime = performance.now()
|
||||
if (this.#queue.size > 0) {
|
||||
const batch = this.#getBatch(this.#batchSize)
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import { describe, expect, test } from 'vitest'
|
||||
import { CachePump } from './cachePump.js'
|
||||
import { Database } from '../operations/interfaces.js'
|
||||
import AsyncGeneratorQueue from './asyncGeneratorQueue.js'
|
||||
import { Item } from '../types/types.js'
|
||||
import { DefermentManager } from './defermentManager.js'
|
||||
|
||||
const makeDatabase = (): Database =>
|
||||
({
|
||||
cacheSaveBatch: async (): Promise<void> => {},
|
||||
getAll: async (): Promise<(Item | undefined)[]> => Promise.resolve([])
|
||||
} as unknown as Database)
|
||||
const makeGathered = (): AsyncGeneratorQueue<Item> =>
|
||||
({
|
||||
add: () => {},
|
||||
async *consume() {}
|
||||
} as unknown as AsyncGeneratorQueue<Item>)
|
||||
const makeDeferments = (): DefermentManager =>
|
||||
({
|
||||
undefer: () => {}
|
||||
} as unknown as DefermentManager)
|
||||
describe('CachePump disposal', () => {
|
||||
test('disposeAsync is idempotent and always resolves', async () => {
|
||||
const pump = new CachePump(makeDatabase(), makeGathered(), makeDeferments(), {
|
||||
maxCacheWriteSize: 2,
|
||||
maxCacheBatchWriteWait: 100,
|
||||
maxCacheBatchReadWait: 1,
|
||||
maxWriteQueueSize: 2,
|
||||
maxCacheReadSize: 2
|
||||
})
|
||||
await pump.disposeAsync()
|
||||
await expect(pump.disposeAsync()).resolves.toBeUndefined()
|
||||
})
|
||||
|
||||
test('should not throw on add after dispose if writeQueue was never created', async () => {
|
||||
const pump = new CachePump(makeDatabase(), makeGathered(), makeDeferments(), {
|
||||
maxCacheWriteSize: 2,
|
||||
maxCacheBatchWriteWait: 100,
|
||||
maxCacheBatchReadWait: 1,
|
||||
maxWriteQueueSize: 2,
|
||||
maxCacheReadSize: 2
|
||||
})
|
||||
await pump.disposeAsync()
|
||||
// Should not throw, but will not add anything
|
||||
expect(() =>
|
||||
pump.add({ baseId: 'a', base: { id: 'b', speckle_type: 'type' } })
|
||||
).not.toThrow()
|
||||
})
|
||||
})
|
||||
@@ -5,6 +5,7 @@ import BufferQueue from './bufferQueue.js'
|
||||
import AsyncGeneratorQueue from './asyncGeneratorQueue.js'
|
||||
import { DefermentManager } from './defermentManager.js'
|
||||
import { MemoryDatabase } from '../operations/databases/memoryDatabase.js'
|
||||
import { Database } from '../operations/interfaces.js'
|
||||
|
||||
describe('CachePump testing', () => {
|
||||
test('write two items to queue use pumpItems that are NOT found', async () => {
|
||||
@@ -71,4 +72,32 @@ describe('CachePump testing', () => {
|
||||
expect(notFoundItems.values()).toMatchSnapshot()
|
||||
await cachePump.disposeAsync()
|
||||
})
|
||||
|
||||
test('can dispose while waiting and not wait', async () => {
|
||||
const i1: Item = { baseId: 'id1', base: { id: 'id', speckle_type: 'type' } }
|
||||
const i2: Item = { baseId: 'id2', base: { id: 'id', speckle_type: 'type' } }
|
||||
|
||||
const db: Database = {
|
||||
getAll: async () => Promise.resolve([])
|
||||
} as unknown as Database
|
||||
const gathered = new AsyncGeneratorQueue<Item>()
|
||||
const deferments = new DefermentManager({ maxSizeInMb: 1, ttlms: 1 })
|
||||
const cachePump = new CachePump(db, gathered, deferments, {
|
||||
maxCacheReadSize: 1,
|
||||
maxCacheWriteSize: 1,
|
||||
maxCacheBatchWriteWait: 1,
|
||||
maxCacheBatchReadWait: 1,
|
||||
maxWriteQueueSize: 1
|
||||
})
|
||||
|
||||
const foundItems = new BufferQueue<Item>()
|
||||
const notFoundItems = new BufferQueue<string>()
|
||||
|
||||
await cachePump.disposeAsync()
|
||||
await cachePump.pumpItems({
|
||||
ids: [i1.baseId, i2.baseId],
|
||||
foundItems,
|
||||
notFoundItems
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -57,6 +57,7 @@ export class CachePump implements Pump {
|
||||
const maxCacheReadSize = this.#options.maxCacheReadSize
|
||||
|
||||
for (let i = 0; i < ids.length; ) {
|
||||
if (this.#writeQueue?.isDisposed()) break
|
||||
if ((this.#writeQueue?.count() ?? 0) > this.#options.maxWriteQueueSize) {
|
||||
this.#logger(
|
||||
'pausing reads (# in write queue: ' + this.#writeQueue?.count() + ')'
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { DefermentManager } from './defermentManager.js'
|
||||
import { DefermentManagerOptions } from '../operations/options.js'
|
||||
import { Item } from '../types/types.js'
|
||||
|
||||
describe('DefermentManager disposal', () => {
|
||||
const options: DefermentManagerOptions = { ttlms: 10, maxSizeInMb: 1 }
|
||||
const makeItem = (id: string): Item => ({
|
||||
baseId: id,
|
||||
base: { id, speckle_type: 'test' }
|
||||
})
|
||||
|
||||
it('should throw on get/defer/undefer after dispose', async () => {
|
||||
const manager = new DefermentManager(options)
|
||||
manager.dispose()
|
||||
expect(() => manager.get('a')).toThrow('DefermentManager is disposed')
|
||||
expect(() => manager.undefer(makeItem('a'))).toThrow('DefermentManager is disposed')
|
||||
await expect(manager.defer({ id: 'a' })).rejects.toThrow(
|
||||
'DefermentManager is disposed'
|
||||
)
|
||||
})
|
||||
|
||||
it('dispose is idempotent', () => {
|
||||
const manager = new DefermentManager(options)
|
||||
manager.dispose()
|
||||
expect(() => manager.dispose()).not.toThrow()
|
||||
})
|
||||
})
|
||||
@@ -7,6 +7,7 @@ export class DefermentManager {
|
||||
private timer?: ReturnType<typeof setTimeout>
|
||||
private logger: CustomLogger
|
||||
private currentSize = 0
|
||||
private disposed = false
|
||||
|
||||
constructor(private options: DefermentManagerOptions) {
|
||||
this.resetGlobalTimer()
|
||||
@@ -22,10 +23,12 @@ export class DefermentManager {
|
||||
}
|
||||
|
||||
get(id: string): DeferredBase | undefined {
|
||||
if (this.disposed) throw new Error('DefermentManager is disposed')
|
||||
return this.deferments.get(id)
|
||||
}
|
||||
|
||||
async defer(params: { id: string }): Promise<Base> {
|
||||
if (this.disposed) throw new Error('DefermentManager is disposed')
|
||||
const now = this.now()
|
||||
const deferredBase = this.deferments.get(params.id)
|
||||
if (deferredBase) {
|
||||
@@ -42,6 +45,7 @@ export class DefermentManager {
|
||||
}
|
||||
|
||||
undefer(item: Item): void {
|
||||
if (this.disposed) throw new Error('DefermentManager is disposed')
|
||||
const now = this.now()
|
||||
this.currentSize += item.size || 0
|
||||
//order matters here with found before undefer
|
||||
@@ -65,6 +69,8 @@ export class DefermentManager {
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
if (this.disposed) return
|
||||
this.disposed = true
|
||||
if (this.timer) {
|
||||
clearTimeout(this.timer)
|
||||
this.timer = undefined
|
||||
|
||||
@@ -48,48 +48,14 @@ describe('downloader', () => {
|
||||
fetch: fetchMocker
|
||||
})
|
||||
downloader.initializePool({ results: pump, total: 2, maxDownloadBatchWait: 200 })
|
||||
downloader.add('id')
|
||||
downloader.add('id1')
|
||||
downloader.add('id2')
|
||||
await downloader.disposeAsync()
|
||||
const r = []
|
||||
for await (const x of pump.gather([i1.baseId, i2.baseId])) {
|
||||
r.push(x)
|
||||
}
|
||||
|
||||
expect(r).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test('download batch of three', async () => {
|
||||
const fetchMocker = createFetchMock(vi)
|
||||
const i1: Item = { baseId: 'id1', base: { id: 'id1', speckle_type: 'type' } }
|
||||
const i2: Item = { baseId: 'id2', base: { id: 'id2', speckle_type: 'type' } }
|
||||
const i3: Item = { baseId: 'id3', base: { id: 'id3', speckle_type: 'type' } }
|
||||
fetchMocker.mockResponseOnce(
|
||||
'id1\t' +
|
||||
JSON.stringify(i1.base) +
|
||||
'\nid2\t' +
|
||||
JSON.stringify(i2.base) +
|
||||
'\nid3\t' +
|
||||
JSON.stringify(i3.base) +
|
||||
'\n'
|
||||
)
|
||||
|
||||
const pump = new MemoryPump()
|
||||
const downloader = new ServerDownloader({
|
||||
serverUrl: 'http://speckle.test',
|
||||
streamId: 'streamId',
|
||||
objectId: 'objectId',
|
||||
token: 'token',
|
||||
|
||||
fetch: fetchMocker
|
||||
})
|
||||
downloader.initializePool({ results: pump, total: 2, maxDownloadBatchWait: 200 })
|
||||
downloader.add('id')
|
||||
await downloader.disposeAsync()
|
||||
const r = []
|
||||
for await (const x of pump.gather([i1.baseId, i2.baseId, i3.baseId])) {
|
||||
r.push(x)
|
||||
}
|
||||
|
||||
expect(r).toMatchSnapshot()
|
||||
await downloader.disposeAsync()
|
||||
})
|
||||
@@ -118,8 +84,10 @@ describe('downloader', () => {
|
||||
|
||||
fetch: fetchMocker
|
||||
})
|
||||
downloader.initializePool({ results: pump, total: 2, maxDownloadBatchWait: 200 })
|
||||
downloader.add('id')
|
||||
downloader.initializePool({ results: pump, total: 3, maxDownloadBatchWait: 200 })
|
||||
downloader.add('id1')
|
||||
downloader.add('id2')
|
||||
downloader.add('id3')
|
||||
await downloader.disposeAsync()
|
||||
const r = []
|
||||
for await (const x of pump.gather([i1.baseId, i2.baseId, i3.baseId])) {
|
||||
|
||||
@@ -13,7 +13,7 @@ const ignore = [
|
||||
|
||||
/** @type {import("mocha").MochaOptions} */
|
||||
const config = {
|
||||
spec: ['modules/**/*.spec.js', 'modules/**/*.spec.ts', 'observability/**/*.spec.ts'],
|
||||
spec: ['modules/**/*.spec.ts', 'observability/**/*.spec.ts'],
|
||||
require: ['test/hooks.ts'],
|
||||
...(ignore.length ? { ignore } : {}),
|
||||
slow: 0,
|
||||
|
||||
@@ -172,7 +172,7 @@ export = FF_GATEKEEPER_MODULE_ENABLED
|
||||
return await canWorkspaceAccessFeatureFactory({
|
||||
getWorkspacePlan: getWorkspacePlanFactory({ db })
|
||||
})({
|
||||
workspaceId: parent.id,
|
||||
workspaceId: parent.workspaceId,
|
||||
workspaceFeature: args.featureName
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user