Merge branch 'develop' into wip/sb/async-execution

This commit is contained in:
somebody1234 2024-11-21 21:38:18 +10:00
commit b7e5b812ca
39 changed files with 2304 additions and 169 deletions

196
.github/workflows/gui-checks.yml vendored Normal file
View File

@ -0,0 +1,196 @@
name: GUI Checks
on: workflow_call
# Cancel in-progress workflows if a new one is started
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}-gui-checks
cancel-in-progress: true
permissions:
contents: read # Read-only access to repository contents
issues: write # Write access to issues
pull-requests: write # Write access to pull requests
statuses: write # Write access to commit statuses
checks: write
jobs:
lint:
name: 👮 Lint GUI
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.ref }}
- name: 📦 Setup pnpm
uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
name: ⎔ Setup Node
with:
node-version-file: .node-version
cache: "pnpm"
- uses: actions/cache/restore@v4
name: Download cache
id: cache
with:
path: |
**/.eslintcache
node_modules/.cache/prettier
key: ${{ runner.os }}-gui-${{ github.run_id }}
restore-keys: |
${{ runner.os }}-gui
- if: startsWith(runner.name, 'GitHub Actions') || startsWith(runner.name, 'Hosted Agent')
name: Installing wasm-pack
uses: jetli/wasm-pack-action@v0.4.0
with:
version: v0.12.1
- name: 📦 Install dependencies
run: pnpm install --frozen-lockfile
- name: 📝 Prettier
id: prettier
continue-on-error: true
run: pnpm run ci:prettier
# Next Tasks are depend on Typecheck, because we build libraries at this stage
- name: 🧠 Typecheck
id: typecheck
continue-on-error: true
run: pnpm run ci:typecheck
- name: 🧹 Lint
id: lint
continue-on-error: true
run: pnpm run ci:lint
- name: 🧪 Unit Tests
id: unit-tests
continue-on-error: true
run: pnpm run ci:test
- name: 💾 Save cache
uses: actions/cache/save@v4
if: always() && steps.cache.outputs.cache-hit != 'true'
id: save-cache
with:
key: ${{ steps.cache.outputs.cache-primary-key }}
path: |
**/.eslintcache
node_modules/.cache/prettier
- name: 📝 Annotate Code Linting Results
uses: ataylorme/eslint-annotate-action@v3
with:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
report-json: "./**/eslint_report.json"
markdown-report-on-step-summary: true
check-name: 🧹 GUI Lint Results
- name: ❌ Fail if any check failed
if: always() && (steps.prettier.outcome == 'failure' || steps.lint.outcome == 'failure' || steps.typecheck.outcome == 'failure' || steps.unit-tests.outcome == 'failure')
run: |
echo "Prettier outcome: ${{ steps.prettier.outcome }}"
echo "Lint outcome: ${{ steps.lint.outcome }}"
echo "Typecheck outcome: ${{ steps.typecheck.outcome }}"
echo "Unit tests outcome: ${{ steps.unit-tests.outcome }}"
exit 1
playwright:
name: 🎭 Playwright Tests
env:
NODE_OPTIONS: --disable-warning=ExperimentalWarning
runs-on:
- self-hosted
- Linux
timeout-minutes: 60
strategy:
fail-fast: false
max-parallel: 24
matrix:
shardIndex: [1, 2, 3, 4, 5, 6]
shardTotal: [6]
steps:
- uses: actions/checkout@v4
- name: 📦 Setup pnpm
uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
name: ⎔ Setup Node
with:
node-version-file: .node-version
cache: "pnpm"
- if: startsWith(runner.name, 'GitHub Actions') || startsWith(runner.name, 'Hosted Agent')
name: Installing wasm-pack
uses: jetli/wasm-pack-action@v0.4.0
with:
version: v0.12.1
- name: 📦 Install dependencies
run: pnpm install --frozen-lockfile
- name: 📺 Install Playwright Browsers
working-directory: app/gui
run: pnpm run playwright:install
- name: 🎭 Playwright Tests
working-directory: app/gui
run: pnpm run e2e --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
- name: ⬆️ Upload blob report to GitHub Actions Artifacts
if: ${{ !cancelled() }}
uses: actions/upload-artifact@v4
with:
name: blob-report-${{ matrix.shardIndex }}
path: app/gui/blob-report
retention-days: 7
merge-reports:
name: 🔗 Merge Playwright Reports
if: ${{ !cancelled() }}
needs: [playwright]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: 📦 Setup pnpm
uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version-file: .node-version
cache: "pnpm"
- if: startsWith(runner.name, 'GitHub Actions') || startsWith(runner.name, 'Hosted Agent')
name: Installing wasm-pack
uses: jetli/wasm-pack-action@v0.4.0
with:
version: v0.12.1
- name: 📦 Install dependencies
run: pnpm install --frozen-lockfile --ignore-scripts
- name: 📥 Download blob reports from GitHub Actions Artifacts
uses: actions/download-artifact@v4
with:
path: app/gui/blob-report/
pattern: blob-report-*
merge-multiple: true
- name: 🔗 Merge into HTML Report
working-directory: app/gui
run: pnpm playwright merge-reports --reporter html ./blob-report
- name: ⬆️ Upload HTML report
uses: actions/upload-artifact@v4
with:
name: playwright-report--attempt-${{ github.run_attempt }}
path: app/gui/playwright-report/
retention-days: 14

83
.github/workflows/gui-pull-request.yml vendored Normal file
View File

