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:
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)
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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')
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user