Merge branch 'main' into iain/preview-service-handle-errors
This commit is contained in:
@@ -522,7 +522,14 @@ jobs:
|
||||
- run:
|
||||
name: 'Run tests'
|
||||
# Extra formatting to get timestamps on each line in CI (for profiling purposes)
|
||||
command: yarn test:report --color=always | while IFS= read -r line; do echo -e "$(date +%T.%3N) > $line"; done
|
||||
command: |
|
||||
GREP_FLAG=""
|
||||
|
||||
if [ "$RUN_TESTS_IN_MULTIREGION_MODE" == "true" ]; then
|
||||
GREP_FLAG="--grep @multiregion"
|
||||
fi
|
||||
|
||||
yarn test:report $GREP_FLAG --color=always | while IFS= read -r line; do echo -e "$(date +%T.%3N) > $line"; done
|
||||
working_directory: 'packages/server'
|
||||
no_output_timeout: 30m
|
||||
|
||||
|
||||
@@ -25,6 +25,19 @@
|
||||
"endpoint": "http://127.0.0.1:9020",
|
||||
"s3Region": "us-east-1"
|
||||
}
|
||||
},
|
||||
"region2": {
|
||||
"postgres": {
|
||||
"connectionUri": "postgresql://speckle:speckle@127.0.0.1:5434/speckle2_test"
|
||||
},
|
||||
"blobStorage": {
|
||||
"accessKey": "minioadmin",
|
||||
"secretKey": "minioadmin",
|
||||
"bucket": "speckle-server",
|
||||
"createBucketIfNotExists": true,
|
||||
"endpoint": "http://127.0.0.1:9040",
|
||||
"s3Region": "us-east-1"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import Environment from '@speckle/shared/dist/commonjs/environment/index.js'
|
||||
import {
|
||||
initPrometheusMetrics,
|
||||
metricDuration,
|
||||
@@ -18,8 +17,6 @@ import { Nullable, Scopes, wait } from '@speckle/shared'
|
||||
import { Knex } from 'knex'
|
||||
import { Logger } from 'pino'
|
||||
|
||||
const { FF_FILEIMPORT_IFC_DOTNET_ENABLED } = Environment.getFeatureFlags()
|
||||
|
||||
const HEALTHCHECK_FILE_PATH = '/tmp/last_successful_query'
|
||||
|
||||
const TMP_INPUT_DIR = '/tmp/file_to_import'
|
||||
@@ -156,27 +153,7 @@ async function doTask(
|
||||
taskLogger.info('Triggering importer for {fileType}')
|
||||
|
||||
if (info.fileType.toLowerCase() === 'ifc') {
|
||||
if (FF_FILEIMPORT_IFC_DOTNET_ENABLED) {
|
||||
await runProcessWithTimeout(
|
||||
taskLogger,
|
||||
process.env['DOTNET_BINARY_PATH'] || 'dotnet',
|
||||
[
|
||||
process.env['IFC_DOTNET_DLL_PATH'] ||
|
||||
'/speckle-server/packages/fileimport-service/src/ifc-dotnet/ifc-converter.dll',
|
||||
TMP_FILE_PATH,
|
||||
TMP_RESULTS_PATH,
|
||||
info.streamId,
|
||||
`File upload: ${info.fileName}`,
|
||||
existingBranch?.id || '',
|
||||
info.branchName,
|
||||
regionName
|
||||
],
|
||||
{
|
||||
USER_TOKEN: tempUserToken
|
||||
},
|
||||
TIME_LIMIT
|
||||
)
|
||||
} else {
|
||||
if (info.fileName.toLowerCase().endsWith('.legacyimporter.ifc')) {
|
||||
await runProcessWithTimeout(
|
||||
taskLogger,
|
||||
process.env['NODE_BINARY_PATH'] || 'node',
|
||||
@@ -199,6 +176,26 @@ async function doTask(
|
||||
},
|
||||
TIME_LIMIT
|
||||
)
|
||||
} else {
|
||||
await runProcessWithTimeout(
|
||||
taskLogger,
|
||||
process.env['DOTNET_BINARY_PATH'] || 'dotnet',
|
||||
[
|
||||
process.env['IFC_DOTNET_DLL_PATH'] ||
|
||||
'/speckle-server/packages/fileimport-service/src/ifc-dotnet/ifc-converter.dll',
|
||||
TMP_FILE_PATH,
|
||||
TMP_RESULTS_PATH,
|
||||
info.streamId,
|
||||
`File upload: ${info.fileName}`,
|
||||
existingBranch?.id || '',
|
||||
info.branchName,
|
||||
regionName
|
||||
],
|
||||
{
|
||||
USER_TOKEN: tempUserToken
|
||||
},
|
||||
TIME_LIMIT
|
||||
)
|
||||
}
|
||||
} else if (info.fileType.toLowerCase() === 'stl') {
|
||||
await runProcessWithTimeout(
|
||||
|
||||
@@ -10,6 +10,9 @@ PORT=3000
|
||||
CANONICAL_URL="http://127.0.0.1:3000"
|
||||
SESSION_SECRET="-> FILL IN <-"
|
||||
|
||||
# Optional license token for paid features like multiregion
|
||||
# LICENSE_TOKEN=
|
||||
|
||||
# Redis connection: default for local development environment
|
||||
REDIS_URL="redis://127.0.0.1:6379"
|
||||
|
||||
|
||||
@@ -222,7 +222,7 @@ describe('Core GraphQL Subscriptions (New)', () => {
|
||||
]
|
||||
|
||||
modes.forEach(({ isMultiRegion }) => {
|
||||
describe(`W/${!isMultiRegion ? 'o' : ''} multiregion`, () => {
|
||||
describe(`W/${!isMultiRegion ? 'o' : ''} @multiregion`, () => {
|
||||
const myMainWorkspace: BasicTestWorkspace = {
|
||||
id: '',
|
||||
ownerId: '',
|
||||
|
||||
@@ -0,0 +1,363 @@
|
||||
import { db } from '@/db/knex'
|
||||
import { AutomationRecord, AutomationRunRecord } from '@/modules/automate/helpers/types'
|
||||
import { CommentRecord } from '@/modules/comments/helpers/types'
|
||||
import { createRandomEmail } from '@/modules/core/helpers/testHelpers'
|
||||
import { StreamRecord } from '@/modules/core/helpers/types'
|
||||
import { getDb } from '@/modules/multiregion/utils/dbSelector'
|
||||
import {
|
||||
createWebhookConfigFactory,
|
||||
createWebhookEventFactory
|
||||
} from '@/modules/webhooks/repositories/webhooks'
|
||||
import {
|
||||
BasicTestWorkspace,
|
||||
createTestWorkspace
|
||||
} from '@/modules/workspaces/tests/helpers/creation'
|
||||
import { BasicTestUser, createTestUser } from '@/test/authHelper'
|
||||
import {
|
||||
UpdateProjectRegionDocument,
|
||||
GetProjectDocument,
|
||||
GetRegionalProjectModelDocument,
|
||||
GetRegionalProjectVersionDocument,
|
||||
GetRegionalProjectObjectDocument,
|
||||
GetRegionalProjectAutomationDocument,
|
||||
GetRegionalProjectCommentDocument,
|
||||
GetRegionalProjectWebhookDocument,
|
||||
GetRegionalProjectBlobDocument
|
||||
} from '@/test/graphql/generated/graphql'
|
||||
import { TestApolloServer, testApolloServer } from '@/test/graphqlHelper'
|
||||
import {
|
||||
createTestAutomation,
|
||||
createTestAutomationRun
|
||||
} from '@/test/speckle-helpers/automationHelper'
|
||||
import { createTestBlob } from '@/test/speckle-helpers/blobHelper'
|
||||
import { BasicTestBranch, createTestBranch } from '@/test/speckle-helpers/branchHelper'
|
||||
import { createTestComment } from '@/test/speckle-helpers/commentHelper'
|
||||
import {
|
||||
BasicTestCommit,
|
||||
createTestObject,
|
||||
createTestCommit
|
||||
} from '@/test/speckle-helpers/commitHelper'
|
||||
import {
|
||||
isMultiRegionTestMode,
|
||||
waitForRegionUser
|
||||
} from '@/test/speckle-helpers/regions'
|
||||
import { BasicTestStream, createTestStream } from '@/test/speckle-helpers/streamHelper'
|
||||
import { retry, Roles } from '@speckle/shared'
|
||||
import { expect } from 'chai'
|
||||
import cryptoRandomString from 'crypto-random-string'
|
||||
import { Knex } from 'knex'
|
||||
import { SetOptional } from 'type-fest'
|
||||
|
||||
const tables = {
|
||||
projects: (db: Knex) => db.table<StreamRecord>('streams')
|
||||
}
|
||||
|
||||
const assertProjectRegion = async (
|
||||
projectId: string,
|
||||
regionKey: string
|
||||
): Promise<void> => {
|
||||
const project = await tables.projects(db).select('*').where('id', projectId).first()
|
||||
|
||||
if (!project || project.regionKey !== regionKey) {
|
||||
expect.fail('Project is not in expected region.')
|
||||
}
|
||||
}
|
||||
|
||||
const ensureProjectRegion = async (
|
||||
projectId: string,
|
||||
regionKey: string
|
||||
): Promise<void> => {
|
||||
await retry(async () => assertProjectRegion(projectId, regionKey), 30, 500)
|
||||
}
|
||||
|
||||
isMultiRegionTestMode()
|
||||
? describe('Workspace project region changes @multiregion', () => {
|
||||
const regionKey1 = 'region1'
|
||||
const regionKey2 = 'region2'
|
||||
|
||||
const adminUser: BasicTestUser = {
|
||||
id: '',
|
||||
name: 'John Speckle',
|
||||
email: createRandomEmail(),
|
||||
role: Roles.Server.Admin
|
||||
}
|
||||
|
||||
const testWorkspace: SetOptional<BasicTestWorkspace, 'slug'> = {
|
||||
id: '',
|
||||
ownerId: '',
|
||||
name: 'Unlimited Workspace'
|
||||
}
|
||||
|
||||
const testProject: BasicTestStream = {
|
||||
id: '',
|
||||
ownerId: '',
|
||||
name: 'Regional Project',
|
||||
isPublic: true
|
||||
}
|
||||
|
||||
const testModel: BasicTestBranch = {
|
||||
id: '',
|
||||
name: cryptoRandomString({ length: 8 }),
|
||||
streamId: '',
|
||||
authorId: ''
|
||||
}
|
||||
|
||||
const testVersion: BasicTestCommit = {
|
||||
id: '',
|
||||
objectId: '',
|
||||
streamId: '',
|
||||
authorId: ''
|
||||
}
|
||||
|
||||
let testAutomation: AutomationRecord
|
||||
let testAutomationRun: AutomationRunRecord
|
||||
|
||||
let testComment: CommentRecord
|
||||
let testWebhookId: string
|
||||
let testBlobId: string
|
||||
|
||||
let apollo: TestApolloServer
|
||||
let sourceRegionDb: Knex
|
||||
|
||||
before(async () => {
|
||||
await createTestUser(adminUser)
|
||||
await waitForRegionUser(adminUser)
|
||||
|
||||
apollo = await testApolloServer({ authUserId: adminUser.id })
|
||||
sourceRegionDb = await getDb({ regionKey: regionKey1 })
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
delete testWorkspace.slug
|
||||
|
||||
await createTestWorkspace(testWorkspace, adminUser, {
|
||||
regionKey: regionKey1,
|
||||
addPlan: {
|
||||
name: 'unlimited',
|
||||
status: 'valid'
|
||||
}
|
||||
})
|
||||
|
||||
testProject.workspaceId = testWorkspace.id
|
||||
|
||||
await createTestStream(testProject, adminUser)
|
||||
await createTestBranch({
|
||||
stream: testProject,
|
||||
branch: testModel,
|
||||
owner: adminUser
|
||||
})
|
||||
|
||||
testVersion.branchName = testModel.name
|
||||
testVersion.objectId = await createTestObject({ projectId: testProject.id })
|
||||
|
||||
await createTestCommit(testVersion, {
|
||||
owner: adminUser,
|
||||
stream: testProject
|
||||
})
|
||||
|
||||
const { automation, revision } = await createTestAutomation({
|
||||
userId: adminUser.id,
|
||||
projectId: testProject.id,
|
||||
revision: {
|
||||
functionId: cryptoRandomString({ length: 9 }),
|
||||
functionReleaseId: cryptoRandomString({ length: 9 })
|
||||
}
|
||||
})
|
||||
|
||||
if (!revision) {
|
||||
throw new Error('Failed to create automation revision.')
|
||||
}
|
||||
|
||||
testAutomation = automation.automation
|
||||
|
||||
const { automationRun } = await createTestAutomationRun({
|
||||
userId: adminUser.id,
|
||||
projectId: testProject.id,
|
||||
automationId: testAutomation.id
|
||||
})
|
||||
|
||||
testAutomationRun = automationRun
|
||||
|
||||
testComment = await createTestComment({
|
||||
userId: adminUser.id,
|
||||
projectId: testProject.id,
|
||||
objectId: testVersion.objectId
|
||||
})
|
||||
|
||||
testWebhookId = await createWebhookConfigFactory({ db: sourceRegionDb })({
|
||||
id: cryptoRandomString({ length: 9 }),
|
||||
streamId: testProject.id,
|
||||
url: 'https://example.org',
|
||||
description: cryptoRandomString({ length: 9 }),
|
||||
secret: cryptoRandomString({ length: 9 }),
|
||||
enabled: false,
|
||||
triggers: ['branch_create']
|
||||
})
|
||||
await createWebhookEventFactory({ db: sourceRegionDb })({
|
||||
id: cryptoRandomString({ length: 9 }),
|
||||
webhookId: testWebhookId,
|
||||
payload: cryptoRandomString({ length: 9 })
|
||||
})
|
||||
|
||||
const testBlob = await createTestBlob({
|
||||
userId: adminUser.id,
|
||||
projectId: testProject.id
|
||||
})
|
||||
testBlobId = testBlob.blobId
|
||||
|
||||
await assertProjectRegion(testProject.id, regionKey1)
|
||||
})
|
||||
|
||||
it('moves project record to target regional db', async () => {
|
||||
const resA = await apollo.execute(UpdateProjectRegionDocument, {
|
||||
projectId: testProject.id,
|
||||
regionKey: regionKey2
|
||||
})
|
||||
expect(resA).to.not.haveGraphQLErrors()
|
||||
|
||||
await ensureProjectRegion(testProject.id, regionKey2)
|
||||
|
||||
const resB = await apollo.execute(GetProjectDocument, {
|
||||
id: testProject.id
|
||||
})
|
||||
expect(resB).to.not.haveGraphQLErrors()
|
||||
|
||||
expect(resB.data?.project.name).to.equal(testProject.name)
|
||||
})
|
||||
|
||||
it('moves project models to target regional db', async () => {
|
||||
const resA = await apollo.execute(UpdateProjectRegionDocument, {
|
||||
projectId: testProject.id,
|
||||
regionKey: regionKey2
|
||||
})
|
||||
expect(resA).to.not.haveGraphQLErrors()
|
||||
|
||||
await ensureProjectRegion(testProject.id, regionKey2)
|
||||
|
||||
const resB = await apollo.execute(GetRegionalProjectModelDocument, {
|
||||
projectId: testProject.id,
|
||||
modelId: testModel.id
|
||||
})
|
||||
expect(resB).to.not.haveGraphQLErrors()
|
||||
|
||||
expect(resB.data?.project.model.name).to.equal(testModel.name)
|
||||
})
|
||||
|
||||
it('moves project model versions to target regional db', async () => {
|
||||
const resA = await apollo.execute(UpdateProjectRegionDocument, {
|
||||
projectId: testProject.id,
|
||||
regionKey: regionKey2
|
||||
})
|
||||
expect(resA).to.not.haveGraphQLErrors()
|
||||
|
||||
await ensureProjectRegion(testProject.id, regionKey2)
|
||||
|
||||
const resB = await apollo.execute(GetRegionalProjectVersionDocument, {
|
||||
projectId: testProject.id,
|
||||
modelId: testModel.id,
|
||||
versionId: testVersion.id
|
||||
})
|
||||
expect(resB).to.not.haveGraphQLErrors()
|
||||
|
||||
expect(resB.data?.project.model.version.referencedObject).to.equal(
|
||||
testVersion.objectId
|
||||
)
|
||||
})
|
||||
|
||||
it('moves project version objects to target regional db', async () => {
|
||||
const resA = await apollo.execute(UpdateProjectRegionDocument, {
|
||||
projectId: testProject.id,
|
||||
regionKey: regionKey2
|
||||
})
|
||||
expect(resA).to.not.haveGraphQLErrors()
|
||||
|
||||
await ensureProjectRegion(testProject.id, regionKey2)
|
||||
|
||||
const resB = await apollo.execute(GetRegionalProjectObjectDocument, {
|
||||
projectId: testProject.id,
|
||||
objectId: testVersion.objectId
|
||||
})
|
||||
expect(resB).to.not.haveGraphQLErrors()
|
||||
|
||||
expect(resB.data?.project.object).to.not.be.undefined
|
||||
})
|
||||
|
||||
it('moves project automations to target regional db', async () => {
|
||||
const resA = await apollo.execute(UpdateProjectRegionDocument, {
|
||||
projectId: testProject.id,
|
||||
regionKey: regionKey2
|
||||
})
|
||||
expect(resA).to.not.haveGraphQLErrors()
|
||||
|
||||
await ensureProjectRegion(testProject.id, regionKey2)
|
||||
|
||||
const resB = await apollo.execute(GetRegionalProjectAutomationDocument, {
|
||||
projectId: testProject.id,
|
||||
automationId: testAutomation.id
|
||||
})
|
||||
expect(resB).to.not.haveGraphQLErrors()
|
||||
|
||||
expect(resB.data?.project.automation.id).to.equal(testAutomation.id)
|
||||
expect(resB.data?.project.automation.runs.items.at(0)?.id).to.equal(
|
||||
testAutomationRun.id
|
||||
)
|
||||
expect(
|
||||
resB.data?.project.automation.runs.items.at(0)?.functionRuns.length
|
||||
).to.not.equal(0)
|
||||
})
|
||||
|
||||
it('moves project comments to target regional db', async () => {
|
||||
const resA = await apollo.execute(UpdateProjectRegionDocument, {
|
||||
projectId: testProject.id,
|
||||
regionKey: regionKey2
|
||||
})
|
||||
expect(resA).to.not.haveGraphQLErrors()
|
||||
|
||||
await ensureProjectRegion(testProject.id, regionKey2)
|
||||
|
||||
const resB = await apollo.execute(GetRegionalProjectCommentDocument, {
|
||||
projectId: testProject.id,
|
||||
commentId: testComment.id
|
||||
})
|
||||
expect(resB).to.not.haveGraphQLErrors()
|
||||
|
||||
expect(resB.data?.project.comment).to.not.be.undefined
|
||||
})
|
||||
|
||||
it('moves project webhooks to target regional db', async () => {
|
||||
const resA = await apollo.execute(UpdateProjectRegionDocument, {
|
||||
projectId: testProject.id,
|
||||
regionKey: regionKey2
|
||||
})
|
||||
expect(resA).to.not.haveGraphQLErrors()
|
||||
|
||||
await ensureProjectRegion(testProject.id, regionKey2)
|
||||
|
||||
const resB = await apollo.execute(GetRegionalProjectWebhookDocument, {
|
||||
projectId: testProject.id,
|
||||
webhookId: testWebhookId
|
||||
})
|
||||
expect(resB).to.not.haveGraphQLErrors()
|
||||
|
||||
expect(resB.data?.project.webhooks.items.length).to.equal(1)
|
||||
})
|
||||
|
||||
it('moves project files and associated blobs to target regional db and object storage', async () => {
|
||||
const resA = await apollo.execute(UpdateProjectRegionDocument, {
|
||||
projectId: testProject.id,
|
||||
regionKey: regionKey2
|
||||
})
|
||||
expect(resA).to.not.haveGraphQLErrors()
|
||||
|
||||
await ensureProjectRegion(testProject.id, regionKey2)
|
||||
|
||||
const resB = await apollo.execute(GetRegionalProjectBlobDocument, {
|
||||
projectId: testProject.id,
|
||||
blobId: testBlobId
|
||||
})
|
||||
expect(resB).to.not.haveGraphQLErrors()
|
||||
|
||||
expect(resB.data?.project.blob).to.not.be.undefined
|
||||
})
|
||||
})
|
||||
: void 0
|
||||
@@ -28,7 +28,7 @@ import { expect } from 'chai'
|
||||
const isEnabled = isMultiRegionEnabled()
|
||||
|
||||
isEnabled
|
||||
? describe('Multi Region Server Settings', () => {
|
||||
? describe('Multi Region Server Settings @multiregion', () => {
|
||||
let testAdminUser: BasicTestUser
|
||||
let testBasicUser: BasicTestUser
|
||||
let apollo: TestApolloServer
|
||||
|
||||
@@ -1,17 +1,8 @@
|
||||
import { db } from '@/db/knex'
|
||||
import { AutomationRecord, AutomationRunRecord } from '@/modules/automate/helpers/types'
|
||||
import { CommentRecord } from '@/modules/comments/helpers/types'
|
||||
import { AllScopes } from '@/modules/core/helpers/mainConstants'
|
||||
import { createRandomEmail } from '@/modules/core/helpers/testHelpers'
|
||||
import { StreamRecord } from '@/modules/core/helpers/types'
|
||||
import { grantStreamPermissionsFactory } from '@/modules/core/repositories/streams'
|
||||
import { WorkspaceSeatType } from '@/modules/gatekeeper/domain/billing'
|
||||
import { getWorkspaceUserSeatsFactory } from '@/modules/gatekeeper/repositories/workspaceSeat'
|
||||
import { getDb } from '@/modules/multiregion/utils/dbSelector'
|
||||
import {
|
||||
createWebhookConfigFactory,
|
||||
createWebhookEventFactory
|
||||
} from '@/modules/webhooks/repositories/webhooks'
|
||||
import { WorkspaceInvalidRoleError } from '@/modules/workspaces/errors/workspace'
|
||||
import {
|
||||
assignToWorkspace,
|
||||
@@ -28,19 +19,10 @@ import {
|
||||
import {
|
||||
ActiveUserProjectsWorkspaceDocument,
|
||||
CreateWorkspaceProjectDocument,
|
||||
GetProjectDocument,
|
||||
GetRegionalProjectAutomationDocument,
|
||||
GetRegionalProjectBlobDocument,
|
||||
GetRegionalProjectCommentDocument,
|
||||
GetRegionalProjectModelDocument,
|
||||
GetRegionalProjectObjectDocument,
|
||||
GetRegionalProjectVersionDocument,
|
||||
GetRegionalProjectWebhookDocument,
|
||||
GetWorkspaceProjectsDocument,
|
||||
GetWorkspaceTeamDocument,
|
||||
MoveProjectToWorkspaceDocument,
|
||||
ProjectUpdateRoleInput,
|
||||
UpdateProjectRegionDocument,
|
||||
UpdateProjectRoleDocument,
|
||||
UpdateWorkspaceProjectRoleDocument
|
||||
} from '@/test/graphql/generated/graphql'
|
||||
@@ -50,57 +32,15 @@ import {
|
||||
TestApolloServer
|
||||
} from '@/test/graphqlHelper'
|
||||
import { beforeEachContext } from '@/test/hooks'
|
||||
import {
|
||||
createTestAutomation,
|
||||
createTestAutomationRun
|
||||
} from '@/test/speckle-helpers/automationHelper'
|
||||
import { createTestBlob } from '@/test/speckle-helpers/blobHelper'
|
||||
import { BasicTestBranch, createTestBranch } from '@/test/speckle-helpers/branchHelper'
|
||||
import { createTestComment } from '@/test/speckle-helpers/commentHelper'
|
||||
import {
|
||||
BasicTestCommit,
|
||||
createTestCommit,
|
||||
createTestObject
|
||||
} from '@/test/speckle-helpers/commitHelper'
|
||||
import {
|
||||
getMainTestRegionKey,
|
||||
isMultiRegionTestMode,
|
||||
waitForRegionUser,
|
||||
waitForRegionUsers
|
||||
} from '@/test/speckle-helpers/regions'
|
||||
import {
|
||||
addToStream,
|
||||
BasicTestStream,
|
||||
createTestStream,
|
||||
getUserStreamRole
|
||||
} from '@/test/speckle-helpers/streamHelper'
|
||||
import { Roles, retry } from '@speckle/shared'
|
||||
import { Roles } from '@speckle/shared'
|
||||
import { expect } from 'chai'
|
||||
import cryptoRandomString from 'crypto-random-string'
|
||||
import { Knex } from 'knex'
|
||||
import { SetOptional } from 'type-fest'
|
||||
|
||||
const tables = {
|
||||
projects: (db: Knex) => db.table<StreamRecord>('streams')
|
||||
}
|
||||
|
||||
const assertProjectRegion = async (
|
||||
projectId: string,
|
||||
regionKey: string
|
||||
): Promise<void> => {
|
||||
const project = await tables.projects(db).select('*').where('id', projectId).first()
|
||||
|
||||
if (!project || project.regionKey !== regionKey) {
|
||||
expect.fail('Project is not in expected region.')
|
||||
}
|
||||
}
|
||||
|
||||
const ensureProjectRegion = async (
|
||||
projectId: string,
|
||||
regionKey: string
|
||||
): Promise<void> => {
|
||||
await retry(async () => assertProjectRegion(projectId, regionKey), 20, 10)
|
||||
}
|
||||
|
||||
const grantStreamPermissions = grantStreamPermissionsFactory({ db })
|
||||
|
||||
@@ -182,12 +122,6 @@ describe('Workspace project GQL CRUD', () => {
|
||||
createTestUser(workspaceEditor),
|
||||
createTestUser(workspaceMemberViewer)
|
||||
])
|
||||
await waitForRegionUsers([
|
||||
serverAdminUser,
|
||||
workspaceGuest,
|
||||
workspaceEditor,
|
||||
workspaceMemberViewer
|
||||
])
|
||||
})
|
||||
|
||||
describeEach(
|
||||
@@ -213,8 +147,7 @@ describe('Workspace project GQL CRUD', () => {
|
||||
await createTestWorkspace(roleWorkspace, serverAdminUser, {
|
||||
addPlan: oldPlan
|
||||
? { name: 'business', status: 'valid' }
|
||||
: { name: 'pro', status: 'valid' },
|
||||
regionKey: isMultiRegionTestMode() ? getMainTestRegionKey() : undefined
|
||||
: { name: 'pro', status: 'valid' }
|
||||
})
|
||||
roleProject.workspaceId = roleWorkspace.id
|
||||
|
||||
@@ -512,296 +445,3 @@ describe('Workspace project GQL CRUD', () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// TODO: These are very flaky for some reason
|
||||
isMultiRegionTestMode()
|
||||
? describe.skip('Workspace project region changes', () => {
|
||||
const regionKey1 = 'region1'
|
||||
const regionKey2 = 'region2'
|
||||
|
||||
const adminUser: BasicTestUser = {
|
||||
id: '',
|
||||
name: 'John Speckle',
|
||||
email: createRandomEmail(),
|
||||
role: Roles.Server.Admin
|
||||
}
|
||||
|
||||
const testWorkspace: SetOptional<BasicTestWorkspace, 'slug'> = {
|
||||
id: '',
|
||||
ownerId: '',
|
||||
name: 'Unlimited Workspace'
|
||||
}
|
||||
|
||||
const testProject: BasicTestStream = {
|
||||
id: '',
|
||||
ownerId: '',
|
||||
name: 'Regional Project',
|
||||
isPublic: true
|
||||
}
|
||||
|
||||
const testModel: BasicTestBranch = {
|
||||
id: '',
|
||||
name: cryptoRandomString({ length: 8 }),
|
||||
streamId: '',
|
||||
authorId: ''
|
||||
}
|
||||
|
||||
const testVersion: BasicTestCommit = {
|
||||
id: '',
|
||||
objectId: '',
|
||||
streamId: '',
|
||||
authorId: ''
|
||||
}
|
||||
|
||||
let testAutomation: AutomationRecord
|
||||
let testAutomationRun: AutomationRunRecord
|
||||
|
||||
let testComment: CommentRecord
|
||||
let testWebhookId: string
|
||||
let testBlobId: string
|
||||
|
||||
let apollo: TestApolloServer
|
||||
let sourceRegionDb: Knex
|
||||
|
||||
before(async () => {
|
||||
await createTestUser(adminUser)
|
||||
await waitForRegionUser(adminUser)
|
||||
|
||||
apollo = await testApolloServer({ authUserId: adminUser.id })
|
||||
sourceRegionDb = await getDb({ regionKey: regionKey1 })
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
delete testWorkspace.slug
|
||||
|
||||
await createTestWorkspace(testWorkspace, adminUser, {
|
||||
regionKey: regionKey1,
|
||||
addPlan: {
|
||||
name: 'unlimited',
|
||||
status: 'valid'
|
||||
}
|
||||
})
|
||||
|
||||
testProject.workspaceId = testWorkspace.id
|
||||
|
||||
await createTestStream(testProject, adminUser)
|
||||
await createTestBranch({
|
||||
stream: testProject,
|
||||
branch: testModel,
|
||||
owner: adminUser
|
||||
})
|
||||
|
||||
testVersion.branchName = testModel.name
|
||||
testVersion.objectId = await createTestObject({ projectId: testProject.id })
|
||||
|
||||
await createTestCommit(testVersion, {
|
||||
owner: adminUser,
|
||||
stream: testProject
|
||||
})
|
||||
|
||||
const { automation, revision } = await createTestAutomation({
|
||||
userId: adminUser.id,
|
||||
projectId: testProject.id,
|
||||
revision: {
|
||||
functionId: cryptoRandomString({ length: 9 }),
|
||||
functionReleaseId: cryptoRandomString({ length: 9 })
|
||||
}
|
||||
})
|
||||
|
||||
if (!revision) {
|
||||
throw new Error('Failed to create automation revision.')
|
||||
}
|
||||
|
||||
testAutomation = automation.automation
|
||||
|
||||
const { automationRun } = await createTestAutomationRun({
|
||||
userId: adminUser.id,
|
||||
projectId: testProject.id,
|
||||
automationId: testAutomation.id
|
||||
})
|
||||
|
||||
testAutomationRun = automationRun
|
||||
|
||||
testComment = await createTestComment({
|
||||
userId: adminUser.id,
|
||||
projectId: testProject.id,
|
||||
objectId: testVersion.objectId
|
||||
})
|
||||
|
||||
testWebhookId = await createWebhookConfigFactory({ db: sourceRegionDb })({
|
||||
id: cryptoRandomString({ length: 9 }),
|
||||
streamId: testProject.id,
|
||||
url: 'https://example.org',
|
||||
description: cryptoRandomString({ length: 9 }),
|
||||
secret: cryptoRandomString({ length: 9 }),
|
||||
enabled: false,
|
||||
triggers: ['branch_create']
|
||||
})
|
||||
await createWebhookEventFactory({ db: sourceRegionDb })({
|
||||
id: cryptoRandomString({ length: 9 }),
|
||||
webhookId: testWebhookId,
|
||||
payload: cryptoRandomString({ length: 9 })
|
||||
})
|
||||
|
||||
const testBlob = await createTestBlob({
|
||||
userId: adminUser.id,
|
||||
projectId: testProject.id
|
||||
})
|
||||
testBlobId = testBlob.blobId
|
||||
|
||||
await assertProjectRegion(testProject.id, regionKey1)
|
||||
})
|
||||
|
||||
it('moves project record to target regional db', async () => {
|
||||
const resA = await apollo.execute(UpdateProjectRegionDocument, {
|
||||
projectId: testProject.id,
|
||||
regionKey: regionKey2
|
||||
})
|
||||
expect(resA).to.not.haveGraphQLErrors()
|
||||
|
||||
await ensureProjectRegion(testProject.id, regionKey2)
|
||||
|
||||
const resB = await apollo.execute(GetProjectDocument, {
|
||||
id: testProject.id
|
||||
})
|
||||
expect(resB).to.not.haveGraphQLErrors()
|
||||
|
||||
expect(resB.data?.project.name).to.equal(testProject.name)
|
||||
})
|
||||
|
||||
it('moves project models to target regional db', async () => {
|
||||
const resA = await apollo.execute(UpdateProjectRegionDocument, {
|
||||
projectId: testProject.id,
|
||||
regionKey: regionKey2
|
||||
})
|
||||
expect(resA).to.not.haveGraphQLErrors()
|
||||
|
||||
await ensureProjectRegion(testProject.id, regionKey2)
|
||||
|
||||
const resB = await apollo.execute(GetRegionalProjectModelDocument, {
|
||||
projectId: testProject.id,
|
||||
modelId: testModel.id
|
||||
})
|
||||
expect(resB).to.not.haveGraphQLErrors()
|
||||
|
||||
expect(resB.data?.project.model.name).to.equal(testModel.name)
|
||||
})
|
||||
|
||||
it('moves project model versions to target regional db', async () => {
|
||||
const resA = await apollo.execute(UpdateProjectRegionDocument, {
|
||||
projectId: testProject.id,
|
||||
regionKey: regionKey2
|
||||
})
|
||||
expect(resA).to.not.haveGraphQLErrors()
|
||||
|
||||
await ensureProjectRegion(testProject.id, regionKey2)
|
||||
|
||||
const resB = await apollo.execute(GetRegionalProjectVersionDocument, {
|
||||
projectId: testProject.id,
|
||||
modelId: testModel.id,
|
||||
versionId: testVersion.id
|
||||
})
|
||||
expect(resB).to.not.haveGraphQLErrors()
|
||||
|
||||
expect(resB.data?.project.model.version.referencedObject).to.equal(
|
||||
testVersion.objectId
|
||||
)
|
||||
})
|
||||
|
||||
it('moves project version objects to target regional db', async () => {
|
||||
const resA = await apollo.execute(UpdateProjectRegionDocument, {
|
||||
projectId: testProject.id,
|
||||
regionKey: regionKey2
|
||||
})
|
||||
expect(resA).to.not.haveGraphQLErrors()
|
||||
|
||||
await ensureProjectRegion(testProject.id, regionKey2)
|
||||
|
||||
const resB = await apollo.execute(GetRegionalProjectObjectDocument, {
|
||||
projectId: testProject.id,
|
||||
objectId: testVersion.objectId
|
||||
})
|
||||
expect(resB).to.not.haveGraphQLErrors()
|
||||
|
||||
expect(resB.data?.project.object).to.not.be.undefined
|
||||
})
|
||||
|
||||
it('moves project automations to target regional db', async () => {
|
||||
const resA = await apollo.execute(UpdateProjectRegionDocument, {
|
||||
projectId: testProject.id,
|
||||
regionKey: regionKey2
|
||||
})
|
||||
expect(resA).to.not.haveGraphQLErrors()
|
||||
|
||||
await ensureProjectRegion(testProject.id, regionKey2)
|
||||
|
||||
const resB = await apollo.execute(GetRegionalProjectAutomationDocument, {
|
||||
projectId: testProject.id,
|
||||
automationId: testAutomation.id
|
||||
})
|
||||
expect(resB).to.not.haveGraphQLErrors()
|
||||
|
||||
expect(resB.data?.project.automation.id).to.equal(testAutomation.id)
|
||||
expect(resB.data?.project.automation.runs.items.at(0)?.id).to.equal(
|
||||
testAutomationRun.id
|
||||
)
|
||||
expect(
|
||||
resB.data?.project.automation.runs.items.at(0)?.functionRuns.length
|
||||
).to.not.equal(0)
|
||||
})
|
||||
|
||||
it('moves project comments to target regional db', async () => {
|
||||
const resA = await apollo.execute(UpdateProjectRegionDocument, {
|
||||
projectId: testProject.id,
|
||||
regionKey: regionKey2
|
||||
})
|
||||
expect(resA).to.not.haveGraphQLErrors()
|
||||
|
||||
await ensureProjectRegion(testProject.id, regionKey2)
|
||||
|
||||
const resB = await apollo.execute(GetRegionalProjectCommentDocument, {
|
||||
projectId: testProject.id,
|
||||
commentId: testComment.id
|
||||
})
|
||||
expect(resB).to.not.haveGraphQLErrors()
|
||||
|
||||
expect(resB.data?.project.comment).to.not.be.undefined
|
||||
})
|
||||
|
||||
it('moves project webhooks to target regional db', async () => {
|
||||
const resA = await apollo.execute(UpdateProjectRegionDocument, {
|
||||
projectId: testProject.id,
|
||||
regionKey: regionKey2
|
||||
})
|
||||
expect(resA).to.not.haveGraphQLErrors()
|
||||
|
||||
await ensureProjectRegion(testProject.id, regionKey2)
|
||||
|
||||
const resB = await apollo.execute(GetRegionalProjectWebhookDocument, {
|
||||
projectId: testProject.id,
|
||||
webhookId: testWebhookId
|
||||
})
|
||||
expect(resB).to.not.haveGraphQLErrors()
|
||||
|
||||
expect(resB.data?.project.webhooks.items.length).to.equal(1)
|
||||
})
|
||||
|
||||
it('moves project files and associated blobs to target regional db and object storage', async () => {
|
||||
const resA = await apollo.execute(UpdateProjectRegionDocument, {
|
||||
projectId: testProject.id,
|
||||
regionKey: regionKey2
|
||||
})
|
||||
expect(resA).to.not.haveGraphQLErrors()
|
||||
|
||||
await ensureProjectRegion(testProject.id, regionKey2)
|
||||
|
||||
const resB = await apollo.execute(GetRegionalProjectBlobDocument, {
|
||||
projectId: testProject.id,
|
||||
blobId: testBlobId
|
||||
})
|
||||
expect(resB).to.not.haveGraphQLErrors()
|
||||
|
||||
expect(resB.data?.project.blob).to.not.be.undefined
|
||||
})
|
||||
})
|
||||
: void 0
|
||||
|
||||
@@ -93,7 +93,7 @@ describe('Workspace GQL Subscriptions', () => {
|
||||
]
|
||||
|
||||
modes.forEach(({ isMultiRegion }) => {
|
||||
describe(`W/${!isMultiRegion ? 'o' : ''} multiregion`, () => {
|
||||
describe(`W/${!isMultiRegion ? 'o' : ''} @multiregion`, () => {
|
||||
const myMainWorkspace: BasicTestWorkspace = {
|
||||
id: '',
|
||||
ownerId: '',
|
||||
|
||||
@@ -27,6 +27,20 @@
|
||||
"endpoint": "http://127.0.0.1:9020",
|
||||
"s3Region": "us-east-1"
|
||||
}
|
||||
},
|
||||
"region2": {
|
||||
"postgres": {
|
||||
"connectionUri": "postgresql://speckle:speckle@127.0.0.1:5402/speckle2_test",
|
||||
"privateConnectionUri": "postgresql://speckle:speckle@postgres-region2:5432/speckle2_test"
|
||||
},
|
||||
"blobStorage": {
|
||||
"accessKey": "minioadmin",
|
||||
"secretKey": "minioadmin",
|
||||
"bucket": "test-speckle-server",
|
||||
"createBucketIfNotExists": true,
|
||||
"endpoint": "http://127.0.0.1:9040",
|
||||
"s3Region": "us-east-1"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
"ts-mocha": "node --require ts-node/register ./bin/mocha",
|
||||
"test": "cross-env NODE_ENV=test LOG_LEVEL=silent LOG_PRETTY=true yarn ts-mocha",
|
||||
"test:all-ff": "cross-env ENABLE_ALL_FFS=true yarn test",
|
||||
"test:multiregion": "cross-env RUN_TESTS_IN_MULTIREGION_MODE=true FF_WORKSPACES_MODULE_ENABLED=true FF_WORKSPACES_MULTI_REGION_ENABLED=true yarn test",
|
||||
"test:multiregion": "cross-env RUN_TESTS_IN_MULTIREGION_MODE=true FF_WORKSPACES_MODULE_ENABLED=true FF_WORKSPACES_MULTI_REGION_ENABLED=true yarn test --grep @multiregion",
|
||||
"test:no-ff": "cross-env DISABLE_ALL_FFS=true yarn test",
|
||||
"test:coverage": "cross-env NODE_ENV=test LOG_LEVEL=silent LOG_PRETTY=true nyc --reporter lcov yarn ts-mocha",
|
||||
"test:report": "MOCHA_FILE=reports/test-results.xml yarn test:coverage -- --reporter mocha-multi --reporter-options spec=-,mocha-junit-reporter=reports/test-results.xml",
|
||||
|
||||
@@ -196,15 +196,17 @@ export const resetPubSubFactory = (deps: { db: Knex }) => async () => {
|
||||
rows: Array<{ pubname: string }>
|
||||
}
|
||||
|
||||
// If we do not wait, the following call occasionally fails because a replication slot is still in use.
|
||||
const dropSubs = async (info: SubInfo) => {
|
||||
await wait(1000)
|
||||
await deps.db.raw(
|
||||
`SELECT * FROM aiven_extras.pg_alter_subscription_disable('${info.subname}');`
|
||||
)
|
||||
// If we do not wait, the following call occasionally fails because a replication slot is still in use.
|
||||
await wait(1000)
|
||||
await deps.db.raw(
|
||||
`SELECT * FROM aiven_extras.pg_drop_subscription('${info.subname}');`
|
||||
)
|
||||
await wait(1000)
|
||||
await deps.db.raw(
|
||||
`SELECT * FROM aiven_extras.dblink_slot_create_or_drop('${info.subconninfo}', '${info.subslotname}', 'drop');`
|
||||
)
|
||||
|
||||
@@ -64,11 +64,6 @@ export const parseFeatureFlags = (
|
||||
schema: z.boolean(),
|
||||
defaults: { production: false, _: false }
|
||||
},
|
||||
// Toggles IFC parsing with experimental .Net parser
|
||||
FF_FILEIMPORT_IFC_DOTNET_ENABLED: {
|
||||
schema: z.boolean(),
|
||||
defaults: { production: false, _: false }
|
||||
},
|
||||
// Forces onboarding for all users
|
||||
FF_FORCE_ONBOARDING: {
|
||||
schema: z.boolean(),
|
||||
@@ -114,7 +109,6 @@ export type FeatureFlags = {
|
||||
FF_GATEKEEPER_FORCE_FREE_PLAN: boolean
|
||||
FF_BILLING_INTEGRATION_ENABLED: boolean
|
||||
FF_WORKSPACES_MULTI_REGION_ENABLED: boolean
|
||||
FF_FILEIMPORT_IFC_DOTNET_ENABLED: boolean
|
||||
FF_FORCE_ONBOARDING: boolean
|
||||
FF_OBJECTS_STREAMING_FIX: boolean
|
||||
FF_MOVE_PROJECT_REGION_ENABLED: boolean
|
||||
|
||||
@@ -15,6 +15,23 @@
|
||||
class="relative overflow-y-scroll h-full pointer-events-none w-auto"
|
||||
></div>
|
||||
</div>
|
||||
<div class="center-wrapper" id="loadingWrapper">
|
||||
<div class="loading-container">
|
||||
<!-- Grayscale Image -->
|
||||
<img
|
||||
class="grayscale-overlay"
|
||||
src="https://avatars.githubusercontent.com/u/65039012?s=280&v=4"
|
||||
alt="Grayscale Image"
|
||||
/>
|
||||
<!-- Colored Image -->
|
||||
<img
|
||||
class="color-image"
|
||||
id="colorImage"
|
||||
src="https://avatars.githubusercontent.com/u/65039012?s=280&v=4"
|
||||
alt="Color Image"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
<!-- <div class="w-screen h-screen flex flex-col" id="multi-root">
|
||||
<div class="h-1/2">
|
||||
|
||||
@@ -1270,6 +1270,7 @@ export default class Sandbox {
|
||||
}
|
||||
|
||||
public async loadUrl(url: string) {
|
||||
const colorImage = document.getElementById('colorImage')
|
||||
const authToken = localStorage.getItem(
|
||||
url.includes('latest') ? 'AuthTokenLatest' : 'AuthToken'
|
||||
) as string
|
||||
@@ -1284,9 +1285,10 @@ export default class Sandbox {
|
||||
undefined
|
||||
)
|
||||
/** Too spammy */
|
||||
// loader.on(LoaderEvent.LoadProgress, (arg: { progress: number; id: string }) => {
|
||||
// console.warn(arg)
|
||||
// })
|
||||
loader.on(LoaderEvent.LoadProgress, (arg: { progress: number; id: string }) => {
|
||||
if (colorImage)
|
||||
colorImage.style.clipPath = `inset(${(1 - arg.progress) * 100}% 0 0 0)`
|
||||
})
|
||||
loader.on(LoaderEvent.LoadCancelled, (resource: string) => {
|
||||
console.warn(`Resource ${resource} loading was canceled`)
|
||||
})
|
||||
|
||||
@@ -74,6 +74,14 @@ const createViewer = async (containerName: string, _stream: string) => {
|
||||
Object.assign(sandbox.sceneParams.worldSize, viewer.World.worldSize)
|
||||
Object.assign(sandbox.sceneParams.worldOrigin, viewer.World.worldOrigin)
|
||||
sandbox.refresh()
|
||||
const loadingWrapper = document.getElementById('loadingWrapper')
|
||||
if (loadingWrapper) {
|
||||
loadingWrapper.addEventListener('transitionend', function () {
|
||||
// Remove the loading wrapper from the page
|
||||
loadingWrapper.style.display = 'none'
|
||||
})
|
||||
loadingWrapper.style.opacity = '0'
|
||||
}
|
||||
})
|
||||
|
||||
viewer.on(ViewerEvent.UnloadComplete, () => {
|
||||
@@ -94,22 +102,16 @@ const createViewer = async (containerName: string, _stream: string) => {
|
||||
sandbox.makeDiffUI()
|
||||
sandbox.makeMeasurementsUI()
|
||||
|
||||
await sandbox.objectLoaderOnly(_stream)
|
||||
// await sandbox.loadUrl(_stream)
|
||||
// await sandbox.objectLoaderOnly(_stream)
|
||||
await sandbox.loadUrl(_stream)
|
||||
// await sandbox.loadJSON(JSONSpeckleStream)
|
||||
}
|
||||
|
||||
const getStream = () => {
|
||||
return (
|
||||
// prettier-ignore
|
||||
`https://latest.speckle.systems/projects/97750296c2/models/767b70fc63@5386a0af02`
|
||||
//crashing out of memory?
|
||||
//`https://latest.speckle.systems/projects/97750296c2/models/767b70fc63@2a6fd781f2`
|
||||
//too big?
|
||||
// `https://latest.speckle.systems/projects/126cd4b7bb/models/032d09f716`
|
||||
// 'https://app.speckle.systems/streams/da9e320dad/commits/5388ef24b8?c=%5B-7.66134,10.82932,6.41935,-0.07739,-13.88552,1.8697,0,1%5D'
|
||||
// Revit sample house (good for bim-like stuff with many display meshes)
|
||||
//'https://app.speckle.systems/streams/da9e320dad/commits/5388ef24b8'
|
||||
'https://app.speckle.systems/streams/da9e320dad/commits/5388ef24b8'
|
||||
// 'https://latest.speckle.systems/streams/c1faab5c62/commits/ab1a1ab2b6'
|
||||
// 'https://app.speckle.systems/streams/da9e320dad/commits/5388ef24b8'
|
||||
// 'https://latest.speckle.systems/streams/58b5648c4d/commits/60371ecb2d'
|
||||
@@ -502,6 +504,14 @@ const getStream = () => {
|
||||
|
||||
// SUPER slow tree build time (LARGE N-GONS TRIANGULATION)
|
||||
// 'https://app.speckle.systems/projects/0edb6ef628/models/ff3d8480bc@cd83d90a2c'
|
||||
|
||||
/* ObjectLoader 2 tests */
|
||||
// `https://latest.speckle.systems/projects/97750296c2/models/767b70fc63@5386a0af02`
|
||||
//crashing out of memory?
|
||||
//`https://latest.speckle.systems/projects/97750296c2/models/767b70fc63@2a6fd781f2`
|
||||
//too big?
|
||||
// `https://latest.speckle.systems/projects/126cd4b7bb/models/032d09f716`
|
||||
// 'https://app.speckle.systems/streams/da9e320dad/commits/5388ef24b8?c=%5B-7.66134,10.82932,6.41935,-0.07739,-13.88552,1.8697,0,1%5D'
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -60,3 +60,41 @@ canvas {
|
||||
border-radius: 0.1rem;
|
||||
border: 4px solid rgb(129, 129, 129);
|
||||
}
|
||||
|
||||
.center-wrapper {
|
||||
position: absolute;
|
||||
top: 95%;
|
||||
left: 92%;
|
||||
transform: translate(-50%, -50%) scale(0.5);
|
||||
transition: opacity 0.5s ease; /* Smooth fade-out for the whole widget */
|
||||
}
|
||||
|
||||
.loading-container {
|
||||
position: relative;
|
||||
width: 280px;
|
||||
height: 280px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Grayscale version */
|
||||
.grayscale-overlay,
|
||||
.color-image {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
transition: opacity 0.8s ease-in-out; /* Smooth fade-out */
|
||||
}
|
||||
|
||||
.grayscale-overlay {
|
||||
filter: grayscale(100%);
|
||||
z-index: 1;
|
||||
opacity: 1; /* Fully visible by default */
|
||||
}
|
||||
|
||||
/* Color version */
|
||||
.color-image {
|
||||
z-index: 2;
|
||||
clip-path: inset(100% 0 0 0);
|
||||
}
|
||||
|
||||
@@ -23,7 +23,8 @@ import {
|
||||
type SpeckleView,
|
||||
type SunLightConfiguration,
|
||||
type ViewerParams,
|
||||
StencilOutlineType
|
||||
StencilOutlineType,
|
||||
UpdateFlags
|
||||
} from '../IViewer.js'
|
||||
import { Viewer } from './Viewer.js'
|
||||
import { SectionTool } from './extensions/SectionTool.js'
|
||||
@@ -207,6 +208,7 @@ export class LegacyViewer extends Viewer {
|
||||
this.selection.clearSelection()
|
||||
if (this.filtering.filteringState.selectedObjects)
|
||||
this.filtering.filteringState.selectedObjects.length = 0
|
||||
this.requestRender(UpdateFlags.RENDER | UpdateFlags.SHADOWS)
|
||||
return Promise.resolve(this.filtering.filteringState)
|
||||
}
|
||||
|
||||
|
||||
@@ -101,15 +101,16 @@ export class Geometry {
|
||||
|
||||
for (let i = 0; i < indexAttributes.length; ++i) {
|
||||
const index = indexAttributes[i]
|
||||
if (!index || !positionAttributes) {
|
||||
const positions = positionAttributes[i]
|
||||
if (!index || !positions) {
|
||||
throw new Error('Cannot merge geometries. Indices or positions are undefined')
|
||||
}
|
||||
|
||||
for (let j = 0; j < index.length; ++j) {
|
||||
mergedIndex.push(index[j] + indexOffset)
|
||||
mergedIndex.push(index[j] + indexOffset / 3)
|
||||
}
|
||||
|
||||
indexOffset += positionAttributes.length
|
||||
indexOffset += positions.length
|
||||
}
|
||||
return mergedIndex
|
||||
}
|
||||
|
||||
@@ -115,10 +115,6 @@ spec:
|
||||
- name: MULTI_REGION_CONFIG_PATH
|
||||
value: "/multi-region-config/multi-region-config.json"
|
||||
{{- end }}
|
||||
{{- if .Values.featureFlags.fileImportIFCDotNetEnabled }}
|
||||
- name: FF_FILEIMPORT_IFC_DOTNET_ENABLED
|
||||
value: {{ .Values.featureFlags.fileImportIFCDotNetEnabled | quote }}
|
||||
{{- end }}
|
||||
{{- if .Values.fileimport_service.affinity }}
|
||||
affinity: {{- include "speckle.renderTpl" (dict "value" .Values.fileimport_service.affinity "context" $) | nindent 8 }}
|
||||
{{- end }}
|
||||
|
||||
@@ -75,11 +75,6 @@
|
||||
"description": "Toggles whether multi-region is available within workspaces. workspacesModuleEnabled must also be enabled.",
|
||||
"default": false
|
||||
},
|
||||
"fileImportIFCDotNetEnabled": {
|
||||
"type": "boolean",
|
||||
"description": "Toggles whether the experimental .Net IFC importer is used for importing IFC files.",
|
||||
"default": false
|
||||
},
|
||||
"forceEmailVerification": {
|
||||
"type": "boolean",
|
||||
"description": "Forces email verification for all users",
|
||||
|
||||
@@ -51,8 +51,6 @@ featureFlags:
|
||||
billingIntegrationEnabled: false
|
||||
## @param featureFlags.workspacesMultiRegionEnabled Toggles whether multi-region is available within workspaces. workspacesModuleEnabled must also be enabled.
|
||||
workspacesMultiRegionEnabled: false
|
||||
## @param featureFlags.fileImportIFCDotNetEnabled Toggles whether the experimental .Net IFC importer is used for importing IFC files.
|
||||
fileImportIFCDotNetEnabled: false
|
||||
## @param featureFlags.forceEmailVerification Forces email verification for all users
|
||||
forceEmailVerification: false
|
||||
## @param featureFlags.forceOnboarding Forces onboarding for all users
|
||||
|
||||
Reference in New Issue
Block a user