@ -0,0 +1,83 @@
# This file is not auto-generated. Feel free to edit it.
name: ✨ GUI Pull Request
on:
push:
branches:
- develop
pull_request:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read # Read-only access to repository contents
issues: write # Write access to issues
pull-requests: write # Write access to pull requests
statuses: write # Write access to commit statuses
checks: write
jobs:
changed-files:
runs-on: ubuntu-latest
name: 🔍 Detect changed files in GUI
outputs:
all_changed_files: ${{ steps.changed-files.outputs.all_changed_files }}
any_changed: ${{ steps.changed-files.outputs.any_changed }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 2
- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@v44
with:
files: |
app/**
package.json
pnpm-lock.yaml
pnpm-workspace.yaml
eslint.config.js
.prettierrc.js
.prettierignore
vitest.workspace.ts
files_ignore: |
app/ide-desktop/**
app/gui/scripts/**
app/gui/.gitignore
.git-*
- name: List all changed files
env:
ALL_CHANGED_FILES: ${{ steps.changed-files.outputs.all_changed_files }}
run: |
for file in ${ALL_CHANGED_FILES}; do
echo "$file was changed"
done
checks:
name: 🧰 Checks
uses: ./.github/workflows/gui-checks.yml
needs: [changed-files]
if: ${{ needs.changed-files.outputs.any_changed == 'true' }}
secrets: inherit
storybook:
name: 📚 Deploy Storybook
uses: ./.github/workflows/storybook.yml
needs: [changed-files]
if: ${{ needs.changed-files.outputs.any_changed == 'true' }}
secrets: inherit
# This job is used to report success if the needed jobs were successful.
# This is a workaround to make optional jobs required if they run
report-success:
name: ✅ Success or skipped due to no changes
runs-on: ubuntu-latest
needs: [checks, storybook]
if: needs.checks.result == 'skipped' && needs.storybook.result == 'skipped' || needs.checks.result == 'success' && needs.storybook.result == 'success'
steps:
- name: Report success
run: echo "Success!"

View File

@ -27,49 +27,6 @@ jobs:
access_token: ${{ github.token }}
permissions:
actions: write
enso-build-ci-gen-job-gui-check-linux-amd64:
name: GUI tests (linux, amd64)
runs-on:
- self-hosted
- Linux
steps:
- if: startsWith(runner.name, 'GitHub Actions') || startsWith(runner.name, 'Hosted Agent')
name: Installing wasm-pack
uses: jetli/wasm-pack-action@v0.4.0
with:
version: v0.12.1
- name: Expose Artifact API and context information.
uses: actions/github-script@v7
with:
script: "\n core.exportVariable(\"ACTIONS_RUNTIME_TOKEN\", process.env[\"ACTIONS_RUNTIME_TOKEN\"])\n core.exportVariable(\"ACTIONS_RUNTIME_URL\", process.env[\"ACTIONS_RUNTIME_URL\"])\n core.exportVariable(\"GITHUB_RETENTION_DAYS\", process.env[\"GITHUB_RETENTION_DAYS\"])\n console.log(context)\n "
- name: Checking out the repository
uses: actions/checkout@v4
with:
clean: false
submodules: recursive
- name: Build Script Setup
run: ./run --help || (git clean -ffdx && ./run --help)
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- if: "(contains(github.event.pull_request.labels.*.name, 'CI: Clean build required') || inputs.clean_build_required)"
name: Clean before
run: ./run git-clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- run: ./run gui check
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- if: failure() && runner.os == 'Windows'
name: List files if failed (Windows)
run: Get-ChildItem -Force -Recurse
- if: failure() && runner.os != 'Windows'
name: List files if failed (non-Windows)
run: ls -lAR
- if: "(always()) && (contains(github.event.pull_request.labels.*.name, 'CI: Clean build required') || inputs.clean_build_required)"
name: Clean after
run: ./run git-clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
enso-build-ci-gen-job-lint-linux-amd64:
name: Lint (linux, amd64)
runs-on:

104
.github/workflows/storybook.yml vendored Normal file
View File

@ -0,0 +1,104 @@
# This file is not auto-generated. Feel free to edit it.
name: Storybook Chromatic Deployment
on: workflow_call
# Cancel in-progress workflows if a new one is started
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}-chromatic
cancel-in-progress: true
permissions:
contents: read # Read-only access to repository contents
issues: write # Write access to issues
pull-requests: write # Write access to pull requests
statuses: write # Write access to commit statuses
env:
ENSO_BUILD_SKIP_VERSION_CHECK: "true"
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
jobs:
deploy-chromatic-react:
name: 🚀 Deploy React to Chromatic
runs-on: ubuntu-latest
outputs:
dashboardUrl: ${{ steps.publish_chromatic.outputs.url }}
dashboardStorybookUrl: ${{ steps.publish_chromatic.outputs.storybookUrl }}
env:
CHROMATIC_RETRIES: 3
CHROMATIC_PROJECT_TOKEN: ${{ secrets.DASHBOARD_CHROMATIC_PROJECT_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- uses: actions/checkout@v4
name: Checkout
with:
fetch-depth: 0
ref: ${{ github.event.pull_request.head.ref }}
fetch-tags: false
- name: 📦 Setup pnpm
uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
name: ⎔ Setup Node
with:
node-version-file: .node-version
cache: "pnpm"
- if: startsWith(runner.name, 'GitHub Actions') || startsWith(runner.name, 'Hosted Agent')
name: Installing wasm-pack
uses: jetli/wasm-pack-action@v0.4.0
with:
version: v0.12.1
- name: 📦 Install dependencies
run: pnpm install --frozen-lockfile
- name: 📥 Download storybook cache
uses: actions/cache@v4
with:
key: ${{ runner.os }}-gui-${{ github.run_id }}
path: app/gui/node_modules/.cache/
restore-keys: |
${{ runner.os }}-gui
- name: 🚀 Deploy to Chromatic
id: publish_chromatic
uses: chromaui/action@v11
with:
workingDir: app/gui
autoAcceptChanges: develop
exitZeroOnChanges: true
exitOnceUploaded: true
configFile: "chromatic.config.json"
comment-on-pr:
name: 💬 Comment on PR
runs-on: ubuntu-latest
needs: deploy-chromatic-react
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: .node-version
- name: 💬 Comment on PR
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const { setMessage } = await import('${{ github.workspace }}/app/gui/scripts/ci/set-message.js')
await setMessage({
header: "## 🧪 Storybook is successfully deployed!",
body: `
### 📊 Dashboard:
- 👀 Review changes: ${{ needs.deploy-chromatic-react.outputs.dashboardUrl }}
- 👨‍🎨 Preview storybook: ${{ needs.deploy-chromatic-react.outputs.dashboardStorybookUrl }}
`,
github,
repo: context.repo,
prNumber: context.payload.pull_request.number
})

2
.gitignore vendored
View File

@ -37,6 +37,8 @@ generated/
############
node_modules/
eslint_report.json
.eslintcache
############
## System ##

View File

@ -29,7 +29,7 @@
},
"scripts": {
"test": "vitest run",
"lint": "eslint . --max-warnings=0"
"lint": "eslint . --cache --max-warnings=0"
},
"peerDependencies": {
"@tanstack/query-core": "5.54.1",

View File

@ -46,24 +46,9 @@ export async function readEnvironmentFromFile() {
}
process.env.ENSO_CLOUD_DASHBOARD_VERSION ??= buildInfo.version ?? '0.0.0-dev'
process.env.ENSO_CLOUD_DASHBOARD_COMMIT_HASH ??= buildInfo.commit
} catch (error) {
} catch {
process.env.ENSO_CLOUD_DASHBOARD_VERSION ??= buildInfo.version
process.env.ENSO_CLOUD_DASHBOARD_COMMIT_HASH ??= buildInfo.commit
const expectedKeys = Object.keys(DUMMY_DEFINES)
.map(key => key.replace(/^process[.]env[.]/, ''))
.filter(key => key !== 'NODE_ENV')
/** @type {string[]} */
const missingKeys = []
for (const key of expectedKeys) {
if (!(key in process.env)) {
missingKeys.push(key)
}
}
if (missingKeys.length !== 0) {
console.warn('Could not load `.env` file; disabling cloud backend.')
console.warn(`Missing keys: ${missingKeys.map(key => `'${key}'`).join(', ')}`)
console.error(error)
}
}
}

