From 88ba6fa8b863099f18a90f1ca0deb404ac2a6d36 Mon Sep 17 00:00:00 2001 From: Sergei Garin Date: Thu, 28 Nov 2024 00:20:36 +0300 Subject: [PATCH] Dashboard Issues (#11685) Closes: https://github.com/enso-org/cloud-v2/issues/1592 --- app/common/src/text/english.json | 1 + app/gui/.storybook/main.ts | 2 + app/gui/index.html | 9 ++ .../dashboard/actions/DrivePageActions.ts | 8 ++ app/gui/integration-test/dashboard/api.ts | 53 +++++++- .../dashboard/assetPanel.spec.ts | 33 +++-- .../dashboard/assetsTableFeatures.spec.ts | 6 +- .../integration-test/dashboard/auth.setup.ts | 22 +--- .../dashboard/editAssetName.spec.ts | 59 +++++++-- .../dashboard/mock/enso-demo.main | 51 ++++++++ app/gui/playwright.config.ts | 5 +- .../src/dashboard/authentication/cognito.ts | 9 +- .../components/AnimatedBackground.tsx | 2 +- .../AriaComponents/Button/Button.tsx | 4 +- .../Inputs/OTPInput/OTPInput.tsx | 2 +- .../components/AriaComponents/Text/Text.tsx | 16 ++- .../src/dashboard/components/Autocomplete.tsx | 114 ++++++++++-------- .../src/dashboard/components/EditableSpan.tsx | 95 ++++++++------- .../MarkdownViewer/MarkdownViewer.tsx | 17 ++- .../components/dashboard/TheModal.tsx | 8 +- .../dashboard/column/SharedWithColumn.tsx | 17 +-- .../dashboard/column/columnUtils.ts | 19 ++- .../columnHeading/SharedWithColumnHeading.tsx | 19 +-- app/gui/src/dashboard/hooks/autoFocusHooks.ts | 2 +- .../dashboard/layouts/AssetDocs/AssetDocs.tsx | 15 ++- .../layouts/AssetPanel/AssetPanel.tsx | 22 ++-- .../AssetPanel/components/AssetPanelTabs.tsx | 8 +- .../src/dashboard/layouts/AssetProperties.tsx | 4 + app/gui/src/dashboard/layouts/AssetsTable.tsx | 63 +++++----- .../dashboard/layouts/CategorySwitcher.tsx | 2 + app/gui/src/dashboard/layouts/Labels.tsx | 1 + .../layouts/Settings/SetupTwoFaForm.tsx | 12 +- .../dashboard/providers/SessionProvider.tsx | 2 +- .../src/dashboard/services/RemoteBackend.ts | 16 ++- .../dashboard/services/remoteBackendPaths.ts | 7 ++ app/gui/src/entrypoint.ts | 2 + app/gui/tailwind.config.js | 4 + 37 files changed, 477 insertions(+), 254 deletions(-) create mode 100644 app/gui/integration-test/dashboard/mock/enso-demo.main diff --git a/app/common/src/text/english.json b/app/common/src/text/english.json index bcd1ea73143a..f92ad01eaf3f 100644 --- a/app/common/src/text/english.json +++ b/app/common/src/text/english.json @@ -960,6 +960,7 @@ "assetDocs.notProject": "Please select a project to view its docs.", "assetDocs.noDocs": "No docs available for this asset.", "assetProperties.notSelected": "Select a single asset to view its properties.", + "assetProperties.localBackend": "Properties are not available for local assets.", "assetVersions.localAssetsDoNotHaveVersions": "Local assets do not have versions.", "assetVersions.notSelected": "Select a single asset to view its versions.", "assetProjectSessions.noSessions": "No sessions yet! Open the project to start a session.", diff --git a/app/gui/.storybook/main.ts b/app/gui/.storybook/main.ts index b3726eba8224..5fe875f1e797 100644 --- a/app/gui/.storybook/main.ts +++ b/app/gui/.storybook/main.ts @@ -45,6 +45,8 @@ const sharedConfig: Partial = { if (window.parent !== window) { window.__REACT_DEVTOOLS_GLOBAL_HOOK__ = window.parent.__REACT_DEVTOOLS_GLOBAL_HOOK__ } + + document.documentElement.classList.add('macos') ${head} ` diff --git a/app/gui/index.html b/app/gui/index.html index 703747374b40..2fd49ff74630 100644 --- a/app/gui/index.html +++ b/app/gui/index.html @@ -38,6 +38,15 @@ user-scalable = no" /> Enso %ENSO_IDE_VERSION% + +
diff --git a/app/gui/integration-test/dashboard/actions/DrivePageActions.ts b/app/gui/integration-test/dashboard/actions/DrivePageActions.ts index 49baf223bc79..805aa4945604 100644 --- a/app/gui/integration-test/dashboard/actions/DrivePageActions.ts +++ b/app/gui/integration-test/dashboard/actions/DrivePageActions.ts @@ -343,6 +343,14 @@ export default class DrivePageActions extends PageActions { }) } + /** Show the Docs tab of the Asset Panel. */ + toggleDocsAssetPanel() { + return this.step('Toggle docs asset panel', async (page) => { + await this.showAssetPanel() + await page.getByTestId('asset-panel-tab-docs').click() + }) + } + /** Interact with the container element of the assets table. */ withAssetsTable(callback: baseActions.LocatorCallback) { return this.step('Interact with drive table', async (page) => { diff --git a/app/gui/integration-test/dashboard/api.ts b/app/gui/integration-test/dashboard/api.ts index c5155c425e4f..aa052fd1611f 100644 --- a/app/gui/integration-test/dashboard/api.ts +++ b/app/gui/integration-test/dashboard/api.ts @@ -12,12 +12,31 @@ import * as uniqueString from 'enso-common/src/utilities/uniqueString' import * as actions from './actions' +import { readFileSync } from 'node:fs' +import { dirname, join } from 'node:path' +import { fileURLToPath } from 'node:url' import LATEST_GITHUB_RELEASES from './latestGithubReleases.json' with { type: 'json' } // ================= // === Constants === // ================= +const __dirname = dirname(fileURLToPath(import.meta.url)) + +const MOCK_SVG = ` + + + + + + + + + + + +` + /** The HTTP status code representing a response with an empty body. */ const HTTP_STATUS_NO_CONTENT = 204 /** The HTTP status code representing a bad request. */ @@ -175,7 +194,17 @@ async function mockApiInternal({ page, setupAPI }: MockParams) { description: null, labels: [], parentId: defaultDirectoryId, - permissions: [], + permissions: [ + { + user: { + organizationId: defaultOrganizationId, + userId: defaultUserId, + name: defaultUsername, + email: defaultEmail, + }, + permission: permissions.PermissionAction.own, + }, + ], parentsPath: '', virtualParentsPath: '', }, @@ -983,6 +1012,7 @@ async function mockApiInternal({ page, setupAPI }: MockParams) { return json }) + await post(remoteBackendPaths.CREATE_DIRECTORY_PATH + '*', (_route, request) => { const body: backend.CreateDirectoryRequestBody = request.postDataJSON() const title = body.title @@ -1011,6 +1041,27 @@ async function mockApiInternal({ page, setupAPI }: MockParams) { return json }) + await get(remoteBackendPaths.getProjectContentPath(GLOB_PROJECT_ID), (route) => { + const content = readFileSync(join(__dirname, './mock/enso-demo.main'), 'utf8') + + return route.fulfill({ + body: content, + contentType: 'text/plain', + }) + }) + + await get(remoteBackendPaths.getProjectAssetPath(GLOB_PROJECT_ID, '*'), (route) => { + return route.fulfill({ + // This is a mock SVG image. Just a square with a black background. + body: '/mock/svg.svg', + contentType: 'text/plain', + }) + }) + + await page.route('mock/svg.svg', (route) => { + return route.fulfill({ body: MOCK_SVG, contentType: 'image/svg+xml' }) + }) + await page.route('*', async (route) => { if (!isOnline) { await route.abort('connectionfailed') diff --git a/app/gui/integration-test/dashboard/assetPanel.spec.ts b/app/gui/integration-test/dashboard/assetPanel.spec.ts index 61e5cb9ff7b1..6126441d2472 100644 --- a/app/gui/integration-test/dashboard/assetPanel.spec.ts +++ b/app/gui/integration-test/dashboard/assetPanel.spec.ts @@ -1,5 +1,5 @@ /** @file Tests for the asset panel. */ -import * as test from '@playwright/test' +import { expect, test } from '@playwright/test' import * as backend from '#/services/Backend' @@ -22,19 +22,18 @@ const EMAIL = 'baz.quux@email.com' // === Tests === // ============= -test.test('open and close asset panel', ({ page }) => +test('open and close asset panel', ({ page }) => actions .mockAllAndLogin({ page }) .withAssetPanel(async (assetPanel) => { - await test.expect(assetPanel).toBeVisible() + await expect(assetPanel).toBeVisible() }) .toggleAssetPanel() .withAssetPanel(async (assetPanel) => { - await test.expect(assetPanel).not.toBeVisible() - }), -) + await expect(assetPanel).not.toBeVisible() + })) -test.test('asset panel contents', ({ page }) => +test('asset panel contents', ({ page }) => actions .mockAllAndLogin({ page, @@ -64,5 +63,21 @@ test.test('asset panel contents', ({ page }) => // `getByText` is required so that this assertion works if there are multiple permissions. // This is not visible; "Shared with" should only be visible on the Enterprise plan. // await test.expect(actions.locateAssetPanelPermissions(page).getByText(USERNAME)).toBeVisible() - }), -) + })) + +test('Asset Panel Documentation view', ({ page }) => { + return actions + .mockAllAndLogin({ + page, + setupAPI: (api) => { + api.addProject('project', { description: DESCRIPTION }) + }, + }) + .driveTable.clickRow(0) + .toggleDocsAssetPanel() + .withAssetPanel(async (assetPanel) => { + await expect(assetPanel.getByTestId('asset-panel-tab-panel-docs')).toBeVisible() + await expect(assetPanel.getByTestId('asset-docs-content')).toBeVisible() + await expect(assetPanel.getByTestId('asset-docs-content')).toHaveText(/Project Goal/) + }) +}) diff --git a/app/gui/integration-test/dashboard/assetsTableFeatures.spec.ts b/app/gui/integration-test/dashboard/assetsTableFeatures.spec.ts index ad19696579a6..3b93ab03d14a 100644 --- a/app/gui/integration-test/dashboard/assetsTableFeatures.spec.ts +++ b/app/gui/integration-test/dashboard/assetsTableFeatures.spec.ts @@ -31,7 +31,7 @@ test.test('extra columns should stick to right side of assets table', ({ page }) const assetsTableRight = await assetsTable.evaluate( (element) => element.getBoundingClientRect().right, ) - test.expect(extraColumnsRight).toEqual(assetsTableRight) + test.expect(extraColumnsRight).toEqual(assetsTableRight - 12) }) .toPass({ timeout: PASS_TIMEOUT }) }), @@ -72,9 +72,9 @@ test.test('extra columns should stick to top of scroll container', async ({ page ) { scrollableParent = scrollableParent.parentElement } - return scrollableParent?.getBoundingClientRect().top + return scrollableParent?.getBoundingClientRect().top ?? 0 }) - test.expect(extraColumnsTop).toEqual(assetsTableTop) + test.expect(extraColumnsTop).toEqual(assetsTableTop + 2) }) .toPass({ timeout: PASS_TIMEOUT }) }) diff --git a/app/gui/integration-test/dashboard/auth.setup.ts b/app/gui/integration-test/dashboard/auth.setup.ts index 11d5b5a9c9ce..e75ba5b31d9e 100644 --- a/app/gui/integration-test/dashboard/auth.setup.ts +++ b/app/gui/integration-test/dashboard/auth.setup.ts @@ -1,29 +1,15 @@ import { test as setup } from '@playwright/test' -import { existsSync } from 'node:fs' import path from 'node:path' import * as actions from './actions' const __dirname = path.dirname(new URL(import.meta.url).pathname) const authFile = path.join(__dirname, '../../playwright/.auth/user.json') -const isProd = process.env.NODE_ENV === 'production' -const isFileExists = () => { - if (isProd) { - return false - } - - return existsSync(authFile) -} - -setup('authenticate', ({ page }) => { - if (isFileExists()) { - return setup.skip() - } - - return actions +setup('authenticate', ({ page }) => + actions .mockAll({ page }) .login() .do(async () => { await page.context().storageState({ path: authFile }) - }) -}) + }), +) diff --git a/app/gui/integration-test/dashboard/editAssetName.spec.ts b/app/gui/integration-test/dashboard/editAssetName.spec.ts index 09eaa8ec09a5..11ce1f5a9296 100644 --- a/app/gui/integration-test/dashboard/editAssetName.spec.ts +++ b/app/gui/integration-test/dashboard/editAssetName.spec.ts @@ -1,11 +1,10 @@ /** @file Test copying, moving, cutting and pasting. */ -import * as test from '@playwright/test' +import { test } from '@playwright/test' import * as actions from './actions' -test.test.beforeEach(({ page }) => actions.mockAllAndLogin({ page })) - -test.test('edit name', async ({ page }) => { +test('edit name (double click)', async ({ page }) => { + await actions.mockAllAndLogin({ page }) const assetRows = actions.locateAssetRows(page) const row = assetRows.nth(0) const newName = 'foo bar baz' @@ -18,7 +17,41 @@ test.test('edit name', async ({ page }) => { await test.expect(row).toHaveText(new RegExp('^' + newName)) }) -test.test('edit name (keyboard)', async ({ page }) => { +test('edit name (context menu)', async ({ page }) => { + await actions.mockAllAndLogin({ + page, + setupAPI: (api) => { + api.addAsset(api.createDirectory('foo')) + }, + }) + + const assetRows = actions.locateAssetRows(page) + const row = assetRows.nth(0) + const newName = 'foo bar baz' + + await actions.locateAssetRowName(row).click({ button: 'right' }) + await actions + .locateContextMenus(page) + .getByText(/Rename/) + .click() + + const input = page.getByTestId('asset-row-name') + + await test.expect(input).toBeVisible() + await test.expect(input).toBeFocused() + + await input.fill(newName) + + await test.expect(input).toHaveValue(newName) + + await input.press('Enter') + + await test.expect(row).toHaveText(new RegExp('^' + newName)) +}) + +test('edit name (keyboard)', async ({ page }) => { + await actions.mockAllAndLogin({ page }) + const assetRows = actions.locateAssetRows(page) const row = assetRows.nth(0) const newName = 'foo bar baz quux' @@ -31,7 +64,9 @@ test.test('edit name (keyboard)', async ({ page }) => { await test.expect(row).toHaveText(new RegExp('^' + newName)) }) -test.test('cancel editing name', async ({ page }) => { +test('cancel editing name (double click)', async ({ page }) => { + await actions.mockAllAndLogin({ page }) + const assetRows = actions.locateAssetRows(page) const row = assetRows.nth(0) const newName = 'foo bar baz' @@ -46,7 +81,9 @@ test.test('cancel editing name', async ({ page }) => { await test.expect(row).toHaveText(new RegExp('^' + oldName)) }) -test.test('cancel editing name (keyboard)', async ({ page }) => { +test('cancel editing name (keyboard)', async ({ page }) => { + await actions.mockAllAndLogin({ page }) + const assetRows = actions.locateAssetRows(page) const row = assetRows.nth(0) const newName = 'foo bar baz quux' @@ -60,7 +97,9 @@ test.test('cancel editing name (keyboard)', async ({ page }) => { await test.expect(row).toHaveText(new RegExp('^' + oldName)) }) -test.test('change to blank name', async ({ page }) => { +test('change to blank name (double click)', async ({ page }) => { + await actions.mockAllAndLogin({ page }) + const assetRows = actions.locateAssetRows(page) const row = assetRows.nth(0) @@ -74,7 +113,9 @@ test.test('change to blank name', async ({ page }) => { await test.expect(row).toHaveText(new RegExp('^' + oldName)) }) -test.test('change to blank name (keyboard)', async ({ page }) => { +test('change to blank name (keyboard)', async ({ page }) => { + await actions.mockAllAndLogin({ page }) + const assetRows = actions.locateAssetRows(page) const row = assetRows.nth(0) diff --git a/app/gui/integration-test/dashboard/mock/enso-demo.main b/app/gui/integration-test/dashboard/mock/enso-demo.main new file mode 100644 index 000000000000..1dc02d6297b0 --- /dev/null +++ b/app/gui/integration-test/dashboard/mock/enso-demo.main @@ -0,0 +1,51 @@ +from Standard.Base import all +from Standard.Table import all +from Standard.Database import all +from Standard.AWS import all +from Standard.Google_Api import all +from Standard.Snowflake import all +import Standard.Visualization + +## ![image](ea.png) + + # Project Goal + + - Rank overall sales performance for the team over the past 3 years. + - The order information lives in the accounting system + - Account Executive information lives in the CRM + - Common ID is the EIN, which has some formatting issues. + + # Enso Benefits + + - Save an average of **7 hours per week** by not repeating work just because the data changes + - Runs locally on Windows, Mac & Linux, as well as in a browser with 100% compabibility. + - All of the configuration is visible on each component, making it easy to identify mistakes, and ensuring auditability of the workflows. + - Data processing is pushed down into the database engine when connected to database sources, using the same components that you use for files. + - Hundreds of components avaiable, ensuring you will have the capabilties to solve any data challenge. + + # Next Steps + + - Download Enso Community Edition from [https://ensoanalytics.com](https://ensoanalytics.com) + - Join the Enso Community at [https://community.ensoanalytics.com](https://community.ensoanalytics.com) + - Spread the word! +main = + node1 = Data.read 'enso:///Teams/Analytics/Demo/Invoice_CRM_Data_June 2024.xlsx' Auto_Detect + node2 = node1.read 'Invoice' + node3 = node1.read 'CRM' + table1 = node2.text_cleanse ['EIN'] [..Symbols] + any1 = table1.set (expr 'text_left([EIN],2) + \'-\' + text_right([EIN],7)') 'EIN' + table2 = any1.parse ['invoice_date'] ..Date 'dd MMMM, yyyy' + any2 = table2.set (..Simple_Expr (..Name 'invoice_date') (..Date ..Year)) 'Year' + any3 = any2.distinct on_problems=..Ignore + table3 = node3.text_cleanse ['EIN'] [..Symbols] + any4 = table3.set (expr 'text_left([EIN],2) + \'-\' + text_right([EIN],7)') 'EIN' + table4 = any3.join any4 ..Inner [(..Equals 'EIN')] + any5 = table4.cross_tab ['account_owner'] 'Year' (..Sum 'invoice_amount') + any6 = any5.sort [(..Name '2024' ..Descending)] + any7 = any6.format [(..By_Type ..Float)] '#,##0.00' + + + +#### METADATA #### +[[{"index":{"value":1396},"size":{"value":72}},"0a68d440-a0b5-4d6e-ad08-4fb7532a69ce"],[{"index":{"value":1396},"size":{"value":84}},"235c06eb-7293-4675-8d18-1396cc74af6f"],[{"index":{"value":1493},"size":{"value":20}},"71560ce4-47ce-4d5f-9c65-cd338e3becc2"],[{"index":{"value":1526},"size":{"value":16}},"3f4b524f-f08d-4508-91f5-c9dfcfdcf326"],[{"index":{"value":1556},"size":{"value":18}},"9d6c459a-c2c2-42dc-ba66-6054834b04d1"],[{"index":{"value":1556},"size":{"value":26}},"78a8a3ef-b447-40e3-874e-ac9fbc63f01f"],[{"index":{"value":1556},"size":{"value":38}},"01372e20-ea45-43d3-9bea-9c5e64c86ab8"],[{"index":{"value":1606},"size":{"value":10}},"40620ee1-9313-4e6c-ad9a-0e76b9aed728"],[{"index":{"value":1606},"size":{"value":68}},"d0eb641d-185a-4494-8c58-3ec6fa441d08"],[{"index":{"value":1606},"size":{"value":74}},"9028d841-3ba4-4053-9e63-3a4c070d72fa"],[{"index":{"value":1694},"size":{"value":10}},"862d0e1a-1cde-45e0-b5d7-2941f0b76795"],[{"index":{"value":1694},"size":{"value":27}},"64d757cc-e159-4241-b7af-e4d195ec4f8c"],[{"index":{"value":1694},"size":{"value":34}},"6788bc65-cef2-43bb-986e-803ae9310b7d"],[{"index":{"value":1694},"size":{"value":50}},"ac101af3-063a-47d1-a252-61d9d7eb0d2f"],[{"index":{"value":1756},"size":{"value":10}},"d10f6ea6-c042-4ec7-bff1-9d578b104621"],[{"index":{"value":1756},"size":{"value":66}},"4446457b-94af-42bd-ae4c-2fa020491b00"],[{"index":{"value":1756},"size":{"value":73}},"a1f38723-34c0-4f3d-b40a-6f35653c9bb3"],[{"index":{"value":1841},"size":{"value":13}},"a794d437-197d-4928-a258-036d78a68e77"],[{"index":{"value":1841},"size":{"value":34}},"f7e98ff9-c11e-4754-a125-21282765a7ba"],[{"index":{"value":1889},"size":{"value":38}},"3d9e288b-a837-4fe9-8151-00267ba5cd6b"],[{"index":{"value":1939},"size":{"value":74}},"f9a338dc-15f0-4274-9ea3-bbd168cfdb03"],[{"index":{"value":2027},"size":{"value":9}},"bdaa0042-93ba-41b9-b55c-6dca20188d42"],[{"index":{"value":2027},"size":{"value":14}},"c64218cc-371d-4966-9c35-193362232836"],[{"index":{"value":2027},"size":{"value":22}},"b7a59daf-d2c8-48cb-aaae-bde8b2eeade1"],[{"index":{"value":2027},"size":{"value":41}},"53d77b34-d71b-48f1-b861-3f18dfdd3d27"],[{"index":{"value":2080},"size":{"value":16}},"92a346f4-d04e-4a53-a669-d0c4f81bf452"],[{"index":{"value":2080},"size":{"value":34}},"e813c5fa-aa4f-4a06-9aa6-9a5760cae47c"],[{"index":{"value":2080},"size":{"value":41}},"3a5a5a46-8a2e-4169-b3cc-fca88346b5e4"],[{"index":{"value":2080},"size":{"value":66}},"f1bd7270-4511-4d17-bc4f-85ad07b203b3"],[{"index":{"value":2158},"size":{"value":9}},"f7d3bfe7-b250-4eb4-a28c-0020259840ed"],[{"index":{"value":2158},"size":{"value":40}},"432cf088-d60f-4d4d-ae56-bb1535f40830"],[{"index":{"value":2210},"size":{"value":11}},"775d1305-355c-46e2-9f66-032a87877a3d"],[{"index":{"value":2210},"size":{"value":33}},"e30d233d-7cd7-4613-83ea-99088ca9180e"],[{"index":{"value":2210},"size":{"value":44}},"90a4ca5f-3fb8-459f-afca-f16f0212f9c6"]] +{"ide":{"node":{"235c06eb-7293-4675-8d18-1396cc74af6f":{"position":{"vector":[453,-1215]},"visualization":{"show":true,"fullscreen":false,"project":{"project":"Builtin"},"name":"Table"}},"0a68d440-a0b5-4d6e-ad08-4fb7532a69ce":{"position":{"vector":[453,-1215]},"visualization":{"show":true,"fullscreen":false,"project":{"project":"Builtin"},"name":"Table"}},"71560ce4-47ce-4d5f-9c65-cd338e3becc2":{"position":{"vector":[453,-1473]}},"3f4b524f-f08d-4508-91f5-c9dfcfdcf326":{"position":{"vector":[1345,-1689]}},"01372e20-ea45-43d3-9bea-9c5e64c86ab8":{"position":{"vector":[453,-1545]}},"78a8a3ef-b447-40e3-874e-ac9fbc63f01f":{"position":{"vector":[453,-1731]}},"9d6c459a-c2c2-42dc-ba66-6054834b04d1":{"position":{"vector":[453,-1731]}},"9028d841-3ba4-4053-9e63-3a4c070d72fa":{"position":{"vector":[453,-1617]}},"d0eb641d-185a-4494-8c58-3ec6fa441d08":{"position":{"vector":[453,-1989]}},"40620ee1-9313-4e6c-ad9a-0e76b9aed728":{"position":{"vector":[453,-1989]}},"ac101af3-063a-47d1-a252-61d9d7eb0d2f":{"position":{"vector":[453,-1689]}},"6788bc65-cef2-43bb-986e-803ae9310b7d":{"position":{"vector":[453,-1875]}},"64d757cc-e159-4241-b7af-e4d195ec4f8c":{"position":{"vector":[453,-1875]}},"862d0e1a-1cde-45e0-b5d7-2941f0b76795":{"position":{"vector":[453,-1875]}},"a1f38723-34c0-4f3d-b40a-6f35653c9bb3":{"position":{"vector":[453,-1779]}},"4446457b-94af-42bd-ae4c-2fa020491b00":{"position":{"vector":[453,-1947]}},"d10f6ea6-c042-4ec7-bff1-9d578b104621":{"position":{"vector":[453,-1947]}},"f7e98ff9-c11e-4754-a125-21282765a7ba":{"position":{"vector":[453,-1851]}},"a794d437-197d-4928-a258-036d78a68e77":{"position":{"vector":[453,-2037]}},"3d9e288b-a837-4fe9-8151-00267ba5cd6b":{"position":{"vector":[1345,-1761]}},"f9a338dc-15f0-4274-9ea3-bbd168cfdb03":{"position":{"vector":[1345,-1833]}},"53d77b34-d71b-48f1-b861-3f18dfdd3d27":{"position":{"vector":[761,-1993]},"visualization":{"show":true,"fullscreen":false}},"b7a59daf-d2c8-48cb-aaae-bde8b2eeade1":{"position":{"vector":[757,-2182]}},"c64218cc-371d-4966-9c35-193362232836":{"position":{"vector":[757,-2182]}},"bdaa0042-93ba-41b9-b55c-6dca20188d42":{"position":{"vector":[757,-2182]}},"f1bd7270-4511-4d17-bc4f-85ad07b203b3":{"position":{"vector":[1269,-2155]},"visualization":{"show":true,"fullscreen":false}},"3a5a5a46-8a2e-4169-b3cc-fca88346b5e4":{"position":{"vector":[761,-2251]}},"e813c5fa-aa4f-4a06-9aa6-9a5760cae47c":{"position":{"vector":[761,-2251]}},"92a346f4-d04e-4a53-a669-d0c4f81bf452":{"position":{"vector":[761,-2251]}},"432cf088-d60f-4d4d-ae56-bb1535f40830":{"position":{"vector":[761,-2509]}},"f7d3bfe7-b250-4eb4-a28c-0020259840ed":{"position":{"vector":[761,-2509]}},"90a4ca5f-3fb8-459f-afca-f16f0212f9c6":{"position":{"vector":[1015,-2691]},"visualization":{"show":true,"fullscreen":false,"width":879.59765625,"height":337.29296875}},"e30d233d-7cd7-4613-83ea-99088ca9180e":{"position":{"vector":[761,-2581]}},"775d1305-355c-46e2-9f66-032a87877a3d":{"position":{"vector":[761,-2581]}}},"import":{}}} \ No newline at end of file diff --git a/app/gui/playwright.config.ts b/app/gui/playwright.config.ts index 512058eb20b0..de0699213873 100644 --- a/app/gui/playwright.config.ts +++ b/app/gui/playwright.config.ts @@ -8,6 +8,7 @@ */ import { defineConfig } from '@playwright/test' import net from 'net' +import path from 'path' const DEBUG = process.env.DEBUG_TEST === 'true' const isCI = process.env.CI === 'true' @@ -22,6 +23,8 @@ const TIMEOUT_MS = // Instead of using workers on CI, we use shards to run tests in parallel. const WORKERS = isCI ? 2 : '35%' +const dirName = path.dirname(new URL(import.meta.url).pathname) + async function findFreePortInRange(min: number, max: number) { for (let i = 0; i < 50; i++) { const portToCheck = Math.floor(Math.random() * (max - min + 1)) + min @@ -122,7 +125,7 @@ export default defineConfig({ use: { baseURL: `http://localhost:${ports.dashboard}`, actionTimeout: TIMEOUT_MS, - storageState: './playwright/.auth/user.json', + storageState: path.join(dirName, './playwright/.auth/user.json'), }, }, { diff --git a/app/gui/src/dashboard/authentication/cognito.ts b/app/gui/src/dashboard/authentication/cognito.ts index f80e08513204..063b40fa4973 100644 --- a/app/gui/src/dashboard/authentication/cognito.ts +++ b/app/gui/src/dashboard/authentication/cognito.ts @@ -221,12 +221,9 @@ export class Cognito { * Will refresh the {@link UserSession} if it has expired. */ async userSession() { - const currentSession = await results.Result.wrapAsync(() => amplify.Auth.currentSession()) - const amplifySession = currentSession.mapErr(intoCurrentSessionErrorType) - - return amplifySession - .map((session) => parseUserSession(session, this.amplifyConfig.userPoolWebClientId)) - .unwrapOr(null) + return amplify.Auth.currentSession() + .then((result) => parseUserSession(result, this.amplifyConfig.userPoolWebClientId)) + .catch(() => null) } /** diff --git a/app/gui/src/dashboard/components/AnimatedBackground.tsx b/app/gui/src/dashboard/components/AnimatedBackground.tsx index d18a8b830021..b56d3b30c293 100644 --- a/app/gui/src/dashboard/components/AnimatedBackground.tsx +++ b/app/gui/src/dashboard/components/AnimatedBackground.tsx @@ -155,7 +155,7 @@ const AnimatedBackgroundItemUnderlay = memo(function AnimatedBackgroundItemUnder {isActive && ( { const delay = 350 @@ -389,7 +391,7 @@ export const Button = memo( variant, iconPosition, showIconOnHover, - extraClickZone: extraClickZoneProp, + extraClickZone, iconOnly: isIconOnly, }) diff --git a/app/gui/src/dashboard/components/AriaComponents/Inputs/OTPInput/OTPInput.tsx b/app/gui/src/dashboard/components/AriaComponents/Inputs/OTPInput/OTPInput.tsx index 09ecd6d118cc..bfaa74ae1b75 100644 --- a/app/gui/src/dashboard/components/AriaComponents/Inputs/OTPInput/OTPInput.tsx +++ b/app/gui/src/dashboard/components/AriaComponents/Inputs/OTPInput/OTPInput.tsx @@ -121,7 +121,7 @@ export const OTPInput = forwardRef(function OTPInput< name, maxLength, noScriptCSSFallback: null, - containerClassName: classes.base(), + containerClassName: classes.base({ className }), onClick: () => { if (innerOtpInputRef.current) { // Check if the input is not already focused diff --git a/app/gui/src/dashboard/components/AriaComponents/Text/Text.tsx b/app/gui/src/dashboard/components/AriaComponents/Text/Text.tsx index 7ee626327ee0..dd288e13188a 100644 --- a/app/gui/src/dashboard/components/AriaComponents/Text/Text.tsx +++ b/app/gui/src/dashboard/components/AriaComponents/Text/Text.tsx @@ -48,13 +48,17 @@ export const TEXT_STYLE = twv.tv({ // leading should always be after the text size to make sure it is not stripped by twMerge variant: { custom: '', - body: 'text-xs leading-[20px] before:h-[1px] after:h-[3px] font-medium', + body: 'text-xs leading-[20px] before:h-[2px] after:h-[2px] macos:before:h-[1px] macos:after:h-[3px] font-medium', // eslint-disable-next-line @typescript-eslint/naming-convention - 'body-sm': 'text-[10.5px] leading-[16px] before:h-[0.5px] after:h-[2.5px] font-medium', - h1: 'text-xl leading-[29px] before:h-0.5 after:h-[5px] font-bold', - subtitle: 'text-[13.5px] leading-[19px] before:h-[1px] after:h-[3px] font-bold', - caption: 'text-[8.5px] leading-[12px] before:h-[0.5px] after:h-[1.5px]', - overline: 'text-[8.5px] leading-[16px] before:h-[0.5px] after:h-[1.5px] uppercase', + 'body-sm': + 'text-[10.5px] leading-[16px] before:h-[1.5px] after:h-[1.5px] macos:before:h-[0.5px] macos:after:h-[2.5px] font-medium', + h1: 'text-xl leading-[29px] before:h-0.5 after:h-[5px] macos:before:h-[3px] macos:after:h-[3px] font-bold', + subtitle: + 'text-[13.5px] leading-[19px] before:h-[2px] after:h-[2px] macos:before:h-[1px] macos:after:h-[3px] font-bold', + caption: + 'text-[8.5px] leading-[12px] before:h-[1px] after:h-[1px] macos:before:h-[0.5px] macos:after:h-[1.5px]', + overline: + 'text-[8.5px] leading-[16px] before:h-[1px] after:h-[1px] macos:before:h-[0.5px] macos:after:h-[1.5px] uppercase', }, weight: { custom: '', diff --git a/app/gui/src/dashboard/components/Autocomplete.tsx b/app/gui/src/dashboard/components/Autocomplete.tsx index d84f4c81add3..67b96556eaf1 100644 --- a/app/gui/src/dashboard/components/Autocomplete.tsx +++ b/app/gui/src/dashboard/components/Autocomplete.tsx @@ -9,7 +9,7 @@ import { } from 'react' import CloseIcon from '#/assets/cross.svg' -import { Button, Input, Text } from '#/components/AriaComponents' +import { Button, Form, Input, Text } from '#/components/AriaComponents' import FocusRing from '#/components/styled/FocusRing' import { twJoin, twMerge } from '#/utilities/tailwindMerge' @@ -185,60 +185,70 @@ export default function Autocomplete(props: AutocompleteProps) { : '', )} > - -
- {canEditText ? - { - setIsDropdownVisible(true) - }} - onBlur={() => { - window.setTimeout(() => { - setIsDropdownVisible(false) - }) - }} - onChange={(event) => { +
+ z.object({ + autocomplete: z.string(), + }) + } + > + +
+ {canEditText ? + { + setIsDropdownVisible(true) + }} + onBlur={() => { + window.setTimeout(() => { + setIsDropdownVisible(false) + }) + }} + onChange={(event) => { + setIsDropdownVisible(true) + setText(event.currentTarget.value === '' ? null : event.currentTarget.value) + }} + /> + : { + setIsDropdownVisible(true) + }} + onBlur={() => { + window.setTimeout(() => { + setIsDropdownVisible(false) + }) + }} + > + {itemsToString?.(values) ?? (values[0] != null ? children(values[0]) : ZWSP)} + + } +
-
+
+
+
{ event.preventDefault() if (inputRef.current != null) { @@ -109,55 +112,57 @@ export default function EditableSpan(props: EditableSpanProps) { } }} > - { - event.stopPropagation() - }} - onKeyDown={(event) => { - if (event.key !== 'Escape') { +
+ { event.stopPropagation() - } - }} - {...(inputPattern == null ? {} : { pattern: inputPattern })} - {...(inputTitle == null ? {} : { title: inputTitle })} - {...(checkSubmittable == null ? - {} - : { - onInput: (event) => { - setIsSubmittable(checkSubmittable(event.currentTarget.value)) - }, - })} - /> - - {isSubmittable && ( + }} + onKeyDown={(event) => { + if (event.key !== 'Escape') { + event.stopPropagation() + } + }} + {...(inputPattern == null ? {} : { pattern: inputPattern })} + {...(inputTitle == null ? {} : { title: inputTitle })} + {...(checkSubmittable == null ? + {} + : { + onInput: (event) => { + setIsSubmittable(checkSubmittable(event.currentTarget.value)) + }, + })} + /> + + {isSubmittable && ( + + )} { + cancelledRef.current = true + onCancel() + window.setTimeout(() => { + cancelledRef.current = false + }) + }} /> - )} - { - cancelledRef.current = true - onCancel() - window.setTimeout(() => { - cancelledRef.current = false - }) - }} - /> - + +
) } else { diff --git a/app/gui/src/dashboard/components/MarkdownViewer/MarkdownViewer.tsx b/app/gui/src/dashboard/components/MarkdownViewer/MarkdownViewer.tsx index 670240cc4308..96970d66f50e 100644 --- a/app/gui/src/dashboard/components/MarkdownViewer/MarkdownViewer.tsx +++ b/app/gui/src/dashboard/components/MarkdownViewer/MarkdownViewer.tsx @@ -4,10 +4,10 @@ import { useSuspenseQuery } from '@tanstack/react-query' import type { RendererObject } from 'marked' import { marked } from 'marked' import { useMemo } from 'react' -import { BUTTON_STYLES, TEXT_STYLE } from '../AriaComponents' +import { BUTTON_STYLES, TEXT_STYLE, type TestIdProps } from '../AriaComponents' /** Props for a {@link MarkdownViewer}. */ -export interface MarkdownViewerProps { +export interface MarkdownViewerProps extends TestIdProps { /** Markdown markup to parse and display. */ readonly text: string readonly imgUrlResolver: (relativePath: string) => Promise @@ -56,7 +56,7 @@ const defaultRenderer: RendererObject = { * Parses markdown passed in as a `text` prop into HTML and displays it. */ export function MarkdownViewer(props: MarkdownViewerProps) { - const { text, imgUrlResolver, renderer = defaultRenderer } = props + const { text, imgUrlResolver, renderer = defaultRenderer, testId } = props const markedInstance = useMemo( () => marked.use({ renderer: Object.assign({}, defaultRenderer, renderer), async: true }), @@ -75,7 +75,12 @@ export function MarkdownViewer(props: MarkdownViewerProps) { }, }), }) - - // eslint-disable-next-line @typescript-eslint/naming-convention - return
+ return ( +
+ ) } diff --git a/app/gui/src/dashboard/components/dashboard/TheModal.tsx b/app/gui/src/dashboard/components/dashboard/TheModal.tsx index c2a3660c7596..947af8abfc3b 100644 --- a/app/gui/src/dashboard/components/dashboard/TheModal.tsx +++ b/app/gui/src/dashboard/components/dashboard/TheModal.tsx @@ -15,7 +15,13 @@ export default function TheModal() { return ( {modal && ( - + {/* This component suppresses the warning about the target not being pressable element. */} diff --git a/app/gui/src/dashboard/components/dashboard/column/SharedWithColumn.tsx b/app/gui/src/dashboard/components/dashboard/column/SharedWithColumn.tsx index 0f7fba36c747..b32857d72d19 100644 --- a/app/gui/src/dashboard/components/dashboard/column/SharedWithColumn.tsx +++ b/app/gui/src/dashboard/components/dashboard/column/SharedWithColumn.tsx @@ -5,9 +5,7 @@ import Plus2Icon from '#/assets/plus2.svg' import { Button } from '#/components/AriaComponents' import type { AssetColumnProps } from '#/components/dashboard/column' import PermissionDisplay from '#/components/dashboard/PermissionDisplay' -import { PaywallDialogButton } from '#/components/Paywall' import AssetEventType from '#/events/AssetEventType' -import { usePaywall } from '#/hooks/billing' import { useDispatchAssetEvent } from '#/layouts/AssetsTable/EventListProvider' import ManagePermissionsModal from '#/modals/ManagePermissionsModal' import { useFullUserSession } from '#/providers/AuthProvider' @@ -37,9 +35,9 @@ export default function SharedWithColumn(props: SharedWithColumnPropsInternal) { const { backend, category, setQuery } = state const { user } = useFullUserSession() + const dispatchAssetEvent = useDispatchAssetEvent() - const { isFeatureUnderPaywall } = usePaywall({ plan: user.plan }) - const isUnderPaywall = isFeatureUnderPaywall('share') + const assetPermissions = item.permissions ?? [] const { setModal } = useSetModal() const self = tryFindSelfPermission(user, item.permissions) @@ -76,16 +74,7 @@ export default function SharedWithColumn(props: SharedWithColumnPropsInternal) { {getAssetPermissionName(other)} ))} - {isUnderPaywall && ( - - )} - {managesThisAsset && !isUnderPaywall && ( + {managesThisAsset && (
) diff --git a/app/gui/src/dashboard/hooks/autoFocusHooks.ts b/app/gui/src/dashboard/hooks/autoFocusHooks.ts index a765d0b0df76..4c1a68c0a3c5 100644 --- a/app/gui/src/dashboard/hooks/autoFocusHooks.ts +++ b/app/gui/src/dashboard/hooks/autoFocusHooks.ts @@ -13,7 +13,7 @@ export interface UseAutoFocusProps { readonly disabled?: boolean | undefined } -const FOCUS_TRYOUT_DELAY = 300 +const FOCUS_TRYOUT_DELAY = 1_000 const FOCUS_DELAY = 100 /** diff --git a/app/gui/src/dashboard/layouts/AssetDocs/AssetDocs.tsx b/app/gui/src/dashboard/layouts/AssetDocs/AssetDocs.tsx index 5584983f4f6f..cdfe66a47fa1 100644 --- a/app/gui/src/dashboard/layouts/AssetDocs/AssetDocs.tsx +++ b/app/gui/src/dashboard/layouts/AssetDocs/AssetDocs.tsx @@ -9,7 +9,6 @@ import { useStore } from '#/utilities/zustand' import { useSuspenseQuery } from '@tanstack/react-query' import { useCallback } from 'react' import * as ast from 'ydoc-shared/ast' -import { normalizedMarkdownToStandard } from 'ydoc-shared/ast/documentation' import { splitFileContents } from 'ydoc-shared/ensoFile' import { versionContentQueryOptions } from '../AssetDiffView/useFetchVersionContent' import { assetPanelStore } from '../AssetPanel' @@ -49,12 +48,12 @@ export function AssetDocsContent(props: AssetDocsContentProps) { const { data: docs } = useSuspenseQuery({ ...versionContentQueryOptions({ backend, projectId: item.id, metadata: false }), select: (data) => { - const withoutMeta = splitFileContents(data) - const module = ast.parseModule(withoutMeta.code) + const { code } = splitFileContents(data) + const module = ast.parseModule(code) for (const statement of module.statements()) { if (statement instanceof ast.MutableFunctionDef && statement.name.code() === 'main') { - return normalizedMarkdownToStandard(statement.mutableDocumentationMarkdown().toJSON()) + return statement.mutableDocumentationMarkdown().toJSON() } } @@ -71,5 +70,11 @@ export function AssetDocsContent(props: AssetDocsContentProps) { return } - return + return ( + + ) } diff --git a/app/gui/src/dashboard/layouts/AssetPanel/AssetPanel.tsx b/app/gui/src/dashboard/layouts/AssetPanel/AssetPanel.tsx index fc20dad83203..b3e17b49e811 100644 --- a/app/gui/src/dashboard/layouts/AssetPanel/AssetPanel.tsx +++ b/app/gui/src/dashboard/layouts/AssetPanel/AssetPanel.tsx @@ -21,7 +21,7 @@ import { AssetDocs } from '../AssetDocs' import AssetProjectSessions from '../AssetProjectSessions' import AssetProperties from '../AssetProperties' import AssetVersions from '../AssetVersions/AssetVersions' -import type { Category } from '../CategorySwitcher/Category' +import { isLocalCategory, type Category } from '../CategorySwitcher/Category' import { assetPanelStore, useIsAssetPanelExpanded, @@ -78,7 +78,7 @@ export function AssetPanel(props: AssetPanelProps) { initial={{ opacity: 0, x: ASSET_SIDEBAR_COLLAPSED_WIDTH }} animate={{ opacity: 1, x: 0 }} exit={{ opacity: 0, x: ASSET_SIDEBAR_COLLAPSED_WIDTH }} - className="absolute bottom-0 right-0 top-0 flex flex-col bg-background-hex" + className="absolute bottom-0 right-0 top-0 flex flex-col" onClick={(event) => { // Prevent deselecting Assets Table rows. event.stopPropagation() @@ -115,6 +115,7 @@ const InternalAssetPanelTabs = memo(function InternalAssetPanelTabs( }) const isReadonly = category.type === 'trash' + const isLocal = isLocalCategory(category) const { getText } = useText() @@ -156,7 +157,6 @@ const InternalAssetPanelTabs = memo(function InternalAssetPanelTabs( {isExpanded && ( - + (null) @@ -59,8 +59,9 @@ export const AssetPanelTab = memo(function AssetPanelTab(props: AssetPanelTabPro ref={tabRef} id={id} aria-label={label} - className="aspect-square w-full cursor-pointer" + className="aspect-square w-full cursor-pointer disabled:cursor-not-allowed disabled:opacity-50" data-testid={`asset-panel-tab-${id}`} + isDisabled={isDisabled} > {({ isSelected, isHovered }) => { const isActive = isSelected && isExpanded @@ -83,7 +84,7 @@ export const AssetPanelTab = memo(function AssetPanelTab(props: AssetPanelTabPro variants={{ active: { opacity: 1 }, inactive: { opacity: 0 } }} initial="inactive" animate={!isActive && isHovered ? 'active' : 'inactive'} - className="absolute inset-x-1.5 inset-y-1.5 rounded-full bg-invert transition-colors duration-300" + className="absolute inset-x-1.5 inset-y-1.5 rounded-full bg-background transition-colors duration-300" />
diff --git a/app/gui/src/dashboard/layouts/AssetProperties.tsx b/app/gui/src/dashboard/layouts/AssetProperties.tsx index ed2c214ca37c..1ebcf95ea778 100644 --- a/app/gui/src/dashboard/layouts/AssetProperties.tsx +++ b/app/gui/src/dashboard/layouts/AssetProperties.tsx @@ -74,6 +74,10 @@ export default function AssetProperties(props: AssetPropertiesProps) { const { getText } = useText() + if (backend.type === BackendType.local) { + return + } + if (item == null || path == null) { return } diff --git a/app/gui/src/dashboard/layouts/AssetsTable.tsx b/app/gui/src/dashboard/layouts/AssetsTable.tsx index dba73cff8954..187cbfa0e0fd 100644 --- a/app/gui/src/dashboard/layouts/AssetsTable.tsx +++ b/app/gui/src/dashboard/layouts/AssetsTable.tsx @@ -1900,9 +1900,36 @@ export default function AssetsTable(props: AssetsTableProps) { resetErrorBoundary={reconnectToProjectManager} /> :
+
+ + {(columnsBarProps) => ( +
()(columnsBarProps, { + className: 'inline-flex gap-icons', + onFocus: () => { + setKeyboardSelectedIndex(null) + }, + })} + > + {hiddenColumns.map((column) => ( + + ))} +
+ )} +
+
+ {(innerProps) => ( - +
()(innerProps, { className: @@ -1943,34 +1970,6 @@ export default function AssetsTable(props: AssetsTableProps) { /> )}
-
-
- - {(columnsBarProps) => ( -
()(columnsBarProps, { - className: 'inline-flex gap-icons', - onFocus: () => { - setKeyboardSelectedIndex(null) - }, - })} - > - {hiddenColumns.map((column) => ( - - ))} -
- )} -
-
-
{table}
@@ -1999,7 +1998,7 @@ export default function AssetsTable(props: AssetsTableProps) { } /** - * + * Props for the HiddenColumn component. */ interface HiddenColumnProps { readonly column: Column @@ -2027,8 +2026,8 @@ const HiddenColumn = memo(function HiddenColumn(props: HiddenColumnProps) { return (