diff --git a/app.vue b/app.vue index b5e9d11..7df545e 100644 --- a/app.vue +++ b/app.vue @@ -15,6 +15,7 @@ import { useConfigStore } from '~/store/config' import { useAccountStore } from '~/store/accounts' import { useHostAppStore } from '~/store/hostApp' import { storeToRefs } from 'pinia' +import { logToSeq } from '~/lib/logger/composables/useLogger' const uiConfigStore = useConfigStore() const { isDarkTheme } = storeToRefs(uiConfigStore) @@ -57,5 +58,7 @@ onMounted(() => { // eslint-disable-next-line @typescript-eslint/no-unused-vars const { $intercom } = useNuxtApp() // needed her for initialisation + + logToSeq('Information', 'DUI3 initialized') }) diff --git a/components/accounts/Menu.vue b/components/accounts/Menu.vue index 320fbf3..d23a2e2 100644 --- a/components/accounts/Menu.vue +++ b/components/accounts/Menu.vue @@ -95,7 +95,7 @@ const showAccountsDialog = defineModel('open', { const isDesktopServiceAvailable = ref(false) // this should be false default because there is a delay if /ping is not successful. -app.$baseBinding.on('documentChanged', () => { +app.$baseBinding?.on('documentChanged', () => { showAccountsDialog.value = false }) diff --git a/components/header/NavBar.vue b/components/header/NavBar.vue index 575643c..0bc5363 100644 --- a/components/header/NavBar.vue +++ b/components/header/NavBar.vue @@ -97,7 +97,7 @@ const showFeedbackDialog = ref(false) const showSendDialog = ref(false) const showReceiveDialog = ref(false) -app.$baseBinding.on('documentChanged', () => { +app.$baseBinding?.on('documentChanged', () => { showSendDialog.value = false showReceiveDialog.value = false }) diff --git a/components/model/ActionsDialog.vue b/components/model/ActionsDialog.vue index 880687a..9c004ab 100644 --- a/components/model/ActionsDialog.vue +++ b/components/model/ActionsDialog.vue @@ -73,7 +73,7 @@ const hasSettings = computed(() => { }) const app = useNuxtApp() -app.$baseBinding.on('documentChanged', () => { +app.$baseBinding?.on('documentChanged', () => { openModelCardActionsDialog.value = false }) diff --git a/components/model/Receiver.vue b/components/model/Receiver.vue index b4f964c..a78d236 100644 --- a/components/model/Receiver.vue +++ b/components/model/Receiver.vue @@ -117,7 +117,7 @@ const projectAccount = computed(() => accountStore.accountWithFallback(props.project.accountId, props.project.serverUrl) ) -app.$baseBinding.on('documentChanged', () => { +app.$baseBinding?.on('documentChanged', () => { openVersionsDialog.value = false }) diff --git a/components/model/Sender.vue b/components/model/Sender.vue index 788cf11..70d0f51 100644 --- a/components/model/Sender.vue +++ b/components/model/Sender.vue @@ -130,7 +130,7 @@ const props = defineProps<{ const store = useHostAppStore() const openFilterDialog = ref(false) -app.$baseBinding.on('documentChanged', () => { +app.$baseBinding?.on('documentChanged', () => { openFilterDialog.value = false }) diff --git a/layouts/default.vue b/layouts/default.vue index 2ecbce1..da722d5 100644 --- a/layouts/default.vue +++ b/layouts/default.vue @@ -8,7 +8,7 @@ v-if="hasNoModelCards" class="px-3 text-body-3xs text-foreground-2 justify-center bg-red-200/1 py-2 flex items-center w-full space-x-2" > - Version {{ hostApp.connectorVersion }} + Version {{ hostApp.connectorVersion || 'dev' }} Toggle theme + + Open dev tools + @@ -27,7 +38,9 @@ import { storeToRefs } from 'pinia' import { useHostAppStore } from '~/store/hostApp' import { useConfigStore } from '~/store/config' -import { MoonIcon, SunIcon } from '@heroicons/vue/24/outline' +import { MoonIcon, SunIcon, WrenchScrewdriverIcon } from '@heroicons/vue/24/outline' + +const app = useNuxtApp() const uiConfigStore = useConfigStore() const { isDarkTheme } = storeToRefs(uiConfigStore) diff --git a/lib/bindings/definitions/IAccountBinding.ts b/lib/bindings/definitions/IAccountBinding.ts index ba6f3bf..b1cc0f1 100644 --- a/lib/bindings/definitions/IAccountBinding.ts +++ b/lib/bindings/definitions/IAccountBinding.ts @@ -31,3 +31,42 @@ export type Account = { } export interface IAccountBindingEvents extends IBindingSharedEvents {} + +export class MockedAccountBinding implements IAccountBinding { + public async getAccounts() { + const config = useRuntimeConfig() + return (await [ + { + id: 'whatever', + isDefault: true, + token: config.public.speckleToken, + serverInfo: { + name: 'test', + url: config.public.speckleUrl, + frontend2: true + }, + userInfo: { + id: 'whatever', + avatar: 'whatever', + email: '' + } + } + ]) as Account[] + } + + public async removeAccount(accountId: string) { + return await console.log('no way dude', accountId) + } + + public async showDevTools() { + await console.log('No way dude') + } + + public async openUrl(url: string) { + await window.open(url) + } + + public on() { + return + } +} diff --git a/lib/bindings/definitions/IBasicConnectorBinding.ts b/lib/bindings/definitions/IBasicConnectorBinding.ts index 14301a8..52071f3 100644 --- a/lib/bindings/definitions/IBasicConnectorBinding.ts +++ b/lib/bindings/definitions/IBasicConnectorBinding.ts @@ -57,3 +57,65 @@ export type ToastAction = { url: string name: string } + +export class MockedBaseBinding implements IBasicConnectorBinding { + public async getSourceApplicationName() { + return await 'headless' + } + + public async getSourceApplicationVersion() { + return await 'dev' + } + + public async getConnectorVersion() { + return await 'dev' + } + + public async getDocumentInfo() { + return (await { + id: 'whatever', + name: 'test', + location: 'whocares' + }) as DocumentInfo + } + + public async getDocumentState() { + return (await { models: [] }) as DocumentModelStore + } + + public async addModel(_model: IModelCard) { + await console.log('no way dude') + } + + public async removeModel(_model: IModelCard) { + await console.log('no way dude') + } + + public async removeModels(_models: IModelCard[]) { + await console.log('no way dude') + } + + public async updateModel(_model: IModelCard) { + await console.log('no way dude') + } + + public async highlightModel(_modelCardId: string) { + await console.log('no way dude') + } + + public async highlightObjects(_objectIds: string[]) { + await console.log('no way dude') + } + + public async showDevTools() { + await console.log('No way dude') + } + + public async openUrl(url: string) { + await window.open(url) + } + + public on() { + return + } +} diff --git a/lib/bindings/definitions/IConfigBinding.ts b/lib/bindings/definitions/IConfigBinding.ts index 6b38b08..d7c443e 100644 --- a/lib/bindings/definitions/IConfigBinding.ts +++ b/lib/bindings/definitions/IConfigBinding.ts @@ -1,4 +1,3 @@ -import { BaseBridge } from '~/lib/bridge/base' import type { IBinding, IBindingSharedEvents @@ -38,4 +37,48 @@ export type WorkspacesConfig = { } // Useless, but will do for now :) -export class MockedConfigBinding extends BaseBridge {} +export class MockedConfigBinding implements IConfigBinding { + public async getIsDevMode() { + return await true + } + + public async getConfig() { + return await { darkTheme: false } + } + + public async updateConfig() { + return await console.log('') + } + + public async setUserSelectedAccountId(accountId: string) { + return await console.log(accountId) + } + + public async setUserSelectedWorkspaceId(workspaceId: string) { + return await console.log(workspaceId) + } + + public async getAccountsConfig() { + return (await { userSelectedAccountId: 'whatever' }) as AccountsConfig + } + + public async getWorkspacesConfig() { + return (await { userSelectedWorkspaceId: 'whatever' }) as WorkspacesConfig + } + + public async getUserSelectedAccountId() { + return (await { userSelectedAccountId: 'whatever' }) as AccountsConfig + } + + public async showDevTools() { + await console.log('No way dude') + } + + public async openUrl(url: string) { + await window.open(url) + } + + public on() { + return + } +} diff --git a/lib/bindings/definitions/IReceiveBinding.ts b/lib/bindings/definitions/IReceiveBinding.ts index 8e98178..c1075cd 100644 --- a/lib/bindings/definitions/IReceiveBinding.ts +++ b/lib/bindings/definitions/IReceiveBinding.ts @@ -24,3 +24,29 @@ export interface IReceiveBindingEvents conversionResults: ConversionResult[] }) => void } + +export class MockedReceiveBinding implements IReceiveBinding { + public async getReceiveSettings() { + return await [] + } + + public async receive(_modelCardId: string) { + return await console.log('no way dude') + } + + public async cancelReceive(_modelCardId: string) { + return await console.log('no way dude') + } + + public async showDevTools() { + await console.log('No way dude') + } + + public async openUrl(url: string) { + await window.open(url) + } + + public on() { + return + } +} diff --git a/lib/bindings/definitions/ISelectionBinding.ts b/lib/bindings/definitions/ISelectionBinding.ts index 6941828..f2204f9 100644 --- a/lib/bindings/definitions/ISelectionBinding.ts +++ b/lib/bindings/definitions/ISelectionBinding.ts @@ -17,3 +17,24 @@ export type SelectionInfo = { summary?: string selectedObjectIds: string[] } + +export class MockedSelectionBinding implements ISelectionBinding { + public async getSelection() { + return (await { + summary: '2 objects selected over mock binding', + selectedObjectIds: ['1', '2', '3'] + }) as SelectionInfo + } + + public async showDevTools() { + await console.log('No way dude') + } + + public async openUrl(url: string) { + await window.open(url) + } + + public on() { + return + } +} diff --git a/lib/bindings/definitions/ISendBinding.ts b/lib/bindings/definitions/ISendBinding.ts index 26684bb..a331034 100644 --- a/lib/bindings/definitions/ISendBinding.ts +++ b/lib/bindings/definitions/ISendBinding.ts @@ -38,3 +38,33 @@ export interface ISendBindingEvents triggerCancel: (modelCardId: string) => void triggerCreateVersion: (args: CreateVersionArgs) => void } + +export class MockedSendBinding implements ISendBinding { + public async getSendFilters() { + return await [] + } + + public async getSendSettings() { + return await [] + } + + public async send(_modelCardId: string) { + return await console.log('no way dude') + } + + public async cancelSend(_modelCardId: string) { + return await console.log('no way dude') + } + + public async showDevTools() { + await console.log('No way dude') + } + + public async openUrl(url: string) { + await window.open(url) + } + + public on() { + return + } +} diff --git a/lib/bindings/definitions/ITestBinding.ts b/lib/bindings/definitions/ITestBinding.ts index 8ec2422..7d3b746 100644 --- a/lib/bindings/definitions/ITestBinding.ts +++ b/lib/bindings/definitions/ITestBinding.ts @@ -1,6 +1,4 @@ /* eslint-disable @typescript-eslint/require-await */ - -import { BaseBridge } from '~~/lib/bridge/base' import type { IBinding, IBindingSharedEvents @@ -38,9 +36,11 @@ export type ComplexType = { count: number } -export class MockedTestBinding extends BaseBridge { +export class MockedTestBinding implements ITestBinding { public async sayHi(name: string, count: number, sayHelloNotHi: boolean) { - return `Hello from mocked bindings. Args: name = ${name}, count = ${count}, sayHelloNotHi = ${sayHelloNotHi.toString()}.` + return [ + `Hello from mocked bindings. Args: name = ${name}, count = ${count}, sayHelloNotHi = ${sayHelloNotHi.toString()}.` + ] } public async goAway() { @@ -56,6 +56,18 @@ export class MockedTestBinding extends BaseBridge { } public async triggerEvent(eventName: string) { - return eventName + return console.log(eventName) + } + + public async showDevTools() { + await console.log('No way dude') + } + + public async openUrl(url: string) { + await window.open(url) + } + + public on() { + return } } diff --git a/lib/common/generated/gql/graphql.ts b/lib/common/generated/gql/graphql.ts index 02dffa5..0cae77c 100644 --- a/lib/common/generated/gql/graphql.ts +++ b/lib/common/generated/gql/graphql.ts @@ -270,12 +270,13 @@ export type AutomateFunction = { isFeatured: Scalars['Boolean']['output']; logo?: Maybe; name: Scalars['String']['output']; + permissions: AutomateFunctionPermissionChecks; releases: AutomateFunctionReleaseCollection; repo: BasicGitRepositoryMetadata; /** SourceAppNames values from @speckle/shared. Empty array means - all of them */ supportedSourceApps: Array; tags: Array; - workspaceIds?: Maybe>; + workspaceIds: Array; }; @@ -292,6 +293,11 @@ export type AutomateFunctionCollection = { totalCount: Scalars['Int']['output']; }; +export type AutomateFunctionPermissionChecks = { + __typename?: 'AutomateFunctionPermissionChecks'; + canRegenerateToken: PermissionCheckResult; +}; + export type AutomateFunctionRelease = { __typename?: 'AutomateFunctionRelease'; commitId: Scalars['String']['output']; @@ -373,6 +379,7 @@ export type AutomateMutations = { __typename?: 'AutomateMutations'; createFunction: AutomateFunction; createFunctionWithoutVersion: AutomateFunctionToken; + regenerateFunctionToken: Scalars['String']['output']; updateFunction: AutomateFunction; }; @@ -387,6 +394,11 @@ export type AutomateMutationsCreateFunctionWithoutVersionArgs = { }; +export type AutomateMutationsRegenerateFunctionTokenArgs = { + functionId: Scalars['String']['input']; +}; + + export type AutomateMutationsUpdateFunctionArgs = { input: UpdateAutomateFunctionInput; }; @@ -918,6 +930,12 @@ export type CreateCommentReplyInput = { threadId: Scalars['String']['input']; }; +export type CreateEmbedTokenReturn = { + __typename?: 'CreateEmbedTokenReturn'; + token: Scalars['String']['output']; + tokenMetadata: EmbedToken; +}; + export type CreateModelInput = { description?: InputMaybe; name: Scalars['String']['input']; @@ -994,6 +1012,32 @@ export type EmailVerificationRequestInput = { id: Scalars['ID']['input']; }; +/** A token used to enable an embedded viewer for a private project */ +export type EmbedToken = { + __typename?: 'EmbedToken'; + createdAt: Scalars['DateTime']['output']; + lastUsed: Scalars['DateTime']['output']; + lifespan: Scalars['BigInt']['output']; + projectId: Scalars['String']['output']; + resourceIdString: Scalars['String']['output']; + tokenId: Scalars['String']['output']; + user?: Maybe; +}; + +export type EmbedTokenCollection = { + __typename?: 'EmbedTokenCollection'; + cursor?: Maybe; + items: Array; + totalCount: Scalars['Int']['output']; +}; + +export type EmbedTokenCreateInput = { + lifespan?: InputMaybe; + projectId: Scalars['String']['input']; + /** The model(s) and version(s) string used in the embed url */ + resourceIdString: Scalars['String']['input']; +}; + export type FileUpload = { __typename?: 'FileUpload'; branchName: Scalars['String']['output']; @@ -1012,11 +1056,14 @@ export type FileUpload = { id: Scalars['String']['output']; /** Model associated with the file upload, if it exists already */ model?: Maybe; + modelId?: Maybe; /** Alias for branchName */ modelName: Scalars['String']['output']; /** Alias for streamId */ projectId: Scalars['String']['output']; streamId: Scalars['String']['output']; + /** Date when upload was last updated */ + updatedAt: Scalars['DateTime']['output']; uploadComplete: Scalars['Boolean']['output']; uploadDate: Scalars['DateTime']['output']; /** The user's id that uploaded this file. */ @@ -2046,9 +2093,7 @@ export type PendingWorkspaceCollaborator = { updatedAt: Scalars['DateTime']['output']; /** Set only if user is registered */ user?: Maybe; - workspaceId: Scalars['String']['output']; - workspaceName: Scalars['String']['output']; - workspaceSlug: Scalars['String']['output']; + workspace: LimitedWorkspace; }; export type PendingWorkspaceCollaboratorsFilter = { @@ -2087,6 +2132,7 @@ export type Project = { description?: Maybe; /** Public project-level configuration for embedded viewer */ embedOptions: ProjectEmbedOptions; + embedTokens: EmbedTokenCollection; hasAccessToFeature: Scalars['Boolean']['output']; id: Scalars['ID']['output']; invitableCollaborators: WorkspaceCollaboratorCollection; @@ -2169,6 +2215,12 @@ export type ProjectCommentThreadsArgs = { }; +export type ProjectEmbedTokensArgs = { + cursor?: InputMaybe; + limit?: InputMaybe; +}; + + export type ProjectHasAccessToFeatureArgs = { featureName: WorkspaceFeatureName; }; @@ -2564,6 +2616,7 @@ export type ProjectMutations = { batchDelete: Scalars['Boolean']['output']; /** Create new project */ create: Project; + createEmbedToken: CreateEmbedTokenReturn; /** * Create onboarding/tutorial project. If one is already created for the active user, that * one will be returned instead. @@ -2575,6 +2628,8 @@ export type ProjectMutations = { invites: ProjectInviteMutations; /** Leave a project. Only possible if you're not the last remaining owner. */ leave: Scalars['Boolean']['output']; + revokeEmbedToken: Scalars['Boolean']['output']; + revokeEmbedTokens: Scalars['Boolean']['output']; /** Updates an existing project */ update: Project; /** Update role for a collaborator */ @@ -2597,6 +2652,11 @@ export type ProjectMutationsCreateArgs = { }; +export type ProjectMutationsCreateEmbedTokenArgs = { + token: EmbedTokenCreateInput; +}; + + export type ProjectMutationsDeleteArgs = { id: Scalars['String']['input']; }; @@ -2607,6 +2667,17 @@ export type ProjectMutationsLeaveArgs = { }; +export type ProjectMutationsRevokeEmbedTokenArgs = { + projectId: Scalars['String']['input']; + token: Scalars['String']['input']; +}; + + +export type ProjectMutationsRevokeEmbedTokensArgs = { + projectId: Scalars['String']['input']; +}; + + export type ProjectMutationsUpdateArgs = { update: ProjectUpdateInput; }; @@ -2647,6 +2718,7 @@ export type ProjectPermissionChecks = { canBroadcastActivity: PermissionCheckResult; canCreateAutomation: PermissionCheckResult; canCreateComment: PermissionCheckResult; + canCreateEmbedTokens: PermissionCheckResult; canCreateModel: PermissionCheckResult; canDelete: PermissionCheckResult; canInvite: PermissionCheckResult; @@ -2655,9 +2727,11 @@ export type ProjectPermissionChecks = { canMoveToWorkspace: PermissionCheckResult; canPublish: PermissionCheckResult; canRead: PermissionCheckResult; + canReadEmbedTokens: PermissionCheckResult; canReadSettings: PermissionCheckResult; canReadWebhooks: PermissionCheckResult; canRequestRender: PermissionCheckResult; + canRevokeEmbedTokens: PermissionCheckResult; canUpdate: PermissionCheckResult; canUpdateAllowPublicComments: PermissionCheckResult; }; diff --git a/lib/logger/composables/useLogger.ts b/lib/logger/composables/useLogger.ts new file mode 100644 index 0000000..ee93562 --- /dev/null +++ b/lib/logger/composables/useLogger.ts @@ -0,0 +1,85 @@ +import md5 from '~/lib/common/helpers/md5' +import { useAccountStore } from '~/store/accounts' +import { useHostAppStore } from '~/store/hostApp' + +const SEQ_URL = 'https://seq-dev.speckle.systems/api/events/raw' + +type LogLevel = 'Verbose' | 'Debug' | 'Information' | 'Warning' | 'Error' | 'Fatal' + +const collectCommonProperties = () => { + const { accounts, activeAccount } = useAccountStore() + const hashedEmail = + '@' + + md5(activeAccount.accountInfo.userInfo.email.toLowerCase() as string).toUpperCase() + return { + user: { + id: activeAccount.accountInfo.userInfo.id, + distinctId: hashedEmail + }, + dui3: true, + accountCount: accounts.length + } +} + +const collectResources = () => { + const hostAppStore = useHostAppStore() + return { + '@ra': { + connector: { + slug: hostAppStore.hostAppName, + hostAppVersion: hostAppStore.hostAppVersion, + version: hostAppStore.connectorVersion + }, + service: { + version: hostAppStore.connectorVersion // this needs alignment with .NET SDK, actually this should be connector.version instead service.version + } + } + } +} + +// const collectServices = () => { +// const hostAppStore = useHostAppStore() +// return { +// '@sa': { +// service: { +// version: hostAppStore.connectorVersion +// } +// } +// } +// } + +export const logToSeq = async ( + level: LogLevel, + message: string, + properties: Record = {} +) => { + try { + const logEvent = { + '@t': new Date().toISOString(), + '@l': level, + '@m': message, + ...collectResources(), + // ...collectServices(), + ...collectCommonProperties(), + ...properties + } + + const response = await fetch(SEQ_URL, { + method: 'POST', + headers: { + 'Content-Type': 'application/vnd.serilog.clef', + 'X-Seq-ApiKey': 'y5YnBp12ZE1Czh4tzZWn' + }, + body: JSON.stringify(logEvent) + '\n' + }) + + if (!response.ok) { + const errorText = await response.text() + console.error( + `[Seq Logger] Failed to log: ${response.status} ${response.statusText} - ${errorText}` + ) + } + } catch (err) { + console.error('[Seq Logger] Failed to log', err) + } +} diff --git a/nuxt.config.ts b/nuxt.config.ts index fd09940..2f41bf6 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -10,7 +10,8 @@ export default defineNuxtConfig({ '@nuxt/eslint', '@nuxtjs/tailwindcss', '@speckle/ui-components-nuxt', - '@pinia/nuxt' + '@pinia/nuxt', + '@nuxt/image' ], alias: { // Rewriting all lodash calls to lodash-es for proper tree-shaking & chunk splitting diff --git a/package.json b/package.json index 1e5f3a3..9ab9ee5 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "@jsonforms/core": "3.1.0", "@jsonforms/vue": "3.1.0", "@jsonforms/vue-vanilla": "3.1.0", + "@nuxt/image": "^1.10.0", "@pinia/nuxt": "^0.4.11", "@speckle/objectloader": "^2.25.0", "@speckle/objectsender": "^2.25.0", diff --git a/pages/index.vue b/pages/index.vue index 3fceeb3..976df22 100644 --- a/pages/index.vue +++ b/pages/index.vue @@ -1,6 +1,6 @@