4
app/gui/.gitignore vendored
View File

@ -25,7 +25,11 @@ mockDist
test-results/
playwright-report/
blob-report/
playwright/
src/project-view/util/iconList.json
src/project-view/util/iconName.ts
*storybook.log
storybook-static

9
app/gui/.storybook/env.d.ts vendored Normal file
View File

@ -0,0 +1,9 @@
declare global {
interface Window {
ENV: {
FRAMEWORK: 'vue' | 'react'
}
}
}
export {}

View File

@ -0,0 +1,63 @@
/**
* @file
*
* Main file for Storybook configuration.
*/
import type { StorybookConfig as ReactStorybookConfig } from '@storybook/react-vite'
import type { StorybookConfig as VueStorybookConfig } from '@storybook/vue3-vite'
import z from 'zod'
const framework = z.enum(['vue', 'react']).parse(process.env.FRAMEWORK)
const sharedConfig: Partial<ReactStorybookConfig> = {
addons: [
'@storybook/addon-onboarding',
'@storybook/addon-essentials',
'@chromatic-com/storybook',
'@storybook/addon-interactions',
],
features: {},
core: { disableTelemetry: true },
env: { FRAMEWORK: framework },
previewHead: (head) => {
return `
<script>
window.global = window;
window.ENV = {
FRAMEWORK: '${framework}',
}
</script>
${head}
`
},
}
const vueConfig: VueStorybookConfig = {
...sharedConfig,
stories: [
'../src/project-view/**/*.mdx',
'../src/project-view/**/*.stories.@(js|jsx|mjs|ts|tsx)',
],
framework: {
name: '@storybook/vue3-vite',
options: {},
},
refs: {
Dashboard: {
title: 'Dashboard',
url: 'http://localhost:6007',
},
},
}
const reactConfig: ReactStorybookConfig = {
...sharedConfig,
stories: ['../src/dashboard/**/*.mdx', '../src/dashboard/**/*.stories.tsx'],
framework: {
name: '@storybook/react-vite',
options: { strictMode: true },
},
}
export default framework === 'vue' ? vueConfig : reactConfig

View File

@ -0,0 +1,69 @@
/**
* @file Storybook preview
*/
import type { Preview as ReactPreview } from '@storybook/react'
import type { Preview as VuePreview } from '@storybook/vue3'
import React, { useLayoutEffect, useState } from 'react'
import invariant from 'tiny-invariant'
import UIProviders from '../src/dashboard/components/UIProviders'
import z from 'zod'
import '../src/dashboard/tailwind.css'
const framework = z.enum(['vue', 'react']).parse(window.ENV.FRAMEWORK)
const vuePreview: VuePreview = {
parameters: {
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
},
},
},
}
const reactPreview: ReactPreview = {
parameters: {
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
},
},
},
// Decorators for all stories
// Decorators are applied in the reverse order they are defined
decorators: [
(Story, context) => {
const [portalRoot, setPortalRoot] = useState<Element | null>(null)
useLayoutEffect(() => {
const portalRoot = document.querySelector('#enso-portal-root')
invariant(portalRoot, 'PortalRoot element not found')
setPortalRoot(portalRoot)
}, [])
if (!portalRoot) return <></>
return (
<UIProviders locale="en-US" portalRoot={portalRoot}>
{Story(context)}
</UIProviders>
)
},
(Story, context) => (
<>
<div className="enso-dashboard">{Story(context)}</div>
<div id="enso-portal-root" className="enso-portal-root" />
</>
),
],
}
const preview = framework === 'vue' ? vuePreview : reactPreview
export default preview

