From 35f96df66eae6cffcb20889750cd669227ffdd23 Mon Sep 17 00:00:00 2001 From: Ken Sternberg Date: Thu, 16 May 2024 15:26:35 -0700 Subject: [PATCH] web: Testing and documenting the simple things This commit adds unit tests for the Alerts and EmptyState elements. It includes a new test/documentation feature; elements can now be fully documented with text description and active controls. It *removes* the `babel` imports entirely. Either we don't need them, or the components that do need them are importing them automatically. [An outstanding bug in WebDriverIO](https://github.com/webdriverio/webdriverio/issues/12056) unfortunately means that the tests cannot be run in parallel for the time being.While one test is running, the compiler for other tests becomes unreliable. They're currently working on this issue. I have set the `maxInstances` to **1**. I have updated the `` component just a bit, providing an attribute alternative to the `Level` property; now instead of passing it a `` properties, you can just say `` and it'll work just fine. The old way is still the default behavior. The default behavior for `EmptyState` was a little confusing; I've re-arranged it for clarity. Since I touched it, I also added the `interface` and `HTMLElementTagNameMap` declarations. Added documentation to all the elements I've touched (so far). --- web/package-lock.json | 481 +++++++++++++----- web/package.json | 16 +- web/src/elements/Alert.ts | 93 +++- web/src/elements/Divider.ts | 14 + web/src/elements/EmptyState.ts | 64 ++- web/src/elements/Expand.ts | 43 +- .../ActionButton/ak-action-button.stories.ts | 4 +- .../buttons/ActionButton/ak-action-button.ts | 16 +- .../buttons/SpinnerButton/BaseTaskButton.ts | 5 +- .../SpinnerButton/ak-spinner-button.ts | 12 +- ...ateCard.tests.ts => AggregateCard.test.ts} | 0 ....tests.ts => AggregatePromiseCard.test.ts} | 0 ...nCard.tests.ts => QuickActionCard.test.ts} | 0 web/src/elements/stories/Alert.docs.mdx | 43 ++ web/src/elements/stories/Alert.stories.ts | 77 +++ web/src/elements/stories/Alert.test.ts | 36 ++ web/src/elements/stories/Divider.docs.mdx | 51 ++ web/src/elements/stories/Divider.stories.ts | 41 ++ web/src/elements/stories/Divider.test.ts | 33 ++ web/src/elements/stories/EmptyState.docs.mdx | 59 +++ .../elements/stories/EmptyState.stories.ts | 108 ++++ .../elements/{ => stories}/EmptyState.test.ts | 4 +- web/src/elements/stories/Expand.docs.mdx | 38 ++ web/src/elements/stories/Expand.stories.ts | 60 +++ web/src/elements/stories/Expand.test.ts | 52 ++ web/wdio.conf.ts | 2 +- 26 files changed, 1195 insertions(+), 157 deletions(-) rename web/src/elements/cards/stories/{AggregateCard.tests.ts => AggregateCard.test.ts} (100%) rename web/src/elements/cards/stories/{AggregatePromiseCard.tests.ts => AggregatePromiseCard.test.ts} (100%) rename web/src/elements/cards/stories/{QuickActionCard.tests.ts => QuickActionCard.test.ts} (100%) create mode 100644 web/src/elements/stories/Alert.docs.mdx create mode 100644 web/src/elements/stories/Alert.stories.ts create mode 100644 web/src/elements/stories/Alert.test.ts create mode 100644 web/src/elements/stories/Divider.docs.mdx create mode 100644 web/src/elements/stories/Divider.stories.ts create mode 100644 web/src/elements/stories/Divider.test.ts create mode 100644 web/src/elements/stories/EmptyState.docs.mdx create mode 100644 web/src/elements/stories/EmptyState.stories.ts rename web/src/elements/{ => stories}/EmptyState.test.ts (95%) create mode 100644 web/src/elements/stories/Expand.docs.mdx create mode 100644 web/src/elements/stories/Expand.stories.ts create mode 100644 web/src/elements/stories/Expand.test.ts diff --git a/web/package-lock.json b/web/package-lock.json index 5ec58978c1..1d61b57cd4 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -47,23 +47,18 @@ "yaml": "^2.4.2" }, "devDependencies": { - "@babel/core": "^7.24.5", - "@babel/plugin-proposal-class-properties": "^7.18.6", - "@babel/plugin-proposal-decorators": "^7.24.1", - "@babel/plugin-transform-private-methods": "^7.24.1", - "@babel/plugin-transform-private-property-in-object": "^7.24.5", - "@babel/plugin-transform-runtime": "^7.24.3", - "@babel/preset-env": "^7.24.5", - "@babel/preset-typescript": "^7.24.1", "@hcaptcha/types": "^1.0.3", "@jeysal/storybook-addon-css-user-preferences": "^0.2.0", "@lit/localize-tools": "^0.7.2", "@rollup/plugin-replace": "^5.0.5", "@spotlightjs/spotlight": "^1.2.17", + "@storybook/addon-controls": "^8.1.1", + "@storybook/addon-docs": "^8.1.1", "@storybook/addon-essentials": "^8.1.1", "@storybook/addon-links": "^8.1.1", "@storybook/api": "^7.6.17", "@storybook/blocks": "^8.0.8", + "@storybook/web-components-vite": "^8.1.1", "@testing-library/webdriverio": "^3.2.1", "@trivago/prettier-plugin-sort-imports": "^4.3.0", "@types/chart.js": "^2.9.41", @@ -77,8 +72,6 @@ "@wdio/cli": "^8.36.1", "@wdio/mocha-framework": "^8.36.1", "@wdio/spec-reporter": "^8.36.1", - "babel-plugin-macros": "^3.1.0", - "babel-plugin-tsconfig-paths": "^1.0.3", "chokidar": "^3.6.0", "cross-env": "^7.0.3", "esbuild": "^0.21.3", @@ -92,10 +85,9 @@ "glob": "^10.3.15", "lit-analyzer": "^2.0.3", "npm-run-all": "^4.1.5", + "polished": "^4.3.1", "prettier": "^3.2.5", "pseudolocale": "^2.0.0", - "react": "^18.2.0", - "react-dom": "^18.3.1", "rollup-plugin-modify": "^3.0.0", "rollup-plugin-postcss-lit": "^2.1.0", "storybook": "^8.1.1", @@ -626,23 +618,6 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-proposal-decorators": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.24.1.tgz", - "integrity": "sha512-zPEvzFijn+hRvJuX2Vu3KbEBN39LN3f7tW3MQO2LsIs57B26KU+kUc82BdAktS1VCM6libzh45eKGI65lg0cpA==", - "dev": true, - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.24.1", - "@babel/helper-plugin-utils": "^7.24.0", - "@babel/plugin-syntax-decorators": "^7.24.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, "node_modules/@babel/plugin-proposal-private-property-in-object": { "version": "7.21.0-placeholder-for-preset-env.2", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", @@ -694,21 +669,6 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-syntax-decorators": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.24.1.tgz", - "integrity": "sha512-05RJdO/cCrtVWuAaSn1tS3bH8jbsJa/Y1uD186u6J4C/1mnHFxseeuWpsqr9anvo7TUulev7tm7GDwRV+VuhDw==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, "node_modules/@babel/plugin-syntax-dynamic-import": { "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", @@ -1598,26 +1558,6 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-runtime": { - "version": "7.24.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.24.3.tgz", - "integrity": "sha512-J0BuRPNlNqlMTRJ72eVptpt9VcInbxO6iP3jaxr+1NPhC0UkKL+6oeX6VXMEYdADnuqmMmsBspt4d5w8Y/TCbQ==", - "dev": true, - "dependencies": { - "@babel/helper-module-imports": "^7.24.3", - "@babel/helper-plugin-utils": "^7.24.0", - "babel-plugin-polyfill-corejs2": "^0.4.10", - "babel-plugin-polyfill-corejs3": "^0.10.1", - "babel-plugin-polyfill-regenerator": "^0.6.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, "node_modules/@babel/plugin-transform-shorthand-properties": { "version": "7.24.1", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.24.1.tgz", @@ -5739,6 +5679,151 @@ "node": ">=14.14" } }, + "node_modules/@storybook/builder-vite": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/@storybook/builder-vite/-/builder-vite-8.1.1.tgz", + "integrity": "sha512-+BSmXuZ9j95oKCvHcKztzjZNzBVeXYMoRO2TuflLnknMUA0v9ySp1PhiQxHM4DgAW6t9db1akzc9HoTA5sjTWg==", + "dev": true, + "dependencies": { + "@storybook/channels": "8.1.1", + "@storybook/client-logger": "8.1.1", + "@storybook/core-common": "8.1.1", + "@storybook/core-events": "8.1.1", + "@storybook/csf-plugin": "8.1.1", + "@storybook/node-logger": "8.1.1", + "@storybook/preview": "8.1.1", + "@storybook/preview-api": "8.1.1", + "@storybook/types": "8.1.1", + "@types/find-cache-dir": "^3.2.1", + "browser-assert": "^1.2.1", + "es-module-lexer": "^1.5.0", + "express": "^4.17.3", + "find-cache-dir": "^3.0.0", + "fs-extra": "^11.1.0", + "magic-string": "^0.30.0", + "ts-dedent": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "@preact/preset-vite": "*", + "typescript": ">= 4.3.x", + "vite": "^4.0.0 || ^5.0.0", + "vite-plugin-glimmerx": "*" + }, + "peerDependenciesMeta": { + "@preact/preset-vite": { + "optional": true + }, + "typescript": { + "optional": true + }, + "vite-plugin-glimmerx": { + "optional": true + } + } + }, + "node_modules/@storybook/builder-vite/node_modules/@storybook/channels": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/@storybook/channels/-/channels-8.1.1.tgz", + "integrity": "sha512-vG7y97QB++TRkuxYLNKaWJmgr9QBUHyjQgNCWvHIeSYW5zxum9sm6VSR2j1r2G3XUGFSxDwenYBTQuwZJLhWNQ==", + "dev": true, + "dependencies": { + "@storybook/client-logger": "8.1.1", + "@storybook/core-events": "8.1.1", + "@storybook/global": "^5.0.0", + "telejson": "^7.2.0", + "tiny-invariant": "^1.3.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + } + }, + "node_modules/@storybook/builder-vite/node_modules/@storybook/client-logger": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/@storybook/client-logger/-/client-logger-8.1.1.tgz", + "integrity": "sha512-9AWPgIN3K0eLusChJUqB5Ft+9P2pW5/s4vOMoj3TCvu8lrdq8AH8ctvxk7x2Kw2wEwQ/g9DyE6C/rDQUARbxew==", + "dev": true, + "dependencies": { + "@storybook/global": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + } + }, + "node_modules/@storybook/builder-vite/node_modules/@storybook/core-events": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/@storybook/core-events/-/core-events-8.1.1.tgz", + "integrity": "sha512-WpeiBV6RWTZ6t8SI1YdQh8NlbvQtZs9WRr4CPfpzHAly+oxFy6PtPz0h5TMKsU5/kt/L9yL7tE9ZzPYzvFWH/A==", + "dev": true, + "dependencies": { + "@storybook/csf": "^0.1.7", + "ts-dedent": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + } + }, + "node_modules/@storybook/builder-vite/node_modules/@storybook/preview-api": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/@storybook/preview-api/-/preview-api-8.1.1.tgz", + "integrity": "sha512-5EcByqtJgj7a7ZWICMLif8mK3cRmdIMbdSPEDf4X6aTQ8LZOg6updLrkb/Eh6qfeYv46TK/MP8BXa89wfOxWGQ==", + "dev": true, + "dependencies": { + "@storybook/channels": "8.1.1", + "@storybook/client-logger": "8.1.1", + "@storybook/core-events": "8.1.1", + "@storybook/csf": "^0.1.7", + "@storybook/global": "^5.0.0", + "@storybook/types": "8.1.1", + "@types/qs": "^6.9.5", + "dequal": "^2.0.2", + "lodash": "^4.17.21", + "memoizerific": "^1.11.3", + "qs": "^6.10.0", + "tiny-invariant": "^1.3.1", + "ts-dedent": "^2.0.0", + "util-deprecate": "^1.0.2" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + } + }, + "node_modules/@storybook/builder-vite/node_modules/@storybook/types": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/@storybook/types/-/types-8.1.1.tgz", + "integrity": "sha512-QSQ63aKr2IXrGjX2/Fg1oiGWk+2Nuf+TplaHRC2NKBMgvyn+M0BHUgMTDHQVrFaH4bpl2PkE0r0tzOKP4JI43A==", + "dev": true, + "dependencies": { + "@storybook/channels": "8.1.1", + "@types/express": "^4.7.0", + "file-system-cache": "2.3.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + } + }, + "node_modules/@storybook/builder-vite/node_modules/fs-extra": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", + "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, "node_modules/@storybook/channels": { "version": "7.6.17", "dev": true, @@ -7088,6 +7173,16 @@ "url": "https://opencollective.com/storybook" } }, + "node_modules/@storybook/preview": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/@storybook/preview/-/preview-8.1.1.tgz", + "integrity": "sha512-P8iBi9v/62AhTztbCYjVxH6idNO0h9uO583GHwi3uq2Io7F1gUSgwG/HYZ7PnclOsMnmG0FJvAwrvdRc6sWSNw==", + "dev": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + } + }, "node_modules/@storybook/preview-api": { "version": "7.6.17", "dev": true, @@ -7289,6 +7384,197 @@ "url": "https://opencollective.com/storybook" } }, + "node_modules/@storybook/web-components": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/@storybook/web-components/-/web-components-8.1.1.tgz", + "integrity": "sha512-o7Lt5RSKlrHG86604exFir5KYl0aUkvwVhoMbqqIhzygMlEllSb8YqJVBlnAmRW3vKd3Bp+FoO0JiQQl8gREJw==", + "dev": true, + "dependencies": { + "@storybook/client-logger": "8.1.1", + "@storybook/docs-tools": "8.1.1", + "@storybook/global": "^5.0.0", + "@storybook/manager-api": "8.1.1", + "@storybook/preview-api": "8.1.1", + "@storybook/types": "8.1.1", + "tiny-invariant": "^1.3.1", + "ts-dedent": "^2.0.0" + }, + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "lit": "^2.0.0 || ^3.0.0" + } + }, + "node_modules/@storybook/web-components-vite": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/@storybook/web-components-vite/-/web-components-vite-8.1.1.tgz", + "integrity": "sha512-96zWh51ugm3DiJlvWgqnFjNmq/6/XaxUoA+nPXHbVra6Y3MW6eUfi6qpErI2umNjjLrjVBPbkKIaMtOI5l90xA==", + "dev": true, + "dependencies": { + "@storybook/builder-vite": "8.1.1", + "@storybook/core-server": "8.1.1", + "@storybook/node-logger": "8.1.1", + "@storybook/types": "8.1.1", + "@storybook/web-components": "8.1.1", + "magic-string": "^0.30.0" + }, + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + } + }, + "node_modules/@storybook/web-components-vite/node_modules/@storybook/channels": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/@storybook/channels/-/channels-8.1.1.tgz", + "integrity": "sha512-vG7y97QB++TRkuxYLNKaWJmgr9QBUHyjQgNCWvHIeSYW5zxum9sm6VSR2j1r2G3XUGFSxDwenYBTQuwZJLhWNQ==", + "dev": true, + "dependencies": { + "@storybook/client-logger": "8.1.1", + "@storybook/core-events": "8.1.1", + "@storybook/global": "^5.0.0", + "telejson": "^7.2.0", + "tiny-invariant": "^1.3.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + } + }, + "node_modules/@storybook/web-components-vite/node_modules/@storybook/client-logger": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/@storybook/client-logger/-/client-logger-8.1.1.tgz", + "integrity": "sha512-9AWPgIN3K0eLusChJUqB5Ft+9P2pW5/s4vOMoj3TCvu8lrdq8AH8ctvxk7x2Kw2wEwQ/g9DyE6C/rDQUARbxew==", + "dev": true, + "dependencies": { + "@storybook/global": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + } + }, + "node_modules/@storybook/web-components-vite/node_modules/@storybook/core-events": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/@storybook/core-events/-/core-events-8.1.1.tgz", + "integrity": "sha512-WpeiBV6RWTZ6t8SI1YdQh8NlbvQtZs9WRr4CPfpzHAly+oxFy6PtPz0h5TMKsU5/kt/L9yL7tE9ZzPYzvFWH/A==", + "dev": true, + "dependencies": { + "@storybook/csf": "^0.1.7", + "ts-dedent": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + } + }, + "node_modules/@storybook/web-components-vite/node_modules/@storybook/types": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/@storybook/types/-/types-8.1.1.tgz", + "integrity": "sha512-QSQ63aKr2IXrGjX2/Fg1oiGWk+2Nuf+TplaHRC2NKBMgvyn+M0BHUgMTDHQVrFaH4bpl2PkE0r0tzOKP4JI43A==", + "dev": true, + "dependencies": { + "@storybook/channels": "8.1.1", + "@types/express": "^4.7.0", + "file-system-cache": "2.3.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + } + }, + "node_modules/@storybook/web-components/node_modules/@storybook/channels": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/@storybook/channels/-/channels-8.1.1.tgz", + "integrity": "sha512-vG7y97QB++TRkuxYLNKaWJmgr9QBUHyjQgNCWvHIeSYW5zxum9sm6VSR2j1r2G3XUGFSxDwenYBTQuwZJLhWNQ==", + "dev": true, + "dependencies": { + "@storybook/client-logger": "8.1.1", + "@storybook/core-events": "8.1.1", + "@storybook/global": "^5.0.0", + "telejson": "^7.2.0", + "tiny-invariant": "^1.3.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + } + }, + "node_modules/@storybook/web-components/node_modules/@storybook/client-logger": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/@storybook/client-logger/-/client-logger-8.1.1.tgz", + "integrity": "sha512-9AWPgIN3K0eLusChJUqB5Ft+9P2pW5/s4vOMoj3TCvu8lrdq8AH8ctvxk7x2Kw2wEwQ/g9DyE6C/rDQUARbxew==", + "dev": true, + "dependencies": { + "@storybook/global": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + } + }, + "node_modules/@storybook/web-components/node_modules/@storybook/core-events": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/@storybook/core-events/-/core-events-8.1.1.tgz", + "integrity": "sha512-WpeiBV6RWTZ6t8SI1YdQh8NlbvQtZs9WRr4CPfpzHAly+oxFy6PtPz0h5TMKsU5/kt/L9yL7tE9ZzPYzvFWH/A==", + "dev": true, + "dependencies": { + "@storybook/csf": "^0.1.7", + "ts-dedent": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + } + }, + "node_modules/@storybook/web-components/node_modules/@storybook/preview-api": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/@storybook/preview-api/-/preview-api-8.1.1.tgz", + "integrity": "sha512-5EcByqtJgj7a7ZWICMLif8mK3cRmdIMbdSPEDf4X6aTQ8LZOg6updLrkb/Eh6qfeYv46TK/MP8BXa89wfOxWGQ==", + "dev": true, + "dependencies": { + "@storybook/channels": "8.1.1", + "@storybook/client-logger": "8.1.1", + "@storybook/core-events": "8.1.1", + "@storybook/csf": "^0.1.7", + "@storybook/global": "^5.0.0", + "@storybook/types": "8.1.1", + "@types/qs": "^6.9.5", + "dequal": "^2.0.2", + "lodash": "^4.17.21", + "memoizerific": "^1.11.3", + "qs": "^6.10.0", + "tiny-invariant": "^1.3.1", + "ts-dedent": "^2.0.0", + "util-deprecate": "^1.0.2" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + } + }, + "node_modules/@storybook/web-components/node_modules/@storybook/types": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/@storybook/types/-/types-8.1.1.tgz", + "integrity": "sha512-QSQ63aKr2IXrGjX2/Fg1oiGWk+2Nuf+TplaHRC2NKBMgvyn+M0BHUgMTDHQVrFaH4bpl2PkE0r0tzOKP4JI43A==", + "dev": true, + "dependencies": { + "@storybook/channels": "8.1.1", + "@types/express": "^4.7.0", + "file-system-cache": "2.3.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + } + }, "node_modules/@swagger-api/apidom-ast": { "version": "0.96.0", "license": "Apache-2.0", @@ -8398,6 +8684,12 @@ "@types/send": "*" } }, + "node_modules/@types/find-cache-dir": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@types/find-cache-dir/-/find-cache-dir-3.2.1.tgz", + "integrity": "sha512-frsJrz2t/CeGifcu/6uRo4b+SzAwT4NYCVPu1GN8IB9XTzrpPkGuV0tmh9mN+/L0PklAlsC3u5Fxt0ju00LXIw==", + "dev": true + }, "node_modules/@types/grecaptcha": { "version": "3.0.9", "dev": true, @@ -8508,12 +8800,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/parse-json": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", - "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", - "dev": true - }, "node_modules/@types/pretty-hrtime": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@types/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz", @@ -10603,21 +10889,6 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/babel-plugin-macros": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", - "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", - "dev": true, - "dependencies": { - "@babel/runtime": "^7.12.5", - "cosmiconfig": "^7.0.0", - "resolve": "^1.19.0" - }, - "engines": { - "node": ">=10", - "npm": ">=6" - } - }, "node_modules/babel-plugin-polyfill-corejs2": { "version": "0.4.11", "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.11.tgz", @@ -10657,15 +10928,6 @@ "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, - "node_modules/babel-plugin-tsconfig-paths": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/babel-plugin-tsconfig-paths/-/babel-plugin-tsconfig-paths-1.0.3.tgz", - "integrity": "sha512-eBTjzXpx0CXO2gooYPyIU1joS/eK1Vk2+oLhJDwRwIgh2+2kD/j649eYNtHjFKuXr36/4Y0ytPORLyiey7MLRA==", - "dev": true, - "peerDependencies": { - "@babel/core": "^7.9.0" - } - }, "node_modules/balanced-match": { "version": "1.0.2", "license": "MIT" @@ -11554,31 +11816,6 @@ "layout-base": "^1.0.0" } }, - "node_modules/cosmiconfig": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", - "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", - "dev": true, - "dependencies": { - "@types/parse-json": "^4.0.0", - "import-fresh": "^3.2.1", - "parse-json": "^5.0.0", - "path-type": "^4.0.0", - "yaml": "^1.10.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/cosmiconfig/node_modules/yaml": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", - "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", - "dev": true, - "engines": { - "node": ">= 6" - } - }, "node_modules/country-flag-icons": { "version": "1.5.11", "license": "MIT" @@ -12872,6 +13109,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/es-module-lexer": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.2.tgz", + "integrity": "sha512-l60ETUTmLqbVbVHv1J4/qj+M8nq7AwMzEcg3kmJDt9dCNrTk+yHcYFf/Kw75pMDwd9mPcIGCG5LcS20SxYRzFA==", + "dev": true + }, "node_modules/es-set-tostringtag": { "version": "2.0.3", "dev": true, diff --git a/web/package.json b/web/package.json index 6d2303e18b..0d6c0c6c17 100644 --- a/web/package.json +++ b/web/package.json @@ -71,23 +71,18 @@ "yaml": "^2.4.2" }, "devDependencies": { - "@babel/core": "^7.24.5", - "@babel/plugin-proposal-class-properties": "^7.18.6", - "@babel/plugin-proposal-decorators": "^7.24.1", - "@babel/plugin-transform-private-methods": "^7.24.1", - "@babel/plugin-transform-private-property-in-object": "^7.24.5", - "@babel/plugin-transform-runtime": "^7.24.3", - "@babel/preset-env": "^7.24.5", - "@babel/preset-typescript": "^7.24.1", "@hcaptcha/types": "^1.0.3", "@jeysal/storybook-addon-css-user-preferences": "^0.2.0", "@lit/localize-tools": "^0.7.2", "@rollup/plugin-replace": "^5.0.5", "@spotlightjs/spotlight": "^1.2.17", + "@storybook/addon-controls": "^8.1.1", + "@storybook/addon-docs": "^8.1.1", "@storybook/addon-essentials": "^8.1.1", "@storybook/addon-links": "^8.1.1", "@storybook/api": "^7.6.17", "@storybook/blocks": "^8.0.8", + "@storybook/web-components-vite": "^8.1.1", "@testing-library/webdriverio": "^3.2.1", "@trivago/prettier-plugin-sort-imports": "^4.3.0", "@types/chart.js": "^2.9.41", @@ -101,8 +96,6 @@ "@wdio/cli": "^8.36.1", "@wdio/mocha-framework": "^8.36.1", "@wdio/spec-reporter": "^8.36.1", - "babel-plugin-macros": "^3.1.0", - "babel-plugin-tsconfig-paths": "^1.0.3", "chokidar": "^3.6.0", "cross-env": "^7.0.3", "esbuild": "^0.21.3", @@ -116,10 +109,9 @@ "glob": "^10.3.15", "lit-analyzer": "^2.0.3", "npm-run-all": "^4.1.5", + "polished": "^4.3.1", "prettier": "^3.2.5", "pseudolocale": "^2.0.0", - "react": "^18.2.0", - "react-dom": "^18.3.1", "rollup-plugin-modify": "^3.0.0", "rollup-plugin-postcss-lit": "^2.1.0", "storybook": "^8.1.1", diff --git a/web/src/elements/Alert.ts b/web/src/elements/Alert.ts index e75d2c3936..c0a642c52f 100644 --- a/web/src/elements/Alert.ts +++ b/web/src/elements/Alert.ts @@ -1,11 +1,20 @@ import { AKElement } from "@goauthentik/elements/Base"; -import { CSSResult, TemplateResult, html } from "lit"; +import { html } from "lit"; import { customElement, property } from "lit/decorators.js"; +import { classMap } from "lit/directives/class-map.js"; import PFAlert from "@patternfly/patternfly/components/Alert/alert.css"; import PFBase from "@patternfly/patternfly/patternfly-base.css"; +export interface IAlert { + inline?: boolean; + warning?: boolean; + info?: boolean; + success?: boolean; + danger?: boolean; +} + export enum Level { Warning = "pf-m-warning", Info = "pf-m-info", @@ -13,20 +22,88 @@ export enum Level { Danger = "pf-m-danger", } +/** + * @class Alert + * @element ak-alert + * + * Alerts are in-page elements intended to draw the user's attention and alert them to important + * details. Alerts are used alongside form elements to warn users of potential mistakes they can + * make, as well as in in-line documentation. + */ @customElement("ak-alert") -export class Alert extends AKElement { +export class Alert extends AKElement implements IAlert { + /** + * Whether or not to display the entire component's contents in-line or not. + * + * @attr + */ @property({ type: Boolean }) inline = false; + /** + * Fallback method of determining severity + * + * @attr + */ @property() level: Level = Level.Warning; - static get styles(): CSSResult[] { + /** + * Highest severity level. + * + * @attr + */ + @property({ type: Boolean }) + danger = false; + + /** + * Next severity level. + * + * @attr + */ + @property({ type: Boolean }) + warning = false; + + /** + * Next severity level. The default severity level. + * + * @attr + */ + @property({ type: Boolean }) + success = false; + + /** + * Lowest severity level. + * + * @attr + */ + @property({ type: Boolean }) + info = false; + + static get styles() { return [PFBase, PFAlert]; } - render(): TemplateResult { - return html`
+ get classmap() { + const leveltags = ["danger", "warning", "success", "info"].filter( + // @ts-ignore + (level) => this[level] && this[level] === true, + ); + + if (leveltags.length > 1) { + console.warn("ak-alert has multiple levels defined"); + } + const level = leveltags.length > 0 ? `pf-m-${leveltags[0]}` : this.level; + + return { + "pf-c-alert": true, + "pf-m-inline": this.inline, + [level]: true, + }; + } + + render() { + return html`
@@ -36,3 +113,9 @@ export class Alert extends AKElement {
`; } } + +declare global { + interface HTMLElementTagNameMap { + "ak-alert": Alert; + } +} diff --git a/web/src/elements/Divider.ts b/web/src/elements/Divider.ts index 3f90bb1c74..7fb18952ee 100644 --- a/web/src/elements/Divider.ts +++ b/web/src/elements/Divider.ts @@ -5,6 +5,14 @@ import { customElement } from "lit/decorators.js"; import PFBase from "@patternfly/patternfly/patternfly-base.css"; +/** + * @class Divider + * @element ak-divider + * + * Divider is a horizontal rule, an in-page element to separate displayed items. + * + * @slot - HTML to display in-line in the middle of the horizontal rule. + */ @customElement("ak-divider") export class Divider extends AKElement { static get styles(): CSSResult[] { @@ -39,3 +47,9 @@ export class Divider extends AKElement { return html`
`; } } + +declare global { + interface HTMLElementTagNameMap { + "ak-divider": Divider; + } +} diff --git a/web/src/elements/EmptyState.ts b/web/src/elements/EmptyState.ts index 8842badea2..ba15b140f3 100644 --- a/web/src/elements/EmptyState.ts +++ b/web/src/elements/EmptyState.ts @@ -9,17 +9,64 @@ import PFEmptyState from "@patternfly/patternfly/components/EmptyState/empty-sta import PFTitle from "@patternfly/patternfly/components/Title/title.css"; import PFBase from "@patternfly/patternfly/patternfly-base.css"; -@customElement("ak-empty-state") -export class EmptyState extends AKElement { - @property({ type: String }) - icon = ""; +export interface IEmptyState { + icon?: string; + loading?: boolean; + fullHeight?: boolean; + header?: string; +} +/** + * @class EmptyState + * @element ak-empty-state + * + * The EmptyState is an in-page element to indicate that something is either loading or unavailable. + * When "loading" is true it displays a spinner, otherwise it displays a static icon. The default + * icon is a question mark in a circle. + * + * @slot body - Optional low-priority text that appears beneath the state indicator. + * @slot primary - Optional high-priority text that appears some distance between the state indicator. + * + * The layout of the component is always centered, and from top to bottom: + * + * ``` + * icon or spinner + * header + * body + * primary + * ``` + */ +@customElement("ak-empty-state") +export class EmptyState extends AKElement implements IEmptyState { + /** + * The Font Awesome icon to display. Defaults to the � symbol. + * + * @attr + */ + @property({ type: String }) + icon = "fa-question-circle"; + + /** + * Whether or not to show the spinner, or the end icon + * + * @attr + */ @property({ type: Boolean }) loading = false; + /** + * If set, will attempt to occupy the full viewport. + * + * @attr + */ @property({ type: Boolean }) fullHeight = false; + /** + * [Optional] If set, will display a message in large text beneath the icon + * + * @attr + */ @property() header = ""; @@ -45,8 +92,7 @@ export class EmptyState extends AKElement {
` : html``}

${this.header}

@@ -60,3 +106,9 @@ export class EmptyState extends AKElement { `; } } + +declare global { + interface HTMLElementTagNameMap { + "ak-empty-state": EmptyState; + } +} diff --git a/web/src/elements/Expand.ts b/web/src/elements/Expand.ts index 1cc1629996..98f706c884 100644 --- a/web/src/elements/Expand.ts +++ b/web/src/elements/Expand.ts @@ -1,24 +1,53 @@ import { AKElement } from "@goauthentik/elements/Base"; import { msg } from "@lit/localize"; -import { CSSResult, TemplateResult, css, html } from "lit"; +import { css, html } from "lit"; import { customElement, property } from "lit/decorators.js"; import PFExpandableSection from "@patternfly/patternfly/components/ExpandableSection/expandable-section.css"; import PFBase from "@patternfly/patternfly/patternfly-base.css"; +export interface IExpand { + expanded?: boolean; + textOpen?: string; + textClosed?: string; +} +/** + * @class Expand + * @element ak-expand + * + * An `ak-expand` is used to hide cluttering details that a user may wish to reveal, such as the raw + * details of an alert or event. + * + * slot - The contents to be hidden or displayed. + */ @customElement("ak-expand") export class Expand extends AKElement { - @property({ type: Boolean }) + /** + * The state of the expanded content + * + * @attr + */ + @property({ type: Boolean, reflect: true }) expanded = false; + /** + * The text to display next to the open/close control when the accordion is closed. + * + * @attr + */ @property() textOpen = msg("Show less"); + /** + * The text to display next to the open/close control when the accordion is . + * + * @attr + */ @property() textClosed = msg("Show more"); - static get styles(): CSSResult[] { + static get styles() { return [ PFBase, PFExpandableSection, @@ -30,7 +59,7 @@ export class Expand extends AKElement { ]; } - render(): TemplateResult { + render() { return html`
`; } } + +declare global { + interface HTMLElementTagNameMap { + "ak-expand": Expand; + } +} diff --git a/web/src/elements/buttons/ActionButton/ak-action-button.stories.ts b/web/src/elements/buttons/ActionButton/ak-action-button.stories.ts index 45430cce4a..150ab62d40 100644 --- a/web/src/elements/buttons/ActionButton/ak-action-button.stories.ts +++ b/web/src/elements/buttons/ActionButton/ak-action-button.stories.ts @@ -4,9 +4,9 @@ import { Meta } from "@storybook/web-components"; import { TemplateResult, html } from "lit"; import "./ak-action-button"; -import AKActionButton from "./ak-action-button"; +import { ActionButton } from "./ak-action-button"; -const metadata: Meta = { +const metadata: Meta = { title: "Elements / Action Button", component: "ak-action-button", parameters: { diff --git a/web/src/elements/buttons/ActionButton/ak-action-button.ts b/web/src/elements/buttons/ActionButton/ak-action-button.ts index e8681cf585..79b1e89cb4 100644 --- a/web/src/elements/buttons/ActionButton/ak-action-button.ts +++ b/web/src/elements/buttons/ActionButton/ak-action-button.ts @@ -1,9 +1,14 @@ import { MessageLevel } from "@goauthentik/common/messages"; -import { BaseTaskButton } from "@goauthentik/elements/buttons/SpinnerButton/BaseTaskButton"; +import { + BaseTaskButton, + type IBaseTaskButton, +} from "@goauthentik/elements/buttons/SpinnerButton/BaseTaskButton"; import { showMessage } from "@goauthentik/elements/messages/MessageContainer"; import { customElement, property } from "lit/decorators.js"; +type IActionButton = IBaseTaskButton & { apiRequest: () => Promise }; + /** * A button associated with an event handler for loading data. Takes an asynchronous function as its * only property. @@ -19,7 +24,7 @@ import { customElement, property } from "lit/decorators.js"; */ @customElement("ak-action-button") -export class ActionButton extends BaseTaskButton { +export class ActionButton extends BaseTaskButton implements IActionButton { /** * The command to run when the button is pressed. Must return a promise. If the promise is a * reject or throw, we process the content of the promise and deliver it to the Notification @@ -27,7 +32,6 @@ export class ActionButton extends BaseTaskButton { * * @attr */ - @property({ attribute: false }) apiRequest: () => Promise = () => { throw new Error(); @@ -52,4 +56,10 @@ export class ActionButton extends BaseTaskButton { } } +declare global { + interface HTMLElementTagNameMap { + "ak-action-button": ActionButton; + } +} + export default ActionButton; diff --git a/web/src/elements/buttons/SpinnerButton/BaseTaskButton.ts b/web/src/elements/buttons/SpinnerButton/BaseTaskButton.ts index 7e07dc3a4b..458e54e39d 100644 --- a/web/src/elements/buttons/SpinnerButton/BaseTaskButton.ts +++ b/web/src/elements/buttons/SpinnerButton/BaseTaskButton.ts @@ -36,6 +36,10 @@ const StatusMap = new Map([ const SPINNER_TIMEOUT = 1000 * 1.5; // milliseconds +export interface IBaseTaskButton { + disabled?: boolean; +} + /** * BaseTaskButton * @@ -46,7 +50,6 @@ const SPINNER_TIMEOUT = 1000 * 1.5; // milliseconds * `onFailure` call their `super.` equivalents. * */ - export abstract class BaseTaskButton extends CustomEmitterElement(AKElement) { eventPrefix = "ak-button"; diff --git a/web/src/elements/buttons/SpinnerButton/ak-spinner-button.ts b/web/src/elements/buttons/SpinnerButton/ak-spinner-button.ts index e3b07e2fb0..9cf75df297 100644 --- a/web/src/elements/buttons/SpinnerButton/ak-spinner-button.ts +++ b/web/src/elements/buttons/SpinnerButton/ak-spinner-button.ts @@ -1,7 +1,7 @@ import { customElement } from "lit/decorators.js"; import { property } from "lit/decorators.js"; -import { BaseTaskButton } from "./BaseTaskButton"; +import { BaseTaskButton, type IBaseTaskButton } from "./BaseTaskButton"; /** * A button associated with an event handler for loading data. Takes an asynchronous function as its @@ -17,8 +17,10 @@ import { BaseTaskButton } from "./BaseTaskButton"; * @fires ak-button-reset - When the button is reset after the async process completes */ +type ISpinnerButton = IBaseTaskButton & { callAction: () => Promise }; + @customElement("ak-spinner-button") -export class SpinnerButton extends BaseTaskButton { +export class SpinnerButton extends BaseTaskButton implements ISpinnerButton { /** * The command to run when the button is pressed. Must return a promise. We don't do anything * with that promise other than check if it's a resolve or reject, and rethrow the event after. @@ -29,4 +31,10 @@ export class SpinnerButton extends BaseTaskButton { callAction!: () => Promise; } +declare global { + interface HTMLElementTagNameMap { + "ak-spinner-button": SpinnerButton; + } +} + export default SpinnerButton; diff --git a/web/src/elements/cards/stories/AggregateCard.tests.ts b/web/src/elements/cards/stories/AggregateCard.test.ts similarity index 100% rename from web/src/elements/cards/stories/AggregateCard.tests.ts rename to web/src/elements/cards/stories/AggregateCard.test.ts diff --git a/web/src/elements/cards/stories/AggregatePromiseCard.tests.ts b/web/src/elements/cards/stories/AggregatePromiseCard.test.ts similarity index 100% rename from web/src/elements/cards/stories/AggregatePromiseCard.tests.ts rename to web/src/elements/cards/stories/AggregatePromiseCard.test.ts diff --git a/web/src/elements/cards/stories/QuickActionCard.tests.ts b/web/src/elements/cards/stories/QuickActionCard.test.ts similarity index 100% rename from web/src/elements/cards/stories/QuickActionCard.tests.ts rename to web/src/elements/cards/stories/QuickActionCard.test.ts diff --git a/web/src/elements/stories/Alert.docs.mdx b/web/src/elements/stories/Alert.docs.mdx new file mode 100644 index 0000000000..92f7ff7bf9 --- /dev/null +++ b/web/src/elements/stories/Alert.docs.mdx @@ -0,0 +1,43 @@ +import { Canvas, Description, Meta, Story, Title } from "@storybook/blocks"; + +import * as AlertStories from "./Alert.stories"; + + + +# Alerts + +Alerts are in-page elements intended to draw the user's attention and alert them to important +details. Alerts are used alongside form elements to warn users of potential mistakes they can +make, as well as in in-line documentation. + +## Usage + +```Typescript +import "@goauthentik/elements/Alert.js"; +``` + +Note that the content of an alert _must_ be a valid HTML component; plain text does not work here. + +```html +

This is the content of your alert!

+``` + +## Demo + +### Default + +The default state of an alert is _warning_. + + + +### Info + + + +### Success + + + +### Danger + + diff --git a/web/src/elements/stories/Alert.stories.ts b/web/src/elements/stories/Alert.stories.ts new file mode 100644 index 0000000000..eea8e624d6 --- /dev/null +++ b/web/src/elements/stories/Alert.stories.ts @@ -0,0 +1,77 @@ +import type { Meta, StoryObj } from "@storybook/web-components"; + +import { html } from "lit"; + +import { Alert, type IAlert } from "../Alert.js"; +import "../Alert.js"; + +type IAlertForTesting = IAlert & { message: string }; + +const metadata: Meta = { + title: "Elements/", + component: "ak-alert", + parameters: { + docs: { + description: "An alert", + }, + }, + argTypes: { + inline: { control: "boolean" }, + warning: { control: "boolean" }, + info: { control: "boolean" }, + success: { control: "boolean" }, + danger: { control: "boolean" }, + // @ts-ignore + message: { control: "text" }, + }, +}; + +export default metadata; + +export const DefaultStory: StoryObj = { + args: { + inline: false, + warning: false, + info: false, + success: false, + danger: false, + message: "You should be alarmed.", + }, + + // @ts-ignore + render: ({ inline, warning, info, success, danger, message }: IAlertForTesting) => { + return html`
+ + +

${message}

+
+
`; + }, +}; + +export const SuccessAlert = { + ...DefaultStory, + args: { ...DefaultStory, ...{ success: true, message: "He's a tribute to your genius!" } }, +}; + +export const InfoAlert = { + ...DefaultStory, + args: { ...DefaultStory, ...{ info: true, message: "An octopus has tastebuds on its arms." } }, +}; + +export const DangerAlert = { + ...DefaultStory, + args: { ...DefaultStory, ...{ danger: true, message: "Danger, Will Robinson! Danger!" } }, +}; diff --git a/web/src/elements/stories/Alert.test.ts b/web/src/elements/stories/Alert.test.ts new file mode 100644 index 0000000000..e8eff1abe3 --- /dev/null +++ b/web/src/elements/stories/Alert.test.ts @@ -0,0 +1,36 @@ +import { $, expect } from "@wdio/globals"; + +import { html, render } from "lit"; + +import "../Alert.js"; +import { Level } from "../Alert.js"; + +describe("ak-alert", () => { + it("should render an alert with the enum", async () => { + render(html`This is an alert`, document.body); + + await expect(await $("ak-alert").$(">>>div")).not.toHaveElementClass("pf-m-inline"); + await expect(await $("ak-alert").$(">>>div")).toHaveElementClass("pf-c-alert"); + await expect(await $("ak-alert").$(">>>div")).toHaveElementClass("pf-m-info"); + await expect(await $("ak-alert").$(">>>.pf-c-alert__title")).toHaveText("This is an alert"); + }); + + it("should render an alert with the attribute", async () => { + render(html`This is an alert`, document.body); + await expect(await $("ak-alert").$(">>>div")).toHaveElementClass("pf-m-info"); + await expect(await $("ak-alert").$(">>>.pf-c-alert__title")).toHaveText("This is an alert"); + }); + + it("should render an alert with conflicting attributes in priority order", async () => { + render(html`This is an alert`, document.body); + await expect(await $("ak-alert").$(">>>div")).toHaveElementClass("pf-m-danger"); + await expect(await $("ak-alert").$(">>>.pf-c-alert__title")).toHaveText("This is an alert"); + }); + + it("should render an alert with an inline class and the default level", async () => { + render(html`This is an alert`, document.body); + await expect(await $("ak-alert").$(">>>div")).toHaveElementClass("pf-m-warning"); + await expect(await $("ak-alert").$(">>>div")).toHaveElementClass("pf-m-inline"); + await expect(await $("ak-alert").$(">>>.pf-c-alert__title")).toHaveText("This is an alert"); + }); +}); diff --git a/web/src/elements/stories/Divider.docs.mdx b/web/src/elements/stories/Divider.docs.mdx new file mode 100644 index 0000000000..7b534a4de3 --- /dev/null +++ b/web/src/elements/stories/Divider.docs.mdx @@ -0,0 +1,51 @@ +import { Canvas, Description, Meta, Story, Title } from "@storybook/blocks"; + +import * as DividerStories from "./Divider.stories"; + + + +# Divider + +Divider is a horizontal rule, an in-page element to separate displayed items. + +It has no configurable attributes. It does have a single unnamed slot, which is displayed in-line in +the center of the rule. If the CSS Base in loaded into the parent context, icons defined in the base +can be used here. + +## Usage + +```Typescript +import "@goauthentik/elements/Divider.js"; +``` + +```html + +``` + +With content: + +```html +

Your content here

+``` + +With an icon: + +```html + +``` + +## Demo + +Note that the Divider inherits its background from its parent component. + +### Default Horizontal Rule + + + +### With A Message + + + +### With an Icon + + diff --git a/web/src/elements/stories/Divider.stories.ts b/web/src/elements/stories/Divider.stories.ts new file mode 100644 index 0000000000..1d424294c7 --- /dev/null +++ b/web/src/elements/stories/Divider.stories.ts @@ -0,0 +1,41 @@ +import type { Meta, StoryObj } from "@storybook/web-components"; + +import { TemplateResult, html } from "lit"; + +import { Divider } from "../Divider.js"; +import "../Divider.js"; + +const metadata: Meta = { + title: "Elements/", + component: "ak-divider", + parameters: { + docs: { + description: "our most simple divider", + }, + }, +}; + +export default metadata; + +const container = (content: TemplateResult) => + html`
+ ${content} +
`; + +export const DefaultStory: StoryObj = { + render: () => container(html` `), +}; + +export const DividerWithSlottedContent: StoryObj = { + render: () => container(html`

Time for bed!

`), +}; + +export const DividerWithSlottedIcon: StoryObj = { + render: () => container(html` `), +}; diff --git a/web/src/elements/stories/Divider.test.ts b/web/src/elements/stories/Divider.test.ts new file mode 100644 index 0000000000..3f87da04d9 --- /dev/null +++ b/web/src/elements/stories/Divider.test.ts @@ -0,0 +1,33 @@ +import { ensureCSSStyleSheet } from "@goauthentik/elements/utils/ensureCSSStyleSheet.js"; +import { $, expect } from "@wdio/globals"; + +import { TemplateResult, html, render as litRender } from "lit"; + +import AKGlobal from "@goauthentik/common/styles/authentik.css"; +import PFBase from "@patternfly/patternfly/patternfly-base.css"; + +import "../Divider.js"; + +const render = (body: TemplateResult) => { + document.adoptedStyleSheets = [ + ...document.adoptedStyleSheets, + ensureCSSStyleSheet(PFBase), + ensureCSSStyleSheet(AKGlobal), + ]; + return litRender(body, document.body); +}; + +describe("ak-divider", () => { + it("should render the divider", async () => { + render(html``); + const empty = await $("ak-divider"); + await expect(empty).toExist(); + }); + + it("should render the divider with the specified text", async () => { + render(html`Your Message Here`); + const span = await $("ak-divider").$("span"); + await expect(span).toExist(); + await expect(span).toHaveText("Your Message Here"); + }); +}); diff --git a/web/src/elements/stories/EmptyState.docs.mdx b/web/src/elements/stories/EmptyState.docs.mdx new file mode 100644 index 0000000000..c5e770871d --- /dev/null +++ b/web/src/elements/stories/EmptyState.docs.mdx @@ -0,0 +1,59 @@ +import { Canvas, Description, Meta, Story, Title } from "@storybook/blocks"; + +import * as EmptyStateStories from "./EmptyState.stories"; + + + +# EmptyState + +The EmptyState is an in-page element to indicate that something is either loading or unavailable. +When "loading" is true it displays a spinner, otherwise it displays a static icon. The default +icon is a question mark in a circle. + +It has two named slots, `body` and `primary`, to communicate further details about the current state +this element is meant to display. + +## Usage + +```Typescript +import "@goauthentik/elements/EmptyState.js"; +``` + +Note that the content of an alert _must_ be a valid HTML component; plain text does not work here. + +```html +This would display in the "primary" slot +``` + +## Demo + +### Default: Loading + +The default state is _loading_ + + + +### Done + + + +### Alternative "Done" Icon + +This also shows the "header" attribute filled, which is rendered in a large, dark typeface. + + + +### The Body Slot Filled + +The body content slot is rendered in a lighter typeface at default size. + + + +### The Body and Primary Slot Filled + +The primary content is rendered in the normal dark typeface at default size. It is also spaced +significantly below the spinner itself. + + diff --git a/web/src/elements/stories/EmptyState.stories.ts b/web/src/elements/stories/EmptyState.stories.ts new file mode 100644 index 0000000000..69d710f5bd --- /dev/null +++ b/web/src/elements/stories/EmptyState.stories.ts @@ -0,0 +1,108 @@ +import type { Meta, StoryObj } from "@storybook/web-components"; + +import { TemplateResult, html } from "lit"; +import { ifDefined } from "lit/directives/if-defined.js"; + +import { EmptyState, type IEmptyState } from "../EmptyState.js"; +import "../EmptyState.js"; + +const metadata: Meta = { + title: "Elements/", + component: "ak-empty-state", + parameters: { + docs: { + description: "Our empty state spinner", + }, + }, + argTypes: { + icon: { control: "text" }, + loading: { control: "boolean" }, + fullHeight: { control: "boolean" }, + header: { control: "text" }, + }, +}; + +export default metadata; + +const container = (content: TemplateResult) => + html`
+ ${content} +
`; + +export const DefaultStory: StoryObj = { + args: { + icon: undefined, + loading: true, + fullHeight: false, + header: undefined, + }, + + render: ({ icon, loading, fullHeight, header }: IEmptyState) => + container( + html` + `, + ), +}; + +export const DefaultAndLoadingDone = { + ...DefaultStory, + args: { ...DefaultStory, ...{ loading: false } }, +}; + +export const DoneWithAlternativeIcon = { + ...DefaultStory, + args: { + ...DefaultStory, + ...{ loading: false, icon: "fa-space-shuttle", header: "The final frontier" }, + }, +}; + +export const WithBodySlotFilled = { + ...DefaultStory, + args: { + ...DefaultStory, + ...{ loading: false, icon: "fa-space-shuttle", header: "The final frontier" }, + }, + render: ({ icon, loading, fullHeight, header }: IEmptyState) => + container(html` + + This is the body content + + `), +}; + +export const WithBodyAndPrimarySlotsFilled = { + ...DefaultStory, + args: { + ...DefaultStory, + ...{ loading: false, icon: "fa-space-shuttle", header: "The final frontier" }, + }, + render: ({ icon, loading, fullHeight, header }: IEmptyState) => + container( + html` + This is the body content slot + This is the primary content slot + `, + ), +}; diff --git a/web/src/elements/EmptyState.test.ts b/web/src/elements/stories/EmptyState.test.ts similarity index 95% rename from web/src/elements/EmptyState.test.ts rename to web/src/elements/stories/EmptyState.test.ts index ac0d68c586..7ef5f46038 100644 --- a/web/src/elements/EmptyState.test.ts +++ b/web/src/elements/stories/EmptyState.test.ts @@ -7,7 +7,7 @@ import { TemplateResult, html, render as litRender } from "lit"; import AKGlobal from "@goauthentik/common/styles/authentik.css"; import PFBase from "@patternfly/patternfly/patternfly-base.css"; -import "./EmptyState.js"; +import "../EmptyState.js"; const render = (body: TemplateResult) => { document.adoptedStyleSheets = [ @@ -58,6 +58,6 @@ describe("ak-empty-state", () => { ); const message = await $("ak-empty-state").$(">>>.pf-c-empty-state__body").$(">>>p"); - await expect(message).toHaveText("Try again with a different filter"); + await expect(message).toHaveText("Try again with a fucked filter"); }); }); diff --git a/web/src/elements/stories/Expand.docs.mdx b/web/src/elements/stories/Expand.docs.mdx new file mode 100644 index 0000000000..c5ee9b455a --- /dev/null +++ b/web/src/elements/stories/Expand.docs.mdx @@ -0,0 +1,38 @@ +import { Canvas, Description, Meta, Story, Title } from "@storybook/blocks"; + +import * as ExpandStories from "./Expand.stories"; + + + +# Expand + +Expand is an in-page element used to hid cluttering details that a user may wish to reveal, such as raw +details of an alert or event. + +It has one unnamed slot for the content to be displayed. + +## Usage + +```Typescript +import "@goauthentik/elements/Expand.js"; +``` + +```html +

Your primary content goes here

+``` + +To show the expanded content on initial render: + +```html +

Your primary content goes here

+``` + +## Demo + +### Default: The content is hidden + + + +### Expanded + + diff --git a/web/src/elements/stories/Expand.stories.ts b/web/src/elements/stories/Expand.stories.ts new file mode 100644 index 0000000000..b7baa4eb69 --- /dev/null +++ b/web/src/elements/stories/Expand.stories.ts @@ -0,0 +1,60 @@ +import type { Meta, StoryObj } from "@storybook/web-components"; + +import { TemplateResult, html } from "lit"; +import { ifDefined } from "lit/directives/if-defined.js"; + +import { Expand, type IExpand } from "../Expand.js"; +import "../Expand.js"; + +const metadata: Meta = { + title: "Elements/", + component: "ak-expand", + parameters: { + docs: { + description: "Our accordion component", + }, + }, + argTypes: { + expanded: { control: "boolean" }, + textOpen: { control: "text" }, + textClosed: { control: "text" }, + }, +}; + +export default metadata; + +const container = (content: TemplateResult) => + html`
+ ${content} +
`; + +export const DefaultStory: StoryObj = { + args: { + expanded: false, + textOpen: undefined, + textClosed: undefined, + }, + + render: ({ expanded, textOpen, textClosed }: IExpand) => + container( + html`
+

Μήτ᾽ ἔμοι μέλι μήτε μέλισσα

+

"Neither the bee nor the honey for me." - Sappho, 600 BC

+
+
`, + ), +}; +export const Expanded = { + ...DefaultStory, + args: { ...DefaultStory, ...{ expanded: true } }, +}; diff --git a/web/src/elements/stories/Expand.test.ts b/web/src/elements/stories/Expand.test.ts new file mode 100644 index 0000000000..3fe64a911d --- /dev/null +++ b/web/src/elements/stories/Expand.test.ts @@ -0,0 +1,52 @@ +import { ensureCSSStyleSheet } from "@goauthentik/elements/utils/ensureCSSStyleSheet.js"; +import { $, expect } from "@wdio/globals"; + +import { TemplateResult, html, render as litRender } from "lit"; + +import AKGlobal from "@goauthentik/common/styles/authentik.css"; +import PFBase from "@patternfly/patternfly/patternfly-base.css"; + +import "../Expand.js"; + +const render = (body: TemplateResult) => { + document.adoptedStyleSheets = [ + ...document.adoptedStyleSheets, + ensureCSSStyleSheet(PFBase), + ensureCSSStyleSheet(AKGlobal), + ]; + return litRender(body, document.body); +}; + +describe("ak-expand", () => { + it("should render the expansion content hidden by default", async () => { + render(html`

This is the expanded text

`); + const text = await $("ak-expand").$(">>>.pf-c-expandable-section__content"); + await expect(text).not.toBeDisplayed(); + }); + + it("should render the expansion content visible on demand", async () => { + render(html`

This is the expanded text

`); + const paragraph = await $("ak-expand").$(">>>p"); + await expect(paragraph).toExist(); + await expect(paragraph).toBeDisplayed(); + await expect(paragraph).toHaveText("This is the expanded text"); + }); + + it("should respond to the click event", async () => { + render(html`

This is the expanded text

`); + let content = await $("ak-expand").$(">>>.pf-c-expandable-section__content"); + await expect(content).toExist(); + await expect(content).not.toBeDisplayed(); + const control = await $("ak-expand").$(">>>button"); + + await control.click(); + content = await $("ak-expand").$(">>>.pf-c-expandable-section__content"); + await expect(content).toExist(); + await expect(content).toBeDisplayed(); + + await control.click(); + content = await $("ak-expand").$(">>>.pf-c-expandable-section__content"); + await expect(content).toExist(); + await expect(content).not.toBeDisplayed(); + }); +}); diff --git a/web/wdio.conf.ts b/web/wdio.conf.ts index f1fd0c9f62..3c02e6a20c 100644 --- a/web/wdio.conf.ts +++ b/web/wdio.conf.ts @@ -87,7 +87,7 @@ export const config: Options.Testrunner = { // and 30 processes will get spawned. The property handles how many capabilities // from the same test should run tests. // - maxInstances: 10, + maxInstances: 1, // // If you have trouble getting all important capabilities together, check out the // Sauce Labs platform configurator - a great tool to configure your capabilities: