From 7ae9426e903da3e95c86d2ad5d1b3d5da378e24f Mon Sep 17 00:00:00 2001 From: Fadhlan Ridhwanallah Date: Thu, 2 Jul 2026 11:48:53 +0700 Subject: [PATCH 1/7] Add Re-index action to workspace chooser tile menu Surface a Re-index item in the "..." menu on each owned workspace tile. It calls RealmService.fullReindex(), which POSTs to the realm's _full-reindex endpoint and manages the indexing animation. The tile's RealmIcon now passes @canAnimate so the indexing pulse is visible for the whole reindex window, and the menu item is owner-gated and disabled while the realm is indexing. Failures surface an auto-dismissing inline error on the tile. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../workspace-chooser/workspace.gts | 70 +++++- .../workspace-chooser-reindex-test.gts | 216 ++++++++++++++++++ 2 files changed, 285 insertions(+), 1 deletion(-) create mode 100644 packages/host/tests/acceptance/workspace-chooser-reindex-test.gts diff --git a/packages/host/app/components/operator-mode/workspace-chooser/workspace.gts b/packages/host/app/components/operator-mode/workspace-chooser/workspace.gts index 796393fb81..f38ce3b1c3 100644 --- a/packages/host/app/components/operator-mode/workspace-chooser/workspace.gts +++ b/packages/host/app/components/operator-mode/workspace-chooser/workspace.gts @@ -9,6 +9,7 @@ import ArchiveIcon from '@cardstack/boxel-icons/archive'; import CircleAlert from '@cardstack/boxel-icons/circle-alert'; import FileSettingsIcon from '@cardstack/boxel-icons/file-settings'; import Home from '@cardstack/boxel-icons/home'; +import RefreshIcon from '@cardstack/boxel-icons/refresh-cw'; import { dropTask, task } from 'ember-concurrency'; import perform from 'ember-concurrency/helpers/perform'; import pluralize from 'pluralize'; @@ -91,6 +92,7 @@ export default class Workspace extends Component { @@ -184,6 +186,11 @@ export default class Workspace extends Component { {{this.visibility}} + {{#if this.reindexError}} +

+ {{this.reindexError}} +

+ {{/if}} {{#if this.showDeleteModal}} { gap: var(--boxel-sp-5xs); max-width: var(--boxel-xxs-container); } + .reindex-error { + max-width: var(--boxel-xxs-container); + margin: var(--boxel-sp-5xs) 0 0; + color: var(--boxel-danger); + font: 600 var(--boxel-font-xs); + text-align: center; + overflow-wrap: anywhere; + } .info > span { text-overflow: ellipsis; overflow: hidden; @@ -1055,12 +1070,19 @@ export default class Workspace extends Component { @tracked private showArchiveModal = false; @tracked private archiveError: string | undefined; @tracked private isHostDropdownOpen = false; + @tracked private reindexError: string | undefined; + private reindexErrorTimer: ReturnType | undefined; constructor(...args: [any, any]) { super(...args); this.loadRealmTask.perform(); } + willDestroy() { + super.willDestroy(); + this.clearReindexError(); + } + private loadRealmTask = task(async () => { await this.realm.login(this.args.realmIdentifier); await this.realm.ensureRealmMeta(this.args.realmIdentifier); @@ -1088,7 +1110,23 @@ export default class Workspace extends Component { action: this.openRealmConfig, }), ]; - // Archive is owner-only and appears only on tiles the user owns. + // Re-index and Archive are owner-only and appear only on tiles the user + // owns. + if (this.canReindexWorkspace) { + items.push( + new MenuItem({ + label: 'Re-index', + icon: RefreshIcon, + action: this.reindexWorkspaceTask.perform, + // Gate on the live indexing flag, not reindexWorkspaceTask.isRunning: + // the task resolves the instant the 204 lands (sub-second), while the + // reindex itself runs much longer. isIndexing stays true for the whole + // pass. Repeat clicks are server-safe regardless (the reindex queue + // coalesces them), so this guard is purely a UX nicety. + disabled: this.isReindexing, + }), + ); + } if (this.canArchiveWorkspace) { items.push( new MenuItem({ @@ -1206,6 +1244,14 @@ export default class Workspace extends Component { return this.realm.isRealmOwner(this.args.realmIdentifier); } + private get canReindexWorkspace() { + return this.realm.isRealmOwner(this.args.realmIdentifier); + } + + private get isReindexing() { + return this.realmInfo.isIndexing; + } + private get deleteSummaryText() { if (!this.deleteSummary) { return null; @@ -1306,6 +1352,28 @@ export default class Workspace extends Component { } }); + private reindexWorkspaceTask = dropTask(async () => { + this.clearReindexError(); + try { + await this.realm.fullReindex(this.args.realmIdentifier); + } catch (error: any) { + this.reindexError = error.message; + // Auto-dismiss after a few seconds. A plain setTimeout (not an + // ember-concurrency timeout) so it does not keep test `settled()` waiting. + this.reindexErrorTimer = setTimeout(() => { + this.reindexError = undefined; + }, 5000); + } + }); + + private clearReindexError() { + if (this.reindexErrorTimer) { + clearTimeout(this.reindexErrorTimer); + this.reindexErrorTimer = undefined; + } + this.reindexError = undefined; + } + private loadDeleteSummaryTask = dropTask(async () => { try { let response = await this.network.authedFetch( diff --git a/packages/host/tests/acceptance/workspace-chooser-reindex-test.gts b/packages/host/tests/acceptance/workspace-chooser-reindex-test.gts new file mode 100644 index 0000000000..55f87df399 --- /dev/null +++ b/packages/host/tests/acceptance/workspace-chooser-reindex-test.gts @@ -0,0 +1,216 @@ +import { click, settled, waitFor } from '@ember/test-helpers'; + +import { getService } from '@universal-ember/test-support'; + +import { module, test } from 'qunit'; + +import { testRealmInfo } from '@cardstack/runtime-common'; +import { APP_BOXEL_REALM_EVENT_TYPE } from '@cardstack/runtime-common/matrix-constants'; + +import { + setupAcceptanceTestRealm, + setupLocalIndexing, + setupRealmCacheTeardown, + setupRealmServerEndpoints, + setupUserSubscription, + visitOperatorMode, + realmConfigCardJSON, +} from '../helpers'; +import { setupBaseRealm } from '../helpers/base-realm'; +import { setupMockMatrix } from '../helpers/mock-matrix'; +import { setupApplicationTest } from '../helpers/setup'; + +// Workspace A is owned by the test user; Workspace C is read/write only (the +// test user is not its owner), so the Re-index action must never appear on it. +const ownedRealmURL = 'http://test-realm/testuser/workspace-a/'; +const readOnlyRealmURL = 'http://test-realm/otheruser/workspace-c/'; + +const cardsGridIndex = { + data: { + type: 'card', + meta: { + adoptsFrom: { + module: '@cardstack/base/cards-grid', + name: 'CardsGrid', + }, + }, + }, +}; + +// The tile stops its indexing animation only when it hears an `index` event for +// the realm, mirroring the real server->host signal. +function simulateIndexDone(mockMatrixUtils: any, realmURL: string) { + mockMatrixUtils.simulateRemoteMessage( + mockMatrixUtils.getRoomIdForRealmAndUser(realmURL, '@testuser:localhost'), + testRealmInfo.realmUserId!, + { + eventName: 'index', + indexType: 'incremental', + invalidations: [], + realmURL, + }, + { type: APP_BOXEL_REALM_EVENT_TYPE }, + ); +} + +module('Acceptance | workspace-chooser re-index', function (hooks) { + setupApplicationTest(hooks); + setupLocalIndexing(hooks); + setupRealmCacheTeardown(hooks); + + let receivedMethod: string | null = null; + let receivedPathname: string | null = null; + let responseStatus = 204; + let responseBody: string | null = null; + + let mockMatrixUtils = setupMockMatrix(hooks, { + loggedInAs: '@testuser:localhost', + activeRealms: [ownedRealmURL, readOnlyRealmURL], + }); + + setupBaseRealm(hooks); + + setupRealmServerEndpoints(hooks, [ + { + route: 'testuser/workspace-a/_full-reindex', + getResponse: async (req: Request) => { + receivedMethod = req.method; + receivedPathname = new URL(req.url).pathname; + return new Response(responseBody, { status: responseStatus }); + }, + }, + ]); + + hooks.beforeEach(async function () { + setupUserSubscription(); + receivedMethod = null; + receivedPathname = null; + responseStatus = 204; + responseBody = null; + + await setupAcceptanceTestRealm({ + realmURL: ownedRealmURL, + mockMatrixUtils, + permissions: { + '@testuser:localhost': ['read', 'write', 'realm-owner'], + }, + contents: { + 'realm.json': realmConfigCardJSON({ name: 'Workspace A' }), + 'index.json': cardsGridIndex, + }, + }); + + await setupAcceptanceTestRealm({ + realmURL: readOnlyRealmURL, + mockMatrixUtils, + permissions: { + '@testuser:localhost': ['read', 'write'], + }, + contents: { + 'realm.json': realmConfigCardJSON({ name: 'Workspace C' }), + 'index.json': cardsGridIndex, + }, + }); + }); + + test('Re-index is owner-only', async function (assert) { + await visitOperatorMode({ workspaceChooserOpened: true }); + + await click(`[data-test-workspace-menu-trigger="${ownedRealmURL}"]`); + assert + .dom('[data-test-boxel-menu-item-text="Re-index"]') + .exists('Re-index appears on a workspace the user owns'); + + await click(`[data-test-workspace-menu-trigger="${readOnlyRealmURL}"]`); + assert + .dom('[data-test-boxel-menu-item-text="Re-index"]') + .doesNotExist('Re-index is absent on a workspace the user does not own'); + assert + .dom('[data-test-boxel-menu-item-text="Realm Settings"]') + .exists('the rest of the tile menu still renders for non-owners'); + }); + + test('Re-index POSTs to the realm endpoint and animates the tile', async function (assert) { + let realmService = getService('realm'); + + await visitOperatorMode({ workspaceChooserOpened: true }); + + assert.false( + realmService.info(ownedRealmURL).isIndexing, + 'the realm is not indexing before Re-index runs', + ); + + await click(`[data-test-workspace-menu-trigger="${ownedRealmURL}"]`); + await click('[data-test-boxel-menu-item-text="Re-index"]'); + + assert.strictEqual(receivedMethod, 'POST', 'Re-index uses POST'); + assert.strictEqual( + receivedPathname, + '/testuser/workspace-a/_full-reindex', + 'Re-index calls the realm _full-reindex endpoint', + ); + assert.true( + realmService.info(ownedRealmURL).isIndexing, + 'the tile starts its indexing animation immediately', + ); + // The indexing indicator only renders once @canAnimate is on and the realm + // is indexing, so its presence proves the tile now animates for a reindex. + assert + .dom( + `[data-test-workspace="Workspace A"] [data-test-realm-indexing-indicator]`, + ) + .exists('the tile icon reflects the indexing pulse'); + + simulateIndexDone(mockMatrixUtils, ownedRealmURL); + await settled(); + + assert.false( + realmService.info(ownedRealmURL).isIndexing, + 'the index event stops the tile animation', + ); + }); + + test('Re-index is disabled while indexing and re-enabled when it finishes', async function (assert) { + await visitOperatorMode({ workspaceChooserOpened: true }); + + await click(`[data-test-workspace-menu-trigger="${ownedRealmURL}"]`); + await click('[data-test-boxel-menu-item-text="Re-index"]'); + + await click(`[data-test-workspace-menu-trigger="${ownedRealmURL}"]`); + assert + .dom('[data-test-boxel-menu-item-text="Re-index"]') + .isDisabled('Re-index is disabled while the realm is indexing'); + + simulateIndexDone(mockMatrixUtils, ownedRealmURL); + await settled(); + + await click(`[data-test-workspace-menu-trigger="${ownedRealmURL}"]`); + assert + .dom('[data-test-boxel-menu-item-text="Re-index"]') + .isNotDisabled('Re-index is enabled again once indexing finishes'); + }); + + test('a failed Re-index surfaces an inline error and restores the tile', async function (assert) { + responseStatus = 500; + responseBody = 'boom'; + + let realmService = getService('realm'); + + await visitOperatorMode({ workspaceChooserOpened: true }); + + await click(`[data-test-workspace-menu-trigger="${ownedRealmURL}"]`); + await click('[data-test-boxel-menu-item-text="Re-index"]'); + + await waitFor('[data-test-reindex-error]'); + assert + .dom('[data-test-reindex-error]') + .hasText( + 'Full reindex realm failed: 500 - boom', + 'the failure is surfaced inline on the tile', + ); + assert.false( + realmService.info(ownedRealmURL).isIndexing, + 'the indexing animation is restored after a failed Re-index', + ); + }); +}); From b534446ee0ea51328dd06c11881b598c09710e29 Mon Sep 17 00:00:00 2001 From: Fadhlan Ridhwanallah Date: Thu, 2 Jul 2026 13:16:15 +0700 Subject: [PATCH 2/7] Fix flaky reindex menu test: assert on the open menu instead of re-toggling The disabled-while-indexing test reopened the tile menu before the final assertion, but the menu was already open from the disabled check, so the extra trigger click toggled it shut and the Re-index item no longer existed. The menu item's disabled state is derived from the live indexing flag and updates in place, so assert on the still-open menu after the index event instead. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../host/tests/acceptance/workspace-chooser-reindex-test.gts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/host/tests/acceptance/workspace-chooser-reindex-test.gts b/packages/host/tests/acceptance/workspace-chooser-reindex-test.gts index 55f87df399..14b53c4064 100644 --- a/packages/host/tests/acceptance/workspace-chooser-reindex-test.gts +++ b/packages/host/tests/acceptance/workspace-chooser-reindex-test.gts @@ -181,10 +181,12 @@ module('Acceptance | workspace-chooser re-index', function (hooks) { .dom('[data-test-boxel-menu-item-text="Re-index"]') .isDisabled('Re-index is disabled while the realm is indexing'); + // Leave the menu open: the item's disabled state is derived from the live + // indexing flag, so the index event flips it back to enabled in place. (A + // second trigger click here would just toggle the open menu shut.) simulateIndexDone(mockMatrixUtils, ownedRealmURL); await settled(); - await click(`[data-test-workspace-menu-trigger="${ownedRealmURL}"]`); assert .dom('[data-test-boxel-menu-item-text="Re-index"]') .isNotDisabled('Re-index is enabled again once indexing finishes'); From f8d31f0ce9272c1374b06da2a69f8b0eb50774e7 Mon Sep 17 00:00:00 2001 From: Fadhlan Ridhwanallah Date: Thu, 2 Jul 2026 16:37:53 +0700 Subject: [PATCH 3/7] Harden reindex error banner and match full-reindex completion event in test Fall back to String(error) when a thrown value has no message, so the inline reindex-error banner always renders. Emit a `full` index-completion event in the acceptance test to mirror the `_full-reindex` action it triggers. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../components/operator-mode/workspace-chooser/workspace.gts | 2 +- .../host/tests/acceptance/workspace-chooser-reindex-test.gts | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/host/app/components/operator-mode/workspace-chooser/workspace.gts b/packages/host/app/components/operator-mode/workspace-chooser/workspace.gts index f38ce3b1c3..65c8f31688 100644 --- a/packages/host/app/components/operator-mode/workspace-chooser/workspace.gts +++ b/packages/host/app/components/operator-mode/workspace-chooser/workspace.gts @@ -1357,7 +1357,7 @@ export default class Workspace extends Component { try { await this.realm.fullReindex(this.args.realmIdentifier); } catch (error: any) { - this.reindexError = error.message; + this.reindexError = String(error?.message ?? error); // Auto-dismiss after a few seconds. A plain setTimeout (not an // ember-concurrency timeout) so it does not keep test `settled()` waiting. this.reindexErrorTimer = setTimeout(() => { diff --git a/packages/host/tests/acceptance/workspace-chooser-reindex-test.gts b/packages/host/tests/acceptance/workspace-chooser-reindex-test.gts index 14b53c4064..01994ef1a5 100644 --- a/packages/host/tests/acceptance/workspace-chooser-reindex-test.gts +++ b/packages/host/tests/acceptance/workspace-chooser-reindex-test.gts @@ -45,8 +45,7 @@ function simulateIndexDone(mockMatrixUtils: any, realmURL: string) { testRealmInfo.realmUserId!, { eventName: 'index', - indexType: 'incremental', - invalidations: [], + indexType: 'full', realmURL, }, { type: APP_BOXEL_REALM_EVENT_TYPE }, From 9d6875f183c755ae2ad97dad6fbd676dfd5c4c78 Mon Sep 17 00:00:00 2001 From: Fadhlan Ridhwanallah Date: Fri, 3 Jul 2026 13:25:52 +0700 Subject: [PATCH 4/7] Make reindex error a dismissible tile banner instead of auto-dismissing The reindex failure error no longer disappears on a timer. It now renders as a banner across the top of the workspace tile and stays until the user closes it with the banner's dismiss button or retries the reindex. Removing the auto-dismiss timer also drops the stale-timer-handle bookkeeping. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../workspace-chooser/workspace.gts | 72 +++++++++++++------ .../workspace-chooser-reindex-test.gts | 11 +++ 2 files changed, 63 insertions(+), 20 deletions(-) diff --git a/packages/host/app/components/operator-mode/workspace-chooser/workspace.gts b/packages/host/app/components/operator-mode/workspace-chooser/workspace.gts index 65c8f31688..b5465a8072 100644 --- a/packages/host/app/components/operator-mode/workspace-chooser/workspace.gts +++ b/packages/host/app/components/operator-mode/workspace-chooser/workspace.gts @@ -28,6 +28,7 @@ import { Group, IconGlobe, IconTrash, + IconX, Lock, Star, StarFilled, @@ -76,6 +77,20 @@ export default class Workspace extends Component { {{on 'mouseleave' this.closeHostDropdown}} ...attributes > + {{#if this.reindexError}} + + {{/if}} { {{this.visibility}} - {{#if this.reindexError}} -

- {{this.reindexError}} -

- {{/if}} {{#if this.showDeleteModal}} { max-width: var(--boxel-xxs-container); } .reindex-error { - max-width: var(--boxel-xxs-container); - margin: var(--boxel-sp-5xs) 0 0; - color: var(--boxel-danger); + position: absolute; + top: 0; + left: 0; + width: var(--boxel-xxs-container); + box-sizing: border-box; + z-index: 21; + display: flex; + align-items: flex-start; + gap: var(--boxel-sp-5xs); + padding: var(--boxel-sp-xxs) var(--boxel-sp-xs); + border-radius: var(--boxel-border-radius-xl) + var(--boxel-border-radius-xl) 0 0; + background-color: var(--boxel-danger); + color: var(--boxel-light); font: 600 var(--boxel-font-xs); - text-align: center; + } + .reindex-error__message { + flex: 1; overflow-wrap: anywhere; } + .reindex-error__dismiss { + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: center; + margin: 0; + padding: 2px; + border: none; + background: transparent; + color: inherit; + --icon-color: currentColor; + cursor: pointer; + border-radius: var(--boxel-border-radius-xs); + } + .reindex-error__dismiss:hover { + background: rgba(255 255 255 / 25%); + } .info > span { text-overflow: ellipsis; overflow: hidden; @@ -1071,7 +1111,6 @@ export default class Workspace extends Component { @tracked private archiveError: string | undefined; @tracked private isHostDropdownOpen = false; @tracked private reindexError: string | undefined; - private reindexErrorTimer: ReturnType | undefined; constructor(...args: [any, any]) { super(...args); @@ -1357,20 +1396,13 @@ export default class Workspace extends Component { try { await this.realm.fullReindex(this.args.realmIdentifier); } catch (error: any) { + // The error stays put until the user dismisses it or retries the + // reindex, so a failed pass never disappears before it's noticed. this.reindexError = String(error?.message ?? error); - // Auto-dismiss after a few seconds. A plain setTimeout (not an - // ember-concurrency timeout) so it does not keep test `settled()` waiting. - this.reindexErrorTimer = setTimeout(() => { - this.reindexError = undefined; - }, 5000); } }); - private clearReindexError() { - if (this.reindexErrorTimer) { - clearTimeout(this.reindexErrorTimer); - this.reindexErrorTimer = undefined; - } + @action private clearReindexError() { this.reindexError = undefined; } diff --git a/packages/host/tests/acceptance/workspace-chooser-reindex-test.gts b/packages/host/tests/acceptance/workspace-chooser-reindex-test.gts index 01994ef1a5..bcf6b0a4d7 100644 --- a/packages/host/tests/acceptance/workspace-chooser-reindex-test.gts +++ b/packages/host/tests/acceptance/workspace-chooser-reindex-test.gts @@ -213,5 +213,16 @@ module('Acceptance | workspace-chooser re-index', function (hooks) { realmService.info(ownedRealmURL).isIndexing, 'the indexing animation is restored after a failed Re-index', ); + + // The error does not auto-dismiss; it stays until the user clears it. + await settled(); + assert + .dom('[data-test-reindex-error]') + .exists('the error persists until it is dismissed'); + + await click('[data-test-reindex-error-dismiss]'); + assert + .dom('[data-test-reindex-error]') + .doesNotExist('dismissing the banner removes the error'); }); }); From c079001c1bb08337378b57b9f9536c9823bfcc1f Mon Sep 17 00:00:00 2001 From: Fadhlan Ridhwanallah Date: Fri, 3 Jul 2026 13:26:32 +0700 Subject: [PATCH 5/7] Add reindex error banner preview image [skip ci] Co-Authored-By: Claude Opus 4.8 (1M context) --- .pr-images/reindex-error-banner/preview.png | Bin 0 -> 11915 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 .pr-images/reindex-error-banner/preview.png diff --git a/.pr-images/reindex-error-banner/preview.png b/.pr-images/reindex-error-banner/preview.png new file mode 100644 index 0000000000000000000000000000000000000000..9a0421c01b4a255f700450cc4b35320ed3754e77 GIT binary patch literal 11915 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 V`1gKAWsjdEE6AuwBVL;Y|1Y(2vBCfV literal 0 HcmV?d00001 From c9a546ed14e8d2b4b0a7e07dc576f1497c00e016 Mon Sep 17 00:00:00 2001 From: Fadhlan Ridhwanallah Date: Fri, 3 Jul 2026 13:26:59 +0700 Subject: [PATCH 6/7] Remove reindex error banner preview image [skip ci] Co-Authored-By: Claude Opus 4.8 (1M context) --- .pr-images/reindex-error-banner/preview.png | Bin 11915 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 .pr-images/reindex-error-banner/preview.png diff --git a/.pr-images/reindex-error-banner/preview.png b/.pr-images/reindex-error-banner/preview.png deleted file mode 100644 index 9a0421c01b4a255f700450cc4b35320ed3754e77..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11915 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 V`1gKAWsjdEE6AuwBVL;Y|1Y(2vBCfV From dcc193548902460c8d91b0a05f9b1a958e415b52 Mon Sep 17 00:00:00 2001 From: Fadhlan Ridhwanallah Date: Fri, 3 Jul 2026 13:33:03 +0700 Subject: [PATCH 7/7] Clamp long reindex error to keep the banner within the tile A long server error body no longer grows the banner past the tile and over the workspace name. The message is clamped to three lines with an ellipsis and the full text stays available via the element's title on hover. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../workspace-chooser/workspace.gts | 12 +++++++- .../workspace-chooser-reindex-test.gts | 30 +++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/packages/host/app/components/operator-mode/workspace-chooser/workspace.gts b/packages/host/app/components/operator-mode/workspace-chooser/workspace.gts index b5465a8072..4d7b6ab59d 100644 --- a/packages/host/app/components/operator-mode/workspace-chooser/workspace.gts +++ b/packages/host/app/components/operator-mode/workspace-chooser/workspace.gts @@ -79,7 +79,10 @@ export default class Workspace extends Component { > {{#if this.reindexError}}