View File

@ -0,0 +1,8 @@
{
"$schema": "https://www.chromatic.com/config-file.schema.json",
"projectId": "Enso Dashboard",
"autoAcceptChanges": "main",
"exitOnceUploaded": true,
"skip": "dependabot/**",
"buildScriptName": "build-storybook:react"
}

View File

@ -22,18 +22,22 @@
"build-cloud": "cross-env CLOUD_BUILD=true corepack pnpm run build",
"preview": "vite preview",
"//": "max-warnings set to 41 to match the amount of warnings introduced by the new react compiler. Eventual goal is to remove all the warnings.",
"lint": "eslint . --max-warnings=39",
"lint": "eslint . --cache --max-warnings=39",
"format": "prettier --version && prettier --write src/ && eslint . --fix",
"dev:vite": "vite",
"test": "corepack pnpm run /^^^^test:.*/",
"test:unit": "vitest run",
"test-dev:unit": "vitest",
"test:e2e": "cross-env NODE_ENV=production playwright test",
"test-dev:e2e": "cross-env NODE_ENV=production playwright test --ui",
"test-dev-dashboard:e2e": "cross-env NODE_ENV=production playwright test ./e2e/dashboard/ --ui",
"preinstall": "corepack pnpm run generate-metadata",
"postinstall": "playwright install",
"generate-metadata": "node scripts/generateIconMetadata.js"
"generate-metadata": "node scripts/generateIconMetadata.js",
"build-storybook:react": "cross-env FRAMEWORK=react storybook build",
"build-storybook:vue": "cross-env FRAMEWORK=vue storybook build",
"chromatic:react": "cross-env FRAMEWORK=react chromatic deploy",
"chromatic:vue": "cross-env FRAMEWORK=vue chromatic deploy",
"e2e": "cross-env NODE_ENV=production playwright test",
"playwright:install": "playwright install chromium"
},
"dependencies": {
"@aws-amplify/auth": "5.6.5",
@ -59,7 +63,7 @@
"react": "^18.3.1",
"react-aria": "^3.34.3",
"react-aria-components": "^1.3.3",
"react-compiler-runtime": "19.0.0-beta-6fc168f-20241025",
"react-compiler-runtime": "19.0.0-beta-a7bf2bd-20241110",
"react-dom": "^18.3.1",
"react-error-boundary": "4.0.13",
"react-hook-form": "^7.51.4",
@ -80,7 +84,7 @@
"@ag-grid-enterprise/core": "^31.1.1",
"@ag-grid-enterprise/range-selection": "^31.1.1",
"@babel/parser": "^7.24.7",
"babel-plugin-react-compiler": "19.0.0-beta-6fc168f-20241025",
"babel-plugin-react-compiler": "19.0.0-beta-a7bf2bd-20241110",
"@codemirror/commands": "^6.6.0",
"@codemirror/language": "^6.10.2",
"@codemirror/lang-markdown": "^v6.3.0",
@ -125,10 +129,20 @@
"marked": "14.1.3"
},
"devDependencies": {
"@chromatic-com/storybook": "^3.2.2",
"@fast-check/vitest": "^0.0.8",
"@modyfi/vite-plugin-yaml": "^1.0.4",
"@playwright/test": "^1.40.0",
"@react-types/shared": "^3.22.1",
"@storybook/addon-essentials": "^8.4.2",
"@storybook/addon-interactions": "^8.4.2",
"@storybook/addon-onboarding": "^8.4.2",
"@storybook/blocks": "^8.4.2",
"@storybook/react": "^8.4.2",
"@storybook/react-vite": "^8.4.2",
"@storybook/test": "^8.4.2",
"@storybook/vue3": "^8.4.2",
"@storybook/vue3-vite": "^8.4.2",
"@tanstack/react-query-devtools": "5.45.1",
"@types/node": "^22.9.0",
"@types/react": "^18.0.27",
@ -181,6 +195,8 @@
"react-dom": "^18.3.1",
"shuffle-seed": "^1.1.6",
"sql-formatter": "^13.0.0",
"storybook": "^8.4.2",
"chromatic": "11.18.1",
"tar": "^6.2.1",
"tsx": "^4.7.1",
"vite-plugin-vue-devtools": "7.6.3",

View File

@ -19,7 +19,8 @@ const TIMEOUT_MS =
: 15_000
// We tend to use less CPU on CI to reduce the number of failures due to timeouts.
const WORKERS = isCI ? '25%' : '35%'
// Instead of using workers on CI, we use shards to run tests in parallel.
const WORKERS = isCI ? 2 : '35%'
async function findFreePortInRange(min: number, max: number) {
for (let i = 0; i < 50; i++) {
@ -62,12 +63,13 @@ process.env.PLAYWRIGHT_PORT_PV = `${ports.projectView}`
export default defineConfig({
fullyParallel: true,
...(WORKERS ? { workers: WORKERS } : {}),
forbidOnly: !!process.env.CI,
repeatEach: process.env.CI ? 3 : 1,
reporter: 'html',
forbidOnly: isCI,
reporter: isCI ? ([['list'], ['blob']] as const) : ([['list']] as const),
retries: isCI ? 3 : 0,
use: {
headless: !DEBUG,
actionTimeout: 5000,
trace: 'retain-on-failure',
...(DEBUG ?
{}

View File

@ -0,0 +1,64 @@
/**
* @param {string | TemplateStringsArray} strings
* @param {...unknown[]} values
* @returns {string}
*/
export function dedent(strings, ...values) {
const raw = typeof strings === 'string' ? [strings] : strings.raw
const escapeSpecialCharacters = Array.isArray(strings)
// first, perform interpolation
let result = ''
for (let i = 0; i < raw.length; i++) {
let next = raw[i]
if (next === undefined) {
continue
}
if (escapeSpecialCharacters) {
// handle escaped newlines, backticks, and interpolation characters
next = next
.replace(/\\\n[ \t]*/g, '')
.replace(/\\`/g, '`')
.replace(/\\\$/g, '$')
.replace(/\\\{/g, '{')
}
result += next
if (i < values.length) {
result += values[i]
}
}
// now strip indentation
const lines = result.split('\n')
let mindent = null
for (const l of lines) {
const m = l.match(/^(\s+)\S+/)
if (m && m[1]) {
const indent = m[1].length
if (!mindent) {
// this is the first indented line
mindent = indent
} else {
mindent = Math.min(mindent, indent)
}
}
}
if (mindent !== null) {
const m = mindent
result = lines.map((l) => (l[0] === ' ' || l[0] === '\t' ? l.slice(m) : l)).join('\n')
}
// dedent eats leading and trailing whitespace too
result = result.trim()
if (escapeSpecialCharacters) {
// handle escaped newlines at the end to ensure they don't get stripped too
result = result.replace(/\\n/g, '\n')
}
return result
}

View File

@ -0,0 +1,35 @@
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-nocheck
import { dedent } from './dedent.js'
export async function setMessage({ header, body, prNumber, repo, github }) {
const commentList = await github.paginate(
'GET /repos/:owner/:repo/issues/:issue_number/comments',
// eslint-disable-next-line camelcase
{ ...repo, issue_number: prNumber },
)
const commentBody = dedent`
${header}
${body}
`
const comment = commentList.find((comment) => comment.body.startsWith(header))
if (!comment) {
await github.rest.issues.createComment({
...repo,
// eslint-disable-next-line camelcase
issue_number: prNumber,
body: commentBody,
})
} else {
await github.rest.issues.updateComment({
...repo,
// eslint-disable-next-line camelcase
comment_id: comment.id,
body: commentBody,
})
}
}

View File

@ -0,0 +1,103 @@
import Enso from '#/assets/enso_logo.svg'
import type * as aria from '#/components/aria'
import { Text } from '#/components/AriaComponents'
import type { Meta, StoryObj } from '@storybook/react'
import { expect, userEvent, within } from '@storybook/test'
import type { BaseButtonProps } from './Button'
import { Button } from './Button'
type Story = StoryObj<BaseButtonProps<aria.ButtonRenderProps>>
export default {
title: 'Components/AriaComponents/Button',
component: Button,
render: (props) => <Button {...props} />,
} as Meta<BaseButtonProps<aria.ButtonRenderProps>>
export const Variants: Story = {
render: () => (
<div className="flex flex-col gap-4">
<Text.Heading>Variants</Text.Heading>
<div className="grid grid-cols-4 place-content-start place-items-start gap-3">
<Button>Default</Button>
<Button variant="primary">Primary</Button>
<Button variant="accent">Accent</Button>
<Button variant="delete">Delete</Button>
<Button variant="ghost-fading">Ghost Fading</Button>
<Button variant="ghost">Ghost</Button>
<Button variant="link">Link</Button>
<Button variant="submit">Submit</Button>
<Button variant="outline">Outline</Button>
</div>
<Text.Heading>Sizes</Text.Heading>
<div className="grid grid-cols-4 place-content-center place-items-start gap-3">
<Button size="hero">Hero</Button>
<Button size="large">Large</Button>
<Button size="medium">Medium</Button>
<Button size="small">Small</Button>
<Button size="xsmall">XSmall</Button>
<Button size="xxsmall">XXSmall</Button>
</div>
<Text.Heading>Icons</Text.Heading>
<div className="grid grid-cols-4 place-content-center place-items-start gap-3">
<Button icon={Enso}>Icon start</Button>
<Button icon={Enso} iconPosition="end">
Icon end
</Button>
<Button icon={Enso} aria-label="Only icon" />
</div>
<Text.Heading>States</Text.Heading>
<div className="grid grid-cols-4 place-content-center place-items-start gap-3">
<Button isDisabled>Disabled</Button>
<Button loading>Loading</Button>
<Button loaderPosition="icon" loading>
Loading
</Button>
<Button isActive>Active</Button>
</div>
</div>
),
}
export const Tooltips: Story = {
render: () => (
<div className="flex flex-col gap-4">
<Text.Heading>Tooltip</Text.Heading>
<div className="grid grid-cols-4 place-content-center place-items-start gap-3">
<Button tooltip="This is a tooltip">Tooltip</Button>
<Button
aria-label="Tooltip uses aria-label for icon buttons"
icon={Enso}
testId="icon-button"
/>
<Button icon={Enso} tooltip={false} testId="icon-button-no-tooltip" />
</div>
</div>
),
}
export const LoadingOnPress: Story = {
render: () => {
return (
<Button
onPress={() => {
return new Promise((resolve) => setTimeout(resolve, 1000))
}}
>
Click me to trigger loading
</Button>
)
},
play: async ({ canvasElement }) => {
const { getByRole, findByTestId } = within(canvasElement)
const button = getByRole('button', { name: 'Click me to trigger loading' })
await userEvent.click(button)
await expect(button).toHaveAttribute('disabled')
// then the spinner appears after some delay
await expect(await findByTestId('spinner')).toBeInTheDocument()
},
}

View File

@ -4,14 +4,14 @@ import * as React from 'react'
import * as focusHooks from '#/hooks/focusHooks'
import * as aria from '#/components/aria'
import * as ariaComponents from '#/components/AriaComponents'
import { StatelessSpinner } from '#/components/StatelessSpinner'
import SvgMask from '#/components/SvgMask'
import { forwardRef } from '#/utilities/react'
import type { ExtractFunction, VariantProps } from '#/utilities/tailwindVariants'
import { tv } from '#/utilities/tailwindVariants'
import { TEXT_STYLE } from '../Text'
import { TEXT_STYLE, useVisualTooltip } from '../Text'
import { Tooltip, TooltipTrigger } from '../Tooltip'
// ==============
// === Button ===
@ -82,7 +82,7 @@ export const BUTTON_STYLES = tv({
],
variants: {
isDisabled: {
true: 'disabled:opacity-50 disabled:cursor-not-allowed aria-disabled:opacity-50 aria-disabled:cursor-not-allowed',
true: 'opacity-50 cursor-not-allowed',
},
isFocused: {
true: 'focus:outline-none focus-visible:outline-2 focus-visible:outline-black focus-visible:outline-offset-[-2px]',
@ -256,6 +256,7 @@ export const BUTTON_STYLES = tv({
variant: 'primary',
iconPosition: 'start',
showIconOnHover: false,
isDisabled: false,
},
compoundVariants: [
{ isFocused: true, iconOnly: true, class: 'focus-visible:outline-offset-[3px]' },
@ -393,14 +394,14 @@ export const Button = forwardRef(function Button(
render: aria.ButtonRenderProps | aria.LinkRenderProps,
): React.ReactNode => {
const iconComponent = (() => {
if (icon == null) {
return null
} else if (isLoading && loaderPosition === 'icon') {
if (isLoading && loaderPosition === 'icon') {
return (
<span className={styles.icon()}>
<StatelessSpinner state="loading-medium" size={16} />
</span>
)
} else if (icon == null) {
return null
} else {
/* @ts-expect-error any here is safe because we transparently pass it to the children, and ts infer the type outside correctly */
const actualIcon = typeof icon === 'function' ? icon(render) : icon
@ -429,7 +430,7 @@ export const Button = forwardRef(function Button(
}
}
const { tooltip: visualTooltip, targetProps } = ariaComponents.useVisualTooltip({
const { tooltip: visualTooltip, targetProps } = useVisualTooltip({
targetRef: contentRef,
children: tooltipElement,
isDisabled: !shouldUseVisualTooltip,
@ -483,14 +484,12 @@ export const Button = forwardRef(function Button(
{button}
{visualTooltip}
</>
: <ariaComponents.TooltipTrigger delay={0} closeDelay={0}>
: <TooltipTrigger delay={0} closeDelay={0}>
{button}
<ariaComponents.Tooltip
{...(tooltipPlacement != null ? { placement: tooltipPlacement } : {})}
>
<Tooltip {...(tooltipPlacement != null ? { placement: tooltipPlacement } : {})}>
{tooltipElement}
</ariaComponents.Tooltip>
</ariaComponents.TooltipTrigger>
</Tooltip>
</TooltipTrigger>
)
})

View File

@ -4,10 +4,10 @@ import * as React from 'react'
import Success from '#/assets/check_mark.svg'
import Error from '#/assets/cross.svg'
import * as ariaComponents from '#/components/AriaComponents'
import * as loader from '#/components/Loader'
import SvgMask from '#/components/SvgMask'
import { tv, type VariantProps } from '#/utilities/tailwindVariants'
import { Text } from './AriaComponents/Text'
import * as loader from './Loader'
import SvgMask from './SvgMask'
// =================
// === Constants ===
@ -15,9 +15,9 @@ import { tv, type VariantProps } from '#/utilities/tailwindVariants'
const INFO_ICON = (
// eslint-disable-next-line no-restricted-syntax
<ariaComponents.Text variant="custom" className="pb-0.5 text-xl leading-[0]" aria-hidden>
<Text variant="custom" className="pb-0.5 text-xl leading-[0]" aria-hidden>
!
</ariaComponents.Text>
</Text>
)
const STATUS_ICON_MAP: Readonly<Record<Status, StatusIcon>> = {
@ -142,15 +142,15 @@ export function Result(props: ResultProps) {
: null}
{typeof title === 'string' ?
<ariaComponents.Text.Heading level={2} className={classes.title()} variant="subtitle">
<Text.Heading level={2} className={classes.title()} variant="subtitle">
{title}
</ariaComponents.Text.Heading>
</Text.Heading>
: title}
{typeof subtitle === 'string' ?
<ariaComponents.Text elementType="p" className={classes.subtitle()} balance variant="body">
<Text elementType="p" className={classes.subtitle()} balance variant="body">
{subtitle}
</ariaComponents.Text>
</Text>
: subtitle}
{children != null && <div className={classes.content()}>{children}</div>}

View File

@ -40,6 +40,7 @@ export function Spinner(props: SpinnerProps) {
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
data-testid="spinner"
>
<rect
x={1.5}

View File

@ -0,0 +1,32 @@
:where(body) {
overflow: hidden;
height: 100vh;
background-blend-mode: lighten;
}
:where(body:not(.vibrancy)) {
&::before {
content: '';
inset: 0 -16vw -16vh 0;
z-index: -1;
background-color: #b09778ff;
@apply pointer-events-none fixed bg-cover;
}
& > * {
@apply bg-white/80;
}
}
:where(.enso-dashboard) {
@apply absolute inset-0 flex flex-col overflow-hidden;
}
/* Disable all animations. */
html.disable-animations * {
transition: none !important;
animation: none !important;
animation-play-state: paused !important;
}

View File

@ -398,43 +398,10 @@
--sidebar-section-heading-margin-b: 0.375rem;
}
/* Disable all animations. */
html.disable-animations * {
transition: none !important;
animation: none !important;
animation-play-state: paused !important;
}
:where(body) {
overflow: hidden;
height: 100vh;
background-blend-mode: lighten;
}
:where(body:not(.vibrancy)) {
&::before {
content: '';
inset: 0 -16vw -16vh 0;
z-index: -1;
background-color: #b09778ff;
@apply pointer-events-none fixed bg-cover;
}
& > * {
@apply bg-white/80;
}
}
.Toastify--animate {
animation-duration: 200ms;
}
:where(.enso-dashboard) {
@apply absolute inset-0 flex flex-col overflow-hidden;
}
/* These styles MUST still be copied
* as `.enso-dashboard body` and `.enso-dashboard html` make no sense. */
:where(.enso-dashboard, .enso-chat, .enso-portal-root) {

View File

@ -1,4 +1,5 @@
import * as dashboard from '#/index'
import '#/styles.css'
import '#/tailwind.css'
import { AsyncApp } from '@/asyncApp'
import { baseConfig, configValue, mergeConfig } from '@/util/config'

View File

@ -49,7 +49,7 @@ const activity = shallowRef<VNode>()
// How much wider a dropdown can be than a port it is attached to, when a long text is present.
// Any text beyond that limit will receive an ellipsis and sliding animation on hover.
const MAX_DROPDOWN_OVERSIZE_PX = 150
const MAX_DROPDOWN_OVERSIZE_PX = 390
const floatReference = computed(
() => enclosingTopLevelArgument(widgetRoot.value, tree) ?? widgetRoot.value,

View File

@ -148,7 +148,7 @@ export interface DropdownEntry {
&:hover {
background-color: color-mix(in oklab, var(--dropdown-bg) 50%, white 50%);
span {
.itemContent {
--text-scroll-max: calc(var(--dropdown-max-width) - 28px);
will-change: transform;
animation: 6s 1s infinite text-scroll;

View File

@ -1,6 +1,7 @@
{
"extends": "./tsconfig.json",
"include": [
".storybook/**/*",
"env.d.ts",
"lib0-ext.d.ts",
"src/**/*.vue",

View File

@ -0,0 +1,10 @@
{
"extends": "./tsconfig.app.json",
"include": [
".storybook/**/*",
".storybook/env.d.ts",
"./src/**/*.stories.tsx",
"./src/**/*.stories.vue",
"./src/**/*.stories.ts"
]
}

View File

@ -30,12 +30,15 @@ const entrypoint =
// and because Vite's HTML env replacements only work with import.meta.env variables, not defines.
process.env.ENSO_IDE_VERSION = process.env.ENSO_CLOUD_DASHBOARD_VERSION
const isCI = process.env.CI === 'true'
// https://vitejs.dev/config/
export default defineConfig({
root: fileURLToPath(new URL('.', import.meta.url)),
cacheDir: fileURLToPath(new URL('../../node_modules/.cache/vite', import.meta.url)),
publicDir: fileURLToPath(new URL('./public', import.meta.url)),
envDir: fileURLToPath(new URL('.', import.meta.url)),
logLevel: isCI ? 'error' : 'info',
plugins: [
wasm(),
...(process.env.NODE_ENV === 'development' ? [await VueDevTools()] : []),

View File

@ -63,7 +63,7 @@
"typecheck": "tsc --build",
"build": "tsx bundle.ts",
"dist": "tsx dist.ts",
"lint": "eslint . --max-warnings=0",
"lint": "eslint . --cache --max-warnings=0",
"watch:windows": "cross-env ENSO_BUILD_IDE=%LOCALAPPDATA%\\Temp\\enso\\dist\\ide ENSO_BUILD_PROJECT_MANAGER=%CD%\\..\\..\\..\\dist\\backend ENSO_BUILD_PROJECT_MANAGER_IN_BUNDLE_PATH=bin\\project-manager.exe ENSO_BUILD_IDE_BUNDLED_ENGINE_VERSION=0 ENSO_POLYGLOT_YDOC_SERVER=wss://localhost:8080 tsx watch.ts",
"watch:linux": "ENSO_BUILD_IDE=\"${ENSO_BUILD_IDE:-/tmp/enso/dist/ide}\" ENSO_BUILD_PROJECT_MANAGER=\"${ENSO_BUILD_PROJECT_MANAGER:-\"$(pwd)/../../../dist/backend\"}\" ENSO_BUILD_PROJECT_MANAGER_IN_BUNDLE_PATH=\"${ENSO_BUILD_PROJECT_MANAGER_IN_BUNDLE_PATH:-bin/project-manager}\" ENSO_BUILD_IDE_BUNDLED_ENGINE_VERSION=\"${ENSO_BUILD_IDE_BUNDLED_ENGINE_VERSION:-0}\" ENSO_POLYGLOT_YDOC_SERVER=\"${ENSO_POLYGLOT_YDOC_SERVER:-wss://localhost:8080}\" tsx watch.ts \"$@\"",
"watch:macos": "ENSO_BUILD_IDE=\"${ENSO_BUILD_IDE:-/tmp/enso/dist/ide}\" ENSO_BUILD_PROJECT_MANAGER=\"${ENSO_BUILD_PROJECT_MANAGER:-\"$(pwd)/../../../dist/backend\"}\" ENSO_BUILD_PROJECT_MANAGER_IN_BUNDLE_PATH=\"${ENSO_BUILD_PROJECT_MANAGER_IN_BUNDLE_PATH:-bin/project-manager}\" ENSO_BUILD_IDE_BUNDLED_ENGINE_VERSION=\"${ENSO_BUILD_IDE_BUNDLED_ENGINE_VERSION:-0}\" ENSO_POLYGLOT_YDOC_SERVER=\"${ENSO_POLYGLOT_YDOC_SERVER:-wss://localhost:8080}\" tsx watch.ts \"$@\""

View File

@ -16,7 +16,7 @@
},
"scripts": {
"build": "node src/index.js",
"lint": "eslint . --max-warnings=0"
"lint": "eslint . --cache --max-warnings=0"
},
"devDependencies": {
"sharp": "^0.31.2",

View File

@ -12,7 +12,7 @@
"compile": "node ./build.mjs build",
"start": "node ./dist/main.mjs",
"dev:watch": "node ./build.mjs watch",
"lint": "eslint . --max-warnings=0",
"lint": "eslint . --cache --max-warnings=0",
"format": "prettier --version && prettier --write src/ && eslint . --fix"
},
"dependencies": {

View File

@ -12,7 +12,7 @@
"compile": "node ./build.mjs build",
"start": "node ./dist/main.cjs",
"dev:watch": "node ./build.mjs watch",
"lint": "eslint . --max-warnings=0"
"lint": "eslint . --cache --max-warnings=0"
},
"dependencies": {
"ydoc-server": "workspace:*",

View File

@ -12,7 +12,7 @@
"test:watch": "vitest",
"typecheck": "tsc",
"compile": "tsc",
"lint": "eslint . --max-warnings=0"
"lint": "eslint . --cache --max-warnings=0"
},
"exports": {
".": {

View File

@ -17,7 +17,7 @@
"generate:ast-schema": "cargo run -p enso-parser-schema > src/ast/generated/ast-schema.json",
"generate:ast-types": "vite-node ./parser-codegen/index.ts src/ast/generated/ast-schema.json src/ast/generated/ast.ts",
"generate:ast-types-lazy": "vite-node ./parser-codegen/index.ts src/ast/generated/ast-schema.json src/ast/generated/ast.ts --if-changed",
"lint": "eslint . --max-warnings=0",
"lint": "eslint . --cache --max-warnings=0",
"format": "prettier --version && prettier --write src/ && eslint . --fix",
"postinstall": "corepack pnpm run generate:ast-schema && corepack pnpm run generate:ast-types-lazy"
},

View File

@ -661,7 +661,6 @@ pub fn gui_tests() -> Result<Workflow> {
workflow.add(PRIMARY_TARGET, job::Lint);
workflow.add(PRIMARY_TARGET, job::WasmTest);
workflow.add(PRIMARY_TARGET, job::NativeTest);
workflow.add(PRIMARY_TARGET, job::GuiCheck);
Ok(workflow)
}

View File

@ -396,16 +396,6 @@ impl JobArchetype for NativeTest {
}
}
#[derive(Clone, Copy, Debug)]
pub struct GuiCheck;
impl JobArchetype for GuiCheck {
fn job(&self, target: Target) -> Job {
plain_job(target, "GUI tests", "gui check")
}
}
#[derive(Clone, Copy, Debug)]
pub struct GuiBuild;

View File

@ -556,4 +556,15 @@ export default [
'no-undef': 'off',
},
},
{
files: ['app/gui/src/dashboard/**/*.stories.tsx'],
rules: {
'no-restricted-syntax': 'off',
'jsdoc/require-jsdoc': 'off',
'jsdoc/require-param-type': 'off',
'jsdoc/require-file-overview': 'off',
'@typescript-eslint/no-magic-numbers': 'off',
'@typescript-eslint/unbound-method': 'off',
},
},
]

View File

@ -1,4 +1,5 @@
{
"type": "module",
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^8.10.0",
"@typescript-eslint/parser": "^8.10.0",
@ -30,10 +31,12 @@
"format": "prettier --write .",
"format:workflows": "prettier --write .github/workflows",
"ci-check": "corepack pnpm run --aggregate-output /^ci:/",
"ci:prettier": "prettier --check .",
"ci:lint": "corepack pnpm run -r lint",
"ci:prettier": "prettier --check --cache .",
"ci:lint": "corepack pnpm run -r --parallel lint --output-file eslint_report.json --format json",
"ci:test": "corepack pnpm run -r --parallel test",
"ci:typecheck": "corepack pnpm run -r typecheck"
"ci:typecheck": "corepack pnpm run -r typecheck",
"ci:chromatic:react": "corepack pnpm run -r --filter enso-gui chromatic:react",
"ci:chromatic:vue": "corepack pnpm run -r --filter enso-gui chromatic:vue"
},
"pnpm": {
"//": "To completely ignore deep dependencies, see .pnpmfile.cjs",

File diff suppressed because it is too large Load Diff