diff --git a/common/config/rush/command-line.json b/common/config/rush/command-line.json index 5375dee1f8..5c30d5bd60 100644 --- a/common/config/rush/command-line.json +++ b/common/config/rush/command-line.json @@ -244,6 +244,14 @@ "safeForSimultaneousRushProcesses": true, "shellCommand": "find .|grep tsconfig.tsbuildinfo | xargs rm | pwd" }, + { + "commandKind": "global", + "name": "model-version", + "summary": "show model version", + "description": "show model version", + "safeForSimultaneousRushProcesses": true, + "shellCommand": "cd ./models/all/ && npx ts-node ./src/__showversion.ts" + }, ], /** diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index cd021f95c3..f0bc85e020 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -85,6 +85,7 @@ specifiers: '@rush-temp/server-contact': file:./projects/server-contact.tgz '@rush-temp/server-contact-resources': file:./projects/server-contact-resources.tgz '@rush-temp/server-core': file:./projects/server-core.tgz + '@rush-temp/server-tool': file:./projects/server-tool.tgz '@rush-temp/server-ws': file:./projects/server-ws.tgz '@rush-temp/setting': file:./projects/setting.tgz '@rush-temp/setting-assets': file:./projects/setting-assets.tgz @@ -275,6 +276,7 @@ dependencies: '@rush-temp/server-contact': file:projects/server-contact.tgz '@rush-temp/server-contact-resources': file:projects/server-contact-resources.tgz '@rush-temp/server-core': file:projects/server-core.tgz + '@rush-temp/server-tool': file:projects/server-tool.tgz '@rush-temp/server-ws': file:projects/server-ws.tgz '@rush-temp/setting': file:projects/setting.tgz '@rush-temp/setting-assets': file:projects/setting-assets.tgz @@ -10679,7 +10681,7 @@ packages: dev: false file:projects/account.tgz: - resolution: {integrity: sha512-/BBGpjzh87DsWYJmHFtYyTBH2FerxHMoGT3xQ4A5IHVtKmx2gTP5mdaY09h+vaNp+a4iTs5rqa7h+Y7l/5uX2w==, tarball: file:projects/account.tgz} + resolution: {integrity: sha512-VNN6Dc2PvOzjbchYoCP7QnVKnUXcy3ByayOx6l8aSdjxxTKBw8XQKrTFgMfBeozvFKzKqf9v7Zz2k9sxzrHw7g==, tarball: file:projects/account.tgz} name: '@rush-temp/account' version: 0.0.0 dependencies: @@ -11646,7 +11648,7 @@ packages: dev: false file:projects/model-all.tgz_typescript@4.5.4: - resolution: {integrity: sha512-5WhGhRWED9z51QVhlJWZJ321v4zkTY33gwVOdMVHidlhnmKJ2LPWfIN8Gs1K2I0Z2Gea3GfcgDbSrTYSGQd7bw==, tarball: file:projects/model-all.tgz} + resolution: {integrity: sha512-KRaYm65Scg01RukljQmwVKLg143zgzLtjWBJQizBtdAU0X8XoHFnW6cOK1Pa3O15Et812fs3HhWWTUj32GJnoQ==, tarball: file:projects/model-all.tgz} id: file:projects/model-all.tgz name: '@rush-temp/model-all' version: 0.0.0 @@ -12543,6 +12545,34 @@ packages: - supports-color dev: false + file:projects/server-tool.tgz: + resolution: {integrity: sha512-jA31a+Q2vADtUml9IaD4IKCxzzqWKz74NIRkBVhY3VqQpiXup+g+Pa2Rf2f3J57yT8aQKCpf63YMysWR42iQjQ==, tarball: file:projects/server-tool.tgz} + name: '@rush-temp/server-tool' + version: 0.0.0 + dependencies: + '@rushstack/heft': 0.41.8 + '@types/heft-jest': 1.0.2 + '@types/minio': 7.0.11 + '@types/ws': 8.2.2 + '@typescript-eslint/eslint-plugin': 5.7.0_c25e8c1f4f4f7aaed27aa6f9ce042237 + '@typescript-eslint/parser': 5.7.0_eslint@7.32.0+typescript@4.5.4 + eslint: 7.32.0 + eslint-config-standard-with-typescript: 21.0.1_ce2fa0c4dfa1c256100cababd749a13a + eslint-plugin-import: 2.25.3_eslint@7.32.0 + eslint-plugin-node: 11.1.0_eslint@7.32.0 + eslint-plugin-promise: 5.2.0_eslint@7.32.0 + jwt-simple: 0.5.6 + minio: 7.0.26 + mongodb: 4.2.2 + prettier: 2.5.1 + typescript: 4.5.4 + ws: 8.3.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + dev: false + file:projects/server-ws.tgz: resolution: {integrity: sha512-MSFFpLjIMFt0oyH4+8JUkNOkCNtdEtMDoxcyN7+kDdz44wSZjSOmheJHYkXO6JTEffcaaRhQ9vO/e7MBNMeoxQ==, tarball: file:projects/server-ws.tgz} name: '@rush-temp/server-ws' diff --git a/dev/prod/package.json b/dev/prod/package.json index 4cc10743bd..5a3bfbc4db 100644 --- a/dev/prod/package.json +++ b/dev/prod/package.json @@ -96,6 +96,7 @@ "@anticrm/server-contact-resources": "~0.6.0", "@anticrm/templates": "~0.6.0", "@anticrm/templates-assets": "~0.6.0", - "@anticrm/templates-resources": "~0.6.0" + "@anticrm/templates-resources": "~0.6.0", + "@anticrm/core": "~0.6.16" } } diff --git a/dev/prod/public/config.json b/dev/prod/public/config.json index 72b566762d..fe8def8763 100644 --- a/dev/prod/public/config.json +++ b/dev/prod/public/config.json @@ -1,4 +1,5 @@ { "ACCOUNTS_URL":"/account", - "UPLOAD_URL":"/files" + "UPLOAD_URL":"/files", + "MODEL_VERSION": null } \ No newline at end of file diff --git a/dev/prod/src/platform.ts b/dev/prod/src/platform.ts index db7653d796..0d19c6f107 100644 --- a/dev/prod/src/platform.ts +++ b/dev/prod/src/platform.ts @@ -14,9 +14,10 @@ // import { addLocation } from '@anticrm/platform' +import {Data, Version} from '@anticrm/core' import login, { loginId } from '@anticrm/login' -import { workbenchId } from '@anticrm/workbench' +import workbench, { workbenchId } from '@anticrm/workbench' import { viewId } from '@anticrm/view' import { taskId } from '@anticrm/task' import { contactId } from '@anticrm/contact' @@ -51,13 +52,15 @@ import '@anticrm/templates-assets' import { setMetadata } from '@anticrm/platform' export async function configurePlatform() { - await fetch('/config.json').then(async (config) => { - await config.json().then(value => { - console.log('loading configuration', value) - setMetadata(login.metadata.AccountsUrl, value.ACCOUNTS_URL) - setMetadata(login.metadata.UploadUrl, value.UPLOAD_URL) - }) - }) + const config = await (await fetch('/config.json')).json() + console.log('loading configuration', config) + setMetadata(login.metadata.AccountsUrl, config.ACCOUNTS_URL) + setMetadata(login.metadata.UploadUrl, config.UPLOAD_URL) + + if( config.MODEL_VERSION != null) { + console.log('Minimal Model version requirement', config.MODEL_VERSION) + setMetadata(workbench.metadata.RequiredVersion, config.MODEL_VERSION) + } setMetadata(login.metadata.TelegramUrl, process.env.TELEGRAM_URL ?? 'http://localhost:8086') setMetadata(login.metadata.GmailUrl, process.env.GMAIL_URL ?? 'http://localhost:8087') setMetadata(login.metadata.OverrideEndpoint, process.env.LOGIN_ENDPOINT) diff --git a/dev/tool/package.json b/dev/tool/package.json index 1d0b0f1b3a..97ca04572e 100644 --- a/dev/tool/package.json +++ b/dev/tool/package.json @@ -71,6 +71,7 @@ "mime-types": "~2.1.34", "@anticrm/attachment": "~0.6.1", "@anticrm/server-contact": "~0.6.1", - "@anticrm/server-contact-resources": "~0.6.0" + "@anticrm/server-contact-resources": "~0.6.0", + "@anticrm/server-tool": "~0.6.0" } } diff --git a/dev/tool/src/index.ts b/dev/tool/src/index.ts index 4bfe8c4a03..80ea0fce1a 100644 --- a/dev/tool/src/index.ts +++ b/dev/tool/src/index.ts @@ -14,7 +14,7 @@ // limitations under the License. // -import accountPlugin, { +import { ACCOUNT_DB, assignWorkspace, createAccount, @@ -22,23 +22,20 @@ import accountPlugin, { dropAccount, dropWorkspace, getAccount, + listAccounts, listWorkspaces, - listAccounts + upgradeWorkspace } from '@anticrm/account' import { setMetadata } from '@anticrm/platform' +import toolPlugin, { prepareTools, version } from '@anticrm/server-tool' import { program } from 'commander' -import { Client } from 'minio' import { Db, MongoClient } from 'mongodb' import { rebuildElastic } from './elastic' import { importXml } from './importer' import { clearTelegramHistory } from './telegram' -import { diffWorkspace, dumpWorkspace, restoreWorkspace, upgradeWorkspace } from './workspace' +import { diffWorkspace, dumpWorkspace, restoreWorkspace } from './workspace' -const mongodbUri = process.env.MONGO_URL -if (mongodbUri === undefined) { - console.error('please provide mongodb url.') - process.exit(1) -} +const { mongodbUri, minio } = prepareTools() const transactorUrl = process.env.TRANSACTOR_URL if (transactorUrl === undefined) { @@ -46,40 +43,14 @@ if (transactorUrl === undefined) { process.exit(1) } -const minioEndpoint = process.env.MINIO_ENDPOINT -if (minioEndpoint === undefined) { - console.error('please provide minio endpoint') - process.exit(1) -} - -const minioAccessKey = process.env.MINIO_ACCESS_KEY -if (minioAccessKey === undefined) { - console.error('please provide minio access key') - process.exit(1) -} - -const minioSecretKey = process.env.MINIO_SECRET_KEY -if (minioSecretKey === undefined) { - console.error('please provide minio secret key') - process.exit(1) -} - const elasticUrl = process.env.ELASTIC_URL if (elasticUrl === undefined) { console.error('please provide elastic url') process.exit(1) } -setMetadata(accountPlugin.metadata.Endpoint, transactorUrl) -setMetadata(accountPlugin.metadata.Transactor, transactorUrl) - -const minio = new Client({ - endPoint: minioEndpoint, - port: 9000, - useSSL: false, - accessKey: minioAccessKey, - secretKey: minioSecretKey -}) +setMetadata(toolPlugin.metadata.Endpoint, transactorUrl) +setMetadata(toolPlugin.metadata.Transactor, transactorUrl) async function withDatabase (uri: string, f: (db: Db, client: MongoClient) => Promise): Promise { console.log(`connecting to database '${uri}'...`) @@ -139,7 +110,9 @@ program .command('upgrade-workspace ') .description('upgrade workspace') .action(async (workspace, cmd) => { - await upgradeWorkspace(mongodbUri, workspace, transactorUrl, minio) + return await withDatabase(mongodbUri, async (db) => { + await upgradeWorkspace(db, workspace) + }) }) program @@ -158,6 +131,8 @@ program return await withDatabase(mongodbUri, async (db) => { const workspacesJSON = JSON.stringify(await listWorkspaces(db), null, 2) console.info(workspacesJSON) + + console.log('latest model version:', JSON.stringify(version)) }) }) diff --git a/dev/tool/src/workspace.ts b/dev/tool/src/workspace.ts index 7226a47fdc..b389057634 100644 --- a/dev/tool/src/workspace.ts +++ b/dev/tool/src/workspace.ts @@ -16,70 +16,16 @@ import contact from '@anticrm/contact' import core, { DOMAIN_TX, Tx } from '@anticrm/core' -import builder, { migrateOperations } from '@anticrm/model-all' +import builder, { version } from '@anticrm/model-all' +import { upgradeModel } from '@anticrm/server-tool' import { existsSync } from 'fs' import { mkdir, open, readFile, writeFile } from 'fs/promises' import { Client } from 'minio' import { Document, MongoClient } from 'mongodb' import { join } from 'path' -import { connect } from './connect' -import { MigrateClientImpl } from './upgrade' +import { rebuildElastic } from './elastic' import { generateModelDiff, printDiff } from './mdiff' import { listMinioObjects, MinioWorkspaceItem } from './minio' -import { rebuildElastic } from './elastic' - -const txes = JSON.parse(JSON.stringify(builder.getTxes())) as Tx[] - -/** - * @public - */ -export async function upgradeWorkspace ( - mongoUrl: string, - dbName: string, - transactorUrl: string, - minio: Client -): Promise { - if (txes.some((tx) => tx.objectSpace !== core.space.Model)) { - throw Error('Model txes must target only core.space.Model') - } - - const client = new MongoClient(mongoUrl) - try { - await client.connect() - const db = client.db(dbName) - - console.log('removing model...') - // we're preserving accounts (created by core.account.System). - const result = await db.collection(DOMAIN_TX).deleteMany({ - objectSpace: core.space.Model, - modifiedBy: core.account.System, - objectClass: { $ne: contact.class.EmployeeAccount } - }) - console.log(`${result.deletedCount} transactions deleted.`) - - console.log('creating model...') - const model = txes - const insert = await db.collection(DOMAIN_TX).insertMany(model as Document[]) - console.log(`${insert.insertedCount} model transactions inserted.`) - - const migrateClient = new MigrateClientImpl(db) - for (const op of migrateOperations) { - await op.migrate(migrateClient) - } - - console.log('Apply upgrade operations') - - const connection = await connect(transactorUrl, dbName) - for (const op of migrateOperations) { - await op.upgrade(connection) - } - - await connection.close() - } finally { - await client.close() - } -} - interface CollectionInfo { name: string file: string @@ -107,7 +53,7 @@ export async function dumpWorkspace (mongoUrl: string, dbName: string, fileName: } const workspaceInfo: WorkspaceInfo = { - version: '0.6.0', + version: `${version.major}.${version.minor}.${version.patch}`, collections: [], minioData: [] } @@ -215,7 +161,7 @@ export async function restoreWorkspace ( } } - await upgradeWorkspace(mongoUrl, dbName, transactorUrl, minio) + await upgradeModel(dbName, transactorUrl) await rebuildElastic(mongoUrl, dbName, minio, elasticUrl) } finally { diff --git a/models/all/package.json b/models/all/package.json index 5377489233..4ab7301574 100644 --- a/models/all/package.json +++ b/models/all/package.json @@ -9,6 +9,7 @@ "build:watch": "tsc", "lint:fix": "eslint --fix src", "genmodel": "ts-node src/__genmodel.ts", + "version": "ts-node src/__showversion.ts", "lint": "eslint src", "format": "prettier --write src && eslint --fix src" }, diff --git a/models/all/src/__showversion.ts b/models/all/src/__showversion.ts new file mode 100644 index 0000000000..842c4ac0d7 --- /dev/null +++ b/models/all/src/__showversion.ts @@ -0,0 +1,17 @@ +// +// Copyright © 2020 Anticrm Platform Contributors. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { version } from '.' +console.log(`"${version.major}.${version.minor}.${version.patch}"`) diff --git a/models/all/src/index.ts b/models/all/src/index.ts index e90dfd6d1f..d0b41cc8b9 100644 --- a/models/all/src/index.ts +++ b/models/all/src/index.ts @@ -13,61 +13,71 @@ // limitations under the License. // +import core, { Data, Version } from '@anticrm/core' import { Builder } from '@anticrm/model' - -import { createModel as coreModel } from '@anticrm/model-core' -import { createModel as viewModel } from '@anticrm/model-view' -import { createModel as workbenchModel } from '@anticrm/model-workbench' -import { createModel as contactModel } from '@anticrm/model-contact' -import { createModel as taskModel } from '@anticrm/model-task' -import { createModel as chunterModel } from '@anticrm/model-chunter' -import { createModel as recruitModel } from '@anticrm/model-recruit' -import { createModel as settingModel } from '@anticrm/model-setting' -import { createModel as telegramModel } from '@anticrm/model-telegram' +import { createModel as activityModel } from '@anticrm/model-activity' import { createModel as attachmentModel } from '@anticrm/model-attachment' -import { createModel as leadModel } from '@anticrm/model-lead' +import { createModel as chunterModel } from '@anticrm/model-chunter' +import { createModel as contactModel } from '@anticrm/model-contact' +import { createModel as coreModel } from '@anticrm/model-core' +import { createDemo } from '@anticrm/model-demo' import { createModel as gmailModel } from '@anticrm/model-gmail' import { createModel as inventoryModel } from '@anticrm/model-inventory' +import { createModel as leadModel } from '@anticrm/model-lead' import { createModel as presentationModel } from '@anticrm/model-presentation' -import { createModel as templatesModel } from '@anticrm/model-templates' -import { createModel as textEditorModel } from '@anticrm/model-text-editor' - -import { createModel as serverCoreModel } from '@anticrm/model-server-core' +import { createModel as recruitModel } from '@anticrm/model-recruit' import { createModel as serverAttachmentModel } from '@anticrm/model-server-attachment' import { createModel as serverContactModel } from '@anticrm/model-server-contact' -import { createModel as activityModel } from '@anticrm/model-activity' - -import { createDemo } from '@anticrm/model-demo' +import { createModel as serverCoreModel } from '@anticrm/model-server-core' +import { createModel as settingModel } from '@anticrm/model-setting' +import { createModel as taskModel } from '@anticrm/model-task' +import { createModel as telegramModel } from '@anticrm/model-telegram' +import { createModel as templatesModel } from '@anticrm/model-templates' +import { createModel as textEditorModel } from '@anticrm/model-text-editor' +import { createModel as viewModel } from '@anticrm/model-view' +import { createModel as workbenchModel } from '@anticrm/model-workbench' const builder = new Builder() -coreModel(builder) -activityModel(builder) -attachmentModel(builder) -viewModel(builder) -workbenchModel(builder) -contactModel(builder) -chunterModel(builder) -taskModel(builder) -recruitModel(builder) -settingModel(builder) -telegramModel(builder) -leadModel(builder) -gmailModel(builder) -inventoryModel(builder) -presentationModel(builder) -templatesModel(builder) -textEditorModel(builder) +const builders = [ + coreModel, + activityModel, + attachmentModel, + viewModel, + workbenchModel, + contactModel, + chunterModel, + taskModel, + recruitModel, + settingModel, + telegramModel, + leadModel, + gmailModel, + inventoryModel, + presentationModel, + templatesModel, + textEditorModel, -serverCoreModel(builder) -serverAttachmentModel(builder) -serverContactModel(builder) + serverCoreModel, + serverAttachmentModel, + serverContactModel, -createDemo(builder) + createDemo +] +for (const b of builders) { + b(builder) +} + +export const version: Data = { + major: 0, + minor: 6, + patch: 0 +} + +builder.createDoc(core.class.Version, core.space.Model, version, core.version.Model) export default builder // Export upgrade procedures -export { migrateOperations } from './migration' - export { createDeps } from './creation' +export { migrateOperations } from './migration' diff --git a/models/core/src/core.ts b/models/core/src/core.ts index f8025bcd62..055cc65e3a 100644 --- a/models/core/src/core.ts +++ b/models/core/src/core.ts @@ -31,7 +31,8 @@ import type { Collection, RefTo, ArrOf, - Interface + Interface, + Version } from '@anticrm/core' import { DOMAIN_MODEL } from '@anticrm/core' import { Model, Prop, TypeRef, TypeString, TypeTimestamp } from '@anticrm/model' @@ -128,3 +129,10 @@ export class TTypeTimestamp extends TType {} @Model(core.class.TypeDate, core.class.Type) export class TTypeDate extends TType {} + +@Model(core.class.Version, core.class.Doc, DOMAIN_MODEL) +export class TVersion extends TDoc implements Version { + major!: number + minor!: number + patch!: number +} diff --git a/models/core/src/index.ts b/models/core/src/index.ts index 26fee6bea6..00f02e3a75 100644 --- a/models/core/src/index.ts +++ b/models/core/src/index.ts @@ -15,15 +15,42 @@ import { Builder } from '@anticrm/model' import core from './component' -import { TAttribute, TArrOf, TClass, TDoc, TMixin, TObj, TType, TTypeString, TTypeBoolean, TTypeTimestamp, TTypeDate, TAttachedDoc, TCollection, TRefTo, TInterface } from './core' -import { TSpace, TAccount } from './security' -import { TTx, TTxCreateDoc, TTxMixin, TTxUpdateDoc, TTxCUD, TTxPutBag, TTxRemoveDoc, TTxBulkWrite, TTxCollectionCUD } from './tx' +import { + TArrOf, + TAttachedDoc, + TAttribute, + TClass, + TCollection, + TDoc, + TInterface, + TMixin, + TObj, + TRefTo, + TType, + TTypeBoolean, + TTypeDate, + TTypeString, + TTypeTimestamp, + TVersion +} from './core' +import { TAccount, TSpace } from './security' +import { + TTx, + TTxBulkWrite, + TTxCollectionCUD, + TTxCreateDoc, + TTxCUD, + TTxMixin, + TTxPutBag, + TTxRemoveDoc, + TTxUpdateDoc +} from './tx' export * from './core' +export { coreOperation } from './migration' export * from './security' export * from './tx' export { core as default } -export { coreOperation } from './migration' export function createModel (builder: Builder): void { builder.createModel( @@ -52,6 +79,7 @@ export function createModel (builder: Builder): void { TRefTo, TCollection, TTypeDate, - TArrOf + TArrOf, + TVersion ) } diff --git a/packages/core/src/classes.ts b/packages/core/src/classes.ts index 4a345f35fa..194876dce1 100644 --- a/packages/core/src/classes.ts +++ b/packages/core/src/classes.ts @@ -221,3 +221,12 @@ export interface Space extends Doc { export interface Account extends Doc { email: string } + +/** + * @public + */ +export interface Version extends Doc { + major: number + minor: number + patch: number +} diff --git a/packages/core/src/component.ts b/packages/core/src/component.ts index 9be0a2419d..1da7a345e3 100644 --- a/packages/core/src/component.ts +++ b/packages/core/src/component.ts @@ -14,7 +14,7 @@ // import type { Plugin, StatusCode } from '@anticrm/platform' import { plugin } from '@anticrm/platform' -import { Mixin } from '.' +import { Mixin, Version } from '.' import type { Account, ArrOf, AnyAttribute, AttachedDoc, Class, Doc, Interface, Obj, PropertyType, Ref, Space, Timestamp, Type, Collection, RefTo } from './classes' import type { Tx, TxBulkWrite, TxCollectionCUD, TxCreateDoc, TxCUD, TxMixin, TxPutBag, TxRemoveDoc, TxUpdateDoc } from './tx' @@ -50,7 +50,8 @@ export default plugin(coreId, { RefTo: '' as Ref>>, ArrOf: '' as Ref>>, Collection: '' as Ref>>, - Bag: '' as Ref>>> + Bag: '' as Ref>>>, + Version: '' as Ref> }, space: { Tx: '' as Ref, @@ -62,5 +63,8 @@ export default plugin(coreId, { status: { ObjectNotFound: '' as StatusCode<{ _id: Ref }>, ItemNotFound: '' as StatusCode<{ _id: Ref, _localId: string }> + }, + version: { + Model: '' as Ref } }) diff --git a/packages/model/src/dsl.ts b/packages/model/src/dsl.ts index 278bc25060..0abb1d2f94 100644 --- a/packages/model/src/dsl.ts +++ b/packages/model/src/dsl.ts @@ -315,7 +315,7 @@ export class Builder { } getTxes (): Tx[] { - return this.txes + return [...this.txes] } } diff --git a/plugins/workbench-resources/src/components/WorkbenchApp.svelte b/plugins/workbench-resources/src/components/WorkbenchApp.svelte index eb8f7b6a0c..c3c3133177 100644 --- a/plugins/workbench-resources/src/components/WorkbenchApp.svelte +++ b/plugins/workbench-resources/src/components/WorkbenchApp.svelte @@ -15,8 +15,9 @@