Merge branch 'main' of github.com:specklesystems/speckle-server into alessandro/web-1767-guest-table-should-show-what-they-have-access-to

This commit is contained in:
Alessandro Magionami
2024-09-16 16:14:54 +02:00
21 changed files with 105 additions and 58 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

@@ -203,9 +203,13 @@ const mixpanel = useMixpanel()
const isOpenMobile = ref(false)
const showWorkspaceCreateDialog = ref(false)
const { result: workspaceResult } = useQuery(settingsSidebarQuery, null, {
enabled: isWorkspacesEnabled.value
})
const { result: workspaceResult, onResult: onWorkspaceResult } = useQuery(
settingsSidebarQuery,
null,
{
enabled: isWorkspacesEnabled.value
}
)
const isActive = (...routes: string[]): boolean => {
return routes.some((routeTo) => route.path === routeTo)
@@ -233,4 +237,16 @@ const openWorkspaceCreateDialog = () => {
source: 'sidebar'
})
}
onWorkspaceResult((result) => {
if (result.data?.activeUser) {
const workspaceIds = result.data.activeUser.workspaces.items.map(
(workspace) => workspace.id
)
if (workspaceIds.length > 0) {
mixpanel.people.set('workspace_id', workspaceIds)
}
}
})
</script>
@@ -171,7 +171,6 @@ const onAcceptClick = (token?: string) => {
// eslint-disable-next-line camelcase
workspace_id: props.invite.workspace.id
})
mixpanel.add_group('workspace_id', props.invite.workspace.id)
}
}
</script>
@@ -1,33 +1,28 @@
<template>
<ClientOnly>
<div class="position left-2 bottom-2 fixed z-[45]">
<div class="position left-2 sm:left-auto right-2 bottom-2 fixed z-[45]">
<div
v-if="showBanner"
class="rounded-lg flex flex-col gap-y-2 max-w-64 p-4 border border-outline-2 shadow-md bg-foundation-3 dark:bg-foundation"
class="rounded-lg flex flex-col w-full sm:max-w-96 border border-outline-2 shadow-md bg-foundation-3 dark:bg-foundation"
>
<FormButton
color="subtle"
size="sm"
class="absolute top-2 right-2 !w-5 !h-5 !p-0"
@click="dismissedCookie = true"
>
<XMarkIcon class="h-5 w-5 text-foreground" />
</FormButton>
<h5 class="text-body-xs md:text-heading-sm text-foreground font-medium">
Still not using workspaces?
</h5>
<p class="text-body-2xs leading-5 md:text-body-xs text-foreground-2">
Be the first to reach better project management with your team
</p>
<FormButton
class="mt-2"
color="primary"
size="sm"
@click="openWorkspaceCreateDialog"
>
Start for free
</FormButton>
<img :src="bannerImage" class="w-full" alt="Try workspaces" />
<div class="px-5 py-6 flex flex-col gap-y-2">
<h5 class="text-body-xs md:text-heading-sm text-foreground font-medium">
Still not using workspaces?
</h5>
<p class="text-body-2xs leading-5 md:text-body-xs text-foreground-2">
Be the first to reach more security options, data control, and better
project management with your team.
</p>
<div class="flex items-center gap-x-2 mt-2">
<FormButton color="primary" size="sm" @click="openWorkspaceCreateDialog">
Start for free
</FormButton>
<FormButton color="subtle" size="sm" @click="dismissedCookie = true">
Dismiss
</FormButton>
</div>
</div>
<WorkspaceCreateDialog
v-model:open="showWorkspaceCreateDialog"
navigate-on-success
@@ -48,8 +43,16 @@ import { CookieKeys } from '~/lib/common/helpers/constants'
import { useIsWorkspacesEnabled } from '~/composables/globals'
import { settingsSidebarQuery } from '~/lib/settings/graphql/queries'
import { useQuery } from '@vue/apollo-composable'
import { XMarkIcon } from '@heroicons/vue/24/outline'
import { useTheme } from '~~/lib/core/composables/theme'
import { useBreakpoints } from '@vueuse/core'
import { TailwindBreakpoints } from '~~/lib/common/helpers/tailwind'
import imageLight from '~/assets/images/banners/workspace-promo-light.png'
import imageDark from '~/assets/images/banners/workspace-promo-dark.png'
import imageMobileLight from '~/assets/images/banners/workspace-promo-mobile-light.png'
import imageMobileDark from '~/assets/images/banners/workspace-promo-mobile-dark.png'
const breakpoints = useBreakpoints(TailwindBreakpoints)
const { isDarkTheme } = useTheme()
const isWorkspacesEnabled = useIsWorkspacesEnabled()
const mixpanel = useMixpanel()
const dismissedCookie = useSynchronizedCookie<boolean>(
@@ -63,7 +66,14 @@ const { result } = useQuery(settingsSidebarQuery, null, {
})
const showWorkspaceCreateDialog = ref(false)
const isMobile = breakpoints.smaller('md')
const bannerImage = computed(() => {
if (isMobile.value) {
return isDarkTheme.value ? imageMobileDark : imageMobileLight
}
return isDarkTheme.value ? imageDark : imageLight
})
const hasWorkspaces = computed(() =>
result.value?.activeUser?.workspaces.items
? result.value.activeUser.workspaces.items.length > 0
@@ -129,7 +129,6 @@ const processJoin = async (accept: boolean) => {
// eslint-disable-next-line camelcase
workspace_id: props.workspace.id
})
mixpanel.add_group('workspace_id', props.workspace.id)
router.push(`/workspaces/${props.workspace.id}`)
} else {
@@ -196,7 +196,6 @@ export const useProcessWorkspaceInvite = () => {
// eslint-disable-next-line camelcase
workspace_id: workspaceId
})
mp.add_group('workspace_id', workspaceId)
} else {
const err = getFirstErrorMessage(errors)
const preventErrorToasts = isFunction(options?.preventErrorToasts)
+2 -1
View File
@@ -41,7 +41,8 @@ const isTimeframe = (date: ConfigType) => {
unit.includes('second') ||
unit.includes('minute') ||
unit.includes('hour') ||
unit.includes('day')
unit.includes('day') ||
unit.includes('just now')
)
}
@@ -1,3 +1,4 @@
import { knexLogger as logger } from '@/observability/logging.js'
import { getPostgresConnectionString, getPostgresMaxConnections } from '@/utils/env.js'
import * as knex from 'knex'
import { get } from 'lodash-es'
@@ -18,6 +19,11 @@ export const db = knexBuilder({
max: getPostgresMaxConnections(),
acquireTimeoutMillis: 16000, //allows for 3x creation attempts plus idle time between attempts
createTimeoutMillis: 5000
},
log: {
warn: (message) => logger.warn(message),
error: (message) => logger.error(message),
debug: (message) => logger.debug(message)
}
// migrations are managed in the server package
})
@@ -1,7 +1,9 @@
import { REQUEST_ID_HEADER } from '@/domain/const.js'
import { logger } from '@/observability/logging.js'
import { randomUUID } from 'crypto'
import type { Request } from 'express'
import type { IncomingHttpHeaders, IncomingMessage } from 'http'
import { get } from 'lodash'
import { pinoHttp } from 'pino-http'
function determineRequestId(headers: IncomingHttpHeaders, uuidGenerator = randomUUID) {
@@ -13,6 +15,13 @@ function determineRequestId(headers: IncomingHttpHeaders, uuidGenerator = random
const generateReqId = (req: IncomingMessage) => determineRequestId(req.headers)
export const getRequestPath = (req: IncomingMessage | Request) => {
const path = ((get(req, 'originalUrl') || get(req, 'url') || '') as string).split(
'?'
)[0]
return path?.length ? path : null
}
export const loggingExpressMiddleware = pinoHttp({
genReqId: generateReqId,
logger,
@@ -21,6 +30,9 @@ export const loggingExpressMiddleware = pinoHttp({
// and we don't really care about 3xx stuff
// all the user related 4xx responses are treated as info
customLogLevel: (req, res, error) => {
const path = getRequestPath(req)
const shouldBeDebug = ['/metrics'].includes(path || '') ?? false
if (res.statusCode >= 400 && res.statusCode < 500) {
return 'info'
} else if (res.statusCode >= 500 || error) {
@@ -29,6 +41,6 @@ export const loggingExpressMiddleware = pinoHttp({
return 'silent'
}
return 'info' //default
return shouldBeDebug ? 'debug' : 'info'
}
})
@@ -11,3 +11,4 @@ export const logger = extendLoggerComponent(
)
export const serverLogger = extendLoggerComponent(logger, 'server')
export const testLogger = getLogger(getLogLevel(), isLogPretty())
export const knexLogger = extendLoggerComponent(logger, 'knex')
+1
View File
@@ -66,6 +66,7 @@ generates:
Webhook: '@/modules/webhooks/helpers/graphTypes#WebhookGraphQLReturn'
SmartTextEditorValue: '@/modules/core/services/richTextEditorService#SmartTextEditorValueSchema'
BlobMetadata: '@/modules/blobstorage/domain/types#BlobStorageItem'
ServerWorkspacesInfo: '@/modules/core/helpers/graphTypes#GraphQLEmptyReturn'
modules/cross-server-sync/graph/generated/graphql.ts:
plugins:
- 'typescript'
+5 -3
View File
@@ -44,6 +44,10 @@ export const LoggingExpressMiddleware = HttpLogger({
autoLogging: true,
genReqId: GenerateRequestId,
customLogLevel: (req, res, err) => {
const path = getRequestPath(req)
const shouldBeDebug =
['/metrics', '/readiness', '/liveness'].includes(path || '') ?? false
if (res.statusCode >= 400 && res.statusCode < 500) {
return 'info'
} else if (res.statusCode >= 500 || err) {
@@ -52,9 +56,7 @@ export const LoggingExpressMiddleware = HttpLogger({
return 'info'
}
if (req.url === '/readiness' || req.url === '/liveness') return 'debug'
if (req.url === '/metrics') return 'debug'
return 'info'
return shouldBeDebug ? 'debug' : 'info'
},
customReceivedMessage() {
@@ -4426,7 +4426,7 @@ export type ResolversTypes = {
ServerAppListItem: ResolverTypeWrapper<ServerAppListItem>;
ServerAutomateInfo: ResolverTypeWrapper<ServerAutomateInfo>;
ServerConfiguration: ResolverTypeWrapper<ServerConfiguration>;
ServerInfo: ResolverTypeWrapper<ServerInfo>;
ServerInfo: ResolverTypeWrapper<Omit<ServerInfo, 'workspaces'> & { workspaces: ResolversTypes['ServerWorkspacesInfo'] }>;
ServerInfoUpdateInput: ServerInfoUpdateInput;
ServerInvite: ResolverTypeWrapper<ServerInviteGraphQLReturnType>;
ServerInviteCreateInput: ServerInviteCreateInput;
@@ -4435,7 +4435,7 @@ export type ResolversTypes = {
ServerRoleItem: ResolverTypeWrapper<ServerRoleItem>;
ServerStatistics: ResolverTypeWrapper<GraphQLEmptyReturn>;
ServerStats: ResolverTypeWrapper<GraphQLEmptyReturn>;
ServerWorkspacesInfo: ResolverTypeWrapper<ServerWorkspacesInfo>;
ServerWorkspacesInfo: ResolverTypeWrapper<GraphQLEmptyReturn>;
SetPrimaryUserEmailInput: SetPrimaryUserEmailInput;
SmartTextEditorValue: ResolverTypeWrapper<SmartTextEditorValueSchema>;
SortDirection: SortDirection;
@@ -4672,7 +4672,7 @@ export type ResolversParentTypes = {
ServerAppListItem: ServerAppListItem;
ServerAutomateInfo: ServerAutomateInfo;
ServerConfiguration: ServerConfiguration;
ServerInfo: ServerInfo;
ServerInfo: Omit<ServerInfo, 'workspaces'> & { workspaces: ResolversParentTypes['ServerWorkspacesInfo'] };
ServerInfoUpdateInput: ServerInfoUpdateInput;
ServerInvite: ServerInviteGraphQLReturnType;
ServerInviteCreateInput: ServerInviteCreateInput;
@@ -4680,7 +4680,7 @@ export type ResolversParentTypes = {
ServerRoleItem: ServerRoleItem;
ServerStatistics: GraphQLEmptyReturn;
ServerStats: GraphQLEmptyReturn;
ServerWorkspacesInfo: ServerWorkspacesInfo;
ServerWorkspacesInfo: GraphQLEmptyReturn;
SetPrimaryUserEmailInput: SetPrimaryUserEmailInput;
SmartTextEditorValue: SmartTextEditorValueSchema;
Stream: StreamGraphQLReturn;
@@ -1,10 +1,10 @@
import type { Request } from 'express'
import type { IncomingMessage } from 'http'
import type express from 'express'
import { get } from 'lodash'
export const getRequestPath = (req: IncomingMessage | express.Request) => {
const path = (get(req, 'originalUrl') || get(req, 'url') || '').split(
export const getRequestPath = (req: IncomingMessage | Request) => {
const path = ((get(req, 'originalUrl') || get(req, 'url') || '') as string).split(
'?'
)[0] as string
)[0]
return path?.length ? path : null
}
@@ -891,6 +891,12 @@ export = FF_WORKSPACES_MODULE_ENABLED
getWorkspaceWithDomains: getWorkspaceWithDomainsFactory({ db })
})({ workspaceId, userId })
}
},
ServerInfo: {
workspaces: () => ({})
},
ServerWorkspacesInfo: {
workspacesEnabled: () => true
}
} as Resolvers)
: {}
@@ -102,6 +102,12 @@ export = !FF_WORKSPACES_MODULE_ENABLED
},
LimitedUser: {
workspaceDomainPolicyCompliant: async () => null
},
ServerInfo: {
workspaces: () => ({})
},
ServerWorkspacesInfo: {
workspacesEnabled: () => false
}
} as Resolvers)
: {}
@@ -66,15 +66,6 @@
<span class="text-body-xs sr-only">Clear input</span>
<XMarkIcon class="h-5 w-5 text-foreground" aria-hidden="true" />
</a>
<div
v-if="errorMessage"
:class="[
'pointer-events-none absolute top-0 bottom-0 right-0 flex items-center h-8',
shouldShowClear ? 'pr-8' : 'pr-2'
]"
>
<ExclamationCircleIcon class="h-4 w-4 text-danger" aria-hidden="true" />
</div>
<div
v-if="!showLabel && showRequired && !errorMessage"
class="ppointer-events-none absolute top-0 bottom-0 mt-2 text-body right-0 flex items-center text-danger pr-2.5"
@@ -95,7 +86,7 @@
</template>
<script setup lang="ts">
import type { RuleExpression } from 'vee-validate'
import { ExclamationCircleIcon, XMarkIcon } from '@heroicons/vue/20/solid'
import { XMarkIcon } from '@heroicons/vue/20/solid'
import { computed, ref, toRefs, useSlots } from 'vue'
import type { PropType } from 'vue'
import type { Nullable, Optional } from '@speckle/shared'
@@ -77,9 +77,7 @@ export function useTextInputCore<V extends string | string[] = string>(params: {
]
if (error.value) {
classParts.push(
'focus:border-danger focus:ring-danger border-2 border-danger text-danger-darker'
)
classParts.push('!border-danger')
} else {
classParts.push('border-0 focus:ring-2 focus:ring-outline-2')
}