diff --git a/.github/workflows/airflow-plugin.yml b/.github/workflows/airflow-plugin.yml
index 1fdfc52857b011..eefa02be4f1af8 100644
--- a/.github/workflows/airflow-plugin.yml
+++ b/.github/workflows/airflow-plugin.yml
@@ -80,10 +80,10 @@ jobs:
!**/binary/**
- name: Upload coverage to Codecov
if: always()
- uses: codecov/codecov-action@v3
+ uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
- directory: .
+ directory: ./build/coverage-reports/
fail_ci_if_error: false
flags: airflow,airflow-${{ matrix.extra_pip_extras }}
name: pytest-airflow-${{ matrix.python-version }}-${{ matrix.extra_pip_requirements }}
diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml
index a5889b2d2f92de..1b10fe6e74372b 100644
--- a/.github/workflows/build-and-test.yml
+++ b/.github/workflows/build-and-test.yml
@@ -126,6 +126,16 @@ jobs:
!**/binary/**
- name: Ensure codegen is updated
uses: ./.github/actions/ensure-codegen-updated
+ - name: Upload coverage to Codecov
+ if: always()
+ uses: codecov/codecov-action@v5
+ with:
+ token: ${{ secrets.CODECOV_TOKEN }}
+ directory: ./build/coverage-reports/
+ fail_ci_if_error: false
+ flags: ${{ matrix.timezone }}
+ name: ${{ matrix.command }}
+ verbose: true
quickstart-compose-validation:
runs-on: ubuntu-latest
diff --git a/.github/workflows/dagster-plugin.yml b/.github/workflows/dagster-plugin.yml
index 37b6c93ec841ab..f512dcf8f3ffd4 100644
--- a/.github/workflows/dagster-plugin.yml
+++ b/.github/workflows/dagster-plugin.yml
@@ -66,10 +66,10 @@ jobs:
**/junit.*.xml
- name: Upload coverage to Codecov
if: always()
- uses: codecov/codecov-action@v3
+ uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
- directory: .
+ directory: ./build/coverage-reports/
fail_ci_if_error: false
flags: dagster-${{ matrix.python-version }}-${{ matrix.extraPythonRequirement }}
name: pytest-dagster
diff --git a/.github/workflows/gx-plugin.yml b/.github/workflows/gx-plugin.yml
index aa7c3f069c7654..595438bd6e4a90 100644
--- a/.github/workflows/gx-plugin.yml
+++ b/.github/workflows/gx-plugin.yml
@@ -70,10 +70,10 @@ jobs:
**/junit.*.xml
- name: Upload coverage to Codecov
if: always()
- uses: codecov/codecov-action@v3
+ uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
- directory: .
+ directory: ./build/coverage-reports/
fail_ci_if_error: false
flags: gx-${{ matrix.python-version }}-${{ matrix.extraPythonRequirement }}
name: pytest-gx
diff --git a/.github/workflows/metadata-ingestion.yml b/.github/workflows/metadata-ingestion.yml
index c0eafe891fb0aa..49def2a863c565 100644
--- a/.github/workflows/metadata-ingestion.yml
+++ b/.github/workflows/metadata-ingestion.yml
@@ -94,10 +94,10 @@ jobs:
!**/binary/**
- name: Upload coverage to Codecov
if: ${{ always() && matrix.python-version == '3.10' }}
- uses: codecov/codecov-action@v3
+ uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
- directory: .
+ directory: ./build/coverage-reports/
fail_ci_if_error: false
flags: pytest-${{ matrix.command }}
name: pytest-${{ matrix.command }}
diff --git a/.github/workflows/metadata-io.yml b/.github/workflows/metadata-io.yml
index 5ee2223d71b039..2225baecde64c6 100644
--- a/.github/workflows/metadata-io.yml
+++ b/.github/workflows/metadata-io.yml
@@ -81,6 +81,15 @@ jobs:
!**/binary/**
- name: Ensure codegen is updated
uses: ./.github/actions/ensure-codegen-updated
+ - name: Upload coverage to Codecov
+ if: ${{ always()}}
+ uses: codecov/codecov-action@v5
+ with:
+ token: ${{ secrets.CODECOV_TOKEN }}
+ directory: ./build/coverage-reports/
+ fail_ci_if_error: false
+ name: metadata-io-test
+ verbose: true
event-file:
runs-on: ubuntu-latest
diff --git a/.github/workflows/prefect-plugin.yml b/.github/workflows/prefect-plugin.yml
index b0af00f92b7727..3c75e8fe9a62ff 100644
--- a/.github/workflows/prefect-plugin.yml
+++ b/.github/workflows/prefect-plugin.yml
@@ -67,10 +67,10 @@ jobs:
!**/binary/**
- name: Upload coverage to Codecov
if: always()
- uses: codecov/codecov-action@v3
+ uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
- directory: .
+ directory: ./build/coverage-reports/
fail_ci_if_error: false
flags: prefect,prefect-${{ matrix.extra_pip_extras }}
name: pytest-prefect-${{ matrix.python-version }}
diff --git a/datahub-frontend/build.gradle b/datahub-frontend/build.gradle
index 7750e169b11fbe..5cc5af50d217ba 100644
--- a/datahub-frontend/build.gradle
+++ b/datahub-frontend/build.gradle
@@ -4,8 +4,9 @@ plugins {
id 'org.gradle.playframework'
}
-apply from: "../gradle/versioning/versioning.gradle"
+apply from: '../gradle/versioning/versioning.gradle'
apply from: './play.gradle'
+apply from: '../gradle/coverage/java-coverage.gradle'
ext {
docker_repo = 'datahub-frontend-react'
@@ -18,6 +19,13 @@ java {
}
}
+test {
+ jacoco {
+ // jacoco instrumentation is failing when dealing with code of this dependency, excluding it.
+ excludes = ["com/gargoylesoftware/**"]
+ }
+}
+
model {
// Must specify the dependency here as "stage" is added by rule based model.
tasks.myTar {
diff --git a/datahub-web-react/.storybook/DocTemplate.mdx b/datahub-web-react/.storybook/DocTemplate.mdx
new file mode 100644
index 00000000000000..9ea1250075e11f
--- /dev/null
+++ b/datahub-web-react/.storybook/DocTemplate.mdx
@@ -0,0 +1,42 @@
+import React from 'react';
+
+import { ThemeProvider } from 'styled-components';
+import { GlobalStyle } from './styledComponents';
+
+import { Meta, Title, Subtitle, Description, Primary, Controls, Stories } from '@storybook/blocks';
+import { CodeBlock } from '../src/alchemy-components/.docs/mdx-components';
+
+{/*
+ * 👇 The isTemplate property is required to tell Storybook that this is a template
+ * See https://storybook.js.org/docs/api/doc-block-meta
+ * to learn how to use
+*/}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ### Import
+
+
+
+
+
+ ### Customize
+
+
+
+
+
+
\ No newline at end of file
diff --git a/datahub-web-react/.storybook/main.js b/datahub-web-react/.storybook/main.js
new file mode 100644
index 00000000000000..2b92dffd88eb3a
--- /dev/null
+++ b/datahub-web-react/.storybook/main.js
@@ -0,0 +1,25 @@
+// Docs for badges: https://storybook.js.org/addons/@geometricpanda/storybook-addon-badges
+
+export default {
+ framework: '@storybook/react-vite',
+ features: {
+ buildStoriesJson: true,
+ },
+ core: {
+ disableTelemetry: true,
+ },
+ stories: [
+ '../src/alchemy-components/.docs/*.mdx',
+ '../src/alchemy-components/components/**/*.stories.@(js|jsx|mjs|ts|tsx)'
+ ],
+ addons: [
+ '@storybook/addon-onboarding',
+ '@storybook/addon-essentials',
+ '@storybook/addon-interactions',
+ '@storybook/addon-links',
+ '@geometricpanda/storybook-addon-badges',
+ ],
+ typescript: {
+ reactDocgen: 'react-docgen-typescript',
+ },
+}
\ No newline at end of file
diff --git a/datahub-web-react/.storybook/manager-head.html b/datahub-web-react/.storybook/manager-head.html
new file mode 100644
index 00000000000000..98e6a2895f45c7
--- /dev/null
+++ b/datahub-web-react/.storybook/manager-head.html
@@ -0,0 +1,33 @@
+
\ No newline at end of file
diff --git a/datahub-web-react/.storybook/manager.js b/datahub-web-react/.storybook/manager.js
new file mode 100644
index 00000000000000..6e9c62dd96c23f
--- /dev/null
+++ b/datahub-web-react/.storybook/manager.js
@@ -0,0 +1,15 @@
+import './storybook-theme.css';
+
+import { addons } from '@storybook/manager-api';
+import acrylTheme from './storybook-theme.js';
+
+// Theme setup
+addons.setConfig({
+ theme: acrylTheme,
+});
+
+// Favicon
+const link = document.createElement('link');
+link.setAttribute('rel', 'shortcut icon');
+link.setAttribute('href', 'https://www.acryldata.io/icons/favicon.ico');
+document.head.appendChild(link);
\ No newline at end of file
diff --git a/datahub-web-react/.storybook/preview-head.html b/datahub-web-react/.storybook/preview-head.html
new file mode 100644
index 00000000000000..98e6a2895f45c7
--- /dev/null
+++ b/datahub-web-react/.storybook/preview-head.html
@@ -0,0 +1,33 @@
+
\ No newline at end of file
diff --git a/datahub-web-react/.storybook/preview.js b/datahub-web-react/.storybook/preview.js
new file mode 100644
index 00000000000000..a497ce7bccf3c8
--- /dev/null
+++ b/datahub-web-react/.storybook/preview.js
@@ -0,0 +1,84 @@
+import './storybook-theme.css';
+// FYI: import of antd styles required to show components based on it correctly
+import 'antd/dist/antd.css';
+
+import { BADGE, defaultBadgesConfig } from '@geometricpanda/storybook-addon-badges';
+import DocTemplate from './DocTemplate.mdx';
+
+const preview = {
+ tags: ['!dev', 'autodocs'],
+ parameters: {
+ previewTabs: {
+ 'storybook/docs/panel': { index: -1 },
+ },
+ controls: {
+ matchers: {
+ color: /(background|color)$/i,
+ date: /Date$/i,
+ },
+ },
+ options: {
+ storySort: {
+ method: 'alphabetical',
+ order: [
+ // Order of Docs Pages
+ 'Introduction',
+ 'Style Guide',
+ 'Design Tokens',
+ 'Style Utilities',
+ 'Icons',
+
+ // Order of Components
+ 'Layout',
+ 'Forms',
+ 'Data Display',
+ 'Feedback',
+ 'Typography',
+ 'Overlay',
+ 'Disclosure',
+ 'Navigation',
+ 'Media',
+ 'Other',
+ ],
+ locales: '',
+ },
+ },
+ docs: {
+ page: DocTemplate,
+ toc: {
+ disable: false,
+ },
+ docs: {
+ source: {
+ format: true,
+ },
+ },
+ },
+
+ // Reconfig the premade badges with better titles
+ badgesConfig: {
+ stable: {
+ ...defaultBadgesConfig[BADGE.STABLE],
+ title: 'Stable',
+ tooltip: 'This component is stable but may have frequent changes. Use at own discretion.',
+ },
+ productionReady: {
+ ...defaultBadgesConfig[BADGE.STABLE],
+ title: 'Production Ready',
+ tooltip: 'This component is production ready and has been tested in a production environment.',
+ },
+ WIP: {
+ ...defaultBadgesConfig[BADGE.BETA],
+ title: 'WIP',
+ tooltip: 'This component is a work in progress and may not be fully functional or tested.',
+ },
+ readyForDesignReview: {
+ ...defaultBadgesConfig[BADGE.NEEDS_REVISION],
+ title: 'Ready for Design Review',
+ tooltip: 'This component is ready for design review and feedback.',
+ },
+ },
+ },
+};
+
+export default preview;
diff --git a/datahub-web-react/.storybook/storybook-logo.svg b/datahub-web-react/.storybook/storybook-logo.svg
new file mode 100644
index 00000000000000..5cc86813b59336
--- /dev/null
+++ b/datahub-web-react/.storybook/storybook-logo.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/datahub-web-react/.storybook/storybook-theme.css b/datahub-web-react/.storybook/storybook-theme.css
new file mode 100644
index 00000000000000..edf93c57cf2086
--- /dev/null
+++ b/datahub-web-react/.storybook/storybook-theme.css
@@ -0,0 +1,263 @@
+/* Storybook Theme CSS Overrides */
+
+/* Regular */
+@font-face {
+ font-family: 'Mulish';
+ font-style: normal;
+ font-weight: 400;
+ src: url('../src/fonts/Mulish-Regular.ttf') format('truetype');
+}
+
+/* Medium */
+@font-face {
+ font-family: 'Mulish';
+ font-style: normal;
+ font-weight: 500;
+ src: url('../src/fonts/Mulish-Medium.ttf') format('truetype');
+}
+
+/* SemiBold */
+@font-face {
+ font-family: 'Mulish';
+ font-style: normal;
+ font-weight: 600;
+ src: url('../src/fonts/Mulish-SemiBold.ttf') format('truetype');
+}
+
+/* Bold */
+@font-face {
+ font-family: 'Mulish';
+ font-style: normal;
+ font-weight: 700;
+ src: url('../src/fonts/Mulish-Bold.ttf') format('truetype');
+}
+
+body {
+ font-family: 'Mulish', sans-serif !important;
+}
+
+::-webkit-scrollbar {
+ height: 8px;
+ width: 8px;
+}
+
+*::-webkit-scrollbar-track {
+ background: rgba(193, 196, 208, 0);
+ border-radius: 10px;
+}
+
+*::-webkit-scrollbar-thumb {
+ background: rgba(193, 196, 208, 0);
+ border-radius: 10px;
+ transition: 0.3s;
+}
+
+*:hover::-webkit-scrollbar-track {
+ background: rgba(193, 196, 208, 0.3);
+}
+
+*:hover::-webkit-scrollbar-thumb {
+ background: rgba(193, 196, 208, 0.8);
+}
+
+.sbdocs-wrapper {
+ max-width: 95% !important;
+}
+
+.sidebar-header img {
+ max-height: 25px !important;
+}
+
+.sb-bar {
+ box-shadow: none !important;
+ border-bottom: 1px solid hsla(203, 50%, 30%, 0.15) !important;
+}
+
+.sbdocs-preview,
+.docblock-argstable-body,
+.docblock-source {
+ box-shadow: none !important;
+ filter: none !important;
+}
+
+.docblock-source {
+ max-width: 100% !important;
+ overflow: auto !important;
+ margin: 1rem 0 !important;
+}
+
+.sidebar-item,
+.sidebar-item[data-selected="true"] {
+ height: 32px !important;
+ display: flex !important;
+ align-items: center !important;
+ padding-right: 0 !important;
+ padding: 6px 12px !important;
+ font-size: 15px !important;
+ margin-bottom: 4px !important;
+ color: #000 !important;
+}
+
+.sidebar-item:hover {
+ background-color: #eff8fc !important;
+}
+
+.sidebar-item>a {
+ align-items: center !important;
+ gap: 8px !important;
+ padding: 0 !important;
+}
+
+.sidebar-item[data-nodetype="group"] {
+ margin-top: 8px !important;
+}
+
+.sidebar-item[data-nodetype="component"] {
+ padding-left: 8px !important;
+}
+
+[data-nodetype="root"]>[data-action="collapse-root"]>div:first-child,
+[data-nodetype="component"] div {
+ display: none !important;
+}
+
+[data-nodetype="document"][data-parent-id],
+[data-nodetype="story"][data-parent-id] {
+ padding: 0 !important;
+ margin-left: 16px !important;
+ height: 18px !important;
+ min-height: auto !important;
+ font-weight: 400 !important;
+}
+
+[data-nodetype="document"][data-parent-id] svg,
+[data-nodetype="story"][data-parent-id] svg {
+ display: none !important;
+}
+
+[data-nodetype="document"][data-parent-id]::before,
+[data-nodetype="story"][data-parent-id]::before {
+ content: '→' !important;
+}
+
+[data-nodetype="document"][data-parent-id]:hover,
+[data-nodetype="story"][data-parent-id]:hover,
+[data-nodetype="document"][data-parent-id][data-selected="true"]:hover,
+[data-nodetype="story"][data-parent-id][data-selected="true"]:hover {
+ background-color: #fff !important;
+ color: #4da1bf !important;
+}
+
+[data-nodetype="document"][data-parent-id][data-selected="true"],
+[data-nodetype="story"][data-parent-id][data-selected="true"] {
+ background-color: #fff !important;
+ height: 18px !important;
+ min-height: auto !important;
+ font-weight: 400 !important;
+}
+
+.sbdocs-content div[id*=--sandbox]~div[id*=--sandbox]~div[id*=--sandbox],
+li:has(a[href="#sandbox"]) {
+ display: none !important;
+}
+
+[data-nodetype="document"]:not([data-parent-id]) {
+ padding-left: 0 !important;
+}
+
+[data-nodetype="document"]:not([data-parent-id]) svg {
+ display: none !important;
+}
+
+[data-nodetype="document"]:not([data-parent-id])>a {
+ font-size: 18px !important;
+ font-weight: 300 !important;
+}
+
+[data-nodetype="component"][aria-expanded="true"],
+[data-nodetype="document"][data-selected="true"] {
+ color: #000 !important;
+ background-color: transparent !important;
+ font-weight: 700 !important;
+}
+
+[data-nodetype="root"][data-selected="true"] {
+ background-color: transparent !important;
+}
+
+[data-nodetype="document"][data-selected="true"],
+[data-nodetype="document"][data-parent-id][data-selected="true"] {
+ color: #4da1bf !important;
+}
+
+.sidebar-subheading {
+ font-size: 12px !important;
+ font-weight: 600 !important;
+ letter-spacing: 1px !important;
+ color: #a9adbd !important;
+}
+
+.sbdocs-wrapper {
+ padding: 2rem !important;
+}
+
+table,
+tr,
+tbody>tr>* {
+ border-color: hsla(203, 50%, 30%, 0.15) !important;
+ background-color: transparent;
+}
+
+:where(table:not(.sb-anchor, .sb-unstyled, .sb-unstyled table)) tr:nth-of-type(2n) {
+ background-color: transparent !important;
+}
+
+tr {
+ border-top: 0 !important;
+}
+
+th {
+ border: 0 !important;
+}
+
+h2#stories {
+ display: none;
+}
+
+.tabbutton {
+ border-bottom: none !important
+}
+
+.tabbutton.tabbutton-active {
+ color: rgb(120, 201, 230) !important;
+}
+
+.toc-wrapper {
+ margin-top: -2.5rem !important;
+ font-family: 'Mulish', sans-serif !important;
+}
+
+/* Custom Doc Styles */
+
+.custom-docs {
+ position: relative;
+}
+
+.acrylBg {
+ position: fixed;
+ bottom: 0;
+ left: -20px;
+ background-repeat: repeat;
+ z-index: 0;
+}
+
+.acrylBg img {
+ filter: invert(8);
+}
+
+.custom-docs p,
+.docsDescription p,
+.custom-docs li {
+ font-size: 16px;
+ line-height: 1.75;
+}
\ No newline at end of file
diff --git a/datahub-web-react/.storybook/storybook-theme.js b/datahub-web-react/.storybook/storybook-theme.js
new file mode 100644
index 00000000000000..462bf2f03da944
--- /dev/null
+++ b/datahub-web-react/.storybook/storybook-theme.js
@@ -0,0 +1,47 @@
+import { create } from '@storybook/theming';
+import brandImage from './storybook-logo.svg';
+
+import theme, { typography } from '../src/alchemy-components/theme';
+
+export default create({
+ // config
+ base: 'light',
+ brandTitle: 'Acryl Design System',
+ brandUrl: '/?path=/docs/',
+ brandImage: brandImage,
+ brandTarget: '_self',
+
+ // styles
+ fontBase: typography.fontFamily,
+ fontCode: 'monospace',
+
+ colorPrimary: theme.semanticTokens.colors.primary,
+ colorSecondary: theme.semanticTokens.colors.secondary,
+
+ // UI
+ appBg: theme.semanticTokens.colors['body-bg'],
+ appContentBg: theme.semanticTokens.colors['body-bg'],
+ appPreviewBg: theme.semanticTokens.colors['body-bg'],
+ appBorderColor: theme.semanticTokens.colors['border-color'],
+ appBorderRadius: 4,
+
+ // Text colors
+ textColor: theme.semanticTokens.colors['body-text'],
+ textInverseColor: theme.semanticTokens.colors['inverse-text'],
+ textMutedColor: theme.semanticTokens.colors['subtle-text'],
+
+ // Toolbar default and active colors
+ barTextColor: theme.semanticTokens.colors['body-text'],
+ barSelectedColor: theme.semanticTokens.colors['subtle-bg'],
+ barHoverColor: theme.semanticTokens.colors['subtle-bg'],
+ barBg: theme.semanticTokens.colors['body-bg'],
+
+ // Form colors
+ inputBg: theme.semanticTokens.colors['body-bg'],
+ inputBorder: theme.semanticTokens.colors['border-color'],
+ inputTextColor: theme.semanticTokens.colors['body-text'],
+ inputBorderRadius: 4,
+
+ // Grid
+ gridCellSize: 6,
+});
\ No newline at end of file
diff --git a/datahub-web-react/.storybook/styledComponents.ts b/datahub-web-react/.storybook/styledComponents.ts
new file mode 100644
index 00000000000000..5951c810d89985
--- /dev/null
+++ b/datahub-web-react/.storybook/styledComponents.ts
@@ -0,0 +1,36 @@
+import { createGlobalStyle } from 'styled-components';
+
+import '../src/fonts/Mulish-Regular.ttf';
+import '../src/fonts/Mulish-Medium.ttf';
+import '../src/fonts/Mulish-SemiBold.ttf';
+import '../src/fonts/Mulish-Bold.ttf';
+
+export const GlobalStyle = createGlobalStyle`
+ @font-face {
+ font-family: 'Mulish';
+ font-style: normal;
+ font-weight: 400;
+ src: url('../src/fonts/Mulish-Regular.ttf) format('truetype');
+ }
+ @font-face {
+ font-family: 'Mulish';
+ font-style: normal;
+ font-weight: 500;
+ src: url('../src/fonts/Mulish-Medium.ttf) format('truetype');
+ }
+ @font-face {
+ font-family: 'Mulish';
+ font-style: normal;
+ font-weight: 600;
+ src: url('../src/fonts/Mulish-SemiBold.ttf) format('truetype');
+ }
+ @font-face {
+ font-family: 'Mulish';
+ font-style: normal;
+ font-weight: 700;
+ src: url('../src/fonts/Mulish-Bold.ttf) format('truetype');
+ }
+ body {
+ font-family: 'Mulish', sans-serif;
+ }
+`;
\ No newline at end of file
diff --git a/datahub-web-react/.storybook/webpack.config.js b/datahub-web-react/.storybook/webpack.config.js
new file mode 100644
index 00000000000000..22e4ec1de63050
--- /dev/null
+++ b/datahub-web-react/.storybook/webpack.config.js
@@ -0,0 +1,13 @@
+const path = require('path');
+
+module.exports = {
+ module: {
+ loaders: [
+ {
+ test: /\.(png|woff|woff2|eot|ttf|svg)$/,
+ loaders: ['file-loader'],
+ include: path.resolve(__dirname, '../'),
+ },
+ ],
+ },
+};
\ No newline at end of file
diff --git a/datahub-web-react/package.json b/datahub-web-react/package.json
index dcaef6004d7022..31c10804482f0c 100644
--- a/datahub-web-react/package.json
+++ b/datahub-web-react/package.json
@@ -9,8 +9,12 @@
"@ant-design/colors": "^5.0.0",
"@ant-design/icons": "^4.3.0",
"@apollo/client": "^3.3.19",
+ "@fontsource/mulish": "^5.0.16",
+ "@geometricpanda/storybook-addon-badges": "^2.0.2",
"@graphql-codegen/fragment-matcher": "^5.0.0",
"@monaco-editor/react": "^4.3.1",
+ "@mui/icons-material": "^5.15.21",
+ "@mui/material": "^5.15.21",
"@react-hook/window-size": "^3.0.7",
"@react-spring/web": "^9.7.3",
"@remirror/pm": "^2.0.3",
@@ -30,6 +34,7 @@
"@uiw/react-md-editor": "^3.3.4",
"@visx/axis": "^3.1.0",
"@visx/curve": "^3.0.0",
+ "@visx/gradient": "^3.3.0",
"@visx/group": "^3.0.0",
"@visx/hierarchy": "^3.0.0",
"@visx/legend": "^3.2.0",
@@ -93,7 +98,9 @@
"format-check": "prettier --check src",
"format": "prettier --write src",
"type-check": "tsc --noEmit",
- "type-watch": "tsc -w --noEmit"
+ "type-watch": "tsc -w --noEmit",
+ "storybook": "storybook dev -p 6006",
+ "build-storybook": "storybook build"
},
"browserslist": {
"production": [
@@ -112,6 +119,16 @@
"@graphql-codegen/near-operation-file-preset": "^1.17.13",
"@graphql-codegen/typescript-operations": "1.17.13",
"@graphql-codegen/typescript-react-apollo": "2.2.1",
+ "@storybook/addon-essentials": "^8.1.11",
+ "@storybook/addon-interactions": "^8.1.11",
+ "@storybook/addon-links": "^8.1.11",
+ "@storybook/addon-onboarding": "^8.1.11",
+ "@storybook/blocks": "^8.1.11",
+ "@storybook/builder-vite": "^8.1.11",
+ "@storybook/manager-api": "^8.1.11",
+ "@storybook/react-vite": "^8.1.11",
+ "@storybook/test": "^8.1.11",
+ "@storybook/theming": "^8.1.11",
"@types/graphql": "^14.5.0",
"@types/query-string": "^6.3.0",
"@types/styled-components": "^5.1.7",
@@ -132,6 +149,7 @@
"less": "^4.2.0",
"prettier": "^2.8.8",
"source-map-explorer": "^2.5.2",
+ "storybook": "^8.1.11",
"vite": "^4.5.5",
"vite-plugin-babel-macros": "^1.0.6",
"vite-plugin-static-copy": "^0.17.0",
diff --git a/datahub-web-react/src/alchemy-components/.docs/Contributing.mdx b/datahub-web-react/src/alchemy-components/.docs/Contributing.mdx
new file mode 100644
index 00000000000000..75a31d011903f8
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/.docs/Contributing.mdx
@@ -0,0 +1,43 @@
+import { Meta } from '@storybook/blocks';
+
+
+
+
+ ## Contributing
+
+ Building and maintinging a design system is a collaborative effort. We welcome contributions from all team members, regardless of their role or experience level. This document outlines the process for contributing to the Acryl Component Library.
+
+ ### Development
+
+ To run Storybook locally, use the following command:
+
+ ```
+ yarn storybook
+ ```
+
+ Storybook will start a local development server and open a new browser window with the Storybook interface on port `6006`. When developing new components or updating existing ones, you can use Storybook to preview your changes in real-time. This will ensure that the component looks and behaves as expected before merging your changes.
+
+ ### Crafting New Components
+
+ When creating new components, make sure to follow the established design patterns and coding standards. This will help maintain consistency across all Acryl products and make it easier for other team members to understand and use your components.
+
+ Design new components with reusability in mind . Components should be flexible, extensible, and easy to customize. Avoid hardcoding values and use props to pass data and styles to your components. This will make it easier to reuse the component in different contexts and scenarios.
+
+ Our design team works exclusively in Figma, so if questions arise about the design or implementation of a component, please refer to the Figma files for more information. If you have any questions or need clarification, feel free to reach out to the design team for assistance.
+
+ ### Pull Requests
+
+ When submitting a pull request, please follow these guidelines:
+
+ 1. Create a new branch for your changes.
+ 2. Make sure your code is well-documented and follows the established coding standards.
+ 3. Write clear and concise commit messages.
+ 4. Include a detailed description of the changes in your pull request.
+
+ If applicable, include screenshots or GIFs to demonstrate the changes visually. This will help reviewers understand the context of your changes and provide more accurate feedback. If a Figma file exists, include a link to the file in the pull request description.
+
+ ### Review Process
+
+ All pull requests will be reviewed by the UI and design team to ensure that the changes align with the design system guidelines and best practices. The team will provide feedback and suggestions for improvement, and you may be asked to make additional changes before your pull request is merged.
+
+
diff --git a/datahub-web-react/src/alchemy-components/.docs/DesignTokens.mdx b/datahub-web-react/src/alchemy-components/.docs/DesignTokens.mdx
new file mode 100644
index 00000000000000..0ebdebbf9db4cb
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/.docs/DesignTokens.mdx
@@ -0,0 +1,63 @@
+import { Meta, Source } from '@storybook/blocks';
+
+import theme from '@components/theme';
+
+import { ColorCard, CopyButton } from './mdx-components';
+
+
+
+
+ ## Design Tokens
+
+ To streamline the design process and ensure consistency across all Acryl products, we use a set of design tokens that define the visual properties of our design system. These tokens include colors, typography, spacing, and other visual elements that can be used to create a cohesive user experience.
+
+ ### Colors
+
+ ```tsx
+ import theme from '@components/theme';
+
+ // Accessing a color via object path
+
Hello, World!
+
+ // Using CSS variables
+
Hello, World!
+ ```
+
+
+
+
+ Token Value
+ Selector
+ CSS Variable (coming soon)
+
+
+
+ {Object.keys(theme.semanticTokens.colors).map((color) => {
+ const objectKey = `colors['${color}']`;
+ const hexValue = theme.semanticTokens.colors[color];
+ const cssVar = `--alch-color-${color}`;
+
+ return (
+
+
+
+
+
+ {color}
+ {hexValue}
+
+
+
+
+
+ {objectKey}
+
+
+ {cssVar}
+
+ );
+ })}
+
+
+
+
diff --git a/datahub-web-react/src/alchemy-components/.docs/Icons.mdx b/datahub-web-react/src/alchemy-components/.docs/Icons.mdx
new file mode 100644
index 00000000000000..e3f6ab68461196
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/.docs/Icons.mdx
@@ -0,0 +1,34 @@
+import { Meta, Source } from '@storybook/blocks';
+
+import { AVAILABLE_ICONS } from '@components';
+import { IconGalleryWithSearch } from './mdx-components';
+
+
+
+
+ ## Icons
+
+ Under the hood, we're utilizing the Material Design Icon Library. However, we've crafted out own resuable component to make it easier to use these icons in our application.
+
+
+ View the component documentation to learn more
+
+
+ In addition to using Materials Design Icons, we've also added a few custom icons to the library. You can access them through the same `
` component and are represented in the list of available options below.
+
+ ```tsx
+ import { Icon } from '@components';
+
+
+ ```
+
+
+
+ ### Gallery
+
+ There are {AVAILABLE_ICONS.length} icons available.
+ Name values populate the `icon` prop on the `
` component.
+
+
+
+
diff --git a/datahub-web-react/src/alchemy-components/.docs/Intro.mdx b/datahub-web-react/src/alchemy-components/.docs/Intro.mdx
new file mode 100644
index 00000000000000..f81d08059c7b44
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/.docs/Intro.mdx
@@ -0,0 +1,14 @@
+import { Meta, Description } from '@storybook/blocks';
+import ReadMe from '../README.mdx';
+
+
+
+
+
+
+
+
+ {/* To simply, we're rendering the root readme here */}
+
+
+
diff --git a/datahub-web-react/src/alchemy-components/.docs/StyleGuide.mdx b/datahub-web-react/src/alchemy-components/.docs/StyleGuide.mdx
new file mode 100644
index 00000000000000..43199cbbca62d1
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/.docs/StyleGuide.mdx
@@ -0,0 +1,209 @@
+import { Meta, Source } from '@storybook/blocks';
+
+import { Heading } from '@components';
+import { colors } from '@components/theme';
+
+import { Grid, FlexGrid, ColorCard, CopyButton, Seperator } from './mdx-components';
+
+import borderSource from '@components/theme/foundations/borders?raw';
+import colorsSource from '@components/theme/foundations/colors?raw';
+import typographySource from '@components/theme/foundations/typography?raw';
+import radiusSource from '@components/theme/foundations/radius?raw';
+import shadowsSource from '@components/theme/foundations/shadows?raw';
+import sizesSource from '@components/theme/foundations/sizes?raw';
+import spacingSource from '@components/theme/foundations/spacing?raw';
+import transitionSource from '@components/theme/foundations/transition?raw';
+import zIndexSource from '@components/theme/foundations/zIndex?raw';
+
+
+
+
+ ## Style Guide
+
+ The purpose of this Style Guide is to establish a unified and cohesive design language that ensures a consistent user experience across all Acryl products. By adhering to these guidelines, we can maintain a high standard of design quality and improve the usability of our applications.
+
+ ### Theme
+
+ You can import the theme object into any component or file in your application and use it to style your components. The theme object is a single source of truth for your application's design system.
+
+ ```tsx
+ import { typography, colors, spacing } from '@components/theme';
+ ```
+
+ ### Colors
+
+ Colors are managed via the `colors.ts` file in the `theme/foundations` directory. The colors are defined as a nested object with the following structure:
+
+
+
+ By default, all `500` values are considered the "default" value of that color range. For example, `gray.500` is the default gray color. The other values are used for shading and highlighting. Color values are defined in hex format and their values range between 25 and 1000. With 25 being the lighest and 1000 being the darkest.
+
+ #### Black & White
+
+
+
+
+ Black
+ {colors['black']}
+
+
+
+
+
+ White
+ {colors['white']}
+
+
+
+
+
+
+ #### Gray
+
+ {Object.keys(colors.gray).map((color) => (
+
+
+
+
+ Gray {color}
+
+ {colors['gray'][color]}
+
+
+ ))}
+
+
+
+
+ #### Violet (Primary)
+
+ {Object.keys(colors.violet).map((color) => (
+
+
+
+
+ Violet {color}
+
+ {colors['violet'][color]}
+
+
+ ))}
+
+
+
+
+ #### Blue
+
+ {Object.keys(colors.blue).map((color) => (
+
+
+
+
+ Blue {color}
+
+ {colors['blue'][color]}
+
+
+ ))}
+
+
+
+
+ #### Green
+
+ {Object.keys(colors.green).map((color) => (
+
+
+
+
+ Green {color}
+
+ {colors['green'][color]}
+
+
+ ))}
+
+
+
+
+ #### Yellow
+
+ {Object.keys(colors.yellow).map((color) => (
+
+
+
+
+ Yellow {color}
+
+ {colors['yellow'][color]}
+
+
+ ))}
+
+
+
+
+ #### Red
+
+ {Object.keys(colors.red).map((color) => (
+
+
+
+
+ Red {color}
+
+ {colors['red'][color]}
+
+
+ ))}
+
+
+ ### Typography
+
+ Font styles are managed via the `typography.ts` file in the `theme/foundations` directory. The primary font family in use is `Mulish`. The font styles are defined as a nested object with the following structure:
+
+
+
+ ### Borders
+
+ A set of border values defined by the border key.
+
+
+
+ ### Border Radius
+
+ A set smooth corner radius values defined by the radii key.
+
+
+
+ ### Shadows
+
+ A set of shadow values defined by the shadows key.
+
+
+
+ ## Sizes
+
+ A set of size values defined by the sizes key.
+
+
+
+ ### Spacing
+
+ A set of spacing values defined by the spacing key.
+
+
+
+ ### Transitions
+
+ A set of transition values defined by the transition key.
+
+
+
+ ### Z-Index
+
+ A set of z-index values defined by the zindex key.
+
+
+
+
diff --git a/datahub-web-react/src/alchemy-components/.docs/mdx-components/CodeBlock.tsx b/datahub-web-react/src/alchemy-components/.docs/mdx-components/CodeBlock.tsx
new file mode 100644
index 00000000000000..43b9ebfae64149
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/.docs/mdx-components/CodeBlock.tsx
@@ -0,0 +1,24 @@
+import React from 'react';
+
+import { Source, DocsContext } from '@storybook/blocks';
+
+export const CodeBlock = () => {
+ const context = React.useContext(DocsContext);
+
+ const { primaryStory } = context as any;
+ const component = context ? primaryStory.component.__docgenInfo.displayName : '';
+
+ if (!context || !primaryStory) return null;
+
+ return (
+
+
+
+ );
+};
diff --git a/datahub-web-react/src/alchemy-components/.docs/mdx-components/CopyButton.tsx b/datahub-web-react/src/alchemy-components/.docs/mdx-components/CopyButton.tsx
new file mode 100644
index 00000000000000..c81aa6ed442892
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/.docs/mdx-components/CopyButton.tsx
@@ -0,0 +1,16 @@
+import React from 'react';
+
+import { Button, Icon } from '@components';
+import { copyToClipboard } from './utils';
+
+interface Props {
+ text: string;
+}
+
+export const CopyButton = ({ text }: Props) => (
+
+ copyToClipboard(text)}>
+
+
+
+);
diff --git a/datahub-web-react/src/alchemy-components/.docs/mdx-components/GridList.tsx b/datahub-web-react/src/alchemy-components/.docs/mdx-components/GridList.tsx
new file mode 100644
index 00000000000000..5cb4bd27e521a4
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/.docs/mdx-components/GridList.tsx
@@ -0,0 +1,32 @@
+/*
+ Docs Only Component that helps to display a list of components in a grid layout.
+*/
+
+import React, { ReactNode } from 'react';
+
+const styles = {
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ gap: '8px',
+};
+
+interface Props {
+ isVertical?: boolean;
+ width?: number | string;
+ children: ReactNode;
+}
+
+export const GridList = ({ isVertical = false, width = '100%', children }: Props) => {
+ return (
+
+ {children}
+
+ );
+};
diff --git a/datahub-web-react/src/alchemy-components/.docs/mdx-components/IconGalleryWithSearch.tsx b/datahub-web-react/src/alchemy-components/.docs/mdx-components/IconGalleryWithSearch.tsx
new file mode 100644
index 00000000000000..d8751509bd6a72
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/.docs/mdx-components/IconGalleryWithSearch.tsx
@@ -0,0 +1,291 @@
+import React, { useState } from 'react';
+
+import { Icon, Button, ButtonProps } from '@components';
+import { IconGrid, IconGridItem, IconDisplayBlock } from './components';
+
+interface Props {
+ icons: string[];
+}
+
+export const IconGalleryWithSearch = ({ icons }: Props) => {
+ const [iconSet, setIconSet] = useState(icons);
+ const [search, setSearch] = useState('');
+ const [variant, setVariant] = useState('outline');
+
+ const filteredIcons = iconSet.filter((icon) => icon.toLowerCase().includes(search.toLowerCase()));
+
+ const arrows = [
+ 'ArrowBack',
+ 'ArrowCircleDown',
+ 'ArrowCircleLeft',
+ 'ArrowCircleRight',
+ 'ArrowCircleUp',
+ 'ArrowDownward',
+ 'ArrowForward',
+ 'ArrowOutward',
+ 'ArrowUpward',
+ 'CloseFullscreen',
+ 'Cached',
+ 'Code',
+ 'CodeOff',
+ 'CompareArrows',
+ 'Compress',
+ 'ChevronLeft',
+ 'ChevronRight',
+ 'DoubleArrow',
+ 'FastForward',
+ 'FastRewind',
+ 'FileDownload',
+ 'FileUpload',
+ 'ForkLeft',
+ 'ForkRight',
+ 'GetApp',
+ 'LastPage',
+ 'Launch',
+ 'Login',
+ 'Logout',
+ 'LowPriority',
+ 'ManageHistory',
+ 'Merge',
+ 'MergeType',
+ 'MoveUp',
+ 'MultipleStop',
+ 'OpenInFull',
+ 'Outbound',
+ 'Outbox',
+ 'Output',
+ 'PlayArrow',
+ 'PlayCircle',
+ 'Publish',
+ 'ReadMore',
+ 'ExitToApp',
+ 'Redo',
+ 'Refresh',
+ 'Replay',
+ 'ReplyAll',
+ 'Reply',
+ 'Restore',
+ 'SaveAlt',
+ 'Shortcut',
+ 'SkipNext',
+ 'SkipPrevious',
+ 'Start',
+ 'Straight',
+ 'SubdirectoryArrowLeft',
+ 'SubdirectoryArrowRight',
+ 'SwapHoriz',
+ 'SwapVert',
+ 'SwitchLeft',
+ 'SwitchRight',
+ 'SyncAlt',
+ 'SyncDisabled',
+ 'SyncLock',
+ 'Sync',
+ 'Shuffle',
+ 'SyncProblem',
+ 'TrendingDown',
+ 'TrendingFlat',
+ 'TrendingUp',
+ 'TurnLeft',
+ 'TurnRight',
+ 'TurnSlightLeft',
+ 'TurnSlightRight',
+ 'Undo',
+ 'UnfoldLessDouble',
+ 'UnfoldLess',
+ 'UnfoldMoreDouble',
+ 'UnfoldMore',
+ 'UpdateDisabled',
+ 'Update',
+ 'Upgrade',
+ 'Upload',
+ 'ZoomInMap',
+ 'ZoomOutMap',
+ ];
+
+ const dataViz = [
+ 'AccountTree',
+ 'Analytics',
+ 'ArtTrack',
+ 'Article',
+ 'BackupTable',
+ 'BarChart',
+ 'BubbleChart',
+ 'Calculate',
+ 'Equalizer',
+ 'List',
+ 'FormatListBulleted',
+ 'FormatListNumbered',
+ 'Grading',
+ 'InsertChart',
+ 'Hub',
+ 'Insights',
+ 'Lan',
+ 'Leaderboard',
+ 'LegendToggle',
+ 'Map',
+ 'MultilineChart',
+ 'Nat',
+ 'PivotTableChart',
+ 'Poll',
+ 'Polyline',
+ 'QueryStats',
+ 'Radar',
+ 'Route',
+ 'Rule',
+ 'Schema',
+ 'Sort',
+ 'SortByAlpha',
+ 'ShowChart',
+ 'Source',
+ 'SsidChart',
+ 'StackedBarChart',
+ 'StackedLineChart',
+ 'Storage',
+ 'TableChart',
+ 'TableRows',
+ 'TableView',
+ 'Timeline',
+ 'ViewAgenda',
+ 'ViewArray',
+ 'ViewCarousel',
+ 'ViewColumn',
+ 'ViewComfy',
+ 'ViewCompact',
+ 'ViewCozy',
+ 'ViewDay',
+ 'ViewHeadline',
+ 'ViewKanban',
+ 'ViewList',
+ 'ViewModule',
+ 'ViewQuilt',
+ 'ViewSidebar',
+ 'ViewStream',
+ 'ViewTimeline',
+ 'ViewWeek',
+ 'Visibility',
+ 'VisibilityOff',
+ 'Webhook',
+ 'Window',
+ ];
+
+ const social = [
+ 'AccountCircle',
+ 'Badge',
+ 'Campaign',
+ 'Celebration',
+ 'Chat',
+ 'ChatBubble',
+ 'CommentBank',
+ 'Comment',
+ 'CommentsDisabled',
+ 'Message',
+ 'ContactPage',
+ 'Contacts',
+ 'GroupAdd',
+ 'Group',
+ 'GroupRemove',
+ 'Groups',
+ 'Handshake',
+ 'ManageAccounts',
+ 'MoodBad',
+ 'SentimentDissatisfied',
+ 'SentimentNeutral',
+ 'SentimentSatisfied',
+ 'Mood',
+ 'NoAccounts',
+ 'People',
+ 'PersonAddAlt1',
+ 'PersonOff',
+ 'Person',
+ 'PersonRemoveAlt1',
+ 'PersonSearch',
+ 'SwitchAccount',
+ 'StarBorder',
+ 'StarHalf',
+ 'Star',
+ 'ThumbDown',
+ 'ThumbUp',
+ 'ThumbsUpDown',
+ 'Verified',
+ 'VerifiedUser',
+ ];
+
+ const notifs = [
+ 'Mail',
+ 'Drafts',
+ 'MarkAsUnread',
+ 'Inbox',
+ 'Outbox',
+ 'MoveToInbox',
+ 'Unsubscribe',
+ 'Upcoming',
+ 'NotificationAdd',
+ 'NotificationImportant',
+ 'NotificationsActive',
+ 'NotificationsOff',
+ 'Notifications',
+ 'NotificationsPaused',
+ ];
+
+ const handleChangeSet = (set) => {
+ setIconSet(set);
+ setSearch('');
+ };
+
+ const handleResetSet = () => {
+ setIconSet(icons);
+ setSearch('');
+ };
+
+ const smButtonProps: ButtonProps = {
+ size: 'sm',
+ color: 'gray',
+ };
+
+ return (
+ <>
+ setSearch(e.target.value)}
+ placeholder="Search for an icon…"
+ style={{ width: '100%', padding: '0.5rem', marginBottom: '0.5rem' }}
+ />
+
+
+
+ All
+
+ handleChangeSet(arrows)} {...smButtonProps}>
+ Arrows
+
+ handleChangeSet(dataViz)} {...smButtonProps}>
+ Data Viz
+
+ handleChangeSet(social)} {...smButtonProps}>
+ Social
+
+ handleChangeSet(notifs)} {...smButtonProps}>
+ Notifications
+
+
+
+ setVariant(variant === 'outline' ? 'filled' : 'outline')} {...smButtonProps}>
+ Variant: {variant === 'filled' ? 'Filled' : 'Outline'}
+
+
+
+
+ {filteredIcons.map((icon) => (
+
+
+
+
+ {icon}
+
+ ))}
+
+ >
+ );
+};
diff --git a/datahub-web-react/src/alchemy-components/.docs/mdx-components/components.ts b/datahub-web-react/src/alchemy-components/.docs/mdx-components/components.ts
new file mode 100644
index 00000000000000..28d428493b17b2
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/.docs/mdx-components/components.ts
@@ -0,0 +1,110 @@
+/*
+ Docs Only Components that helps to display information in info guides.
+*/
+
+import styled from 'styled-components';
+
+import theme from '@components/theme';
+
+export const Grid = styled.div`
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
+ gap: 16px;
+`;
+
+export const FlexGrid = styled.div`
+ display: flex;
+ gap: 16px;
+`;
+
+export const VerticalFlexGrid = styled.div`
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+`;
+
+export const Seperator = styled.div`
+ height: 16px;
+`;
+
+export const ColorCard = styled.div<{ color: string; size?: string }>`
+ display: flex;
+ gap: 16px;
+ align-items: center;
+
+ ${({ size }) =>
+ size === 'sm' &&
+ `
+ gap: 8px;
+ `}
+
+ & span {
+ display: block;
+ line-height: 1.3;
+ }
+
+ & .colorChip {
+ background: ${({ color }) => color};
+ width: 3rem;
+ height: 3rem;
+
+ ${({ size }) =>
+ size === 'sm' &&
+ `
+ width: 2rem;
+ height: 2rem;
+ border-radius: 4px;
+ `}
+
+ border-radius: 8px;
+ box-shadow: rgba(0, 0, 0, 0.06) 0px 2px 4px 0px inset;
+ }
+
+ & .colorValue {
+ display: flex;
+ align-items: center;
+ gap: 0;
+ font-weight: bold;
+ font-size: 14px;
+ }
+
+ & .hex {
+ font-size: 11px;
+ opacity: 0.5;
+ text-transform: uppercase;
+ }
+`;
+
+export const IconGrid = styled.div`
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
+ gap: 16px;
+ margin-top: 20px;
+`;
+
+export const IconGridItem = styled.div`
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: space-between;
+
+ border: 1px solid ${theme.semanticTokens.colors['border-color']};
+ border-radius: 8px;
+ overflow: hidden;
+
+ & span {
+ width: 100%;
+ border-top: 1px solid ${theme.semanticTokens.colors['border-color']};
+ background-color: ${theme.semanticTokens.colors['subtle-bg']};
+ text-align: center;
+ padding: 4px 8px;
+ font-size: 10px;
+ }
+`;
+
+export const IconDisplayBlock = styled.div`
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ height: 50px;
+`;
diff --git a/datahub-web-react/src/alchemy-components/.docs/mdx-components/index.ts b/datahub-web-react/src/alchemy-components/.docs/mdx-components/index.ts
new file mode 100644
index 00000000000000..d1c1848d1eb378
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/.docs/mdx-components/index.ts
@@ -0,0 +1,6 @@
+export * from './CodeBlock';
+export * from './CopyButton';
+export * from './GridList';
+export * from './IconGalleryWithSearch';
+export * from './components';
+export * from './utils';
diff --git a/datahub-web-react/src/alchemy-components/.docs/mdx-components/utils.ts b/datahub-web-react/src/alchemy-components/.docs/mdx-components/utils.ts
new file mode 100644
index 00000000000000..d4fa47dc9e9674
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/.docs/mdx-components/utils.ts
@@ -0,0 +1,15 @@
+/*
+ Docs related utils
+*/
+
+/**
+ * Copies the given text to the clipboard.
+ * @param {string} text - The text to be copied to the clipboard.
+ * @returns {Promise} A promise that resolves when the text is copied.
+ */
+export const copyToClipboard = (text: string) => {
+ return navigator.clipboard
+ .writeText(text)
+ .then(() => console.log(`${text} copied to clipboard`))
+ .catch();
+};
diff --git a/datahub-web-react/src/alchemy-components/README.mdx b/datahub-web-react/src/alchemy-components/README.mdx
new file mode 100644
index 00000000000000..5373432c0ede03
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/README.mdx
@@ -0,0 +1,73 @@
+# Alchemy Component Library
+
+This is a comprehensive library of accessible and reusable React components that streamlines the development of Acryl's applications and websites. The library offers a diverse range of components that can be easily combined to build complex user interfaces while adhering to accessibility best practices.
+
+### Component Usage
+
+It's easy to use the components availble in the library. Simply import the component and use it anywhere you're rendering React components.
+
+```tsx
+import { Button } from '@components';
+
+function YourComponent() {
+ return Click me! ;
+}
+```
+
+In addition to the components themselves, you can also import their types:
+
+```tsx
+import type { ButtonProps } from '@components';
+```
+
+### Theme Usage
+
+This component library comes with a complete theme utility that pre-defines all of our styling atoms and makes them accessible at `@components/theme`.
+
+```tsx
+import { colors } from '@components/theme';
+
+function YourComponent() {
+ return (
+
+ This div has a green background!
+
+ )
+}
+```
+
+You can access the theme types at `@components/theme/types` and the theme config at `@components/theme/config`.
+
+### Writing Docs
+
+Our docs are generated using [Storybook](https://storybook.js.org/) and deployed to [Cloudfare](https://www.cloudflare.com/).
+
+- Storybook config is located at `.storybook`
+- Static doc files are located at `alchemy-components/.docs`
+- Component stories are located in each component directory: `alchemy-components/components/Component/Component.stories.tsx`
+
+Storybook serves as our playground for developing components. You can start it locally:
+
+```bash
+yarn storybook
+```
+
+This launches the docs app at `localhost:6006` and enables everything you need to quickly develop and document components.
+
+### Contributing
+
+Building a component library is a collaboriate effort! We're aiming to provide a first-class experience, so here's a list of the standards we'll be looking for:
+
+- Consitent prop and variant naming conventions:
+ -- `variant` is used to define style types, such as `outline` or `filled`.
+ -- `color` is used to define the components color, such as `violet` or `blue`.
+ -- `size` is used to define the components size, such as `xs` or `4xl`.
+ -- Booleans are prefixed with `is`: `isLoading` or `isDisabled`.
+- All style props have a correseponding theme type, ie. `FontSizeOptions`.
+- All components have an export of default props.
+- Styles are defined using `style objects` instead of `tagged template literals`.
+- Stories are organized into the correct directory .
+
+### FAQs
+
+- **How are components being styled?** Our components are built using [Styled Components](https://styled-components.com/) that dynamically generate styles based on variant selection.
diff --git a/datahub-web-react/src/alchemy-components/components/Avatar/Avatar.stories.tsx b/datahub-web-react/src/alchemy-components/components/Avatar/Avatar.stories.tsx
new file mode 100644
index 00000000000000..09d0d37f15421a
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/components/Avatar/Avatar.stories.tsx
@@ -0,0 +1,133 @@
+import { BADGE } from '@geometricpanda/storybook-addon-badges';
+import { GridList } from '@src/alchemy-components/.docs/mdx-components';
+import { Meta, StoryObj } from '@storybook/react';
+import React from 'react';
+import { Avatar, avatarDefaults } from './Avatar';
+
+const IMAGE_URL =
+ 'https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/78/cb/e1/78cbe16d-28d9-057e-9f73-524c32eb5fe5/AppIcon-0-0-1x_U007emarketing-0-7-0-85-220.png/512x512bb.jpg';
+
+// Auto Docs
+const meta = {
+ title: 'Components / Avatar',
+ component: Avatar,
+
+ // Display Properties
+ parameters: {
+ layout: 'centered',
+ badges: [BADGE.STABLE, 'readyForDesignReview'],
+ docs: {
+ subtitle: 'This component allows users to render a user pill with picture and name',
+ },
+ },
+
+ // Component-level argTypes
+ argTypes: {
+ name: {
+ description: 'Name of the user.',
+ table: {
+ defaultValue: { summary: `${avatarDefaults.name}` },
+ },
+ control: 'text',
+ },
+ imageUrl: {
+ description: 'URL of the user image.',
+ control: 'text',
+ },
+ onClick: {
+ description: 'On click function for the Avatar.',
+ },
+ size: {
+ description: 'Size of the Avatar.',
+ table: {
+ defaultValue: { summary: `${avatarDefaults.size}` },
+ },
+ control: 'select',
+ },
+ showInPill: {
+ description: 'Whether Avatar is shown in pill format with name.',
+ table: {
+ defaultValue: { summary: `${avatarDefaults.showInPill}` },
+ },
+ control: 'boolean',
+ },
+
+ isOutlined: {
+ description: 'Whether Avatar is outlined.',
+ table: {
+ defaultValue: { summary: `${avatarDefaults.isOutlined}` },
+ },
+ control: 'boolean',
+ },
+ },
+
+ // Define defaults
+ args: {
+ name: 'John Doe',
+ size: 'default',
+ showInPill: false,
+ isOutlined: false,
+ },
+} satisfies Meta;
+
+export default meta;
+
+// Stories
+
+type Story = StoryObj;
+
+// Basic story is what is displayed 1st in storybook & is used as the code sandbox
+// Pass props to this so that it can be customized via the UI props panel
+export const sandbox: Story = {
+ tags: ['dev'],
+ render: (props) => ,
+};
+
+export const sizes = () => (
+
+
+
+
+
+
+);
+
+export const withImage = () => (
+
+
+
+
+
+
+);
+
+export const pills = () => (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+);
+
+export const outlined = () => (
+
+
+
+
+);
+
+export const withOnClick = () => (
+
+ window.alert('Avatar clicked')} />
+ window.alert('Avatar clicked')} showInPill />
+
+);
diff --git a/datahub-web-react/src/alchemy-components/components/Avatar/Avatar.tsx b/datahub-web-react/src/alchemy-components/components/Avatar/Avatar.tsx
new file mode 100644
index 00000000000000..9e5ec025e08e3d
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/components/Avatar/Avatar.tsx
@@ -0,0 +1,40 @@
+import React, { useState } from 'react';
+import { AvatarImage, AvatarImageWrapper, AvatarText, Container } from './components';
+import { AvatarProps } from './types';
+import getAvatarColor, { getNameInitials } from './utils';
+
+export const avatarDefaults: AvatarProps = {
+ name: 'User name',
+ size: 'default',
+ showInPill: false,
+ isOutlined: false,
+};
+
+export const Avatar = ({
+ name = avatarDefaults.name,
+ imageUrl,
+ size = avatarDefaults.size,
+ onClick,
+ showInPill = avatarDefaults.showInPill,
+ isOutlined = avatarDefaults.isOutlined,
+}: AvatarProps) => {
+ const [hasError, setHasError] = useState(false);
+
+ return (
+
+
+ {!hasError && imageUrl ? (
+ setHasError(true)} />
+ ) : (
+ <>{getNameInitials(name)} >
+ )}
+
+ {showInPill && {name} }
+
+ );
+};
diff --git a/datahub-web-react/src/alchemy-components/components/Avatar/_tests_/getNameInitials.test.ts b/datahub-web-react/src/alchemy-components/components/Avatar/_tests_/getNameInitials.test.ts
new file mode 100644
index 00000000000000..54bb258acb0d81
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/components/Avatar/_tests_/getNameInitials.test.ts
@@ -0,0 +1,34 @@
+import { getNameInitials } from '../utils';
+
+describe('get initials of the name', () => {
+ it('get initials of name with first name and last name', () => {
+ expect(getNameInitials('John Doe ')).toEqual('JD');
+ });
+ it('get initials of name with first name and last name in lower case', () => {
+ expect(getNameInitials('john doe')).toEqual('JD');
+ });
+ it('get initials of name with only first name', () => {
+ expect(getNameInitials('Robert')).toEqual('RO');
+ });
+ it('get initials of name with only first name in lower case', () => {
+ expect(getNameInitials('robert')).toEqual('RO');
+ });
+ it('get initials of name with three names', () => {
+ expect(getNameInitials('James Edward Brown')).toEqual('JB');
+ });
+ it('get initials of name with four names', () => {
+ expect(getNameInitials('Michael James Alexander Scott')).toEqual('MS');
+ });
+ it('get initials of name with a hyphen', () => {
+ expect(getNameInitials('Mary-Jane Watson')).toEqual('MW');
+ });
+ it('get initials of name with an apostrophe', () => {
+ expect(getNameInitials("O'Connor")).toEqual('OC');
+ });
+ it('get initials of name with a single letter', () => {
+ expect(getNameInitials('J')).toEqual('J');
+ });
+ it('get initials of name with an empty string', () => {
+ expect(getNameInitials('')).toEqual('');
+ });
+});
diff --git a/datahub-web-react/src/alchemy-components/components/Avatar/components.ts b/datahub-web-react/src/alchemy-components/components/Avatar/components.ts
new file mode 100644
index 00000000000000..bcd23a8ab086c9
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/components/Avatar/components.ts
@@ -0,0 +1,51 @@
+import { colors } from '@src/alchemy-components/theme';
+import { AvatarSizeOptions } from '@src/alchemy-components/theme/config';
+import styled from 'styled-components';
+import { getAvatarColorStyles, getAvatarNameSizes, getAvatarSizes } from './utils';
+
+export const Container = styled.div<{ $hasOnClick: boolean; $showInPill?: boolean }>`
+ display: inline-flex;
+ align-items: center;
+ gap: 4px;
+ border-radius: 20px;
+ border: ${(props) => props.$showInPill && `1px solid ${colors.gray[100]}`};
+ padding: ${(props) => props.$showInPill && '3px 6px 3px 4px'};
+
+ ${(props) =>
+ props.$hasOnClick &&
+ `
+ :hover {
+ cursor: pointer;
+ }
+ `}
+`;
+
+export const AvatarImageWrapper = styled.div<{
+ $color: string;
+ $size?: AvatarSizeOptions;
+ $isOutlined?: boolean;
+ $hasImage?: boolean;
+}>`
+ ${(props) => getAvatarSizes(props.$size)}
+
+ border-radius: 50%;
+ color: ${(props) => props.$color};
+ border: ${(props) => props.$isOutlined && `1px solid ${colors.gray[1800]}`};
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ ${(props) => !props.$hasImage && getAvatarColorStyles(props.$color)}
+`;
+
+export const AvatarImage = styled.img`
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ border-radius: 50%;
+`;
+
+export const AvatarText = styled.span<{ $size?: AvatarSizeOptions }>`
+ color: ${colors.gray[1700]};
+ font-weight: 600;
+ font-size: ${(props) => getAvatarNameSizes(props.$size)};
+`;
diff --git a/datahub-web-react/src/alchemy-components/components/Avatar/index.ts b/datahub-web-react/src/alchemy-components/components/Avatar/index.ts
new file mode 100644
index 00000000000000..d3fb6dfa7c09e1
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/components/Avatar/index.ts
@@ -0,0 +1 @@
+export { Avatar } from './Avatar';
diff --git a/datahub-web-react/src/alchemy-components/components/Avatar/types.ts b/datahub-web-react/src/alchemy-components/components/Avatar/types.ts
new file mode 100644
index 00000000000000..98c554b620dcbd
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/components/Avatar/types.ts
@@ -0,0 +1,10 @@
+import { AvatarSizeOptions } from '@src/alchemy-components/theme/config';
+
+export interface AvatarProps {
+ name: string;
+ imageUrl?: string;
+ onClick?: () => void;
+ size?: AvatarSizeOptions;
+ showInPill?: boolean;
+ isOutlined?: boolean;
+}
diff --git a/datahub-web-react/src/alchemy-components/components/Avatar/utils.ts b/datahub-web-react/src/alchemy-components/components/Avatar/utils.ts
new file mode 100644
index 00000000000000..46b2ee25488b89
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/components/Avatar/utils.ts
@@ -0,0 +1,64 @@
+import { colors } from '@src/alchemy-components/theme';
+
+export const getNameInitials = (userName: string) => {
+ if (!userName) return '';
+ const names = userName.trim().split(/[\s']+/); // Split by spaces or apostrophes
+ if (names.length === 1) {
+ const firstName = names[0];
+ return firstName.length > 1 ? firstName[0]?.toUpperCase() + firstName[1]?.toUpperCase() : firstName[0];
+ }
+ return names[0][0]?.toUpperCase() + names[names.length - 1][0]?.toUpperCase() || '';
+};
+
+export function hashString(str: string) {
+ let hash = 0;
+ if (str.length === 0) {
+ return hash;
+ }
+ for (let i = 0; i < str.length; i++) {
+ const char = str.charCodeAt(i);
+ // eslint-disable-next-line
+ hash = (hash << 5) - hash + char;
+ // eslint-disable-next-line
+ hash = hash & hash; // Convert to 32bit integer
+ }
+ return Math.abs(hash);
+}
+
+const colorMap = {
+ [colors.violet[500]]: { backgroundColor: colors.gray[1000], border: `1px solid ${colors.violet[1000]}` },
+ [colors.blue[1000]]: { backgroundColor: colors.gray[1100], border: `1px solid ${colors.blue[200]}` },
+ [colors.gray[600]]: { backgroundColor: colors.gray[1500], border: `1px solid ${colors.gray[100]}` },
+};
+
+const avatarColors = Object.keys(colorMap);
+
+export const getAvatarColorStyles = (color) => {
+ return {
+ ...colorMap[color],
+ };
+};
+
+export default function getAvatarColor(name: string) {
+ return avatarColors[hashString(name) % avatarColors.length];
+}
+
+export const getAvatarSizes = (size) => {
+ const sizeMap = {
+ sm: { width: '18px', height: '18px', fontSize: '8px' },
+ md: { width: '24px', height: '24px', fontSize: '12px' },
+ lg: { width: '28px', height: '28px', fontSize: '14px' },
+ default: { width: '20px', height: '20px', fontSize: '10px' },
+ };
+
+ return {
+ ...sizeMap[size],
+ };
+};
+
+export const getAvatarNameSizes = (size) => {
+ if (size === 'lg') return '16px';
+ if (size === 'sm') return '10px';
+ if (size === 'md') return '14px';
+ return '12px';
+};
diff --git a/datahub-web-react/src/alchemy-components/components/Badge/Badge.stories.tsx b/datahub-web-react/src/alchemy-components/components/Badge/Badge.stories.tsx
new file mode 100644
index 00000000000000..88d499226feafd
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/components/Badge/Badge.stories.tsx
@@ -0,0 +1,102 @@
+import React from 'react';
+
+import { BADGE } from '@geometricpanda/storybook-addon-badges';
+import type { Meta, StoryObj } from '@storybook/react';
+
+import { GridList } from '@components/.docs/mdx-components';
+import { Badge, badgeDefault } from './Badge';
+import pillMeta from '../Pills/Pill.stories';
+import { omitKeys } from './utils';
+
+const pillMetaArgTypes = omitKeys(pillMeta.argTypes, ['label']);
+const pillMetaArgs = omitKeys(pillMeta.args, ['label']);
+
+const meta = {
+ title: 'Components / Badge',
+ component: Badge,
+
+ // Display Properties
+ parameters: {
+ layout: 'centered',
+ badges: [BADGE.EXPERIMENTAL],
+ docs: {
+ subtitle: 'A component that is used to get badge',
+ },
+ },
+
+ // Component-level argTypes
+ argTypes: {
+ count: {
+ description: 'Count to show.',
+ table: {
+ defaultValue: { summary: `${badgeDefault.count}` },
+ },
+ control: {
+ type: 'number',
+ },
+ },
+ overflowCount: {
+ description: 'Max count to show.',
+ table: {
+ defaultValue: { summary: `${badgeDefault.overflowCount}` },
+ },
+ control: {
+ type: 'number',
+ },
+ },
+ showZero: {
+ description: 'Whether to show badge when `count` is zero.',
+ table: {
+ defaultValue: { summary: `${badgeDefault.showZero}` },
+ },
+ control: {
+ type: 'boolean',
+ },
+ },
+ ...pillMetaArgTypes,
+ },
+
+ // Define defaults
+ args: {
+ count: 100,
+ overflowCount: badgeDefault.overflowCount,
+ showZero: badgeDefault.showZero,
+ ...pillMetaArgs,
+ },
+} satisfies Meta;
+
+export default meta;
+
+type Story = StoryObj;
+
+export const sandbox: Story = {
+ tags: ['dev'],
+ render: (props) => ,
+};
+
+export const sizes = () => (
+
+
+
+
+
+);
+
+export const colors = () => (
+
+
+
+
+
+
+
+
+);
+
+export const withIcon = () => (
+
+
+
+
+
+);
diff --git a/datahub-web-react/src/alchemy-components/components/Badge/Badge.tsx b/datahub-web-react/src/alchemy-components/components/Badge/Badge.tsx
new file mode 100644
index 00000000000000..1c934ef120eee8
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/components/Badge/Badge.tsx
@@ -0,0 +1,29 @@
+import { Pill } from '@components';
+import React, { useMemo } from 'react';
+
+import { BadgeProps } from './types';
+import { formatBadgeValue } from './utils';
+import { BadgeContainer } from './components';
+
+export const badgeDefault: BadgeProps = {
+ count: 0,
+ overflowCount: 99,
+ showZero: false,
+};
+
+export function Badge({
+ count = badgeDefault.count,
+ overflowCount = badgeDefault.overflowCount,
+ showZero = badgeDefault.showZero,
+ ...props
+}: BadgeProps) {
+ const label = useMemo(() => formatBadgeValue(count, overflowCount), [count, overflowCount]);
+
+ if (!showZero && count === 0) return null;
+
+ return (
+
+
+
+ );
+}
diff --git a/datahub-web-react/src/alchemy-components/components/Badge/components.ts b/datahub-web-react/src/alchemy-components/components/Badge/components.ts
new file mode 100644
index 00000000000000..a7791cd4f5ff88
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/components/Badge/components.ts
@@ -0,0 +1,6 @@
+import styled from 'styled-components';
+
+export const BadgeContainer = styled.div({
+ // Base root styles
+ display: 'inline-flex',
+});
diff --git a/datahub-web-react/src/alchemy-components/components/Badge/index.ts b/datahub-web-react/src/alchemy-components/components/Badge/index.ts
new file mode 100644
index 00000000000000..26a9e305c7ffd5
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/components/Badge/index.ts
@@ -0,0 +1 @@
+export { Badge } from './Badge';
diff --git a/datahub-web-react/src/alchemy-components/components/Badge/types.ts b/datahub-web-react/src/alchemy-components/components/Badge/types.ts
new file mode 100644
index 00000000000000..21348f2a083419
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/components/Badge/types.ts
@@ -0,0 +1,8 @@
+import { HTMLAttributes } from 'react';
+import { PillProps } from '../Pills/types';
+
+export interface BadgeProps extends HTMLAttributes, Omit {
+ count: number;
+ overflowCount?: number;
+ showZero?: boolean;
+}
diff --git a/datahub-web-react/src/alchemy-components/components/Badge/utils.ts b/datahub-web-react/src/alchemy-components/components/Badge/utils.ts
new file mode 100644
index 00000000000000..e59ec2af998e74
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/components/Badge/utils.ts
@@ -0,0 +1,15 @@
+export const formatBadgeValue = (value: number, overflowCount?: number): string => {
+ if (overflowCount === undefined || value < overflowCount) return String(value);
+
+ return `${overflowCount}+`;
+};
+
+export function omitKeys(obj: T, keys: K[]): Omit {
+ const { ...rest } = obj;
+
+ keys.forEach((key) => {
+ delete rest[key];
+ });
+
+ return rest;
+}
diff --git a/datahub-web-react/src/alchemy-components/components/BarChart/BarChart.stories.tsx b/datahub-web-react/src/alchemy-components/components/BarChart/BarChart.stories.tsx
new file mode 100644
index 00000000000000..1258ff398c0a7e
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/components/BarChart/BarChart.stories.tsx
@@ -0,0 +1,90 @@
+import React from 'react';
+import { BADGE } from '@geometricpanda/storybook-addon-badges';
+import type { Meta, StoryObj } from '@storybook/react';
+import { BarChart } from './BarChart';
+import { getMockedProps } from './utils';
+
+const meta = {
+ title: 'Charts / BarChart',
+ component: BarChart,
+
+ // Display Properties
+ parameters: {
+ layout: 'centered',
+ badges: [BADGE.EXPERIMENTAL],
+ docs: {
+ subtitle: 'A component that is used to show BarChart',
+ },
+ },
+
+ // Component-level argTypes
+ argTypes: {
+ data: {
+ description: 'Array of datum to show',
+ },
+ xAccessor: {
+ description: 'A function to convert datum to value of X',
+ },
+ yAccessor: {
+ description: 'A function to convert datum to value of Y',
+ },
+ renderTooltipContent: {
+ description: 'A function to replace default rendering of toolbar',
+ },
+ margin: {
+ description: 'Add margins to chart',
+ },
+ leftAxisTickFormat: {
+ description: 'A function to format labels of left axis',
+ },
+ leftAxisTickLabelProps: {
+ description: 'Props for label of left axis',
+ },
+ bottomAxisTickFormat: {
+ description: 'A function to format labels of bottom axis',
+ },
+ bottomAxisTickLabelProps: {
+ description: 'Props for label of bottom axis',
+ },
+ barColor: {
+ description: 'Color of bar',
+ control: {
+ type: 'color',
+ },
+ },
+ barSelectedColor: {
+ description: 'Color of selected bar',
+ control: {
+ type: 'color',
+ },
+ },
+ gridColor: {
+ description: "Color of grid's lines",
+ control: {
+ type: 'color',
+ },
+ },
+ renderGradients: {
+ description: 'A function to render different gradients that can be used as colors',
+ },
+ },
+
+ // Define defaults
+ args: {
+ ...getMockedProps(),
+ renderTooltipContent: (datum) => <>DATUM: {JSON.stringify(datum)}>,
+ },
+} satisfies Meta;
+
+export default meta;
+
+type Story = StoryObj;
+
+export const sandbox: Story = {
+ tags: ['dev'],
+ render: (props) => (
+
+
+
+ ),
+};
diff --git a/datahub-web-react/src/alchemy-components/components/BarChart/BarChart.tsx b/datahub-web-react/src/alchemy-components/components/BarChart/BarChart.tsx
new file mode 100644
index 00000000000000..eb5465a1d1217b
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/components/BarChart/BarChart.tsx
@@ -0,0 +1,152 @@
+import React, { useState } from 'react';
+import { colors } from '@src/alchemy-components/theme';
+import { TickLabelProps } from '@visx/axis';
+import { LinearGradient } from '@visx/gradient';
+import { ParentSize } from '@visx/responsive';
+import { Axis, AxisScale, BarSeries, Grid, Tooltip, XYChart } from '@visx/xychart';
+import dayjs from 'dayjs';
+import { Popover } from '../Popover';
+import { ChartWrapper, StyledBarSeries } from './components';
+import { BarChartProps } from './types';
+import { abbreviateNumber } from '../dataviz/utils';
+
+const commonTickLabelProps: TickLabelProps = {
+ fontSize: 10,
+ fontFamily: 'Mulish',
+ fill: colors.gray[1700],
+};
+
+export const barChartDefault: BarChartProps = {
+ data: [],
+ xAccessor: (datum) => datum?.x,
+ yAccessor: (datum) => datum?.y,
+ leftAxisTickFormat: abbreviateNumber,
+ leftAxisTickLabelProps: {
+ ...commonTickLabelProps,
+ textAnchor: 'end',
+ },
+ bottomAxisTickFormat: (value) => dayjs(value).format('DD MMM'),
+ bottomAxisTickLabelProps: {
+ ...commonTickLabelProps,
+ textAnchor: 'middle',
+ verticalAnchor: 'start',
+ width: 20,
+ },
+ barColor: 'url(#bar-gradient)',
+ barSelectedColor: colors.violet[500],
+ gridColor: '#e0e0e0',
+ renderGradients: () => ,
+};
+
+export function BarChart({
+ data,
+ xAccessor = barChartDefault.xAccessor,
+ yAccessor = barChartDefault.yAccessor,
+ renderTooltipContent,
+ margin,
+ leftAxisTickFormat = barChartDefault.leftAxisTickFormat,
+ leftAxisTickLabelProps = barChartDefault.leftAxisTickLabelProps,
+ bottomAxisTickFormat = barChartDefault.bottomAxisTickFormat,
+ bottomAxisTickLabelProps = barChartDefault.bottomAxisTickLabelProps,
+ barColor = barChartDefault.barColor,
+ barSelectedColor = barChartDefault.barSelectedColor,
+ gridColor = barChartDefault.gridColor,
+ renderGradients = barChartDefault.renderGradients,
+}: BarChartProps) {
+ const [hasSelectedBar, setHasSelectedBar] = useState(false);
+
+ // FYI: additional margins to show left and bottom axises
+ const internalMargin = {
+ top: (margin?.top ?? 0) + 30,
+ right: margin?.right ?? 0,
+ bottom: (margin?.bottom ?? 0) + 35,
+ left: (margin?.left ?? 0) + 40,
+ };
+
+ const accessors = { xAccessor, yAccessor };
+
+ return (
+
+
+ {({ width, height }) => {
+ return (
+
+ {renderGradients?.()}
+
+
+
+
+
+
+
+
+
+ }
+ $hasSelectedItem={hasSelectedBar}
+ $color={barColor}
+ $selectedColor={barSelectedColor}
+ dataKey="bar-seria-0"
+ data={data}
+ radius={4}
+ radiusTop
+ onBlur={() => setHasSelectedBar(false)}
+ onFocus={() => setHasSelectedBar(true)}
+ // Internally the library doesn't emmit these events if handlers are empty
+ // They are requred to show/hide/move tooltip
+ onPointerMove={() => null}
+ onPointerUp={() => null}
+ onPointerOut={() => null}
+ {...accessors}
+ />
+
+
+ snapTooltipToDatumX
+ snapTooltipToDatumY
+ unstyled
+ applyPositionStyle
+ renderTooltip={({ tooltipData }) => {
+ return (
+ tooltipData?.nearestDatum && (
+
+ )
+ );
+ }}
+ />
+
+ );
+ }}
+
+
+ );
+}
diff --git a/datahub-web-react/src/alchemy-components/components/BarChart/components.tsx b/datahub-web-react/src/alchemy-components/components/BarChart/components.tsx
new file mode 100644
index 00000000000000..aa8f1320ef21dd
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/components/BarChart/components.tsx
@@ -0,0 +1,34 @@
+import { colors } from '@src/alchemy-components/theme';
+import { BarSeries } from '@visx/xychart';
+import styled from 'styled-components';
+
+export const ChartWrapper = styled.div`
+ width: 100%;
+ height: 100%;
+ position: relative;
+`;
+
+export const StyledBarSeries = styled(BarSeries)<{
+ $hasSelectedItem?: boolean;
+ $color?: string;
+ $selectedColor?: string;
+}>`
+ & {
+ cursor: pointer;
+
+ fill: ${(props) => (props.$hasSelectedItem ? props.$selectedColor : props.$color) || colors.violet[500]};
+ ${(props) => props.$hasSelectedItem && 'opacity: 0.3;'}
+
+ :hover {
+ fill: ${(props) => props.$selectedColor || colors.violet[500]};
+ filter: drop-shadow(0px -2px 5px rgba(33, 23, 95, 0.3));
+ opacity: 1;
+ }
+
+ :focus {
+ fill: ${(props) => props.$selectedColor || colors.violet[500]};
+ outline: none;
+ opacity: 1;
+ }
+ }
+`;
diff --git a/datahub-web-react/src/alchemy-components/components/BarChart/index.ts b/datahub-web-react/src/alchemy-components/components/BarChart/index.ts
new file mode 100644
index 00000000000000..fdfc3f3ab44a89
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/components/BarChart/index.ts
@@ -0,0 +1 @@
+export { BarChart } from './BarChart';
diff --git a/datahub-web-react/src/alchemy-components/components/BarChart/types.ts b/datahub-web-react/src/alchemy-components/components/BarChart/types.ts
new file mode 100644
index 00000000000000..5fd7e2e63e2411
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/components/BarChart/types.ts
@@ -0,0 +1,18 @@
+import { TickFormatter, TickLabelProps } from '@visx/axis';
+import { Margin } from '@visx/xychart';
+
+export type BarChartProps = {
+ data: DatumType[];
+ xAccessor: (datum: DatumType) => string | number;
+ yAccessor: (datum: DatumType) => number;
+ renderTooltipContent?: (datum: DatumType) => React.ReactNode;
+ margin?: Margin;
+ leftAxisTickFormat?: TickFormatter;
+ leftAxisTickLabelProps?: TickLabelProps;
+ bottomAxisTickFormat?: TickFormatter;
+ bottomAxisTickLabelProps?: TickLabelProps;
+ barColor?: string;
+ barSelectedColor?: string;
+ gridColor?: string;
+ renderGradients?: () => React.ReactNode;
+};
diff --git a/datahub-web-react/src/alchemy-components/components/BarChart/utils.ts b/datahub-web-react/src/alchemy-components/components/BarChart/utils.ts
new file mode 100644
index 00000000000000..0b592da7f59b08
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/components/BarChart/utils.ts
@@ -0,0 +1,26 @@
+import dayjs from 'dayjs';
+
+export function generateMockData(length = 30, maxValue = 50_000, minValue = 0) {
+ return Array(length)
+ .fill(0)
+ .map((_, index) => {
+ const date = dayjs()
+ .startOf('day')
+ .add(index - length, 'days')
+ .toDate();
+ const value = Math.max(Math.random() * maxValue, minValue);
+
+ return {
+ x: date,
+ y: value,
+ };
+ });
+}
+
+export function getMockedProps() {
+ return {
+ data: generateMockData(),
+ xAccessor: (datum) => datum.x,
+ yAccessor: (datum) => Math.max(datum.y, 1000),
+ };
+}
diff --git a/datahub-web-react/src/alchemy-components/components/Button/Button.stories.tsx b/datahub-web-react/src/alchemy-components/components/Button/Button.stories.tsx
new file mode 100644
index 00000000000000..e2d7c2852da519
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/components/Button/Button.stories.tsx
@@ -0,0 +1,203 @@
+import React from 'react';
+
+import type { Meta, StoryObj } from '@storybook/react';
+import { BADGE } from '@geometricpanda/storybook-addon-badges';
+
+import { GridList } from '@components/.docs/mdx-components';
+import { AVAILABLE_ICONS } from '@components';
+
+import { Button, buttonDefaults } from '.';
+
+// Auto Docs
+const meta = {
+ title: 'Forms / Button',
+ component: Button,
+
+ // Display Properties
+ parameters: {
+ layout: 'centered',
+ badges: [BADGE.STABLE, 'readyForDesignReview'],
+ docs: {
+ subtitle:
+ 'Buttons are used to trigger an action or event, such as submitting a form, opening a dialog, canceling an action, or performing a delete operation.',
+ },
+ },
+
+ // Component-level argTypes
+ argTypes: {
+ children: {
+ description: 'The content of the Button.',
+ control: {
+ type: 'text',
+ },
+ },
+ variant: {
+ description: 'The variant of the Button.',
+ options: ['filled', 'outline', 'text'],
+ table: {
+ defaultValue: { summary: buttonDefaults.variant },
+ },
+ control: {
+ type: 'radio',
+ },
+ },
+ color: {
+ description: 'The color of the Button.',
+ options: ['violet', 'green', 'red', 'blue', 'gray'],
+ table: {
+ defaultValue: { summary: buttonDefaults.color },
+ },
+ control: {
+ type: 'select',
+ },
+ },
+ size: {
+ description: 'The size of the Button.',
+ options: ['sm', 'md', 'lg', 'xl'],
+ table: {
+ defaultValue: { summary: buttonDefaults.size },
+ },
+ control: {
+ type: 'select',
+ },
+ },
+ icon: {
+ description: 'The icon to display in the Button.',
+ type: 'string',
+ options: AVAILABLE_ICONS,
+ table: {
+ defaultValue: { summary: 'undefined' },
+ },
+ control: {
+ type: 'select',
+ },
+ },
+ iconPosition: {
+ description: 'The position of the icon in the Button.',
+ options: ['left', 'right'],
+ table: {
+ defaultValue: { summary: buttonDefaults.iconPosition },
+ },
+ control: {
+ type: 'radio',
+ },
+ },
+ isCircle: {
+ description:
+ 'Whether the Button should be a circle. If this is selected, the Button will ignore children content, so add an Icon to the Button.',
+ table: {
+ defaultValue: { summary: buttonDefaults?.isCircle?.toString() },
+ },
+ control: {
+ type: 'boolean',
+ },
+ },
+ isLoading: {
+ description: 'Whether the Button is in a loading state.',
+ table: {
+ defaultValue: { summary: buttonDefaults?.isLoading?.toString() },
+ },
+ control: {
+ type: 'boolean',
+ },
+ },
+ isDisabled: {
+ description: 'Whether the Button is disabled.',
+ table: {
+ defaultValue: { summary: buttonDefaults?.isDisabled?.toString() },
+ },
+ control: {
+ type: 'boolean',
+ },
+ },
+ isActive: {
+ description: 'Whether the Button is active.',
+ table: {
+ defaultValue: { summary: buttonDefaults?.isActive?.toString() },
+ },
+ control: {
+ type: 'boolean',
+ },
+ },
+ onClick: {
+ description: 'Function to call when the button is clicked',
+ table: {
+ defaultValue: { summary: 'undefined' },
+ },
+ action: 'clicked',
+ },
+ },
+
+ // Define defaults
+ args: {
+ children: 'Button Content',
+ variant: buttonDefaults.variant,
+ color: buttonDefaults.color,
+ size: buttonDefaults.size,
+ icon: undefined,
+ iconPosition: buttonDefaults.iconPosition,
+ isCircle: buttonDefaults.isCircle,
+ isLoading: buttonDefaults.isLoading,
+ isDisabled: buttonDefaults.isDisabled,
+ isActive: buttonDefaults.isActive,
+ onClick: () => console.log('Button clicked'),
+ },
+} satisfies Meta;
+
+export default meta;
+
+// Stories
+
+type Story = StoryObj;
+
+// Basic story is what is displayed 1st in storybook & is used as the code sandbox
+// Pass props to this so that it can be customized via the UI props panel
+export const sandbox: Story = {
+ tags: ['dev'],
+ render: (props) => Button ,
+};
+
+export const states = () => (
+
+ Default
+ Loading State
+ Active/Focus State
+ Disabled State
+
+);
+
+export const colors = () => (
+
+ Violet Button
+ Green Button
+ Red Button
+ Blue Button
+ Gray Button
+
+);
+
+export const sizes = () => (
+
+ Small Button
+ Regular Button
+ Large Button
+ XLarge Button
+
+);
+
+export const withIcon = () => (
+
+ Icon Left
+
+ Icon Right
+
+
+);
+
+export const circleShape = () => (
+
+
+
+
+
+);
diff --git a/datahub-web-react/src/alchemy-components/components/Button/Button.tsx b/datahub-web-react/src/alchemy-components/components/Button/Button.tsx
new file mode 100644
index 00000000000000..a727b0faf97a99
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/components/Button/Button.tsx
@@ -0,0 +1,60 @@
+import React from 'react';
+
+import { LoadingOutlined } from '@ant-design/icons';
+
+import { Icon } from '@components';
+
+import { ButtonBase } from './components';
+import { ButtonProps } from './types';
+
+export const buttonDefaults: ButtonProps = {
+ variant: 'filled',
+ color: 'violet',
+ size: 'md',
+ iconPosition: 'left',
+ isCircle: false,
+ isLoading: false,
+ isDisabled: false,
+ isActive: false,
+};
+
+export const Button = ({
+ variant = buttonDefaults.variant,
+ color = buttonDefaults.color,
+ size = buttonDefaults.size,
+ icon, // default undefined
+ iconPosition = buttonDefaults.iconPosition,
+ isCircle = buttonDefaults.isCircle,
+ isLoading = buttonDefaults.isLoading,
+ isDisabled = buttonDefaults.isDisabled,
+ isActive = buttonDefaults.isActive,
+ children,
+ ...props
+}: ButtonProps) => {
+ const sharedProps = {
+ variant,
+ color,
+ size,
+ isCircle,
+ isLoading,
+ isActive,
+ isDisabled,
+ disabled: isDisabled,
+ };
+
+ if (isLoading) {
+ return (
+
+ {!isCircle && children}
+
+ );
+ }
+
+ return (
+
+ {icon && iconPosition === 'left' && }
+ {!isCircle && children}
+ {icon && iconPosition === 'right' && }
+
+ );
+};
diff --git a/datahub-web-react/src/alchemy-components/components/Button/components.ts b/datahub-web-react/src/alchemy-components/components/Button/components.ts
new file mode 100644
index 00000000000000..49fa9a12ede6e2
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/components/Button/components.ts
@@ -0,0 +1,27 @@
+import styled from 'styled-components';
+
+import { spacing } from '@components/theme';
+import { ButtonProps } from './types';
+import { getButtonStyle } from './utils';
+
+export const ButtonBase = styled.button(
+ // Dynamic styles
+ (props: ButtonProps) => ({ ...getButtonStyle(props as ButtonProps) }),
+ {
+ // Base root styles
+ display: 'flex',
+ alignItems: 'center',
+ gap: spacing.xsm,
+ cursor: 'pointer',
+ transition: `all 0.15s ease`,
+
+ // For transitions between focus/active and hover states
+ outlineColor: 'transparent',
+ outlineStyle: 'solid',
+
+ // Base Disabled styles
+ '&:disabled': {
+ cursor: 'not-allowed',
+ },
+ },
+);
diff --git a/datahub-web-react/src/alchemy-components/components/Button/index.ts b/datahub-web-react/src/alchemy-components/components/Button/index.ts
new file mode 100644
index 00000000000000..745d8377f9fbb4
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/components/Button/index.ts
@@ -0,0 +1,2 @@
+export { Button, buttonDefaults } from './Button';
+export type { ButtonProps } from './types';
diff --git a/datahub-web-react/src/alchemy-components/components/Button/types.ts b/datahub-web-react/src/alchemy-components/components/Button/types.ts
new file mode 100644
index 00000000000000..f510ff4c6c13c5
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/components/Button/types.ts
@@ -0,0 +1,16 @@
+import { ButtonHTMLAttributes } from 'react';
+
+import type { IconNames } from '@components';
+import type { SizeOptions, ColorOptions } from '@components/theme/config';
+
+export interface ButtonProps extends ButtonHTMLAttributes {
+ variant?: 'filled' | 'outline' | 'text';
+ color?: ColorOptions;
+ size?: SizeOptions;
+ icon?: IconNames;
+ iconPosition?: 'left' | 'right';
+ isCircle?: boolean;
+ isLoading?: boolean;
+ isDisabled?: boolean;
+ isActive?: boolean;
+}
diff --git a/datahub-web-react/src/alchemy-components/components/Button/utils.ts b/datahub-web-react/src/alchemy-components/components/Button/utils.ts
new file mode 100644
index 00000000000000..c08f4f067304d1
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/components/Button/utils.ts
@@ -0,0 +1,238 @@
+/*
+ * Button Style Utilities
+ */
+
+import { typography, colors, shadows, radius, spacing } from '@components/theme';
+import { getColor, getFontSize } from '@components/theme/utils';
+import { ButtonProps } from './types';
+
+// Utility function to get color styles for button - does not generate CSS
+const getButtonColorStyles = (variant, color) => {
+ const color500 = getColor(color, 500); // value of 500 shade
+ const isViolet = color === 'violet';
+
+ const base = {
+ // Backgrounds
+ bgColor: color500,
+ hoverBgColor: getColor(color, 600),
+ activeBgColor: getColor(color, 700),
+ disabledBgColor: getColor('gray', 100),
+
+ // Borders
+ borderColor: color500,
+ activeBorderColor: getColor(color, 300),
+ disabledBorderColor: getColor('gray', 200),
+
+ // Text
+ textColor: colors.white,
+ disabledTextColor: getColor('gray', 300),
+ };
+
+ // Specific color override for white
+ if (color === 'white') {
+ base.textColor = colors.black;
+ base.disabledTextColor = getColor('gray', 500);
+ }
+
+ // Specific color override for gray
+ if (color === 'gray') {
+ base.textColor = getColor('gray', 500);
+ base.bgColor = getColor('gray', 100);
+ base.borderColor = getColor('gray', 100);
+
+ base.hoverBgColor = getColor('gray', 100);
+ base.activeBgColor = getColor('gray', 200);
+ }
+
+ // Override styles for outline variant
+ if (variant === 'outline') {
+ return {
+ ...base,
+ bgColor: colors.transparent,
+ borderColor: color500,
+ textColor: color500,
+
+ hoverBgColor: isViolet ? getColor(color, 100) : getColor(color, 100),
+ activeBgColor: isViolet ? getColor(color, 100) : getColor(color, 200),
+
+ disabledBgColor: 'transparent',
+ };
+ }
+
+ // Override styles for text variant
+ if (variant === 'text') {
+ return {
+ ...base,
+ textColor: color500,
+
+ bgColor: colors.transparent,
+ borderColor: colors.transparent,
+ hoverBgColor: colors.transparent,
+ activeBgColor: colors.transparent,
+ disabledBgColor: colors.transparent,
+ disabledBorderColor: colors.transparent,
+ };
+ }
+
+ // Filled variable is the base style
+ return base;
+};
+
+// Generate color styles for button
+const getButtonVariantStyles = (variant, color) => {
+ const variantStyles = {
+ filled: {
+ backgroundColor: color.bgColor,
+ border: `1px solid ${color.borderColor}`,
+ color: color.textColor,
+ '&:hover': {
+ backgroundColor: color.hoverBgColor,
+ border: `1px solid ${color.hoverBgColor}`,
+ boxShadow: shadows.sm,
+ },
+ '&:disabled': {
+ backgroundColor: color.disabledBgColor,
+ border: `1px solid ${color.disabledBorderColor}`,
+ color: color.disabledTextColor,
+ boxShadow: shadows.xs,
+ },
+ },
+ outline: {
+ backgroundColor: 'transparent',
+ border: `1px solid ${color.borderColor}`,
+ color: color.textColor,
+ '&:hover': {
+ backgroundColor: color.hoverBgColor,
+ boxShadow: 'none',
+ },
+ '&:disabled': {
+ backgroundColor: color.disabledBgColor,
+ border: `1px solid ${color.disabledBorderColor}`,
+ color: color.disabledTextColor,
+ boxShadow: shadows.xs,
+ },
+ },
+ text: {
+ backgroundColor: 'transparent',
+ border: 'none',
+ color: color.textColor,
+ '&:hover': {
+ backgroundColor: color.hoverBgColor,
+ },
+ '&:disabled': {
+ backgroundColor: color.disabledBgColor,
+ color: color.disabledTextColor,
+ },
+ },
+ };
+
+ return variantStyles[variant];
+};
+
+// Generate font styles for button
+const getButtonFontStyles = (size) => {
+ const baseFontStyles = {
+ fontFamily: typography.fonts.body,
+ fontWeight: typography.fontWeights.normal,
+ lineHeight: typography.lineHeights.none,
+ };
+
+ const sizeStyles = {
+ sm: {
+ ...baseFontStyles,
+ fontSize: getFontSize(size), // 12px
+ },
+ md: {
+ ...baseFontStyles,
+ fontSize: getFontSize(size), // 14px
+ },
+ lg: {
+ ...baseFontStyles,
+ fontSize: getFontSize(size), // 16px
+ },
+ xl: {
+ ...baseFontStyles,
+ fontSize: getFontSize(size), // 18px
+ },
+ };
+
+ return sizeStyles[size];
+};
+
+// Generate radii styles for button
+const getButtonRadiiStyles = (isCircle) => {
+ if (isCircle) return { borderRadius: radius.full };
+ return { borderRadius: radius.sm }; // radius is the same for all button sizes
+};
+
+// Generate padding styles for button
+const getButtonPadding = (size, isCircle) => {
+ if (isCircle) return { padding: spacing.xsm };
+
+ const paddingStyles = {
+ sm: {
+ padding: '8px 12px',
+ },
+ md: {
+ padding: '10px 12px',
+ },
+ lg: {
+ padding: '10px 16px',
+ },
+ xl: {
+ padding: '12px 20px',
+ },
+ };
+
+ return paddingStyles[size];
+};
+
+// Generate active styles for button
+const getButtonActiveStyles = (styleColors) => ({
+ borderColor: 'transparent',
+ backgroundColor: styleColors.activeBgColor,
+ // TODO: Figure out how to make the #fff interior border transparent
+ boxShadow: `0 0 0 2px #fff, 0 0 0 4px ${styleColors.activeBgColor}`,
+});
+
+// Generate loading styles for button
+const getButtonLoadingStyles = () => ({
+ pointerEvents: 'none',
+ opacity: 0.75,
+});
+
+/*
+ * Main function to generate styles for button
+ */
+export const getButtonStyle = (props: ButtonProps) => {
+ const { variant, color, size, isCircle, isActive, isLoading } = props;
+
+ // Get map of colors
+ const colorStyles = getButtonColorStyles(variant, color) || ({} as any);
+
+ // Define styles for button
+ const variantStyles = getButtonVariantStyles(variant, colorStyles);
+ const fontStyles = getButtonFontStyles(size);
+ const radiiStyles = getButtonRadiiStyles(isCircle);
+ const paddingStyles = getButtonPadding(size, isCircle);
+
+ // Base of all generated styles
+ let styles = {
+ ...variantStyles,
+ ...fontStyles,
+ ...radiiStyles,
+ ...paddingStyles,
+ };
+
+ // Focus & Active styles are the same, but active styles are applied conditionally & override prevs styles
+ const activeStyles = { ...getButtonActiveStyles(colorStyles) };
+ styles['&:focus'] = activeStyles;
+ styles['&:active'] = activeStyles;
+ if (isActive) styles = { ...styles, ...activeStyles };
+
+ // Loading styles
+ if (isLoading) styles = { ...styles, ...getButtonLoadingStyles() };
+
+ // Return generated styles
+ return styles;
+};
diff --git a/datahub-web-react/src/alchemy-components/components/Card/Card.stories.tsx b/datahub-web-react/src/alchemy-components/components/Card/Card.stories.tsx
new file mode 100644
index 00000000000000..336831fd15cfab
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/components/Card/Card.stories.tsx
@@ -0,0 +1,141 @@
+import { BADGE } from '@geometricpanda/storybook-addon-badges';
+import { GridList } from '@src/alchemy-components/.docs/mdx-components';
+import { colors } from '@src/alchemy-components/theme';
+import type { Meta, StoryObj } from '@storybook/react';
+import React from 'react';
+import { Card, cardDefaults } from '.';
+import { Icon } from '../Icon';
+
+// Auto Docs
+const meta = {
+ title: 'Components / Card',
+ component: Card,
+
+ // Display Properties
+ parameters: {
+ layout: 'centered',
+ badges: [BADGE.STABLE, 'readyForDesignReview'],
+ docs: {
+ subtitle: 'Used to render a card.',
+ },
+ },
+
+ // Component-level argTypes
+ argTypes: {
+ title: {
+ description: 'The title of the card',
+ table: {
+ defaultValue: { summary: `${cardDefaults.title}` },
+ },
+ control: {
+ type: 'text',
+ },
+ },
+ subTitle: {
+ description: 'The subtitle of the card',
+ control: {
+ type: 'text',
+ },
+ },
+ icon: {
+ description: 'The icon on the card',
+ control: {
+ type: 'text',
+ },
+ },
+ iconAlignment: {
+ description: 'Whether the alignment of icon is horizontal or vertical',
+ table: {
+ defaultValue: { summary: `${cardDefaults.iconAlignment}` },
+ },
+ control: {
+ type: 'select',
+ },
+ },
+ percent: {
+ description: 'The percent value on the pill of the card',
+ control: {
+ type: 'number',
+ },
+ },
+ button: {
+ description: 'The button on the card',
+ control: {
+ type: 'text',
+ },
+ },
+ width: {
+ description: 'The width of the card',
+ control: {
+ type: 'text',
+ },
+ },
+ onClick: {
+ description: 'The on click function for the card',
+ },
+ },
+
+ // Define default args
+ args: {
+ title: 'Title',
+ subTitle: 'Subtitle',
+ iconAlignment: 'horizontal',
+ width: '150px',
+ },
+} satisfies Meta;
+
+export default meta;
+
+// Stories
+
+type Story = StoryObj;
+
+// Basic story is what is displayed 1st in storybook
+// Pass props to this so that it can be customized via the UI props panel
+export const sandbox: Story = {
+ tags: ['dev'],
+ render: (props) => ,
+};
+
+export const withChildren = () => (
+
+ Children of the card (Swap me)
+
+);
+
+export const withoutSubtitle = () => (
+
+ Children of the card (Swap me)
+
+);
+
+export const withIcon = () => (
+
+ } />
+ } iconAlignment="vertical" />
+
+);
+
+export const withButton = () => (
+ }
+ onClick={() => window.alert('Card clicked')}
+ />
+);
+
+export const withPercentPill = () => ;
+
+export const withAllTheElements = () => (
+ }
+ button={ }
+ onClick={() => window.alert('Card clicked')}
+ >
+ Children of the card (Swap me)
+
+);
diff --git a/datahub-web-react/src/alchemy-components/components/Card/Card.tsx b/datahub-web-react/src/alchemy-components/components/Card/Card.tsx
new file mode 100644
index 00000000000000..55c581251bea99
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/components/Card/Card.tsx
@@ -0,0 +1,48 @@
+import React from 'react';
+import { CardProps } from './types';
+import { CardContainer, Header, SubTitle, SubTitleContainer, Title, TitleContainer } from './components';
+import { Pill } from '../Pills';
+
+export const cardDefaults: CardProps = {
+ title: 'Title',
+ iconAlignment: 'horizontal',
+};
+
+export const Card = ({
+ title = cardDefaults.title,
+ iconAlignment = cardDefaults.iconAlignment,
+ subTitle,
+ percent,
+ button,
+ onClick,
+ icon,
+ children,
+ width,
+}: CardProps) => {
+ return (
+
+
+ {icon && {icon}
}
+
+
+ {title}
+ {!!percent && (
+
+ )}
+
+
+ {subTitle}
+ {button}
+
+
+
+ {children}
+
+ );
+};
diff --git a/datahub-web-react/src/alchemy-components/components/Card/components.ts b/datahub-web-react/src/alchemy-components/components/Card/components.ts
new file mode 100644
index 00000000000000..bb3821fffc7f58
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/components/Card/components.ts
@@ -0,0 +1,59 @@
+import { colors, radius, spacing, typography } from '@src/alchemy-components/theme';
+import { IconAlignmentOptions } from '@src/alchemy-components/theme/config';
+import styled from 'styled-components';
+
+export const CardContainer = styled.div<{ hasButton: boolean; width?: string }>(({ hasButton, width }) => ({
+ border: `1px solid ${colors.gray[100]}`,
+ borderRadius: radius.lg,
+ padding: spacing.md,
+ minWidth: '150px',
+ boxShadow: '0px 1px 2px 0px rgba(33, 23, 95, 0.07)',
+ backgroundColor: colors.white,
+ display: 'flex',
+ flexDirection: 'column',
+ gap: spacing.md,
+ width,
+
+ '&:hover': hasButton
+ ? {
+ border: `1px solid ${colors.violet[500]}`,
+ cursor: 'pointer',
+ }
+ : {},
+}));
+
+export const Header = styled.div<{ iconAlignment?: IconAlignmentOptions }>(({ iconAlignment }) => ({
+ display: 'flex',
+ flexDirection: iconAlignment === 'horizontal' ? 'row' : 'column',
+ alignItems: iconAlignment === 'horizontal' ? 'center' : 'start',
+ gap: spacing.sm,
+ width: '100%',
+}));
+
+export const TitleContainer = styled.div({
+ display: 'flex',
+ flexDirection: 'column',
+ gap: 2,
+ width: '100%',
+});
+
+export const Title = styled.div({
+ fontSize: typography.fontSizes.lg,
+ fontWeight: typography.fontWeights.bold,
+ color: colors.gray[600],
+ display: 'flex',
+ alignItems: 'center',
+ gap: spacing.xsm,
+});
+
+export const SubTitleContainer = styled.div({
+ display: 'flex',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+});
+
+export const SubTitle = styled.div({
+ fontSize: typography.fontSizes.md,
+ fontWeight: typography.fontWeights.normal,
+ color: colors.gray[1700],
+});
diff --git a/datahub-web-react/src/alchemy-components/components/Card/index.ts b/datahub-web-react/src/alchemy-components/components/Card/index.ts
new file mode 100644
index 00000000000000..b0eed059aafd8a
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/components/Card/index.ts
@@ -0,0 +1,2 @@
+export { Card, cardDefaults } from './Card';
+export type { CardProps } from './types';
diff --git a/datahub-web-react/src/alchemy-components/components/Card/types.ts b/datahub-web-react/src/alchemy-components/components/Card/types.ts
new file mode 100644
index 00000000000000..e5b0e36f83e4ce
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/components/Card/types.ts
@@ -0,0 +1,13 @@
+import { IconAlignmentOptions } from '@src/alchemy-components/theme/config';
+
+export interface CardProps {
+ title: string;
+ subTitle?: string;
+ percent?: number;
+ button?: React.ReactNode;
+ onClick?: () => void;
+ icon?: React.ReactNode;
+ iconAlignment?: IconAlignmentOptions;
+ children?: React.ReactNode;
+ width?: string;
+}
diff --git a/datahub-web-react/src/alchemy-components/components/Checkbox/Checkbox.stories.tsx b/datahub-web-react/src/alchemy-components/components/Checkbox/Checkbox.stories.tsx
new file mode 100644
index 00000000000000..e546c2ea526cbc
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/components/Checkbox/Checkbox.stories.tsx
@@ -0,0 +1,156 @@
+import React from 'react';
+import type { Meta, StoryObj } from '@storybook/react';
+import { GridList } from '@components/.docs/mdx-components';
+import { Checkbox, checkboxDefaults, CheckboxGroup } from './Checkbox';
+import { CheckboxProps } from './types';
+import { Heading } from '../Heading';
+
+const MOCK_CHECKBOXES: CheckboxProps[] = [
+ {
+ label: 'Label 1',
+ error: '',
+ isChecked: false,
+ isDisabled: false,
+ isIntermediate: false,
+ isRequired: false,
+ },
+ {
+ label: 'Label 2',
+ error: '',
+ isChecked: false,
+ isDisabled: false,
+ isIntermediate: false,
+ isRequired: false,
+ },
+ {
+ label: 'Label 3',
+ error: '',
+ isChecked: false,
+ isDisabled: false,
+ isIntermediate: false,
+ isRequired: false,
+ },
+];
+
+const meta = {
+ title: 'Forms / Checkbox',
+ component: Checkbox,
+ parameters: {
+ layout: 'centered',
+ docs: {
+ subtitle: 'A component that is used to get user input in the state of a check box.',
+ },
+ },
+ argTypes: {
+ label: {
+ description: 'Label for the Checkbox.',
+ table: {
+ defaultValue: { summary: checkboxDefaults.label },
+ },
+ control: {
+ type: 'text',
+ },
+ },
+ error: {
+ description: 'Enforce error state on the Checkbox.',
+ table: {
+ defaultValue: { summary: checkboxDefaults.error },
+ },
+ control: {
+ type: 'text',
+ },
+ },
+ isChecked: {
+ description: 'Whether the Checkbox is checked.',
+ table: {
+ defaultValue: { summary: checkboxDefaults?.isChecked?.toString() },
+ },
+ control: {
+ type: 'boolean',
+ },
+ },
+ isDisabled: {
+ description: 'Whether the Checkbox is in disabled state.',
+ table: {
+ defaultValue: { summary: checkboxDefaults?.isDisabled?.toString() },
+ },
+ control: {
+ type: 'boolean',
+ },
+ },
+ isIntermediate: {
+ description: 'Whether the Checkbox is in intermediate state.',
+ table: {
+ defaultValue: { summary: checkboxDefaults?.isIntermediate?.toString() },
+ },
+ control: {
+ type: 'boolean',
+ },
+ },
+ isRequired: {
+ description: 'Whether the Checkbox is a required field.',
+ table: {
+ defaultValue: { summary: checkboxDefaults?.isRequired?.toString() },
+ },
+ control: {
+ type: 'boolean',
+ },
+ },
+ },
+ args: {
+ label: checkboxDefaults.label,
+ error: checkboxDefaults.error,
+ isChecked: checkboxDefaults.isChecked,
+ isDisabled: checkboxDefaults.isDisabled,
+ isIntermediate: checkboxDefaults.isIntermediate,
+ isRequired: checkboxDefaults.isRequired,
+ },
+} satisfies Meta;
+
+export default meta;
+
+type Story = StoryObj;
+
+export const sandbox: Story = {
+ tags: ['dev'],
+ render: (props) => ,
+};
+
+export const states = () => (
+
+
+
+
+
+
+);
+
+export const intermediate = () => {
+ return (
+
+
+
+
+ );
+};
+
+export const disabledStates = () => (
+
+
+
+
+
+);
+
+export const checkboxGroups = () => (
+
+
+ Horizontal Checkbox Group
+
+
+
+ Vertical Checkbox Group
+
+
+
+);
diff --git a/datahub-web-react/src/alchemy-components/components/Checkbox/Checkbox.tsx b/datahub-web-react/src/alchemy-components/components/Checkbox/Checkbox.tsx
new file mode 100644
index 00000000000000..6ab4db74610e49
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/components/Checkbox/Checkbox.tsx
@@ -0,0 +1,103 @@
+import React, { useEffect, useState } from 'react';
+import { CheckboxProps, CheckboxGroupProps } from './types';
+import {
+ CheckboxBase,
+ CheckboxContainer,
+ CheckboxGroupContainer,
+ Checkmark,
+ HoverState,
+ Label,
+ Required,
+ StyledCheckbox,
+} from './components';
+
+export const checkboxDefaults: CheckboxProps = {
+ label: 'Label',
+ error: '',
+ isChecked: false,
+ isDisabled: false,
+ isIntermediate: false,
+ isRequired: false,
+ setIsChecked: () => {},
+};
+
+export const Checkbox = ({
+ label = checkboxDefaults.label,
+ error = checkboxDefaults.error,
+ isChecked = checkboxDefaults.isChecked,
+ isDisabled = checkboxDefaults.isDisabled,
+ isIntermediate = checkboxDefaults.isIntermediate,
+ isRequired = checkboxDefaults.isRequired,
+ setIsChecked = checkboxDefaults.setIsChecked,
+ ...props
+}: CheckboxProps) => {
+ const [checked, setChecked] = useState(isChecked || false);
+ const [isHovering, setIsHovering] = useState(false);
+
+ useEffect(() => {
+ setChecked(isChecked || false);
+ }, [isChecked]);
+
+ const id = props.id || `checkbox-${label}`;
+
+ return (
+
+
+ {label} {isRequired && * }
+
+ {
+ if (!isDisabled) {
+ setChecked(!checked);
+ setIsChecked?.(!checked);
+ }
+ }}
+ >
+ null}
+ aria-labelledby={id}
+ aria-checked={checked}
+ {...props}
+ />
+ setIsHovering(true)}
+ onMouseLeave={() => setIsHovering(false)}
+ />
+
+
+
+ );
+};
+
+export const CheckboxGroup = ({ isVertical, checkboxes }: CheckboxGroupProps) => {
+ if (!checkboxes.length) {
+ return <>>;
+ }
+
+ return (
+
+ {checkboxes.map((checkbox) => {
+ const props = { ...checkbox };
+ return (
+
+
+
+ );
+ })}
+
+ );
+};
diff --git a/datahub-web-react/src/alchemy-components/components/Checkbox/components.ts b/datahub-web-react/src/alchemy-components/components/Checkbox/components.ts
new file mode 100644
index 00000000000000..6a4ad08c9c4ce6
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/components/Checkbox/components.ts
@@ -0,0 +1,91 @@
+import { borders, colors, spacing, transform, zIndices, radius } from '@components/theme';
+import styled from 'styled-components';
+import { getCheckboxColor, getCheckboxHoverBackgroundColor } from './utils';
+import { formLabelTextStyles } from '../commonStyles';
+
+export const CheckboxContainer = styled.div({
+ display: 'flex',
+ justifyContent: 'center',
+ alignItems: 'center',
+});
+
+export const Label = styled.div({
+ ...formLabelTextStyles,
+});
+
+export const Required = styled.span({
+ color: colors.red[500],
+ marginLeft: spacing.xxsm,
+});
+
+export const CheckboxBase = styled.div({
+ position: 'relative',
+ width: '30px',
+ height: '30px',
+});
+
+export const StyledCheckbox = styled.input<{
+ checked: boolean;
+ error: string;
+ disabled: boolean;
+}>(({ error, checked, disabled }) => ({
+ position: 'absolute',
+ opacity: 0,
+ height: 0,
+ width: 0,
+ '&:checked + div': {
+ backgroundColor: getCheckboxColor(checked, error, disabled, 'background'),
+ },
+ '&:checked + div:after': {
+ display: 'block',
+ },
+}));
+
+export const Checkmark = styled.div<{ intermediate?: boolean; error: string; checked: boolean; disabled: boolean }>(
+ ({ intermediate, checked, error, disabled }) => ({
+ position: 'absolute',
+ top: '4px',
+ left: '11px',
+ zIndex: zIndices.docked,
+ height: '18px',
+ width: '18px',
+ borderRadius: '3px',
+ border: `${borders['2px']} ${getCheckboxColor(checked, error, disabled, undefined)}`,
+ transition: 'all 0.2s ease-in-out',
+ cursor: 'pointer',
+ '&:after': {
+ content: '""',
+ position: 'absolute',
+ display: 'none',
+ left: !intermediate ? '6px' : '8px',
+ top: !intermediate ? '1px' : '3px',
+ width: !intermediate ? '5px' : '0px',
+ height: '10px',
+ border: 'solid white',
+ borderWidth: '0 3px 3px 0',
+ transform: !intermediate ? 'rotate(45deg)' : transform.rotate[90],
+ },
+ }),
+);
+
+export const HoverState = styled.div<{ isHovering: boolean; error: string; checked: boolean; disabled: boolean }>(
+ ({ isHovering, error, checked }) => ({
+ width: '40px',
+ height: '40px',
+ backgroundColor: !isHovering ? 'transparent' : getCheckboxHoverBackgroundColor(checked, error),
+ position: 'absolute',
+ borderRadius: radius.full,
+ top: '-5px',
+ left: '2px',
+ zIndex: zIndices.hide,
+ }),
+);
+
+export const CheckboxGroupContainer = styled.div<{ isVertical?: boolean }>(({ isVertical }) => ({
+ display: 'flex',
+ flexDirection: isVertical ? 'column' : 'row',
+ justifyContent: 'center',
+ alignItems: 'center',
+ gap: spacing.md,
+ margin: spacing.xxsm,
+}));
diff --git a/datahub-web-react/src/alchemy-components/components/Checkbox/index.ts b/datahub-web-react/src/alchemy-components/components/Checkbox/index.ts
new file mode 100644
index 00000000000000..57e3d6d27856a5
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/components/Checkbox/index.ts
@@ -0,0 +1,2 @@
+export { Checkbox, CheckboxGroup, checkboxDefaults } from './Checkbox';
+export type { CheckboxProps } from './types';
diff --git a/datahub-web-react/src/alchemy-components/components/Checkbox/types.ts b/datahub-web-react/src/alchemy-components/components/Checkbox/types.ts
new file mode 100644
index 00000000000000..7ee10011689397
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/components/Checkbox/types.ts
@@ -0,0 +1,16 @@
+import { InputHTMLAttributes } from 'react';
+
+export interface CheckboxProps extends InputHTMLAttributes {
+ label: string;
+ error?: string;
+ isChecked?: boolean;
+ setIsChecked?: React.Dispatch>;
+ isDisabled?: boolean;
+ isIntermediate?: boolean;
+ isRequired?: boolean;
+}
+
+export interface CheckboxGroupProps {
+ isVertical?: boolean;
+ checkboxes: CheckboxProps[];
+}
diff --git a/datahub-web-react/src/alchemy-components/components/Checkbox/utils.ts b/datahub-web-react/src/alchemy-components/components/Checkbox/utils.ts
new file mode 100644
index 00000000000000..edf5d24596e1b4
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/components/Checkbox/utils.ts
@@ -0,0 +1,27 @@
+import theme, { colors } from '@components/theme';
+
+const checkboxBackgroundDefault = {
+ default: colors.white,
+ checked: theme.semanticTokens.colors.primary,
+ error: theme.semanticTokens.colors.error,
+ disabled: colors.gray[300],
+};
+
+const checkboxHoverColors = {
+ default: colors.gray[100],
+ error: colors.red[100],
+ checked: colors.violet[100],
+};
+
+export function getCheckboxColor(checked: boolean, error: string, disabled: boolean, mode: 'background' | undefined) {
+ if (disabled) return checkboxBackgroundDefault.disabled;
+ if (error) return checkboxBackgroundDefault.error;
+ if (checked) return checkboxBackgroundDefault.checked;
+ return mode === 'background' ? checkboxBackgroundDefault.default : colors.gray[500];
+}
+
+export function getCheckboxHoverBackgroundColor(checked: boolean, error: string) {
+ if (error) return checkboxHoverColors.error;
+ if (checked) return checkboxHoverColors.checked;
+ return checkboxHoverColors.default;
+}
diff --git a/datahub-web-react/src/alchemy-components/components/Heading/Heading.stories.tsx b/datahub-web-react/src/alchemy-components/components/Heading/Heading.stories.tsx
new file mode 100644
index 00000000000000..b8bd9f6420c006
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/components/Heading/Heading.stories.tsx
@@ -0,0 +1,98 @@
+import React from 'react';
+
+import type { Meta, StoryObj, StoryFn } from '@storybook/react';
+import { BADGE } from '@geometricpanda/storybook-addon-badges';
+
+import { VerticalFlexGrid } from '@components/.docs/mdx-components';
+import { Heading, headingDefaults } from '.';
+
+// Auto Docs
+const meta = {
+ title: 'Typography / Heading',
+ component: Heading,
+
+ // Display Properties
+ parameters: {
+ layout: 'centered',
+ badges: [BADGE.STABLE, 'readyForDesignReview'],
+ docs: {
+ subtitle: 'Used to render semantic HTML heading elements.',
+ },
+ },
+
+ // Component-level argTypes
+ argTypes: {
+ children: {
+ description: 'The content to display within the heading.',
+ table: {
+ type: { summary: 'string' },
+ },
+ control: {
+ type: 'text',
+ },
+ },
+ type: {
+ description: 'The type of heading to display.',
+ table: {
+ defaultValue: { summary: headingDefaults.type },
+ },
+ },
+ size: {
+ description: 'Override the size of the heading.',
+ table: {
+ defaultValue: { summary: `${headingDefaults.size}` },
+ },
+ },
+ color: {
+ description: 'Override the color of the heading.',
+ table: {
+ defaultValue: { summary: headingDefaults.color },
+ },
+ },
+ weight: {
+ description: 'Override the weight of the heading.',
+ table: {
+ defaultValue: { summary: `${headingDefaults.weight}` },
+ },
+ },
+ },
+
+ // Define defaults
+ args: {
+ children: 'The content to display within the heading.',
+ type: headingDefaults.type,
+ size: headingDefaults.size,
+ color: headingDefaults.color,
+ weight: headingDefaults.weight,
+ },
+} satisfies Meta;
+
+export default meta;
+
+// Stories
+
+type Story = StoryObj;
+
+// Basic story is what is displayed 1st in storybook & is used as the code sandbox
+// Pass props to this so that it can be customized via the UI props panel
+export const sandbox: Story = {
+ tags: ['dev'],
+ render: (props) => {props.children} ,
+};
+
+export const sizes: StoryFn = (props: any) => (
+
+ H1 {props.children}
+ H2 {props.children}
+ H3 {props.children}
+ H4 {props.children}
+ H5 {props.children}
+ H6 {props.children}
+
+);
+
+export const withLink = () => (
+
+ The content to display within the heading
+
+);
diff --git a/datahub-web-react/src/alchemy-components/components/Heading/Heading.tsx b/datahub-web-react/src/alchemy-components/components/Heading/Heading.tsx
new file mode 100644
index 00000000000000..6449ff512adacc
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/components/Heading/Heading.tsx
@@ -0,0 +1,38 @@
+import React from 'react';
+
+import { HeadingProps } from './types';
+import { H1, H2, H3, H4, H5, H6 } from './components';
+
+export const headingDefaults: HeadingProps = {
+ type: 'h1',
+ color: 'inherit',
+ size: '2xl',
+ weight: 'medium',
+};
+
+export const Heading = ({
+ type = headingDefaults.type,
+ size = headingDefaults.size,
+ color = headingDefaults.color,
+ weight = headingDefaults.weight,
+ children,
+}: HeadingProps) => {
+ const sharedProps = { size, color, weight };
+
+ switch (type) {
+ case 'h1':
+ return {children} ;
+ case 'h2':
+ return {children} ;
+ case 'h3':
+ return {children} ;
+ case 'h4':
+ return {children} ;
+ case 'h5':
+ return {children} ;
+ case 'h6':
+ return {children} ;
+ default:
+ return {children} ;
+ }
+};
diff --git a/datahub-web-react/src/alchemy-components/components/Heading/components.ts b/datahub-web-react/src/alchemy-components/components/Heading/components.ts
new file mode 100644
index 00000000000000..beea5338585d83
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/components/Heading/components.ts
@@ -0,0 +1,70 @@
+import styled from 'styled-components';
+
+import { typography, colors } from '@components/theme';
+import { getColor, getFontSize } from '@components/theme/utils';
+import { HeadingProps } from './types';
+
+const headingStyles = {
+ H1: {
+ fontSize: typography.fontSizes['4xl'],
+ lineHeight: typography.lineHeights['2xl'],
+ },
+ H2: {
+ fontSize: typography.fontSizes['3xl'],
+ lineHeight: typography.lineHeights.xl,
+ },
+ H3: {
+ fontSize: typography.fontSizes['2xl'],
+ lineHeight: typography.lineHeights.lg,
+ },
+ H4: {
+ fontSize: typography.fontSizes.xl,
+ lineHeight: typography.lineHeights.lg,
+ },
+ H5: {
+ fontSize: typography.fontSizes.lg,
+ lineHeight: typography.lineHeights.md,
+ },
+ H6: {
+ fontSize: typography.fontSizes.md,
+ lineHeight: typography.lineHeights.xs,
+ },
+};
+
+// Default styles
+const baseStyles = {
+ fontFamily: typography.fonts.heading,
+ margin: 0,
+
+ '& a': {
+ color: colors.violet[400],
+ textDecoration: 'none',
+ transition: 'color 0.15s ease',
+
+ '&:hover': {
+ color: colors.violet[500],
+ },
+ },
+};
+
+// Prop Driven Styles
+const propStyles = (props, isText = false) => {
+ const styles = {} as any;
+ if (props.size) styles.fontSize = getFontSize(props.size);
+ if (props.color) styles.color = getColor(props.color);
+ if (props.weight) styles.fontWeight = typography.fontWeights[props.weight];
+ if (isText) styles.lineHeight = typography.lineHeights[props.size];
+ return styles;
+};
+
+// Generate Headings
+const headings = {} as any;
+
+['H1', 'H2', 'H3', 'H4', 'H5', 'H6'].forEach((heading) => {
+ const component = styled[heading.toLowerCase()];
+ headings[heading] = component({ ...baseStyles, ...headingStyles[heading] }, (props: HeadingProps) => ({
+ ...propStyles(props as HeadingProps),
+ }));
+});
+
+export const { H1, H2, H3, H4, H5, H6 } = headings;
diff --git a/datahub-web-react/src/alchemy-components/components/Heading/index.ts b/datahub-web-react/src/alchemy-components/components/Heading/index.ts
new file mode 100644
index 00000000000000..c414de6cc92f79
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/components/Heading/index.ts
@@ -0,0 +1,2 @@
+export { Heading, headingDefaults } from './Heading';
+export type { HeadingProps } from './types';
diff --git a/datahub-web-react/src/alchemy-components/components/Heading/types.ts b/datahub-web-react/src/alchemy-components/components/Heading/types.ts
new file mode 100644
index 00000000000000..96fcf1ea292bf7
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/components/Heading/types.ts
@@ -0,0 +1,9 @@
+import { HTMLAttributes } from 'react';
+import type { FontSizeOptions, FontColorOptions, FontWeightOptions } from '@components/theme/config';
+
+export interface HeadingProps extends HTMLAttributes {
+ type?: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
+ size?: FontSizeOptions;
+ color?: FontColorOptions;
+ weight?: FontWeightOptions;
+}
diff --git a/datahub-web-react/src/alchemy-components/components/Icon/Icon.stories.tsx b/datahub-web-react/src/alchemy-components/components/Icon/Icon.stories.tsx
new file mode 100644
index 00000000000000..3dcbd74ceb0b71
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/components/Icon/Icon.stories.tsx
@@ -0,0 +1,131 @@
+import React from 'react';
+
+import type { Meta, StoryObj } from '@storybook/react';
+
+import { GridList } from '@components/.docs/mdx-components';
+import { Icon, iconDefaults, AVAILABLE_ICONS } from '.';
+
+// Auto Docs
+const meta = {
+ title: 'Media / Icon',
+ component: Icon,
+
+ // Display Properties
+ parameters: {
+ layout: 'centered',
+ badges: ['productionReady'],
+ docs: {
+ subtitle: 'A singular component for rendering the icons used throughout the application.',
+ description: {
+ component: '👉 See the [Icons Gallery](/docs/icons--docs) for more information.',
+ },
+ },
+ },
+
+ // Component-level argTypes
+ argTypes: {
+ icon: {
+ description: `The name of the icon to display.`,
+ type: 'string',
+ options: AVAILABLE_ICONS,
+ table: {
+ defaultValue: { summary: 'undefined' },
+ },
+ control: {
+ type: 'select',
+ },
+ },
+ variant: {
+ description: 'The variant of the icon to display.',
+ defaultValue: 'outline',
+ options: ['outline', 'filled'],
+ table: {
+ defaultValue: { summary: iconDefaults.variant },
+ },
+ },
+ size: {
+ description: 'The size of the icon to display.',
+ defaultValue: 'lg',
+ table: {
+ defaultValue: { summary: iconDefaults.size },
+ },
+ },
+ color: {
+ description: 'The color of the icon to display.',
+ options: ['inherit', 'white', 'black', 'violet', 'green', 'red', 'blue', 'gray'],
+ type: 'string',
+ table: {
+ defaultValue: { summary: iconDefaults.color },
+ },
+ control: {
+ type: 'select',
+ },
+ },
+ rotate: {
+ description: 'The rotation of the icon. Applies a CSS transformation.',
+ table: {
+ defaultValue: { summary: iconDefaults.rotate },
+ },
+ },
+ },
+
+ // Define defaults for required args
+ args: {
+ icon: iconDefaults.icon,
+ },
+} satisfies Meta;
+
+export default meta;
+
+// Stories
+
+type Story = StoryObj;
+
+// Basic story is what is displayed 1st in storybook
+// Pass props to this so that it can be customized via the UI props panel
+export const sandbox: Story = {
+ tags: ['dev'],
+ render: (props) => ,
+};
+
+export const filled = () => (
+
+
+
+
+
+);
+
+export const sizes = () => (
+
+
+
+
+
+
+
+
+
+
+);
+
+export const colors = () => (
+
+
+
+
+
+
+
+
+
+);
+
+export const rotation = () => (
+
+
+
+
+
+
+);
diff --git a/datahub-web-react/src/alchemy-components/components/Icon/Icon.tsx b/datahub-web-react/src/alchemy-components/components/Icon/Icon.tsx
new file mode 100644
index 00000000000000..50c30d7203aed8
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/components/Icon/Icon.tsx
@@ -0,0 +1,59 @@
+import React from 'react';
+
+import { getFontSize, getColor, getRotationTransform } from '@components/theme/utils';
+
+import { IconProps } from './types';
+import { IconWrapper } from './components';
+import { getIconNames, getIconComponent } from './utils';
+
+export const iconDefaults: IconProps = {
+ icon: 'AccountCircle',
+ variant: 'outline',
+ size: '4xl',
+ color: 'inherit',
+ rotate: '0',
+};
+
+export const Icon = ({
+ icon,
+ variant = iconDefaults.variant,
+ size = iconDefaults.size,
+ color = iconDefaults.color,
+ rotate = iconDefaults.rotate,
+ ...props
+}: IconProps) => {
+ const { filled, outlined } = getIconNames();
+
+ // Return early if no icon is provided
+ if (!icon) return null;
+
+ // Get outlined icon component name
+ const isOutlined = variant === 'outline';
+ const outlinedIconName = `${icon}Outlined`;
+
+ // Warn if the icon does not have the specified variant
+ if (variant === 'outline' && !outlined.includes(outlinedIconName)) {
+ console.warn(`Icon "${icon}" does not have an outlined variant.`);
+ return null;
+ }
+
+ // Warn if the icon does not have the specified variant
+ if (variant === 'filled' && !filled.includes(icon)) {
+ console.warn(`Icon "${icon}" does not have a filled variant.`);
+ return null;
+ }
+
+ // Get outlined icon component
+ const IconComponent = getIconComponent(isOutlined ? outlinedIconName : icon);
+
+ return (
+
+
+
+ );
+};
diff --git a/datahub-web-react/src/alchemy-components/components/Icon/components.ts b/datahub-web-react/src/alchemy-components/components/Icon/components.ts
new file mode 100644
index 00000000000000..82e9c9a8fcae00
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/components/Icon/components.ts
@@ -0,0 +1,19 @@
+import styled from 'styled-components';
+
+export const IconWrapper = styled.div<{ size: string; rotate?: string }>`
+ position: relative;
+
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ width: ${({ size }) => size};
+ height: ${({ size }) => size};
+
+ & svg {
+ width: 100%;
+ height: 100%;
+
+ transform: ${({ rotate }) => rotate};
+ }
+`;
diff --git a/datahub-web-react/src/alchemy-components/components/Icon/constants.ts b/datahub-web-react/src/alchemy-components/components/Icon/constants.ts
new file mode 100644
index 00000000000000..25145a5970f0f2
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/components/Icon/constants.ts
@@ -0,0 +1,547 @@
+export const AVAILABLE_ICONS = [
+ 'AccountCircle',
+ 'AccountTree',
+ 'AddCircle',
+ 'AddLink',
+ 'Add',
+ 'AddTask',
+ 'AddToPhotos',
+ 'Adjust',
+ 'AllInclusive',
+ 'Analytics',
+ 'Anchor',
+ 'Animation',
+ 'Announcement',
+ 'Api',
+ 'Approval',
+ 'Archive',
+ 'ArrowBack',
+ 'ArrowCircleDown',
+ 'ArrowCircleLeft',
+ 'ArrowCircleRight',
+ 'ArrowCircleUp',
+ 'ArrowDownward',
+ 'ArrowForward',
+ 'ArrowOutward',
+ 'ArrowUpward',
+ 'ArtTrack',
+ 'Article',
+ 'Assistant',
+ 'AttachFile',
+ 'Attachment',
+ 'AutoAwesome',
+ 'AutoFixHigh',
+ 'AutoFixOff',
+ 'AutoGraph',
+ 'AutoMode',
+ 'AutoStories',
+ 'AvTimer',
+ 'Backspace',
+ 'Backup',
+ 'BackupTable',
+ 'Badge',
+ 'Balance',
+ 'BarChart',
+ 'BatchPrediction',
+ 'Block',
+ 'Bolt',
+ 'Book',
+ 'BookmarkAdd',
+ 'BookmarkAdded',
+ 'BookmarkBorder',
+ 'Bookmark',
+ 'BookmarkRemove',
+ 'Bookmarks',
+ 'Brush',
+ 'BubbleChart',
+ 'BugReport',
+ 'BuildCircle',
+ 'Build',
+ 'BusinessCenter',
+ 'Business',
+ 'Cable',
+ 'Cached',
+ 'Calculate',
+ 'CalendarMonth',
+ 'CalendarToday',
+ 'CalendarViewDay',
+ 'Campaign',
+ 'Cancel',
+ 'CandlestickChart',
+ 'CardGiftcard',
+ 'CardMembership',
+ 'Cases',
+ 'Cast',
+ 'Category',
+ 'Celebration',
+ 'CellTower',
+ 'ChangeHistory',
+ 'ChatBubble',
+ 'Chat',
+ 'CheckBox',
+ 'CheckCircle',
+ 'Check',
+ 'Checklist',
+ 'ChevronLeft',
+ 'ChevronRight',
+ 'Class',
+ 'CloseFullscreen',
+ 'Close',
+ 'CloudCircle',
+ 'CloudDone',
+ 'CloudDownload',
+ 'CloudOff',
+ 'Cloud',
+ 'CloudQueue',
+ 'CloudSync',
+ 'CloudUpload',
+ 'CoPresent',
+ 'CodeOff',
+ 'Code',
+ 'ColorLens',
+ 'Colorize',
+ 'CommentBank',
+ 'Comment',
+ 'CommentsDisabled',
+ 'Commit',
+ 'CompareArrows',
+ 'Compare',
+ 'Compress',
+ 'Computer',
+ 'Construction',
+ 'ContactPage',
+ 'ContactSupport',
+ 'Contacts',
+ 'ContentCopy',
+ 'ContentCut',
+ 'Contrast',
+ 'ControlPoint',
+ 'Cookie',
+ 'CopyAll',
+ 'Copyright',
+ 'CorporateFare',
+ 'Cottage',
+ 'CreateNewFolder',
+ 'CrisisAlert',
+ 'Cyclone',
+ 'Dangerous',
+ 'DarkMode',
+ 'DashboardCustomize',
+ 'Dashboard',
+ 'DataArray',
+ 'DataObject',
+ 'DataThresholding',
+ 'DataUsage',
+ 'DatasetLinked',
+ 'Dataset',
+ 'DateRange',
+ 'DeleteForever',
+ 'Delete',
+ 'DeleteSweep',
+ 'Description',
+ 'Deselect',
+ 'DesignServices',
+ 'Details',
+ 'DeviceHub',
+ 'DeviceThermostat',
+ 'Diamond',
+ 'Difference',
+ 'DisabledByDefault',
+ 'DiscFull',
+ 'Discount',
+ 'DisplaySettings',
+ 'Diversity2',
+ 'Dns',
+ 'DoNotDisturb',
+ 'DocumentScanner',
+ 'DomainAdd',
+ 'DomainDisabled',
+ 'Domain',
+ 'DomainVerification',
+ 'DoneAll',
+ 'DonutLarge',
+ 'DonutSmall',
+ 'DoubleArrow',
+ 'DownloadDone',
+ 'DownloadForOffline',
+ 'Download',
+ 'Downloading',
+ 'Drafts',
+ 'DragHandle',
+ 'DragIndicator',
+ 'Draw',
+ 'DriveFileMove',
+ 'DriveFolderUpload',
+ 'DynamicFeed',
+ 'DynamicForm',
+ 'EditCalendar',
+ 'EditLocation',
+ 'EditNote',
+ 'EditOff',
+ 'Edit',
+ 'Eject',
+ 'ElectricBolt',
+ 'EmergencyShare',
+ 'EnhancedEncryption',
+ 'Equalizer',
+ 'Error',
+ 'EventAvailable',
+ 'EventBusy',
+ 'EventNote',
+ 'Event',
+ 'EventRepeat',
+ 'ExitToApp',
+ 'Expand',
+ 'ExploreOff',
+ 'Explore',
+ 'Exposure',
+ 'ExtensionOff',
+ 'Extension',
+ 'FastForward',
+ 'FastRewind',
+ 'FavoriteBorder',
+ 'Favorite',
+ 'FeaturedPlayList',
+ 'Feed',
+ 'Feedback',
+ 'FileCopy',
+ 'FileDownloadOff',
+ 'FileDownload',
+ 'FileOpen',
+ 'FilePresent',
+ 'FileUpload',
+ 'FilterAltOff',
+ 'FilterAlt',
+ 'FilterListOff',
+ 'FindInPage',
+ 'FindReplace',
+ 'FirstPage',
+ 'FitScreen',
+ 'FlagCircle',
+ 'Flag',
+ 'Flaky',
+ 'Flare',
+ 'FlashOff',
+ 'FlashOn',
+ 'FlightLand',
+ 'Flight',
+ 'FlightTakeoff',
+ 'FmdBad',
+ 'FmdGood',
+ 'FolderCopy',
+ 'FolderDelete',
+ 'FolderOff',
+ 'FolderOpen',
+ 'Folder',
+ 'FolderShared',
+ 'FolderSpecial',
+ 'FolderZip',
+ 'ForkLeft',
+ 'ForkRight',
+ 'FormatListBulleted',
+ 'FormatListNumbered',
+ 'Forum',
+ 'FullscreenExit',
+ 'Fullscreen',
+ 'Functions',
+ 'GetApp',
+ 'GppBad',
+ 'GppGood',
+ 'GppMaybe',
+ 'GpsFixed',
+ 'GpsNotFixed',
+ 'GpsOff',
+ 'Grading',
+ 'Grain',
+ 'GraphicEq',
+ 'Grid3x3',
+ 'Grid4x4',
+ 'GridGoldenratio',
+ 'GridOff',
+ 'GridOn',
+ 'GridView',
+ 'GroupAdd',
+ 'Group',
+ 'GroupRemove',
+ 'GroupWork',
+ 'Groups',
+ 'Handshake',
+ 'Handyman',
+ 'Hardware',
+ 'HealthAndSafety',
+ 'HelpCenter',
+ 'Help',
+ 'Hexagon',
+ 'HideSource',
+ 'Highlight',
+ 'History',
+ 'HistoryToggleOff',
+ 'Home',
+ 'Hub',
+ 'Image',
+ 'ImageSearch',
+ 'Inbox',
+ 'Info',
+ 'Input',
+ 'InsertChart',
+ 'InsertComment',
+ 'InsertDriveFile',
+ 'Insights',
+ 'Interests',
+ 'Inventory2',
+ 'Inventory',
+ 'KeyOff',
+ 'Key',
+ 'LabelImportant',
+ 'LabelOff',
+ 'Label',
+ 'Lan',
+ 'Landscape',
+ 'Language',
+ 'LastPage',
+ 'Launch',
+ 'LayersClear',
+ 'Layers',
+ 'Leaderboard',
+ 'LegendToggle',
+ 'LibraryAddCheck',
+ 'LibraryAdd',
+ 'LightMode',
+ 'Lightbulb',
+ 'LineAxis',
+ 'LineStyle',
+ 'LineWeight',
+ 'LinearScale',
+ 'LinkOff',
+ 'Link',
+ 'List',
+ 'LockOpen',
+ 'Lock',
+ 'LockReset',
+ 'Login',
+ 'Logout',
+ 'Loupe',
+ 'LowPriority',
+ 'Loyalty',
+ 'Mail',
+ 'ManageAccounts',
+ 'ManageHistory',
+ 'ManageSearch',
+ 'Map',
+ 'MapsUgc',
+ 'MarkAsUnread',
+ 'MeetingRoom',
+ 'Memory',
+ 'MenuBook',
+ 'MenuOpen',
+ 'Menu',
+ 'Merge',
+ 'MergeType',
+ 'Message',
+ 'MiscellaneousServices',
+ 'MoodBad',
+ 'Mood',
+ 'MoreHoriz',
+ 'MoreTime',
+ 'MoreVert',
+ 'MoveDown',
+ 'MoveToInbox',
+ 'MoveUp',
+ 'MultilineChart',
+ 'MultipleStop',
+ 'Nat',
+ 'NewReleases',
+ 'NightsStay',
+ 'NoAccounts',
+ 'NoEncryption',
+ 'NotStarted',
+ 'NoteAdd',
+ 'NotificationAdd',
+ 'NotificationImportant',
+ 'NotificationsActive',
+ 'NotificationsOff',
+ 'Notifications',
+ 'NotificationsPaused',
+ 'OpenInFull',
+ 'OpenInNew',
+ 'Outbound',
+ 'Outbox',
+ 'Output',
+ 'Pageview',
+ 'Password',
+ 'PauseCircle',
+ 'PendingActions',
+ 'Pending',
+ 'People',
+ 'PersonAddAlt1',
+ 'PersonOff',
+ 'Person',
+ 'PersonRemoveAlt1',
+ 'PersonSearch',
+ 'PinDrop',
+ 'PivotTableChart',
+ 'Place',
+ 'PlayArrow',
+ 'PlayCircle',
+ 'Policy',
+ 'Poll',
+ 'Polyline',
+ 'PostAdd',
+ 'Preview',
+ 'PrivacyTip',
+ 'PublicOff',
+ 'Public',
+ 'Publish',
+ 'PushPin',
+ 'QueryStats',
+ 'QuestionAnswer',
+ 'Queue',
+ 'Radar',
+ 'ReadMore',
+ 'Redo',
+ 'Refresh',
+ 'RemoveCircle',
+ 'Replay',
+ 'ReplyAll',
+ 'Reply',
+ 'Report',
+ 'ReportProblem',
+ 'Restore',
+ 'RocketLaunch',
+ 'Rocket',
+ 'Route',
+ 'RssFeed',
+ 'Rule',
+ 'RunningWithErrors',
+ 'SatelliteAlt',
+ 'SaveAlt',
+ 'Schedule',
+ 'Schema',
+ 'Science',
+ 'SearchOff',
+ 'Search',
+ 'Security',
+ 'Sell',
+ 'Sensors',
+ 'SentimentDissatisfied',
+ 'SentimentNeutral',
+ 'SentimentSatisfied',
+ 'Settings',
+ 'Share',
+ 'Shield',
+ 'ShortText',
+ 'Shortcut',
+ 'ShowChart',
+ 'Shuffle',
+ 'Signpost',
+ 'SkipNext',
+ 'SkipPrevious',
+ 'SortByAlpha',
+ 'Sort',
+ 'Source',
+ 'SpaceDashboard',
+ 'Speed',
+ 'SsidChart',
+ 'StackedBarChart',
+ 'StackedLineChart',
+ 'StarBorder',
+ 'StarHalf',
+ 'Star',
+ 'Start',
+ 'StickyNote2',
+ 'StopCircle',
+ 'Storage',
+ 'Storm',
+ 'Straight',
+ 'Stream',
+ 'Style',
+ 'SubdirectoryArrowLeft',
+ 'SubdirectoryArrowRight',
+ 'Subject',
+ 'Subscriptions',
+ 'SubtitlesOff',
+ 'Support',
+ 'SwapHoriz',
+ 'SwapVert',
+ 'SwitchAccount',
+ 'SwitchLeft',
+ 'SwitchRight',
+ 'SyncAlt',
+ 'SyncDisabled',
+ 'SyncLock',
+ 'Sync',
+ 'SyncProblem',
+ 'TableChart',
+ 'TableRows',
+ 'TableView',
+ 'Tag',
+ 'TaskAlt',
+ 'Terminal',
+ 'ThumbDown',
+ 'ThumbUp',
+ 'ThumbsUpDown',
+ 'Timelapse',
+ 'Timeline',
+ 'TipsAndUpdates',
+ 'Toc',
+ 'TrackChanges',
+ 'TrendingDown',
+ 'TrendingFlat',
+ 'TrendingUp',
+ 'Tune',
+ 'Tungsten',
+ 'TurnLeft',
+ 'TurnRight',
+ 'TurnSlightLeft',
+ 'TurnSlightRight',
+ 'Unarchive',
+ 'Undo',
+ 'UnfoldLessDouble',
+ 'UnfoldLess',
+ 'UnfoldMoreDouble',
+ 'UnfoldMore',
+ 'Unsubscribe',
+ 'Upcoming',
+ 'UpdateDisabled',
+ 'Update',
+ 'Upgrade',
+ 'UploadFile',
+ 'Upload',
+ 'Verified',
+ 'VerifiedUser',
+ 'ViewAgenda',
+ 'ViewArray',
+ 'ViewCarousel',
+ 'ViewColumn',
+ 'ViewComfy',
+ 'ViewCompact',
+ 'ViewCozy',
+ 'ViewDay',
+ 'ViewHeadline',
+ 'ViewKanban',
+ 'ViewList',
+ 'ViewModule',
+ 'ViewQuilt',
+ 'ViewSidebar',
+ 'ViewStream',
+ 'ViewTimeline',
+ 'ViewWeek',
+ 'VisibilityOff',
+ 'Visibility',
+ 'Warehouse',
+ 'Warning',
+ 'Webhook',
+ 'Whatshot',
+ 'Widgets',
+ 'Wifi',
+ 'Window',
+ 'WorkHistory',
+ 'WorkOff',
+ 'Work',
+ 'WorkspacePremium',
+ 'Workspaces',
+ 'Wysiwyg',
+ 'ZoomInMap',
+ 'ZoomIn',
+ 'ZoomOutMap',
+];
diff --git a/datahub-web-react/src/alchemy-components/components/Icon/index.ts b/datahub-web-react/src/alchemy-components/components/Icon/index.ts
new file mode 100644
index 00000000000000..23ca0a7ef7da2f
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/components/Icon/index.ts
@@ -0,0 +1,3 @@
+export { Icon, iconDefaults } from './Icon';
+export type { IconProps, IconNames } from './types';
+export { AVAILABLE_ICONS } from './constants';
diff --git a/datahub-web-react/src/alchemy-components/components/Icon/types.ts b/datahub-web-react/src/alchemy-components/components/Icon/types.ts
new file mode 100644
index 00000000000000..f5a050e9338a71
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/components/Icon/types.ts
@@ -0,0 +1,23 @@
+import { HTMLAttributes } from 'react';
+
+import type { FontSizeOptions, FontColorOptions, RotationOptions } from '@components/theme/config';
+import { AVAILABLE_ICONS } from './constants';
+
+// Utility function to create an enum from an array of strings
+function createEnum(values: T[]): { [K in T]: K } {
+ return values.reduce((acc, value) => {
+ acc[value] = value;
+ return acc;
+ }, Object.create(null));
+}
+
+const names = createEnum(AVAILABLE_ICONS);
+export type IconNames = keyof typeof names;
+
+export interface IconProps extends HTMLAttributes {
+ icon: IconNames;
+ variant?: 'filled' | 'outline';
+ size?: FontSizeOptions;
+ color?: FontColorOptions;
+ rotate?: RotationOptions;
+}
diff --git a/datahub-web-react/src/alchemy-components/components/Icon/utils.ts b/datahub-web-react/src/alchemy-components/components/Icon/utils.ts
new file mode 100644
index 00000000000000..1137b3da28bc7a
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/components/Icon/utils.ts
@@ -0,0 +1,29 @@
+import * as materialIcons from '@mui/icons-material';
+
+export const getIconNames = () => {
+ // We only want "Filled" (mui default) and "Outlined" icons
+ const filtered = Object.keys(materialIcons).filter(
+ (key) =>
+ !key.includes('Filled') && !key.includes('TwoTone') && !key.includes('Rounded') && !key.includes('Sharp'),
+ );
+
+ const filled: string[] = [];
+ const outlined: string[] = [];
+
+ filtered.forEach((key) => {
+ if (key.includes('Outlined')) {
+ outlined.push(key);
+ } else if (!key.includes('Outlined')) {
+ filled.push(key);
+ }
+ });
+
+ return {
+ filled,
+ outlined,
+ };
+};
+
+export const getIconComponent = (icon: string) => {
+ return materialIcons[icon];
+};
diff --git a/datahub-web-react/src/alchemy-components/components/Input/Input.stories.tsx b/datahub-web-react/src/alchemy-components/components/Input/Input.stories.tsx
new file mode 100644
index 00000000000000..053e952b62a2e9
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/components/Input/Input.stories.tsx
@@ -0,0 +1,177 @@
+import React from 'react';
+
+import type { Meta, StoryObj } from '@storybook/react';
+import { BADGE } from '@geometricpanda/storybook-addon-badges';
+
+import { GridList } from '@components/.docs/mdx-components';
+import { AVAILABLE_ICONS } from '../Icon';
+
+import { Input, inputDefaults } from './Input';
+
+const meta = {
+ title: 'Forms / Input',
+ component: Input,
+
+ // Display Properties
+ parameters: {
+ layout: 'centered',
+ badges: [BADGE.STABLE, 'readyForDesignReview'],
+ docs: {
+ subtitle: 'A component that is used to get user input in a single line field.',
+ },
+ },
+
+ // Component-level argTypes
+ argTypes: {
+ value: {
+ description: 'Value for the Input.',
+ table: {
+ defaultValue: { summary: inputDefaults.value as string },
+ },
+ control: {
+ type: 'text',
+ },
+ },
+ label: {
+ description: 'Label for the Input.',
+ table: {
+ defaultValue: { summary: inputDefaults.label },
+ },
+ control: {
+ type: 'text',
+ },
+ },
+ placeholder: {
+ description: 'Placeholder for the Input.',
+ table: {
+ defaultValue: { summary: inputDefaults.placeholder },
+ },
+ control: {
+ type: 'text',
+ },
+ },
+ icon: {
+ description: 'The icon to display in the Input.',
+ type: 'string',
+ options: AVAILABLE_ICONS,
+ table: {
+ defaultValue: { summary: 'undefined' },
+ },
+ control: {
+ type: 'select',
+ },
+ },
+ error: {
+ description: 'Enforce error state on the Input.',
+ table: {
+ defaultValue: { summary: inputDefaults.error },
+ },
+ control: {
+ type: 'text',
+ },
+ },
+ warning: {
+ description: 'Enforce warning state on the Input.',
+ table: {
+ defaultValue: { summary: inputDefaults.warning },
+ },
+ control: {
+ type: 'text',
+ },
+ },
+ isSuccess: {
+ description: 'Enforce success state on the Input.',
+ table: {
+ defaultValue: { summary: inputDefaults?.isSuccess?.toString() },
+ },
+ control: {
+ type: 'boolean',
+ },
+ },
+ isDisabled: {
+ description: 'Whether the Input is in disabled state.',
+ table: {
+ defaultValue: { summary: inputDefaults?.isDisabled?.toString() },
+ },
+ control: {
+ type: 'boolean',
+ },
+ },
+ isInvalid: {
+ description: 'Whether the Input is an invalid state.',
+ table: {
+ defaultValue: { summary: inputDefaults?.isInvalid?.toString() },
+ },
+ control: {
+ type: 'boolean',
+ },
+ },
+ isReadOnly: {
+ description: 'Whether the Input is in readonly mode.',
+ table: {
+ defaultValue: { summary: inputDefaults?.isReadOnly?.toString() },
+ },
+ control: {
+ type: 'boolean',
+ },
+ },
+ isPassword: {
+ description: 'Whether the Input has a password type.',
+ table: {
+ defaultValue: { summary: inputDefaults?.isPassword?.toString() },
+ },
+ control: {
+ type: 'boolean',
+ },
+ },
+ isRequired: {
+ description: 'Whether the Input is a required field.',
+ table: {
+ defaultValue: { summary: inputDefaults?.isRequired?.toString() },
+ },
+ control: {
+ type: 'boolean',
+ },
+ },
+ },
+ args: {
+ value: inputDefaults.value,
+ label: inputDefaults.label,
+ placeholder: inputDefaults.placeholder,
+ icon: inputDefaults.icon,
+ error: inputDefaults.error,
+ warning: inputDefaults.warning,
+ isSuccess: inputDefaults.isSuccess,
+ isDisabled: inputDefaults.isDisabled,
+ isInvalid: inputDefaults.isInvalid,
+ isReadOnly: inputDefaults.isReadOnly,
+ isPassword: inputDefaults.isPassword,
+ isRequired: inputDefaults.isRequired,
+ },
+} satisfies Meta;
+
+export default meta;
+
+type Story = StoryObj;
+
+export const sandbox: Story = {
+ tags: ['dev'],
+ render: (props) => ,
+};
+
+export const status = () => (
+
+
+
+
+
+);
+
+export const states = () => (
+
+
+
+
+
+
+);
diff --git a/datahub-web-react/src/alchemy-components/components/Input/Input.tsx b/datahub-web-react/src/alchemy-components/components/Input/Input.tsx
new file mode 100644
index 00000000000000..976fc47ffc5948
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/components/Input/Input.tsx
@@ -0,0 +1,97 @@
+import { Tooltip } from '@components';
+import React from 'react';
+
+import { InputProps } from './types';
+
+import { ErrorMessage, InputContainer, InputField, InputWrapper, Label, Required, WarningMessage } from './components';
+
+import { Icon } from '../Icon';
+import { getInputType } from './utils';
+
+export const inputDefaults: InputProps = {
+ value: '',
+ setValue: () => {},
+ label: 'Label',
+ placeholder: 'Placeholder',
+ error: '',
+ warning: '',
+ isSuccess: false,
+ isDisabled: false,
+ isInvalid: false,
+ isReadOnly: false,
+ isPassword: false,
+ isRequired: false,
+ errorOnHover: false,
+ type: 'text',
+};
+
+export const Input = ({
+ value = inputDefaults.value,
+ setValue = inputDefaults.setValue,
+ label = inputDefaults.label,
+ placeholder = inputDefaults.placeholder,
+ icon, // default undefined
+ error = inputDefaults.error,
+ warning = inputDefaults.warning,
+ isSuccess = inputDefaults.isSuccess,
+ isDisabled = inputDefaults.isDisabled,
+ isInvalid = inputDefaults.isInvalid,
+ isReadOnly = inputDefaults.isReadOnly,
+ isPassword = inputDefaults.isPassword,
+ isRequired = inputDefaults.isRequired,
+ errorOnHover = inputDefaults.errorOnHover,
+ type = inputDefaults.type,
+ id,
+ ...props
+}: InputProps) => {
+ // Invalid state is always true if error is present
+ let invalid = isInvalid;
+ if (error) invalid = true;
+
+ // Show/hide password text
+ const [showPassword, setShowPassword] = React.useState(false);
+ const passwordIcon = showPassword ? 'Visibility' : 'VisibilityOff';
+
+ // Input base props
+ const inputBaseProps = {
+ label,
+ isSuccess,
+ error,
+ warning,
+ isDisabled,
+ isInvalid: invalid,
+ };
+
+ return (
+
+ {label && (
+
+ {label} {isRequired && * }
+
+ )}
+
+ {icon && }
+ setValue?.(e.target.value)}
+ type={getInputType(type, isPassword, showPassword)}
+ placeholder={placeholder}
+ readOnly={isReadOnly}
+ disabled={isDisabled}
+ required={isRequired}
+ id={id}
+ />
+ {!isPassword && (
+
+ {invalid && }
+ {isSuccess && }
+ {warning && }
+
+ )}
+ {isPassword && setShowPassword(!showPassword)} icon={passwordIcon} size="lg" />}
+
+ {invalid && error && !errorOnHover && {error} }
+ {warning && {warning} }
+
+ );
+};
diff --git a/datahub-web-react/src/alchemy-components/components/Input/components.ts b/datahub-web-react/src/alchemy-components/components/Input/components.ts
new file mode 100644
index 00000000000000..d1c337642d9cd8
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/components/Input/components.ts
@@ -0,0 +1,92 @@
+import styled from 'styled-components';
+
+import theme, { borders, colors, radius, spacing, typography } from '@components/theme';
+import { getStatusColors } from '@components/theme/utils';
+
+import {
+ INPUT_MAX_HEIGHT,
+ formLabelTextStyles,
+ inputValueTextStyles,
+ inputPlaceholderTextStyles,
+} from '../commonStyles';
+
+import type { InputProps } from './types';
+
+const defaultFlexStyles = {
+ display: 'flex',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+};
+
+const defaultMessageStyles = {
+ marginTop: spacing.xxsm,
+ fontSize: typography.fontSizes.sm,
+};
+
+export const InputWrapper = styled.div({
+ ...defaultFlexStyles,
+ alignItems: 'flex-start',
+ flexDirection: 'column',
+ width: '100%',
+});
+
+export const InputContainer = styled.div(
+ ({ isSuccess, warning, isDisabled, isInvalid }: InputProps) => ({
+ border: `${borders['1px']} ${getStatusColors(isSuccess, warning, isInvalid)}`,
+ backgroundColor: isDisabled ? colors.gray[100] : colors.white,
+ paddingRight: spacing.md,
+ }),
+ {
+ ...defaultFlexStyles,
+ width: '100%',
+ maxHeight: INPUT_MAX_HEIGHT,
+ overflow: 'hidden',
+ borderRadius: radius.md,
+ flex: 1,
+ color: colors.gray[400], // 1st icon color
+
+ '&:focus-within': {
+ borderColor: colors.violet[200],
+ outline: `${borders['1px']} ${colors.violet[200]}`,
+ },
+ },
+);
+
+export const InputField = styled.input({
+ padding: `${spacing.sm} ${spacing.md}`,
+ lineHeight: typography.lineHeights.normal,
+ maxHeight: INPUT_MAX_HEIGHT,
+ border: borders.none,
+ width: '100%',
+
+ // Shared common input text styles
+ ...inputValueTextStyles(),
+
+ '&::placeholder': {
+ ...inputPlaceholderTextStyles,
+ },
+
+ '&:focus': {
+ outline: 'none',
+ },
+});
+
+export const Required = styled.span({
+ color: colors.red[500],
+});
+
+export const Label = styled.div({
+ ...formLabelTextStyles,
+ marginBottom: spacing.xsm,
+ textAlign: 'left',
+});
+
+export const ErrorMessage = styled.div({
+ ...defaultMessageStyles,
+ color: theme.semanticTokens.colors.error,
+});
+
+export const WarningMessage = styled.div({
+ ...defaultMessageStyles,
+ color: theme.semanticTokens.colors.warning,
+});
diff --git a/datahub-web-react/src/alchemy-components/components/Input/index.ts b/datahub-web-react/src/alchemy-components/components/Input/index.ts
new file mode 100644
index 00000000000000..336a9b4dd08e97
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/components/Input/index.ts
@@ -0,0 +1,2 @@
+export { Input, inputDefaults } from './Input';
+export type { InputProps } from './types';
diff --git a/datahub-web-react/src/alchemy-components/components/Input/types.ts b/datahub-web-react/src/alchemy-components/components/Input/types.ts
new file mode 100644
index 00000000000000..1b2abf132d3283
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/components/Input/types.ts
@@ -0,0 +1,22 @@
+import { InputHTMLAttributes } from 'react';
+
+import { IconNames } from '../Icon';
+
+export interface InputProps extends InputHTMLAttributes {
+ value?: string | number | readonly string[] | undefined;
+ setValue?: React.Dispatch>;
+ label: string;
+ placeholder?: string;
+ icon?: IconNames;
+ error?: string;
+ warning?: string;
+ isSuccess?: boolean;
+ isDisabled?: boolean;
+ isInvalid?: boolean;
+ isReadOnly?: boolean;
+ isPassword?: boolean;
+ isRequired?: boolean;
+ errorOnHover?: boolean;
+ id?: string;
+ type?: string;
+}
diff --git a/datahub-web-react/src/alchemy-components/components/Input/utils.ts b/datahub-web-react/src/alchemy-components/components/Input/utils.ts
new file mode 100644
index 00000000000000..142a93232485b3
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/components/Input/utils.ts
@@ -0,0 +1,5 @@
+export const getInputType = (type?: string, isPassword?: boolean, showPassword?: boolean) => {
+ if (type) return type;
+ if (isPassword && !showPassword) return 'password';
+ return 'text';
+};
diff --git a/datahub-web-react/src/alchemy-components/components/LineChart/LineChart.stories.tsx b/datahub-web-react/src/alchemy-components/components/LineChart/LineChart.stories.tsx
new file mode 100644
index 00000000000000..8cce0369918a2e
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/components/LineChart/LineChart.stories.tsx
@@ -0,0 +1,96 @@
+import React from 'react';
+import { BADGE } from '@geometricpanda/storybook-addon-badges';
+import type { Meta, StoryObj } from '@storybook/react';
+import { LineChart } from './LineChart';
+import { getMockedProps } from '../BarChart/utils';
+
+const meta = {
+ title: 'Charts / LineChart',
+ component: LineChart,
+
+ // Display Properties
+ parameters: {
+ layout: 'centered',
+ badges: [BADGE.EXPERIMENTAL],
+ docs: {
+ subtitle: 'A component that is used to show LineChart',
+ },
+ },
+
+ // Component-level argTypes
+ argTypes: {
+ data: {
+ description: 'Array of datum to show',
+ },
+ xAccessor: {
+ description: 'A function to convert datum to value of X',
+ },
+ yAccessor: {
+ description: 'A function to convert datum to value of Y',
+ },
+ renderTooltipContent: {
+ description: 'A function to replace default rendering of toolbar',
+ },
+ margin: {
+ description: 'Add margins to chart',
+ },
+ leftAxisTickFormat: {
+ description: 'A function to format labels of left axis',
+ },
+ leftAxisTickLabelProps: {
+ description: 'Props for label of left axis',
+ },
+ bottomAxisTickFormat: {
+ description: 'A function to format labels of bottom axis',
+ },
+ bottomAxisTickLabelProps: {
+ description: 'Props for label of bottom axis',
+ },
+ lineColor: {
+ description: 'Color of line on chart',
+ control: {
+ type: 'color',
+ },
+ },
+ areaColor: {
+ description: 'Color of area under line',
+ control: {
+ type: 'color',
+ },
+ },
+ gridColor: {
+ description: "Color of grid's lines",
+ control: {
+ type: 'color',
+ },
+ },
+ renderGradients: {
+ description: 'A function to render different gradients that can be used as colors',
+ },
+ toolbarVerticalCrosshairStyle: {
+ description: "Styles of toolbar's vertical line",
+ },
+ renderTooltipGlyph: {
+ description: 'A function to render a glyph',
+ },
+ },
+
+ // Define defaults
+ args: {
+ ...getMockedProps(),
+ renderTooltipContent: (datum) => <>DATUM: {JSON.stringify(datum)}>,
+ },
+} satisfies Meta;
+
+export default meta;
+
+type Story = StoryObj;
+
+export const sandbox: Story = {
+ tags: ['dev'],
+ render: (props) => (
+
+
+
+ ),
+};
diff --git a/datahub-web-react/src/alchemy-components/components/LineChart/LineChart.tsx b/datahub-web-react/src/alchemy-components/components/LineChart/LineChart.tsx
new file mode 100644
index 00000000000000..22580122ccf84f
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/components/LineChart/LineChart.tsx
@@ -0,0 +1,178 @@
+import { colors } from '@src/alchemy-components/theme';
+// import { abbreviateNumber } from '@src/app/dataviz/utils';
+import { TickLabelProps } from '@visx/axis';
+import { curveMonotoneX } from '@visx/curve';
+import { LinearGradient } from '@visx/gradient';
+import { ParentSize } from '@visx/responsive';
+import { AreaSeries, Axis, AxisScale, Grid, LineSeries, Tooltip, XYChart } from '@visx/xychart';
+import dayjs from 'dayjs';
+import React, { useState } from 'react';
+import { Popover } from '../Popover';
+import { ChartWrapper } from './components';
+import { LineChartProps } from './types';
+import { abbreviateNumber } from '../dataviz/utils';
+
+const commonTickLabelProps: TickLabelProps = {
+ fontSize: 10,
+ fontFamily: 'Mulish',
+ fill: colors.gray[1700],
+};
+
+const GLYPH_DROP_SHADOW_FILTER = `
+ drop-shadow(0px 1px 3px rgba(33, 23, 95, 0.30))
+ drop-shadow(0px 2px 5px rgba(33, 23, 95, 0.25))
+ drop-shadow(0px -2px 5px rgba(33, 23, 95, 0.25)
+`;
+
+export const lineChartDefault: LineChartProps = {
+ data: [],
+ xAccessor: (datum) => datum?.x,
+ yAccessor: (datum) => datum?.y,
+ leftAxisTickFormat: abbreviateNumber,
+ leftAxisTickLabelProps: {
+ ...commonTickLabelProps,
+ textAnchor: 'end',
+ },
+ bottomAxisTickFormat: (x) => dayjs(x).format('D MMM'),
+ bottomAxisTickLabelProps: {
+ ...commonTickLabelProps,
+ textAnchor: 'middle',
+ verticalAnchor: 'start',
+ },
+ lineColor: colors.violet[500],
+ areaColor: 'url(#line-gradient)',
+ gridColor: '#e0e0e0',
+ renderGradients: () => (
+
+ ),
+ toolbarVerticalCrosshairStyle: {
+ stroke: colors.white,
+ strokeWidth: 2,
+ filter: GLYPH_DROP_SHADOW_FILTER,
+ },
+ renderTooltipGlyph: (props) => {
+ return (
+ <>
+
+
+ >
+ );
+ },
+};
+
+export function LineChart({
+ data,
+ xAccessor = lineChartDefault.xAccessor,
+ yAccessor = lineChartDefault.yAccessor,
+ renderTooltipContent,
+ margin,
+ leftAxisTickFormat = lineChartDefault.leftAxisTickFormat,
+ leftAxisTickLabelProps = lineChartDefault.leftAxisTickLabelProps,
+ bottomAxisTickFormat = lineChartDefault.bottomAxisTickFormat,
+ bottomAxisTickLabelProps = lineChartDefault.bottomAxisTickLabelProps,
+ lineColor = lineChartDefault.lineColor,
+ areaColor = lineChartDefault.areaColor,
+ gridColor = lineChartDefault.gridColor,
+ renderGradients = lineChartDefault.renderGradients,
+ toolbarVerticalCrosshairStyle = lineChartDefault.toolbarVerticalCrosshairStyle,
+ renderTooltipGlyph = lineChartDefault.renderTooltipGlyph,
+}: LineChartProps) {
+ const [showGrid, setShowGrid] = useState(false);
+
+ // FYI: additional margins to show left and bottom axises
+ const internalMargin = {
+ top: (margin?.top ?? 0) + 30,
+ right: (margin?.right ?? 0) + 20,
+ bottom: (margin?.bottom ?? 0) + 35,
+ left: (margin?.left ?? 0) + 40,
+ };
+
+ const accessors = { xAccessor, yAccessor };
+
+ return (
+ setShowGrid(true)} onMouseLeave={() => setShowGrid(false)}>
+
+ {({ width, height }) => {
+ return (
+
+ {renderGradients?.()}
+
+
+
+
+
+
+
+ {showGrid && (
+
+ )}
+
+
+ dataKey="line-chart-seria-01"
+ data={data}
+ fill={areaColor}
+ curve={curveMonotoneX}
+ {...accessors}
+ />
+
+ dataKey="line-chart-seria-01"
+ data={data}
+ stroke={lineColor}
+ curve={curveMonotoneX}
+ {...accessors}
+ />
+
+
+ snapTooltipToDatumX
+ snapTooltipToDatumY
+ showVerticalCrosshair
+ applyPositionStyle
+ showSeriesGlyphs
+ verticalCrosshairStyle={toolbarVerticalCrosshairStyle}
+ renderGlyph={renderTooltipGlyph}
+ unstyled
+ renderTooltip={({ tooltipData }) => {
+ return (
+ tooltipData?.nearestDatum && (
+
+ )
+ );
+ }}
+ />
+
+ );
+ }}
+
+
+ );
+}
diff --git a/datahub-web-react/src/alchemy-components/components/LineChart/components.tsx b/datahub-web-react/src/alchemy-components/components/LineChart/components.tsx
new file mode 100644
index 00000000000000..fb6c0cf1ced784
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/components/LineChart/components.tsx
@@ -0,0 +1,8 @@
+import styled from 'styled-components';
+
+export const ChartWrapper = styled.div`
+ width: 100%;
+ height: 100%;
+ position: relative;
+ cursor: pointer;
+`;
diff --git a/datahub-web-react/src/alchemy-components/components/LineChart/index.ts b/datahub-web-react/src/alchemy-components/components/LineChart/index.ts
new file mode 100644
index 00000000000000..7fca9300d578ca
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/components/LineChart/index.ts
@@ -0,0 +1 @@
+export { LineChart } from './LineChart';
diff --git a/datahub-web-react/src/alchemy-components/components/LineChart/types.ts b/datahub-web-react/src/alchemy-components/components/LineChart/types.ts
new file mode 100644
index 00000000000000..cf45662ba7cf90
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/components/LineChart/types.ts
@@ -0,0 +1,22 @@
+import { TickFormatter, TickLabelProps } from '@visx/axis';
+import { Margin } from '@visx/xychart';
+import { RenderTooltipGlyphProps } from '@visx/xychart/lib/components/Tooltip';
+import React from 'react';
+
+export type LineChartProps = {
+ data: DatumType[];
+ xAccessor: (datum: DatumType) => string | number;
+ yAccessor: (datum: DatumType) => number;
+ renderTooltipContent?: (datum: DatumType) => React.ReactNode;
+ margin?: Margin;
+ leftAxisTickFormat?: TickFormatter;
+ leftAxisTickLabelProps?: TickLabelProps;
+ bottomAxisTickFormat?: TickFormatter;
+ bottomAxisTickLabelProps?: TickLabelProps;
+ lineColor?: string;
+ areaColor?: string;
+ gridColor?: string;
+ renderGradients?: () => React.ReactNode;
+ toolbarVerticalCrosshairStyle?: React.SVGProps;
+ renderTooltipGlyph?: (props: RenderTooltipGlyphProps) => React.ReactNode | undefined;
+};
diff --git a/datahub-web-react/src/alchemy-components/components/PageTitle/PageTitle.stories.tsx b/datahub-web-react/src/alchemy-components/components/PageTitle/PageTitle.stories.tsx
new file mode 100644
index 00000000000000..7016ecbc7c90a0
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/components/PageTitle/PageTitle.stories.tsx
@@ -0,0 +1,71 @@
+import React from 'react';
+
+import type { Meta, StoryObj } from '@storybook/react';
+import { BADGE } from '@geometricpanda/storybook-addon-badges';
+import { PageTitle } from '.';
+
+// Auto Docs
+const meta = {
+ title: 'Pages / Page Title',
+ component: PageTitle,
+
+ // Display Properties
+ parameters: {
+ layout: 'centered',
+ badges: [BADGE.STABLE, 'readyForDesignReview'],
+ docs: {
+ subtitle: 'Used to render the title and subtitle for a page.',
+ },
+ },
+
+ // Component-level argTypes
+ argTypes: {
+ title: {
+ description: 'The title text',
+ },
+ subTitle: {
+ description: 'The subtitle text',
+ },
+ variant: {
+ description: 'The variant of header based on its usage',
+ },
+ },
+
+ // Define default args
+ args: {
+ title: 'Automations',
+ subTitle: 'Create & manage automations',
+ variant: 'pageHeader',
+ },
+} satisfies Meta;
+
+export default meta;
+
+// Stories
+
+type Story = StoryObj;
+
+// Basic story is what is displayed 1st in storybook
+// Pass props to this so that it can be customized via the UI props panel
+export const sandbox: Story = {
+ tags: ['dev'],
+ render: (props) => ,
+};
+
+export const withLink = () => (
+
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas aliquet nulla id felis vehicula, et
+ posuere dui dapibus. Nullam rhoncus massa non tortor convallis , in blandit turpis
+ rutrum. Morbi tempus velit mauris, at mattis metus mattis sed. Nunc molestie efficitur lectus, vel
+ mollis eros.
+ >
+ }
+ />
+);
+
+export const sectionHeader = () => (
+
+);
diff --git a/datahub-web-react/src/alchemy-components/components/PageTitle/PageTitle.tsx b/datahub-web-react/src/alchemy-components/components/PageTitle/PageTitle.tsx
new file mode 100644
index 00000000000000..3dcf42ff2fc0e2
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/components/PageTitle/PageTitle.tsx
@@ -0,0 +1,17 @@
+import React from 'react';
+import { PageTitleProps } from './types';
+import { Container, SubTitle, Title } from './components';
+import { Pill } from '../Pills';
+
+export const PageTitle = ({ title, subTitle, pillLabel, variant = 'pageHeader' }: PageTitleProps) => {
+ return (
+
+
+ {title}
+ {pillLabel ? : null}
+
+
+ {subTitle ? {subTitle} : null}
+
+ );
+};
diff --git a/datahub-web-react/src/alchemy-components/components/PageTitle/components.ts b/datahub-web-react/src/alchemy-components/components/PageTitle/components.ts
new file mode 100644
index 00000000000000..328323434e0403
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/components/PageTitle/components.ts
@@ -0,0 +1,52 @@
+import styled from 'styled-components';
+import { typography, colors } from '@components/theme';
+import { getHeaderSubtitleStyles, getHeaderTitleStyles } from './utils';
+
+// Text Styles
+const titleStyles = {
+ display: 'flex',
+ alignItems: 'center',
+ gap: 8,
+ fontWeight: typography.fontWeights.bold,
+ color: colors.gray[600],
+};
+
+const subTitleStyles = {
+ fontWeight: typography.fontWeights.normal,
+ color: colors.gray[1700],
+};
+
+// Default styles
+const baseStyles = {
+ fontFamily: typography.fonts.body,
+ margin: 0,
+
+ '& a': {
+ color: colors.violet[400],
+ textDecoration: 'none',
+ transition: 'color 0.15s ease',
+
+ '&:hover': {
+ color: colors.violet[500],
+ },
+ },
+};
+
+export const Container = styled.div`
+ display: flex;
+ flex-direction: column;
+ align-items: start;
+ justify-content: start;
+`;
+
+export const Title = styled.div<{ variant: string }>(({ variant }) => ({
+ ...baseStyles,
+ ...titleStyles,
+ ...getHeaderTitleStyles(variant),
+}));
+
+export const SubTitle = styled.div<{ variant: string }>(({ variant }) => ({
+ ...baseStyles,
+ ...subTitleStyles,
+ ...getHeaderSubtitleStyles(variant),
+}));
diff --git a/datahub-web-react/src/alchemy-components/components/PageTitle/index.ts b/datahub-web-react/src/alchemy-components/components/PageTitle/index.ts
new file mode 100644
index 00000000000000..2888306f7c9a66
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/components/PageTitle/index.ts
@@ -0,0 +1 @@
+export { PageTitle } from './PageTitle';
diff --git a/datahub-web-react/src/alchemy-components/components/PageTitle/types.ts b/datahub-web-react/src/alchemy-components/components/PageTitle/types.ts
new file mode 100644
index 00000000000000..fb1e207d0bbd7b
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/components/PageTitle/types.ts
@@ -0,0 +1,8 @@
+import React from 'react';
+
+export interface PageTitleProps {
+ title: string;
+ subTitle?: string | React.ReactNode;
+ pillLabel?: string;
+ variant?: 'pageHeader' | 'sectionHeader';
+}
diff --git a/datahub-web-react/src/alchemy-components/components/PageTitle/utils.ts b/datahub-web-react/src/alchemy-components/components/PageTitle/utils.ts
new file mode 100644
index 00000000000000..fe6d18688f31f1
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/components/PageTitle/utils.ts
@@ -0,0 +1,27 @@
+import { typography } from '@components/theme';
+
+export const getHeaderTitleStyles = (variant) => {
+ if (variant === 'sectionHeader') {
+ return {
+ fontSize: typography.fontSizes.lg,
+ lineHeight: typography.lineHeights.lg,
+ };
+ }
+ return {
+ fontSize: typography.fontSizes['3xl'],
+ lineHeight: typography.lineHeights['3xl'],
+ };
+};
+
+export const getHeaderSubtitleStyles = (variant) => {
+ if (variant === 'sectionHeader') {
+ return {
+ fontSize: typography.fontSizes.md,
+ lineHeight: typography.lineHeights.md,
+ };
+ }
+ return {
+ fontSize: typography.fontSizes.lg,
+ lineHeight: typography.lineHeights.lg,
+ };
+};
diff --git a/datahub-web-react/src/alchemy-components/components/Pills/Pill.stories.tsx b/datahub-web-react/src/alchemy-components/components/Pills/Pill.stories.tsx
new file mode 100644
index 00000000000000..d5cdffef6d6bd3
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/components/Pills/Pill.stories.tsx
@@ -0,0 +1,126 @@
+import React from 'react';
+
+import { BADGE } from '@geometricpanda/storybook-addon-badges';
+import type { Meta, StoryObj } from '@storybook/react';
+
+import { GridList } from '@components/.docs/mdx-components';
+import { AVAILABLE_ICONS } from '../Icon';
+import { Pill, pillDefault } from './Pill';
+
+const meta = {
+ title: 'Components / Pill',
+ component: Pill,
+
+ // Display Properties
+ parameters: {
+ layout: 'centered',
+ badges: [BADGE.EXPERIMENTAL],
+ docs: {
+ subtitle: 'A component that is used to get pill',
+ },
+ },
+
+ // Component-level argTypes
+ argTypes: {
+ label: {
+ description: 'Label for the Pill.',
+ table: {
+ defaultValue: { summary: pillDefault.label },
+ },
+ control: {
+ type: 'text',
+ },
+ },
+ leftIcon: {
+ description: 'The icon to display in the Pill icon.',
+ type: 'string',
+ options: AVAILABLE_ICONS,
+ control: {
+ type: 'select',
+ },
+ },
+ rightIcon: {
+ description: 'The icon to display in the Pill icon.',
+ type: 'string',
+ options: AVAILABLE_ICONS,
+ control: {
+ type: 'select',
+ },
+ },
+ size: {
+ description: 'The size of the pill.',
+ options: ['sm', 'md', 'lg', 'xl'],
+ table: {
+ defaultValue: { summary: pillDefault.size },
+ },
+ control: {
+ type: 'select',
+ },
+ },
+ variant: {
+ description: 'The size of the Pill.',
+ options: ['filled', 'outline'],
+ table: {
+ defaultValue: { summary: pillDefault.variant },
+ },
+ control: {
+ type: 'select',
+ },
+ },
+ colorScheme: {
+ description: 'The color of the Pill.',
+ options: ['violet', 'green', 'red', 'blue', 'gray'],
+ table: {
+ defaultValue: { summary: pillDefault.color },
+ },
+ control: {
+ type: 'select',
+ },
+ },
+ },
+
+ // Define defaults
+ args: {
+ label: pillDefault.label,
+ leftIcon: pillDefault.leftIcon,
+ rightIcon: pillDefault.rightIcon,
+ size: pillDefault.size,
+ variant: pillDefault.variant,
+ },
+} satisfies Meta;
+
+export default meta;
+
+type Story = StoryObj;
+
+export const sandbox: Story = {
+ tags: ['dev'],
+ render: (props) => ,
+};
+
+export const sizes = () => (
+
+
+
+
+
+);
+
+export const colors = () => (
+
+
+
+
+
+
+
+
+);
+
+export const withIcon = () => (
+
+
+
+
+
+);
diff --git a/datahub-web-react/src/alchemy-components/components/Pills/Pill.tsx b/datahub-web-react/src/alchemy-components/components/Pills/Pill.tsx
new file mode 100644
index 00000000000000..898ec89fce5957
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/components/Pills/Pill.tsx
@@ -0,0 +1,42 @@
+import { Icon } from '@components';
+import React from 'react';
+
+import { PillContainer, PillText } from './components';
+import { PillProps } from './types';
+
+export const pillDefault: PillProps = {
+ label: 'Label',
+ size: 'md',
+ variant: 'filled',
+ clickable: true,
+};
+
+export function Pill({
+ label = pillDefault.label,
+ size = pillDefault.size,
+ leftIcon,
+ rightIcon,
+ colorScheme,
+ variant = pillDefault.variant,
+ clickable = pillDefault.clickable,
+ id,
+ onClickRightIcon,
+ onClickLeftIcon,
+ onPillClick,
+}: PillProps) {
+ return (
+
+ {leftIcon && }
+ {label}
+ {rightIcon && }
+
+ );
+}
diff --git a/datahub-web-react/src/alchemy-components/components/Pills/components.ts b/datahub-web-react/src/alchemy-components/components/Pills/components.ts
new file mode 100644
index 00000000000000..79734561a92da6
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/components/Pills/components.ts
@@ -0,0 +1,33 @@
+import { spacing } from '@components/theme';
+import styled from 'styled-components';
+
+import { PillStyleProps } from './types';
+import { getPillStyle } from './utils';
+
+export const PillContainer = styled.div(
+ // Dynamic styles
+ (props: PillStyleProps) => ({ ...getPillStyle(props as PillStyleProps) }),
+ {
+ // Base root styles
+ display: 'inline-flex',
+ alignItems: 'center',
+ gap: spacing.xxsm,
+ cursor: 'pointer',
+ padding: '0px 8px',
+ borderRadius: '200px',
+ maxWidth: '100%',
+
+ // Base Disabled styles
+ '&:disabled': {
+ cursor: 'not-allowed',
+ },
+ },
+);
+
+export const PillText = styled.span({
+ maxWidth: '100%',
+ display: 'block',
+ whiteSpace: 'nowrap',
+ overflow: 'hidden',
+ textOverflow: 'ellipsis',
+});
diff --git a/datahub-web-react/src/alchemy-components/components/Pills/index.ts b/datahub-web-react/src/alchemy-components/components/Pills/index.ts
new file mode 100644
index 00000000000000..85a76193db2670
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/components/Pills/index.ts
@@ -0,0 +1 @@
+export { Pill } from './Pill';
diff --git a/datahub-web-react/src/alchemy-components/components/Pills/types.ts b/datahub-web-react/src/alchemy-components/components/Pills/types.ts
new file mode 100644
index 00000000000000..17d4d12465e1ef
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/components/Pills/types.ts
@@ -0,0 +1,18 @@
+import { ColorOptions, SizeOptions, VariantOptions } from '@src/alchemy-components/theme/config';
+import { HTMLAttributes } from 'react';
+
+export interface PillStyleProps {
+ colorScheme?: ColorOptions; // need to keep colorScheme because HTMLAttributes also have color property
+ variant?: VariantOptions;
+ size?: SizeOptions;
+ clickable?: boolean;
+}
+
+export interface PillProps extends HTMLAttributes, PillStyleProps {
+ label: string;
+ rightIcon?: string;
+ leftIcon?: string;
+ onClickRightIcon?: (e: React.MouseEvent) => void;
+ onClickLeftIcon?: (e: React.MouseEvent) => void;
+ onPillClick?: (e: React.MouseEvent) => void;
+}
diff --git a/datahub-web-react/src/alchemy-components/components/Pills/utils.ts b/datahub-web-react/src/alchemy-components/components/Pills/utils.ts
new file mode 100644
index 00000000000000..832bf95640982b
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/components/Pills/utils.ts
@@ -0,0 +1,147 @@
+import { colors, typography } from '@src/alchemy-components/theme';
+import { getColor, getFontSize } from '@src/alchemy-components/theme/utils';
+import { PillStyleProps } from './types';
+
+// Utility function to get color styles for pill - does not generate CSS
+const getPillColorStyles = (variant, color) => {
+ const defaultStyles = {
+ bgColor: getColor(color, 100),
+ hoverBgColor: getColor('gray', 100),
+ borderColor: '',
+ activeBorderColor: getColor('violet', 500),
+ textColor: getColor(color, 600),
+ };
+
+ const colorOverrides = {
+ violet: {
+ textColor: getColor(color, 500),
+ bgColor: getColor('gray', 1000),
+ borderColor: 'transparent',
+ hoverBgColor: getColor(color, 100),
+ activeBorderColor: getColor(color, 500),
+ },
+ blue: {
+ textColor: getColor(color, 1000),
+ bgColor: getColor('gray', 1100),
+ borderColor: 'transparent',
+ hoverBgColor: getColor(color, 1100),
+ activeBorderColor: getColor(color, 1000),
+ },
+ red: {
+ textColor: getColor(color, 1000),
+ bgColor: getColor('gray', 1200),
+ hoverBgColor: getColor(color, 1100),
+ activeBorderColor: getColor(color, 1000),
+ },
+ green: {
+ textColor: getColor(color, 1000),
+ bgColor: getColor('gray', 1300),
+ hoverBgColor: getColor(color, 1100),
+ activeBorderColor: getColor(color, 1000),
+ },
+ yellow: {
+ textColor: getColor(color, 1000),
+ bgColor: getColor('gray', 1400),
+ hoverBgColor: getColor(color, 1100),
+ activeBorderColor: getColor(color, 1000),
+ },
+ };
+
+ const styles = colorOverrides[color] || defaultStyles;
+
+ if (variant === 'outline') {
+ return {
+ bgColor: colors.transparent,
+ borderColor: getColor('gray', 1400),
+ textColor: getColor(color, 600),
+ };
+ }
+
+ return styles;
+};
+
+// Generate variant styles for pill
+const getPillVariantStyles = (variant, colorStyles) =>
+ ({
+ filled: {
+ backgroundColor: colorStyles.bgColor,
+ border: `1px solid transparent`,
+ color: colorStyles.textColor,
+ '&:hover': {
+ backgroundColor: colorStyles.hoverBgColor,
+ },
+ },
+ outline: {
+ backgroundColor: 'transparent',
+ border: `1px solid ${colorStyles.borderColor}`,
+ color: colorStyles.textColor,
+ '&:hover': {
+ backgroundColor: colorStyles.hoverBgColor,
+ border: `1px solid transparent`,
+ },
+ '&:disabled': {
+ border: `1px solid transparent`,
+ },
+ },
+ text: {
+ color: colorStyles.textColor,
+ },
+ }[variant]);
+
+// Generate font styles for pill
+const getPillFontStyles = (size) => {
+ const baseFontStyles = {
+ fontFamily: typography.fonts.body,
+ fontWeight: typography.fontWeights.normal,
+ lineHeight: typography.lineHeights.none,
+ };
+
+ const sizeMap = {
+ xs: { fontSize: getFontSize(size), lineHeight: '16px' },
+ sm: { fontSize: getFontSize(size), lineHeight: '22px' },
+ md: { fontSize: getFontSize(size), lineHeight: '24px' },
+ lg: { fontSize: getFontSize(size), lineHeight: '30px' },
+ xl: { fontSize: getFontSize(size), lineHeight: '34px' },
+ };
+
+ return {
+ ...baseFontStyles,
+ ...sizeMap[size],
+ };
+};
+
+// Generate active styles for pill
+const getPillActiveStyles = (styleColors) => ({
+ borderColor: styleColors.activeBorderColor,
+});
+
+/*
+ * Main function to generate styles for pill
+ */
+export const getPillStyle = (props: PillStyleProps) => {
+ const { variant, colorScheme = 'gray', size, clickable = true } = props;
+
+ // Get map of colors
+ const colorStyles = getPillColorStyles(variant, colorScheme);
+
+ // Define styles for pill
+ let styles = {
+ ...getPillVariantStyles(variant, colorStyles),
+ ...getPillFontStyles(size),
+ '&:focus': {
+ ...getPillActiveStyles(colorStyles),
+ outline: 'none', // Remove default browser focus outline if needed
+ },
+ '&:active': {
+ ...getPillActiveStyles(colorStyles),
+ },
+ };
+ if (!clickable) {
+ styles = {
+ ...styles,
+ pointerEvents: 'none',
+ };
+ }
+
+ return styles;
+};
diff --git a/datahub-web-react/src/alchemy-components/components/Popover/Popover.tsx b/datahub-web-react/src/alchemy-components/components/Popover/Popover.tsx
new file mode 100644
index 00000000000000..8f6ca61976b206
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/components/Popover/Popover.tsx
@@ -0,0 +1,6 @@
+import { Popover, PopoverProps } from 'antd';
+import * as React from 'react';
+
+export default function DataHubPopover(props: PopoverProps & React.RefAttributes) {
+ return ;
+}
diff --git a/datahub-web-react/src/alchemy-components/components/Popover/index.ts b/datahub-web-react/src/alchemy-components/components/Popover/index.ts
new file mode 100644
index 00000000000000..02df6c38e8c4ea
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/components/Popover/index.ts
@@ -0,0 +1 @@
+export { default as Popover } from './Popover';
diff --git a/datahub-web-react/src/alchemy-components/components/Radio/Radio.stories.tsx b/datahub-web-react/src/alchemy-components/components/Radio/Radio.stories.tsx
new file mode 100644
index 00000000000000..cb3116d7b8941b
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/components/Radio/Radio.stories.tsx
@@ -0,0 +1,136 @@
+import React from 'react';
+import type { Meta, StoryObj } from '@storybook/react';
+import { GridList } from '@components/.docs/mdx-components';
+import { Radio, radioDefaults, RadioGroup } from './Radio';
+import { Heading } from '../Heading';
+import { RadioProps } from './types';
+
+const MOCK_RADIOS: RadioProps[] = [
+ {
+ label: 'Label 1',
+ error: '',
+ isChecked: false,
+ isDisabled: false,
+ isIntermediate: false,
+ isRequired: false,
+ },
+ {
+ label: 'Label 2',
+ error: '',
+ isChecked: false,
+ isDisabled: false,
+ isIntermediate: false,
+ isRequired: false,
+ },
+ {
+ label: 'Label 3',
+ error: '',
+ isChecked: false,
+ isDisabled: false,
+ isIntermediate: false,
+ isRequired: false,
+ },
+];
+
+const meta = {
+ title: 'Forms / Radio',
+ component: Radio,
+ parameters: {
+ layout: 'centered',
+ docs: {
+ subtitle: 'A component that is used to get user input in the state of a radio button.',
+ },
+ },
+ argTypes: {
+ label: {
+ description: 'Label for the Radio.',
+ table: {
+ defaultValue: { summary: radioDefaults.label },
+ },
+ control: {
+ type: 'text',
+ },
+ },
+ error: {
+ description: 'Enforce error state on the Radio.',
+ table: {
+ defaultValue: { summary: radioDefaults.error },
+ },
+ control: {
+ type: 'text',
+ },
+ },
+ isChecked: {
+ description: 'Whether the Radio is checked.',
+ table: {
+ defaultValue: { summary: radioDefaults?.isChecked?.toString() },
+ },
+ control: {
+ type: 'boolean',
+ },
+ },
+ isDisabled: {
+ description: 'Whether the Radio is in disabled state.',
+ table: {
+ defaultValue: { summary: radioDefaults?.isDisabled?.toString() },
+ },
+ control: {
+ type: 'boolean',
+ },
+ },
+ isRequired: {
+ description: 'Whether the Radio is a required field.',
+ table: {
+ defaultValue: { summary: radioDefaults?.isRequired?.toString() },
+ },
+ control: {
+ type: 'boolean',
+ },
+ },
+ },
+ args: {
+ label: radioDefaults.label,
+ error: radioDefaults.error,
+ isChecked: radioDefaults.isChecked,
+ isDisabled: radioDefaults.isDisabled,
+ isRequired: radioDefaults.isRequired,
+ },
+} satisfies Meta;
+
+export default meta;
+
+type Story = StoryObj;
+
+export const sandbox: Story = {
+ tags: ['dev'],
+ render: (props) => ,
+};
+
+export const states = () => (
+
+
+
+
+
+
+);
+
+export const disabledStates = () => (
+
+
+
+
+);
+
+export const radioGroups = () => (
+
+
+ Horizontal Radio Group
+
+
+
+ Vertical Radio Group
+
+
+
+);
diff --git a/datahub-web-react/src/alchemy-components/components/Radio/Radio.tsx b/datahub-web-react/src/alchemy-components/components/Radio/Radio.tsx
new file mode 100644
index 00000000000000..592c10ec88de8a
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/components/Radio/Radio.tsx
@@ -0,0 +1,89 @@
+import React, { useEffect, useState } from 'react';
+import { RadioGroupProps, RadioProps } from './types';
+import {
+ RadioWrapper,
+ Checkmark,
+ HiddenInput,
+ Label,
+ Required,
+ RadioLabel,
+ RadioBase,
+ RadioGroupContainer,
+} from './components';
+
+export const radioDefaults = {
+ label: 'Label',
+ error: '',
+ isChecked: false,
+ isDisabled: false,
+ isRequired: false,
+ isVertical: false,
+ setIsChecked: () => {},
+};
+
+export const Radio = ({
+ label = radioDefaults.label,
+ error = radioDefaults.error,
+ isChecked = radioDefaults.isChecked,
+ isDisabled = radioDefaults.isDisabled,
+ isRequired = radioDefaults.isRequired,
+ setIsChecked = radioDefaults.setIsChecked,
+ ...props
+}: RadioProps) => {
+ const [checked, setChecked] = useState(isChecked || false);
+
+ useEffect(() => {
+ setChecked(isChecked || false);
+ }, [isChecked]);
+
+ const id = props.id || `checkbox-${label}`;
+
+ return (
+
+
+ {
+ setChecked(true);
+ setIsChecked?.(true);
+ }}
+ aria-label={label}
+ aria-labelledby={id}
+ aria-checked={checked}
+ {...props}
+ />
+
+
+ {label && (
+
+ setChecked(true)}>
+ {label} {isRequired && * }
+
+
+ )}
+
+ );
+};
+
+export const RadioGroup = ({ isVertical, radios }: RadioGroupProps) => {
+ if (!radios.length) {
+ return <>>;
+ }
+
+ return (
+
+ {radios.map((checkbox) => {
+ const props = { ...checkbox };
+ return (
+
+
+
+ );
+ })}
+
+ );
+};
diff --git a/datahub-web-react/src/alchemy-components/components/Radio/components.ts b/datahub-web-react/src/alchemy-components/components/Radio/components.ts
new file mode 100644
index 00000000000000..027971be179584
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/components/Radio/components.ts
@@ -0,0 +1,83 @@
+import { borders, colors, radius, spacing } from '@components/theme';
+import styled from 'styled-components';
+import { formLabelTextStyles } from '../commonStyles';
+import { getRadioBorderColor, getRadioCheckmarkColor } from './utils';
+
+export const RadioWrapper = styled.div<{ disabled: boolean; error: string }>(({ disabled, error }) => ({
+ position: 'relative',
+ margin: '20px',
+ width: '20px',
+ height: '20px',
+ border: `${borders['2px']} ${getRadioBorderColor(disabled, error)}`,
+ backgroundColor: colors.white,
+ borderRadius: radius.full,
+ display: 'flex',
+ justifyContent: 'flex-start',
+ alignItems: 'center',
+ marginRight: '40px',
+ cursor: !disabled ? 'pointer' : 'none',
+ transition: 'border 0.3s ease, outline 0.3s ease',
+ '&:hover': {
+ border: `${borders['2px']} ${!disabled && !error ? colors.violet[500] : getRadioBorderColor(disabled, error)}`,
+ outline: !disabled && !error ? `${borders['2px']} ${colors.gray[200]}` : 'none',
+ },
+}));
+
+export const RadioBase = styled.div({});
+
+export const Label = styled.div({
+ ...formLabelTextStyles,
+ display: 'flex',
+ justifyContent: 'center',
+ alignItems: 'center',
+});
+
+export const RadioLabel = styled.div({
+ display: 'flex',
+ justifyContent: 'center',
+ alignItems: 'center',
+});
+
+export const Required = styled.span({
+ color: colors.red[500],
+ marginLeft: spacing.xxsm,
+});
+
+export const RadioHoverState = styled.div({
+ border: `${borders['2px']} ${colors.violet[500]}`,
+ width: 'calc(100% - -3px)',
+ height: 'calc(100% - -3px)',
+ display: 'flex',
+ justifyContent: 'center',
+ alignItems: 'center',
+ borderRadius: radius.full,
+});
+
+export const Checkmark = styled.div<{ checked: boolean; disabled: boolean; error: string }>(
+ ({ checked, disabled, error }) => ({
+ width: 'calc(100% - 6px)',
+ height: 'calc(100% - 6px)',
+ borderRadius: radius.full,
+ background: getRadioCheckmarkColor(checked, disabled, error),
+ display: checked ? 'inline-block' : 'none',
+ position: 'absolute',
+ top: '50%',
+ left: '50%',
+ transform: 'translate(-50%, -50%)',
+ }),
+);
+
+export const HiddenInput = styled.input<{ checked: boolean }>({
+ opacity: 0,
+ width: '20px',
+ height: '20px',
+});
+
+export const RadioGroupContainer = styled.div<{ isVertical?: boolean }>(({ isVertical }) => ({
+ display: 'flex',
+ flexDirection: isVertical ? 'column' : 'row',
+ justifyContent: 'center',
+ alignItems: 'center',
+ gap: !isVertical ? spacing.md : spacing.none,
+ margin: !isVertical ? spacing.xxsm : spacing.none,
+}));
diff --git a/datahub-web-react/src/alchemy-components/components/Radio/types.ts b/datahub-web-react/src/alchemy-components/components/Radio/types.ts
new file mode 100644
index 00000000000000..59fd15654f916a
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/components/Radio/types.ts
@@ -0,0 +1,16 @@
+import { InputHTMLAttributes } from 'react';
+
+export interface RadioProps extends InputHTMLAttributes {
+ label?: string;
+ error?: string;
+ isChecked?: boolean;
+ setIsChecked?: React.Dispatch>;
+ isDisabled?: boolean;
+ isIntermediate?: boolean;
+ isRequired?: boolean;
+}
+
+export interface RadioGroupProps {
+ isVertical?: boolean;
+ radios: RadioProps[];
+}
diff --git a/datahub-web-react/src/alchemy-components/components/Radio/utils.ts b/datahub-web-react/src/alchemy-components/components/Radio/utils.ts
new file mode 100644
index 00000000000000..ed9dcc35d303b4
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/components/Radio/utils.ts
@@ -0,0 +1,27 @@
+import { colors } from '@components/theme';
+
+const radioBorderColors = {
+ default: colors.gray[400],
+ disabled: colors.gray[300],
+ error: colors.red[500],
+};
+
+const radioCheckmarkColors = {
+ default: colors.white,
+ disabled: colors.gray[300],
+ checked: colors.violet[500],
+ error: colors.red[500],
+};
+
+export function getRadioBorderColor(disabled: boolean, error: string) {
+ if (disabled) return radioBorderColors.disabled;
+ if (error) return radioCheckmarkColors.error;
+ return radioBorderColors.default;
+}
+
+export function getRadioCheckmarkColor(checked: boolean, disabled: boolean, error: string) {
+ if (disabled) return radioCheckmarkColors.disabled;
+ if (error) return radioCheckmarkColors.error;
+ if (checked) return radioCheckmarkColors.checked;
+ return radioCheckmarkColors.default;
+}
diff --git a/datahub-web-react/src/alchemy-components/components/Select/BasicSelect.tsx b/datahub-web-react/src/alchemy-components/components/Select/BasicSelect.tsx
new file mode 100644
index 00000000000000..b49159ba38a758
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/components/Select/BasicSelect.tsx
@@ -0,0 +1,339 @@
+import { Button, Icon, Pill, Text } from '@components';
+import { isEqual } from 'lodash';
+import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
+import {
+ ActionButtonsContainer,
+ Container,
+ DescriptionContainer,
+ Dropdown,
+ FooterBase,
+ LabelContainer,
+ LabelsWrapper,
+ OptionContainer,
+ OptionLabel,
+ OptionList,
+ Placeholder,
+ SearchIcon,
+ SearchInput,
+ SearchInputContainer,
+ SelectAllOption,
+ SelectBase,
+ SelectLabel,
+ SelectValue,
+ StyledCancelButton,
+ StyledCheckbox,
+ StyledClearButton,
+} from './components';
+import { ActionButtonsProps, SelectLabelDisplayProps, SelectOption, SelectProps } from './types';
+import { getFooterButtonSize } from './utils';
+
+const SelectLabelDisplay = ({
+ selectedValues,
+ options,
+ placeholder,
+ isMultiSelect,
+ removeOption,
+ disabledValues,
+ showDescriptions,
+}: SelectLabelDisplayProps) => {
+ const selectedOptions = options.filter((opt) => selectedValues.includes(opt.value));
+ return (
+
+ {!!selectedOptions.length &&
+ isMultiSelect &&
+ selectedOptions.map((o) => {
+ const isDisabled = disabledValues?.includes(o.value);
+ return (
+ {
+ e.stopPropagation();
+ removeOption?.(o);
+ }}
+ clickable={!isDisabled}
+ />
+ );
+ })}
+ {!selectedValues.length && {placeholder} }
+ {!isMultiSelect && (
+ <>
+ {selectedOptions[0]?.label}
+ {showDescriptions && !!selectedValues.length && (
+ {selectedOptions[0]?.description}
+ )}
+ >
+ )}
+
+ );
+};
+
+const SelectActionButtons = ({
+ selectedValues,
+ isOpen,
+ isDisabled,
+ isReadOnly,
+ showClear,
+ handleClearSelection,
+ fontSize = 'md',
+}: ActionButtonsProps) => {
+ return (
+
+ {showClear && selectedValues.length > 0 && !isDisabled && !isReadOnly && (
+
+ )}
+
+
+ );
+};
+
+// Updated main component
+export const selectDefaults: SelectProps = {
+ options: [],
+ label: '',
+ size: 'md',
+ showSearch: false,
+ isDisabled: false,
+ isReadOnly: false,
+ isRequired: false,
+ isMultiSelect: false,
+ showClear: false,
+ placeholder: 'Select an option',
+ showSelectAll: false,
+ selectAllLabel: 'Select All',
+ showDescriptions: false,
+};
+
+export const BasicSelect = ({
+ options = selectDefaults.options,
+ label = selectDefaults.label,
+ values = [],
+ onCancel,
+ onUpdate,
+ showSearch = selectDefaults.showSearch,
+ isDisabled = selectDefaults.isDisabled,
+ isReadOnly = selectDefaults.isReadOnly,
+ isRequired = selectDefaults.isRequired,
+ showClear = selectDefaults.showClear,
+ size = selectDefaults.size,
+ isMultiSelect = selectDefaults.isMultiSelect,
+ placeholder = selectDefaults.placeholder,
+ disabledValues = [],
+ showSelectAll = selectDefaults.showSelectAll,
+ selectAllLabel = selectDefaults.selectAllLabel,
+ showDescriptions = selectDefaults.showDescriptions,
+ ...props
+}: SelectProps) => {
+ const [searchQuery, setSearchQuery] = useState('');
+ const [isOpen, setIsOpen] = useState(false);
+ const [selectedValues, setSelectedValues] = useState(values);
+ const [tempValues, setTempValues] = useState(values);
+ const selectRef = useRef(null);
+ const [areAllSelected, setAreAllSelected] = useState(false);
+
+ useEffect(() => {
+ if (values?.length > 0 && !isEqual(selectedValues, values)) {
+ setSelectedValues(values);
+ }
+ }, [values, selectedValues]);
+
+ useEffect(() => {
+ setAreAllSelected(tempValues.length === options.length);
+ }, [options, tempValues]);
+
+ const filteredOptions = useMemo(
+ () => options.filter((option) => option.label.toLowerCase().includes(searchQuery.toLowerCase())),
+ [options, searchQuery],
+ );
+
+ const handleDocumentClick = useCallback((e: MouseEvent) => {
+ if (selectRef.current && !selectRef.current.contains(e.target as Node)) {
+ setIsOpen(false);
+ }
+ }, []);
+
+ useEffect(() => {
+ document.addEventListener('click', handleDocumentClick);
+ return () => {
+ document.removeEventListener('click', handleDocumentClick);
+ };
+ }, [handleDocumentClick]);
+
+ const handleSelectClick = useCallback(() => {
+ if (!isDisabled && !isReadOnly) {
+ setTempValues(selectedValues);
+ setIsOpen((prev) => !prev);
+ }
+ }, [isDisabled, isReadOnly, selectedValues]);
+
+ const handleOptionChange = useCallback(
+ (option: SelectOption) => {
+ const updatedValues = tempValues.includes(option.value)
+ ? tempValues.filter((val) => val !== option.value)
+ : [...tempValues, option.value];
+
+ setTempValues(isMultiSelect ? updatedValues : [option.value]);
+ },
+ [tempValues, isMultiSelect],
+ );
+
+ const removeOption = useCallback(
+ (option: SelectOption) => {
+ const updatedValues = selectedValues.filter((val) => val !== option.value);
+ setSelectedValues(updatedValues);
+ },
+ [selectedValues],
+ );
+
+ const handleUpdateClick = useCallback(() => {
+ setSelectedValues(tempValues);
+ setIsOpen(false);
+ if (onUpdate) {
+ onUpdate(tempValues);
+ }
+ }, [tempValues, onUpdate]);
+
+ const handleCancelClick = useCallback(() => {
+ setIsOpen(false);
+ setTempValues(selectedValues);
+ if (onCancel) {
+ onCancel();
+ }
+ }, [selectedValues, onCancel]);
+
+ const handleClearSelection = useCallback(() => {
+ setSelectedValues([]);
+ setAreAllSelected(false);
+ setTempValues([]);
+ setIsOpen(false);
+ if (onUpdate) {
+ onUpdate([]);
+ }
+ }, [onUpdate]);
+
+ const handleSelectAll = () => {
+ if (areAllSelected) {
+ setTempValues([]);
+ onUpdate?.([]);
+ } else {
+ const allValues = options.map((option) => option.value);
+ setTempValues(allValues);
+ onUpdate?.(allValues);
+ }
+ setAreAllSelected(!areAllSelected);
+ };
+
+ return (
+
+ {label && {label} }
+
+
+
+
+ {isOpen && (
+
+ {showSearch && (
+
+ setSearchQuery(e.target.value)}
+ style={{ fontSize: size || 'md' }}
+ />
+
+
+ )}
+
+ {showSelectAll && isMultiSelect && (
+ !(disabledValues.length === options.length) && handleSelectAll()}
+ isDisabled={disabledValues.length === options.length}
+ >
+
+ {selectAllLabel}
+
+
+
+ )}
+ {filteredOptions.map((option) => (
+ !isMultiSelect && handleOptionChange(option)}
+ isSelected={tempValues.includes(option.value)}
+ isMultiSelect={isMultiSelect}
+ isDisabled={disabledValues?.includes(option.value)}
+ >
+ {isMultiSelect ? (
+
+ {option.label}
+ handleOptionChange(option)}
+ checked={tempValues.includes(option.value)}
+ disabled={disabledValues?.includes(option.value)}
+ />
+
+ ) : (
+
+
+ {option.label}
+
+ {!!option.description && (
+
+ {option.description}
+
+ )}
+
+ )}
+
+ ))}
+
+
+
+ Cancel
+
+
+ Update
+
+
+
+ )}
+
+ );
+};
+
+export default BasicSelect;
diff --git a/datahub-web-react/src/alchemy-components/components/Select/Nested/NestedOption.tsx b/datahub-web-react/src/alchemy-components/components/Select/Nested/NestedOption.tsx
new file mode 100644
index 00000000000000..8a7d3670b2b1b9
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/components/Select/Nested/NestedOption.tsx
@@ -0,0 +1,309 @@
+import React, { useState, useMemo, useEffect } from 'react';
+
+import { colors, Icon } from '@components';
+import theme from '@components/theme';
+import styled from 'styled-components';
+import { Checkbox } from 'antd';
+
+import { OptionLabel } from '../components';
+import { SelectOption } from './types';
+
+const ParentOption = styled.div`
+ display: flex;
+ align-items: center;
+`;
+
+const ChildOptions = styled.div`
+ padding-left: 20px;
+`;
+
+const StyledCheckbox = styled(Checkbox)<{ checked: boolean; indeterminate?: boolean }>`
+ .ant-checkbox-inner {
+ border: 1px solid ${colors.gray[300]} !important;
+ border-radius: 3px;
+ }
+ margin-left: auto;
+ ${(props) =>
+ props.checked &&
+ !props.indeterminate &&
+ `
+ .ant-checkbox-inner {
+ background-color: ${theme.semanticTokens.colors.primary};
+ border-color: ${theme.semanticTokens.colors.primary} !important;
+ }
+ `}
+ ${(props) =>
+ props.indeterminate &&
+ `
+ .ant-checkbox-inner {
+ &:after {
+ background-color: ${theme.semanticTokens.colors.primary};
+ }
+ }
+ `}
+ ${(props) =>
+ props.disabled &&
+ `
+ .ant-checkbox-inner {
+ background-color: ${colors.gray[200]} !important;
+ }
+ `}
+`;
+
+function getChildrenRecursively(
+ directChildren: SelectOption[],
+ parentValueToOptions: { [parentValue: string]: SelectOption[] },
+) {
+ const visitedParents = new Set();
+ let allChildren: SelectOption[] = [];
+
+ function getChildren(parentValue: string) {
+ const newChildren = parentValueToOptions[parentValue] || [];
+ if (visitedParents.has(parentValue) || !newChildren.length) {
+ return;
+ }
+
+ visitedParents.add(parentValue);
+ allChildren = [...allChildren, ...newChildren];
+ newChildren.forEach((child) => getChildren(child.value || child.value));
+ }
+
+ directChildren.forEach((c) => getChildren(c.value || c.value));
+
+ return allChildren;
+}
+
+interface OptionProps {
+ option: SelectOption;
+ selectedOptions: SelectOption[];
+ parentValueToOptions: { [parentValue: string]: SelectOption[] };
+ areParentsSelectable: boolean;
+ handleOptionChange: (node: SelectOption) => void;
+ addOptions: (nodes: SelectOption[]) => void;
+ removeOptions: (nodes: SelectOption[]) => void;
+ loadData?: (node: SelectOption) => void;
+ isMultiSelect?: boolean;
+ isLoadingParentChildList?: boolean;
+ setSelectedOptions: React.Dispatch>;
+}
+
+export const NestedOption = ({
+ option,
+ selectedOptions,
+ parentValueToOptions,
+ handleOptionChange,
+ addOptions,
+ removeOptions,
+ loadData,
+ isMultiSelect,
+ areParentsSelectable,
+ isLoadingParentChildList,
+ setSelectedOptions,
+}: OptionProps) => {
+ const [autoSelectChildren, setAutoSelectChildren] = useState(false);
+ const [loadingParentUrns, setLoadingParentUrns] = useState([]);
+ const [isOpen, setIsOpen] = useState(false);
+ const directChildren = useMemo(
+ () => parentValueToOptions[option.value] || [],
+ [parentValueToOptions, option.value],
+ );
+
+ const recursiveChildren = useMemo(
+ () => getChildrenRecursively(directChildren, parentValueToOptions),
+ [directChildren, parentValueToOptions],
+ );
+
+ const children = useMemo(() => [...directChildren, ...recursiveChildren], [directChildren, recursiveChildren]);
+ const selectableChildren = useMemo(
+ () => (areParentsSelectable ? children : children.filter((c) => !c.isParent)),
+ [areParentsSelectable, children],
+ );
+ const parentChildren = useMemo(() => children.filter((c) => c.isParent), [children]);
+
+ useEffect(() => {
+ if (autoSelectChildren && selectableChildren.length) {
+ addOptions(selectableChildren);
+ setAutoSelectChildren(false);
+ }
+ }, [autoSelectChildren, selectableChildren, addOptions]);
+
+ const areAllChildrenSelected = useMemo(
+ () => selectableChildren.every((child) => selectedOptions.find((o) => o.value === child.value)),
+ [selectableChildren, selectedOptions],
+ );
+
+ const areAnyChildrenSelected = useMemo(
+ () => selectableChildren.some((child) => selectedOptions.find((o) => o.value === child.value)),
+ [selectableChildren, selectedOptions],
+ );
+
+ const areAnyUnselectableChildrenUnexpanded = !!parentChildren.find(
+ (parent) => !selectableChildren.find((child) => child.parentValue === parent.value),
+ );
+
+ const isSelected = useMemo(
+ () =>
+ !!selectedOptions.find((o) => o.value === option.value) ||
+ (!areParentsSelectable &&
+ !!option.isParent &&
+ !!selectableChildren.length &&
+ areAllChildrenSelected &&
+ !areAnyUnselectableChildrenUnexpanded),
+ [
+ selectedOptions,
+ areAllChildrenSelected,
+ areAnyUnselectableChildrenUnexpanded,
+ areParentsSelectable,
+ option.isParent,
+ option.value,
+ selectableChildren.length,
+ ],
+ );
+
+ const isImplicitlySelected = useMemo(
+ () => !option.isParent && !!selectedOptions.find((o) => o.value === option.parentValue),
+ [selectedOptions, option.isParent, option.parentValue],
+ );
+
+ const isParentMissingChildren = useMemo(() => !!option.isParent && !children.length, [children, option.isParent]);
+
+ const isPartialSelected = useMemo(
+ () =>
+ (!areAllChildrenSelected && areAnyChildrenSelected) ||
+ (isSelected && isParentMissingChildren) ||
+ (isSelected && areAnyUnselectableChildrenUnexpanded) ||
+ (areAnyUnselectableChildrenUnexpanded && areAnyChildrenSelected) ||
+ (isSelected && !!children.length && !areAnyChildrenSelected),
+ [
+ isSelected,
+ children,
+ areAllChildrenSelected,
+ areAnyChildrenSelected,
+ areAnyUnselectableChildrenUnexpanded,
+ isParentMissingChildren,
+ ],
+ );
+
+ const selectOption = () => {
+ if (areParentsSelectable && option.isParent) {
+ const existingSelectedOptions = new Set(selectedOptions.map((opt) => opt.value));
+ const existingChildSelectedOptions =
+ selectedOptions.filter((opt) => opt.parentValue === option.value) || [];
+ if (existingSelectedOptions.has(option.value)) {
+ removeOptions([option]);
+ } else {
+ // filter out the childrens of parent selection as we are allowing implicitly selection
+ const filteredOptions = selectedOptions.filter(
+ (selectedOption) => !existingChildSelectedOptions.find((o) => o.value === selectedOption.value),
+ );
+ const newSelectedOptions = [...filteredOptions, option];
+
+ setSelectedOptions(newSelectedOptions);
+ }
+ } else if (isPartialSelected || (!isSelected && !areAnyChildrenSelected)) {
+ const optionsToAdd = option.isParent && !areParentsSelectable ? selectableChildren : [option];
+ addOptions(optionsToAdd);
+ } else if (areAllChildrenSelected) {
+ removeOptions([option, ...selectableChildren]);
+ } else {
+ handleOptionChange(option);
+ }
+ };
+
+ // one loader variable for fetching data for expanded parents and their respective child nodes
+ useEffect(() => {
+ // once loading has been done just remove all the parent node urn
+ if (!isLoadingParentChildList) {
+ setLoadingParentUrns([]);
+ }
+ }, [isLoadingParentChildList]);
+
+ return (
+
+
+ {
+ e.preventDefault();
+ if (isImplicitlySelected) {
+ return;
+ }
+ if (isParentMissingChildren) {
+ setLoadingParentUrns((previousIds) => [...previousIds, option.value]);
+ loadData?.(option);
+ }
+ if (option.isParent) {
+ setIsOpen(!isOpen);
+ } else {
+ selectOption();
+ }
+ }}
+ isSelected={!isMultiSelect && isSelected}
+ // added hack to show cursor in wait untill we get the inline spinner
+ style={{ width: '100%', cursor: loadingParentUrns.includes(option.value) ? 'wait' : 'pointer' }}
+ >
+ {option.isParent && {option.label} }
+ {!option.isParent && <>{option.label}>}
+ {option.isParent && (
+ {
+ e.stopPropagation();
+ e.preventDefault();
+ setIsOpen(!isOpen);
+ if (!isOpen && isParentMissingChildren) {
+ setLoadingParentUrns((previousIds) => [...previousIds, option.value]);
+ loadData?.(option);
+ }
+ }}
+ icon="ChevronLeft"
+ rotate={isOpen ? '90' : '270'}
+ size="xl"
+ color="gray"
+ style={{ cursor: 'pointer', marginLeft: '4px' }}
+ />
+ )}
+ {
+ e.preventDefault();
+ if (isImplicitlySelected) {
+ return;
+ }
+ e.stopPropagation();
+ if (isParentMissingChildren) {
+ loadData?.(option);
+ if (!areParentsSelectable) {
+ setAutoSelectChildren(true);
+ }
+ }
+ selectOption();
+ }}
+ disabled={isImplicitlySelected}
+ />
+
+
+ {isOpen && (
+
+ {directChildren.map((child) => (
+
+ ))}
+
+ )}
+
+ );
+};
diff --git a/datahub-web-react/src/alchemy-components/components/Select/Nested/NestedSelect.tsx b/datahub-web-react/src/alchemy-components/components/Select/Nested/NestedSelect.tsx
new file mode 100644
index 00000000000000..744c7bfcfec0d2
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/components/Select/Nested/NestedSelect.tsx
@@ -0,0 +1,312 @@
+import React, { useCallback, useEffect, useRef, useState } from 'react';
+import styled from 'styled-components';
+
+import { Icon, Pill } from '@components';
+
+import {
+ ActionButtonsContainer,
+ Container,
+ Dropdown,
+ OptionList,
+ Placeholder,
+ SearchIcon,
+ SearchInput,
+ SearchInputContainer,
+ SelectBase,
+ SelectLabel,
+ StyledClearButton,
+} from '../components';
+
+import { SelectSizeOptions } from '../types';
+import { NestedOption } from './NestedOption';
+import { SelectOption } from './types';
+
+const NO_PARENT_VALUE = 'no_parent_value';
+
+const LabelDisplayWrapper = styled.div`
+ display: flex;
+ flex-wrap: wrap;
+ gap: 4px;
+ max-height: 125px;
+ min-height: 16px;
+`;
+
+interface SelectLabelDisplayProps {
+ selectedOptions: SelectOption[];
+ placeholder: string;
+ handleOptionChange: (node: SelectOption) => void;
+}
+
+const SelectLabelDisplay = ({ selectedOptions, placeholder, handleOptionChange }: SelectLabelDisplayProps) => {
+ return (
+
+ {!!selectedOptions.length &&
+ selectedOptions.map((o) => (
+ {
+ e.stopPropagation();
+ handleOptionChange(o);
+ }}
+ />
+ ))}
+ {!selectedOptions.length && {placeholder} }
+
+ );
+};
+
+export interface ActionButtonsProps {
+ fontSize?: SelectSizeOptions;
+ selectedOptions: SelectOption[];
+ isOpen: boolean;
+ isDisabled: boolean;
+ isReadOnly: boolean;
+ handleClearSelection: () => void;
+}
+
+const SelectActionButtons = ({
+ selectedOptions,
+ isOpen,
+ isDisabled,
+ isReadOnly,
+ handleClearSelection,
+ fontSize = 'md',
+}: ActionButtonsProps) => {
+ return (
+
+ {!!selectedOptions.length && !isDisabled && !isReadOnly && (
+
+ )}
+
+
+ );
+};
+
+export interface SelectProps {
+ options: SelectOption[];
+ label: string;
+ value?: string;
+ initialValues?: SelectOption[];
+ onCancel?: () => void;
+ onUpdate?: (selectedValues: SelectOption[]) => void;
+ size?: SelectSizeOptions;
+ showSearch?: boolean;
+ isDisabled?: boolean;
+ isReadOnly?: boolean;
+ isRequired?: boolean;
+ isMultiSelect?: boolean;
+ areParentsSelectable?: boolean;
+ loadData?: (node: SelectOption) => void;
+ onSearch?: (query: string) => void;
+ width?: number | 'full';
+ height?: number;
+ placeholder?: string;
+ searchPlaceholder?: string;
+ isLoadingParentChildList?: boolean;
+}
+
+export const selectDefaults: SelectProps = {
+ options: [],
+ label: '',
+ size: 'md',
+ showSearch: false,
+ isDisabled: false,
+ isReadOnly: false,
+ isRequired: false,
+ isMultiSelect: false,
+ width: 255,
+ height: 425,
+};
+
+export const NestedSelect = ({
+ options = selectDefaults.options,
+ label = selectDefaults.label,
+ initialValues = [],
+ onUpdate,
+ loadData,
+ onSearch,
+ showSearch = selectDefaults.showSearch,
+ isDisabled = selectDefaults.isDisabled,
+ isReadOnly = selectDefaults.isReadOnly,
+ isRequired = selectDefaults.isRequired,
+ isMultiSelect = selectDefaults.isMultiSelect,
+ size = selectDefaults.size,
+ areParentsSelectable = true,
+ placeholder,
+ searchPlaceholder,
+ height = selectDefaults.height,
+ isLoadingParentChildList = false,
+ ...props
+}: SelectProps) => {
+ const [searchQuery, setSearchQuery] = useState('');
+ const [isOpen, setIsOpen] = useState(false);
+ const [selectedOptions, setSelectedOptions] = useState(initialValues);
+ const selectRef = useRef(null);
+
+ // TODO: handle searching inside of a nested component on the FE only
+
+ const handleDocumentClick = useCallback((e: MouseEvent) => {
+ if (selectRef.current && !selectRef.current.contains(e.target as Node)) {
+ setIsOpen(false);
+ }
+ }, []);
+
+ useEffect(() => {
+ document.addEventListener('click', handleDocumentClick);
+ return () => {
+ document.removeEventListener('click', handleDocumentClick);
+ };
+ }, [handleDocumentClick]);
+
+ const handleSelectClick = useCallback(() => {
+ if (!isDisabled && !isReadOnly) {
+ setIsOpen((prev) => !prev);
+ }
+ }, [isDisabled, isReadOnly]);
+
+ const handleSearch = useCallback(
+ (query: string) => {
+ setSearchQuery(query);
+ onSearch?.(query);
+ },
+ [onSearch],
+ );
+
+ // Instead of calling the update function individually whenever selectedOptions changes,
+ // we use the useEffect hook to trigger the onUpdate function automatically when selectedOptions is updated.
+ useEffect(() => {
+ if (onUpdate) {
+ onUpdate(selectedOptions);
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [selectedOptions]);
+
+ const handleOptionChange = useCallback(
+ (option: SelectOption) => {
+ let newSelectedOptions: SelectOption[];
+ if (selectedOptions.find((o) => o.value === option.value)) {
+ newSelectedOptions = selectedOptions.filter((o) => o.value !== option.value);
+ } else {
+ newSelectedOptions = [...selectedOptions, option];
+ }
+ setSelectedOptions(newSelectedOptions);
+ if (!isMultiSelect) {
+ setIsOpen(false);
+ }
+ },
+ [selectedOptions, isMultiSelect],
+ );
+
+ const addOptions = useCallback(
+ (optionsToAdd: SelectOption[]) => {
+ const existingValues = new Set(selectedOptions.map((option) => option.value));
+ const filteredOptionsToAdd = optionsToAdd.filter((option) => !existingValues.has(option.value));
+ if (filteredOptionsToAdd.length) {
+ const newSelectedOptions = [...selectedOptions, ...filteredOptionsToAdd];
+ setSelectedOptions(newSelectedOptions);
+ }
+ },
+ [selectedOptions],
+ );
+
+ const removeOptions = useCallback(
+ (optionsToRemove: SelectOption[]) => {
+ const newValues = selectedOptions.filter(
+ (selectedOption) => !optionsToRemove.find((o) => o.value === selectedOption.value),
+ );
+ setSelectedOptions(newValues);
+ },
+ [selectedOptions],
+ );
+
+ const handleClearSelection = useCallback(() => {
+ setSelectedOptions([]);
+ setIsOpen(false);
+ if (onUpdate) {
+ onUpdate([]);
+ }
+ }, [onUpdate]);
+
+ // generate map for options to quickly fetch children
+ const parentValueToOptions: { [parentValue: string]: SelectOption[] } = {};
+ options.forEach((o) => {
+ const parentValue = o.parentValue || NO_PARENT_VALUE;
+ parentValueToOptions[parentValue] = parentValueToOptions[parentValue]
+ ? [...parentValueToOptions[parentValue], o]
+ : [o];
+ });
+
+ const rootOptions = parentValueToOptions[NO_PARENT_VALUE] || [];
+
+ return (
+
+ {label && {label} }
+
+
+
+
+ {isOpen && (
+
+ {showSearch && (
+
+ handleSearch(e.target.value)}
+ style={{ fontSize: size || 'md', width: '100%' }}
+ />
+
+
+ )}
+
+ {rootOptions.map((option) => (
+
+ ))}
+
+
+ )}
+
+ );
+};
diff --git a/datahub-web-react/src/alchemy-components/components/Select/Nested/types.ts b/datahub-web-react/src/alchemy-components/components/Select/Nested/types.ts
new file mode 100644
index 00000000000000..62d4541fce0d3d
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/components/Select/Nested/types.ts
@@ -0,0 +1,9 @@
+import { Entity } from '@src/types.generated';
+
+export interface SelectOption {
+ value: string;
+ label: string;
+ parentValue?: string;
+ isParent?: boolean;
+ entity?: Entity;
+}
diff --git a/datahub-web-react/src/alchemy-components/components/Select/Select.stories.tsx b/datahub-web-react/src/alchemy-components/components/Select/Select.stories.tsx
new file mode 100644
index 00000000000000..0ec20b15e771ab
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/components/Select/Select.stories.tsx
@@ -0,0 +1,431 @@
+import { GridList } from '@components/.docs/mdx-components';
+import { BADGE } from '@geometricpanda/storybook-addon-badges';
+import type { Meta, StoryObj } from '@storybook/react';
+import React from 'react';
+import { Select, selectDefaults } from './Select';
+import { SimpleSelect } from './SimpleSelect';
+import { SelectSizeOptions } from './types';
+
+// Auto Docs
+const meta: Meta = {
+ title: 'Forms / Select',
+ component: Select,
+
+ // Display Properties
+ parameters: {
+ layout: 'centered',
+ badges: [BADGE.STABLE, 'readyForDesignReview'],
+ docs: {
+ subtitle: 'This component allows users to select one or multiple input options from a dropdown list.',
+ },
+ },
+
+ // Component-level argTypes
+ argTypes: {
+ options: {
+ description: 'Array of options for the Select component.',
+ control: {
+ type: 'object',
+ },
+ table: {
+ defaultValue: { summary: JSON.stringify(selectDefaults.options) },
+ },
+ },
+ label: {
+ description: 'Label for the Select component.',
+ control: {
+ type: 'text',
+ },
+ table: {
+ defaultValue: { summary: selectDefaults.label },
+ },
+ },
+ values: {
+ description: 'Selected values for the Select component.',
+ control: {
+ type: 'object',
+ },
+ table: {
+ defaultValue: { summary: selectDefaults.values?.toString() },
+ },
+ },
+ showSearch: {
+ description: 'Whether to show the search input.',
+ control: {
+ type: 'boolean',
+ },
+ table: {
+ defaultValue: { summary: selectDefaults.showSearch?.toString() },
+ },
+ },
+ isDisabled: {
+ description: 'Whether the Select component is disabled.',
+ control: {
+ type: 'boolean',
+ },
+ table: {
+ defaultValue: { summary: selectDefaults.isDisabled?.toString() },
+ },
+ },
+ isReadOnly: {
+ description: 'Whether the Select component is read-only.',
+ control: {
+ type: 'boolean',
+ },
+ table: {
+ defaultValue: { summary: selectDefaults.isReadOnly?.toString() },
+ },
+ },
+ isRequired: {
+ description: 'Whether the Select component is required.',
+ control: {
+ type: 'boolean',
+ },
+ table: {
+ defaultValue: { summary: selectDefaults.isRequired?.toString() },
+ },
+ },
+ size: {
+ description: 'Size of the Select component.',
+ control: {
+ type: 'select',
+ options: ['sm', 'md', 'lg'],
+ },
+ table: {
+ defaultValue: { summary: selectDefaults.size },
+ },
+ },
+ width: {
+ description: 'Width of the Select component.',
+ control: {
+ type: 'number',
+ },
+ table: {
+ defaultValue: { summary: `${selectDefaults.width}` },
+ },
+ },
+ isMultiSelect: {
+ description: 'Whether the Select component allows multiple values to be selected.',
+ control: {
+ type: 'boolean',
+ },
+ table: {
+ defaultValue: { summary: selectDefaults.isMultiSelect?.toString() },
+ },
+ },
+ placeholder: {
+ description: 'Placeholder for the Select component.',
+ control: {
+ type: 'text',
+ },
+ table: {
+ defaultValue: { summary: selectDefaults.placeholder },
+ },
+ },
+ disabledValues: {
+ description: 'Disabled values for the multi-select component.',
+ control: {
+ type: 'object',
+ },
+ table: {
+ defaultValue: { summary: selectDefaults.disabledValues?.toString() },
+ },
+ },
+ showSelectAll: {
+ description: 'Whether the multi select component shows Select All button.',
+ control: {
+ type: 'boolean',
+ },
+ table: {
+ defaultValue: { summary: selectDefaults.showSelectAll?.toString() },
+ },
+ },
+ selectAllLabel: {
+ description: 'Label for Select All button.',
+ control: {
+ type: 'text',
+ },
+ table: {
+ defaultValue: { summary: selectDefaults.selectAllLabel },
+ },
+ },
+ showDescriptions: {
+ description: 'Whether to show descriptions with the select options.',
+ control: {
+ type: 'boolean',
+ },
+ table: {
+ defaultValue: { summary: selectDefaults.showDescriptions?.toString() },
+ },
+ },
+ },
+
+ // Define defaults
+ args: {
+ options: [
+ { label: 'Option 1', value: '1' },
+ { label: 'Option 2', value: '2' },
+ { label: 'Option 3', value: '3' },
+ ],
+ label: 'Select Label',
+ values: undefined,
+ showSearch: selectDefaults.showSearch,
+ isDisabled: selectDefaults.isDisabled,
+ isReadOnly: selectDefaults.isReadOnly,
+ isRequired: selectDefaults.isRequired,
+ onCancel: () => console.log('Cancel clicked'),
+ onUpdate: (selectedValues: string[]) => console.log('Update clicked', selectedValues),
+ size: 'md', // Default size
+ width: 255,
+ isMultiSelect: selectDefaults.isMultiSelect,
+ placeholder: selectDefaults.placeholder,
+ disabledValues: undefined,
+ showSelectAll: false,
+ selectAllLabel: 'Select All',
+ showDescriptions: false,
+ },
+} satisfies Meta;
+
+export default meta;
+
+// Stories
+
+type Story = StoryObj;
+
+const sizeOptions: SelectSizeOptions[] = ['sm', 'md', 'lg'];
+
+export const simpleSelectSandbox: Story = {
+ tags: ['dev'],
+
+ render: (props) => (
+
+ ),
+};
+
+export const simpleSelectStates = () => (
+
+ <>
+
+
+
+ >
+
+);
+
+export const simpleSelectWithSearch = () => (
+
+);
+
+export const simpleSelectWithMultiSelect = () => (
+
+);
+
+export const simpleSelectWithDisabledValues = () => (
+
+);
+
+export const simpleSelectWithSelectAll = () => (
+
+);
+
+export const simpleSelectWithDescriptions = () => (
+
+);
+
+export const simpleSelectSizes = () => (
+
+ {sizeOptions.map((size, index) => (
+
+ ))}
+
+);
+
+// Basic story is what is displayed 1st in storybook & is used as the code sandbox
+// Pass props to this so that it can be customized via the UI props panel
+export const BasicSelectSandbox: Story = {
+ tags: ['dev'],
+
+ render: (props) => (
+
+ ),
+};
+
+export const states = () => (
+
+ <>
+
+
+
+ >
+
+);
+
+export const withSearch = () => (
+
+);
+
+export const withMultiSelect = () => (
+
+);
+
+export const sizes = () => (
+
+ {sizeOptions.map((size, index) => (
+ alert('Cancel clicked')}
+ onUpdate={(selectedValues) => alert(`Update clicked with values: ${selectedValues}`)}
+ size={size}
+ width={255 + 50 * index}
+ />
+ ))}
+
+);
+
+export const footerActions = () => (
+
+ alert('Cancel clicked')}
+ onUpdate={(selectedValues) => alert(`Update clicked with values: ${selectedValues}`)}
+ size="md"
+ />
+
+);
diff --git a/datahub-web-react/src/alchemy-components/components/Select/Select.tsx b/datahub-web-react/src/alchemy-components/components/Select/Select.tsx
new file mode 100644
index 00000000000000..da28f090565431
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/components/Select/Select.tsx
@@ -0,0 +1,65 @@
+import React from 'react';
+import { BasicSelect } from './BasicSelect';
+import { SelectProps } from './types';
+
+export const selectDefaults: SelectProps = {
+ options: [],
+ label: '',
+ showSearch: false,
+ values: undefined,
+ size: 'md',
+ isDisabled: false,
+ isReadOnly: false,
+ isRequired: false,
+ width: 255,
+ isMultiSelect: false,
+ placeholder: 'Select an option',
+ disabledValues: undefined,
+ showSelectAll: false,
+ selectAllLabel: 'Select All',
+ showDescriptions: false,
+};
+
+export const Select = ({
+ options = selectDefaults.options,
+ label = selectDefaults.label,
+ values = [],
+ onCancel,
+ onUpdate,
+ showSearch = selectDefaults.showSearch,
+ isDisabled = selectDefaults.isDisabled,
+ isReadOnly = selectDefaults.isReadOnly,
+ isRequired = selectDefaults.isRequired,
+ size = selectDefaults.size,
+ width = selectDefaults.width,
+ isMultiSelect = selectDefaults.isMultiSelect,
+ placeholder = selectDefaults.placeholder,
+ disabledValues = selectDefaults.disabledValues,
+ showSelectAll = selectDefaults.showSelectAll,
+ selectAllLabel = selectDefaults.selectAllLabel,
+ showDescriptions = selectDefaults.showDescriptions,
+ ...props
+}: SelectProps) => {
+ return (
+
+ );
+};
diff --git a/datahub-web-react/src/alchemy-components/components/Select/SimpleSelect.tsx b/datahub-web-react/src/alchemy-components/components/Select/SimpleSelect.tsx
new file mode 100644
index 00000000000000..be1184cee9e9f5
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/components/Select/SimpleSelect.tsx
@@ -0,0 +1,299 @@
+import { Icon, Pill, Text } from '@components';
+import { isEqual } from 'lodash';
+import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
+import {
+ ActionButtonsContainer,
+ Container,
+ DescriptionContainer,
+ Dropdown,
+ LabelContainer,
+ LabelsWrapper,
+ OptionContainer,
+ OptionLabel,
+ OptionList,
+ Placeholder,
+ SearchIcon,
+ SearchInput,
+ SearchInputContainer,
+ SelectAllOption,
+ SelectBase,
+ SelectLabel,
+ SelectValue,
+ StyledCheckbox,
+ StyledClearButton,
+} from './components';
+import { ActionButtonsProps, SelectLabelDisplayProps, SelectOption, SelectProps } from './types';
+
+const SelectLabelDisplay = ({
+ selectedValues,
+ options,
+ placeholder,
+ isMultiSelect,
+ removeOption,
+ disabledValues,
+ showDescriptions,
+}: SelectLabelDisplayProps) => {
+ const selectedOptions = options.filter((opt) => selectedValues.includes(opt.value));
+ return (
+
+ {!!selectedOptions.length &&
+ isMultiSelect &&
+ selectedOptions.map((o) => {
+ const isDisabled = disabledValues?.includes(o.value);
+ return (
+ {
+ e.stopPropagation();
+ removeOption?.(o);
+ }}
+ clickable={!isDisabled}
+ />
+ );
+ })}
+ {!selectedValues.length && {placeholder} }
+ {!isMultiSelect && (
+ <>
+ {selectedOptions[0]?.label}
+ {showDescriptions && !!selectedValues.length && (
+ {selectedOptions[0]?.description}
+ )}
+ >
+ )}
+
+ );
+};
+
+const SelectActionButtons = ({
+ selectedValues,
+ isOpen,
+ isDisabled,
+ isReadOnly,
+ showClear,
+ handleClearSelection,
+ fontSize = 'md',
+}: ActionButtonsProps) => {
+ return (
+
+ {showClear && selectedValues.length > 0 && !isDisabled && !isReadOnly && (
+
+ )}
+
+
+ );
+};
+
+export const selectDefaults: SelectProps = {
+ options: [],
+ label: '',
+ size: 'md',
+ showSearch: false,
+ isDisabled: false,
+ isReadOnly: false,
+ isRequired: false,
+ showClear: true,
+ width: 255,
+ isMultiSelect: false,
+ placeholder: 'Select an option ',
+ showSelectAll: false,
+ selectAllLabel: 'Select All',
+ showDescriptions: false,
+};
+
+export const SimpleSelect = ({
+ options = selectDefaults.options,
+ label = selectDefaults.label,
+ values = [],
+ onUpdate,
+ showSearch = selectDefaults.showSearch,
+ isDisabled = selectDefaults.isDisabled,
+ isReadOnly = selectDefaults.isReadOnly,
+ isRequired = selectDefaults.isRequired,
+ showClear = selectDefaults.showClear,
+ size = selectDefaults.size,
+ isMultiSelect = selectDefaults.isMultiSelect,
+ placeholder = selectDefaults.placeholder,
+ disabledValues = [],
+ showSelectAll = selectDefaults.showSelectAll,
+ selectAllLabel = selectDefaults.selectAllLabel,
+ optionListTestId,
+ showDescriptions = selectDefaults.showDescriptions,
+ ...props
+}: SelectProps) => {
+ const [searchQuery, setSearchQuery] = useState('');
+ const [isOpen, setIsOpen] = useState(false);
+ const [selectedValues, setSelectedValues] = useState(values);
+ const selectRef = useRef(null);
+ const [areAllSelected, setAreAllSelected] = useState(false);
+
+ useEffect(() => {
+ if (values?.length > 0 && !isEqual(selectedValues, values)) {
+ setSelectedValues(values);
+ }
+ }, [values, selectedValues]);
+
+ useEffect(() => {
+ setAreAllSelected(selectedValues.length === options.length);
+ }, [options, selectedValues]);
+
+ const filteredOptions = useMemo(
+ () => options.filter((option) => option.label.toLowerCase().includes(searchQuery.toLowerCase())),
+ [options, searchQuery],
+ );
+
+ const handleDocumentClick = useCallback((e: MouseEvent) => {
+ if (selectRef.current && !selectRef.current.contains(e.target as Node)) {
+ setIsOpen(false);
+ }
+ }, []);
+
+ useEffect(() => {
+ document.addEventListener('click', handleDocumentClick);
+ return () => {
+ document.removeEventListener('click', handleDocumentClick);
+ };
+ }, [handleDocumentClick]);
+
+ const handleSelectClick = useCallback(() => {
+ if (!isDisabled && !isReadOnly) {
+ setIsOpen((prev) => !prev);
+ }
+ }, [isDisabled, isReadOnly]);
+
+ const handleOptionChange = useCallback(
+ (option: SelectOption) => {
+ const updatedValues = selectedValues.includes(option.value)
+ ? selectedValues.filter((val) => val !== option.value)
+ : [...selectedValues, option.value];
+
+ setSelectedValues(isMultiSelect ? updatedValues : [option.value]);
+ if (onUpdate) {
+ onUpdate(isMultiSelect ? updatedValues : [option.value]);
+ }
+ if (!isMultiSelect) setIsOpen(false);
+ },
+ [onUpdate, isMultiSelect, selectedValues],
+ );
+
+ const handleClearSelection = useCallback(() => {
+ setSelectedValues([]);
+ setAreAllSelected(false);
+ setIsOpen(false);
+ if (onUpdate) {
+ onUpdate([]);
+ }
+ }, [onUpdate]);
+
+ const handleSelectAll = () => {
+ if (areAllSelected) {
+ setSelectedValues([]);
+ onUpdate?.([]);
+ } else {
+ const allValues = options.map((option) => option.value);
+ setSelectedValues(allValues);
+ onUpdate?.(allValues);
+ }
+ setAreAllSelected(!areAllSelected);
+ };
+
+ return (
+
+ {label && {label} }
+
+
+
+
+ {isOpen && (
+
+ {showSearch && (
+
+ setSearchQuery(e.target.value)}
+ style={{ fontSize: size || 'md' }}
+ />
+
+
+ )}
+
+ {showSelectAll && isMultiSelect && (
+ !(disabledValues.length === options.length) && handleSelectAll()}
+ isDisabled={disabledValues.length === options.length}
+ >
+
+ {selectAllLabel}
+
+
+
+ )}
+ {filteredOptions.map((option) => (
+ !isMultiSelect && handleOptionChange(option)}
+ isSelected={selectedValues.includes(option.value)}
+ isMultiSelect={isMultiSelect}
+ isDisabled={disabledValues?.includes(option.value)}
+ >
+ {isMultiSelect ? (
+
+ {option.label}
+ handleOptionChange(option)}
+ checked={selectedValues.includes(option.value)}
+ disabled={disabledValues?.includes(option.value)}
+ />
+
+ ) : (
+
+
+ {option.label}
+
+ {!!option.description && (
+
+ {option.description}
+
+ )}
+
+ )}
+
+ ))}
+
+
+ )}
+
+ );
+};
diff --git a/datahub-web-react/src/alchemy-components/components/Select/components.ts b/datahub-web-react/src/alchemy-components/components/Select/components.ts
new file mode 100644
index 00000000000000..a360238fef4923
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/components/Select/components.ts
@@ -0,0 +1,235 @@
+import { Button, Icon } from '@components';
+import { borders, colors, radius, shadows, spacing, transition, typography } from '@components/theme';
+import { Checkbox } from 'antd';
+import styled from 'styled-components';
+import { formLabelTextStyles, inputPlaceholderTextStyles, inputValueTextStyles } from '../commonStyles';
+import { SelectSizeOptions, SelectStyleProps } from './types';
+import { getOptionLabelStyle, getSelectFontStyles, getSelectStyle } from './utils';
+
+const sharedTransition = `${transition.property.colors} ${transition.easing['ease-in-out']} ${transition.duration.normal}`;
+
+/**
+ * Base Select component styling
+ */
+export const SelectBase = styled.div(({ isDisabled, isReadOnly, fontSize, isOpen }) => ({
+ ...getSelectStyle({ isDisabled, isReadOnly, fontSize, isOpen }),
+ display: 'flex',
+ flexDirection: 'row' as const,
+ gap: spacing.xsm,
+ transition: sharedTransition,
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ overflow: 'auto',
+ backgroundColor: isDisabled ? colors.gray[100] : 'white',
+}));
+
+/**
+ * Styled components specific to the Basic version of the Select component
+ */
+
+// Container for the Basic Select component
+interface ContainerProps {
+ size: SelectSizeOptions;
+ width?: number | 'full';
+}
+
+export const Container = styled.div(({ size, width }) => ({
+ position: 'relative',
+ display: 'flex',
+ flexDirection: 'column',
+ width: width === 'full' ? '100%' : `${width}px`,
+ gap: '4px',
+ transition: sharedTransition,
+ minWidth: '175px',
+ ...getSelectFontStyles(size),
+ ...inputValueTextStyles(size),
+}));
+
+export const Dropdown = styled.div({
+ position: 'absolute',
+ top: '100%',
+ left: 0,
+ right: 0,
+ borderRadius: radius.md,
+ background: colors.white,
+ zIndex: 1,
+ transition: sharedTransition,
+ boxShadow: shadows.dropdown,
+ padding: spacing.xsm,
+ display: 'flex',
+ flexDirection: 'column',
+ gap: '8px',
+ marginTop: '4px',
+ maxHeight: '360px',
+ overflow: 'auto',
+});
+
+export const SearchInputContainer = styled.div({
+ position: 'relative',
+ width: '100%',
+ display: 'flex',
+ justifyContent: 'center',
+});
+
+export const SearchInput = styled.input({
+ width: '100%',
+ borderRadius: radius.md,
+ border: `1px solid ${colors.gray[200]}`,
+ color: colors.gray[500],
+ fontFamily: typography.fonts.body,
+ fontSize: typography.fontSizes.sm,
+ padding: spacing.xsm,
+ paddingRight: spacing.xlg,
+
+ '&:focus': {
+ borderColor: colors.violet[200],
+ outline: `${borders['1px']} ${colors.violet[200]}`,
+ },
+});
+
+export const SearchIcon = styled(Icon)({
+ position: 'absolute',
+ right: spacing.sm,
+ top: '50%',
+ transform: 'translateY(-50%)',
+ pointerEvents: 'none',
+});
+
+// Styled components for SelectValue (Selected value display)
+export const SelectValue = styled.span({
+ ...inputValueTextStyles(),
+});
+
+export const Placeholder = styled.span({
+ ...inputPlaceholderTextStyles,
+});
+
+export const ActionButtonsContainer = styled.div({
+ display: 'flex',
+ gap: '6px',
+ flexDirection: 'row',
+ alignItems: 'center',
+});
+
+/**
+ * Components that can be reused to create new Select variants
+ */
+
+export const FooterBase = styled.div({
+ display: 'flex',
+ justifyContent: 'flex-end',
+ gap: spacing.sm,
+ paddingTop: spacing.sm,
+ borderTop: `1px solid ${colors.gray[100]}`,
+});
+
+export const OptionList = styled.div({
+ display: 'flex',
+ flexDirection: 'column' as const,
+});
+
+export const LabelContainer = styled.div({
+ display: 'flex',
+ justifyContent: 'space-between',
+ width: '100%',
+});
+
+export const OptionContainer = styled.div({
+ display: 'flex',
+ flexDirection: 'column',
+});
+
+export const DescriptionContainer = styled.span({
+ whiteSpace: 'nowrap',
+ overflow: 'hidden',
+ textOverflow: 'ellipsis',
+ width: '100%',
+ color: colors.gray[500],
+ lineHeight: 'normal',
+ fontSize: typography.fontSizes.sm,
+ marginTop: spacing.xxsm,
+});
+
+export const LabelsWrapper = styled.div({
+ display: 'flex',
+ flexWrap: 'wrap',
+ gap: spacing.xxsm,
+ maxHeight: '150px',
+ maxWidth: 'calc(100% - 54px)',
+});
+
+export const OptionLabel = styled.label<{ isSelected: boolean; isMultiSelect?: boolean; isDisabled?: boolean }>(
+ ({ isSelected, isMultiSelect, isDisabled }) => ({
+ ...getOptionLabelStyle(isSelected, isMultiSelect, isDisabled),
+ }),
+);
+
+export const SelectAllOption = styled.div<{ isSelected: boolean; isDisabled?: boolean }>(
+ ({ isSelected, isDisabled }) => ({
+ cursor: isDisabled ? 'not-allowed' : 'pointer',
+ padding: spacing.xsm,
+ color: isSelected ? colors.violet[700] : colors.gray[500],
+ fontWeight: typography.fontWeights.semiBold,
+ fontSize: typography.fontSizes.md,
+ display: 'flex',
+ alignItems: 'center',
+ }),
+);
+
+export const SelectLabel = styled.label({
+ ...formLabelTextStyles,
+ marginBottom: spacing.xxsm,
+ textAlign: 'left',
+});
+
+export const StyledCancelButton = styled(Button)({
+ backgroundColor: colors.violet[100],
+ color: colors.violet[500],
+ borderColor: colors.violet[100],
+
+ '&:hover': {
+ backgroundColor: colors.violet[200],
+ borderColor: colors.violet[200],
+ },
+});
+
+export const StyledClearButton = styled(Button)({
+ backgroundColor: colors.gray[200],
+ border: `1px solid ${colors.gray[200]}`,
+ color: colors.black,
+ padding: '1px',
+
+ '&:hover': {
+ backgroundColor: colors.violet[100],
+ color: colors.violet[700],
+ borderColor: colors.violet[100],
+ boxShadow: shadows.none,
+ },
+
+ '&:focus': {
+ backgroundColor: colors.violet[100],
+ color: colors.violet[700],
+ boxShadow: `0 0 0 2px ${colors.white}, 0 0 0 4px ${colors.violet[50]}`,
+ },
+});
+
+export const ClearIcon = styled.span({
+ cursor: 'pointer',
+ marginLeft: '8px',
+});
+
+export const ArrowIcon = styled.span<{ isOpen: boolean }>(({ isOpen }) => ({
+ marginLeft: 'auto',
+ border: 'solid black',
+ borderWidth: '0 1px 1px 0',
+ display: 'inline-block',
+ padding: '3px',
+ transform: isOpen ? 'rotate(-135deg)' : 'rotate(45deg)',
+}));
+
+export const StyledCheckbox = styled(Checkbox)({
+ '.ant-checkbox-checked:not(.ant-checkbox-disabled) .ant-checkbox-inner': {
+ backgroundColor: colors.violet[500],
+ borderColor: `${colors.violet[500]} !important`,
+ },
+});
diff --git a/datahub-web-react/src/alchemy-components/components/Select/index.ts b/datahub-web-react/src/alchemy-components/components/Select/index.ts
new file mode 100644
index 00000000000000..eb469d0edc0046
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/components/Select/index.ts
@@ -0,0 +1,3 @@
+export { Select, selectDefaults } from './Select';
+export { SimpleSelect } from './SimpleSelect';
+export type { SelectProps, SelectOption } from './types';
diff --git a/datahub-web-react/src/alchemy-components/components/Select/types.ts b/datahub-web-react/src/alchemy-components/components/Select/types.ts
new file mode 100644
index 00000000000000..5ccde408b76999
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/components/Select/types.ts
@@ -0,0 +1,61 @@
+export type SelectSizeOptions = 'sm' | 'md' | 'lg';
+
+export interface SelectOption {
+ value: string;
+ label: string;
+ description?: string;
+}
+
+export interface SelectProps {
+ options: SelectOption[];
+ label?: string;
+ values?: string[];
+ onCancel?: () => void;
+ onUpdate?: (selectedValues: string[]) => void;
+ size?: SelectSizeOptions;
+ showSearch?: boolean;
+ isDisabled?: boolean;
+ isReadOnly?: boolean;
+ isRequired?: boolean;
+ showClear?: boolean;
+ width?: number | 'full';
+ isMultiSelect?: boolean;
+ placeholder?: string;
+ disabledValues?: string[];
+ showSelectAll?: boolean;
+ selectAllLabel?: string;
+ optionListTestId?: string;
+ showDescriptions?: boolean;
+}
+
+export interface SelectStyleProps {
+ fontSize?: SelectSizeOptions;
+ isDisabled?: boolean;
+ isReadOnly?: boolean;
+ isRequired?: boolean;
+ isOpen?: boolean;
+}
+
+export interface ActionButtonsProps {
+ fontSize?: SelectSizeOptions;
+ selectedValues: string[];
+ isOpen: boolean;
+ isDisabled: boolean;
+ isReadOnly: boolean;
+ showClear: boolean;
+ handleClearSelection: () => void;
+}
+
+export interface SelectLabelDisplayProps {
+ selectedValues: string[];
+ options: SelectOption[];
+ placeholder: string;
+ isMultiSelect?: boolean;
+ removeOption?: (option: SelectOption) => void;
+ disabledValues?: string[];
+ showDescriptions?: boolean;
+}
+
+export interface SearchInputProps extends React.InputHTMLAttributes {
+ fontSize: SelectSizeOptions;
+}
diff --git a/datahub-web-react/src/alchemy-components/components/Select/utils.ts b/datahub-web-react/src/alchemy-components/components/Select/utils.ts
new file mode 100644
index 00000000000000..d054dd8ff737ad
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/components/Select/utils.ts
@@ -0,0 +1,125 @@
+import { borders, colors, radius, spacing, typography } from '@components/theme';
+import { getFontSize } from '@components/theme/utils';
+
+import { SelectStyleProps } from './types';
+
+export const getOptionLabelStyle = (isSelected: boolean, isMultiSelect?: boolean, isDisabled?: boolean) => ({
+ cursor: isDisabled ? 'not-allowed' : 'pointer',
+ padding: spacing.xsm,
+ borderRadius: radius.md,
+ lineHeight: typography.lineHeights.normal,
+ backgroundColor: isSelected && !isMultiSelect ? colors.violet[100] : 'transparent',
+ color: isSelected ? colors.violet[700] : colors.gray[500],
+ fontWeight: typography.fontWeights.medium,
+ fontSize: typography.fontSizes.md,
+ display: 'flex',
+ alignItems: 'center',
+
+ '&:hover': {
+ backgroundColor: isSelected ? colors.violet[100] : colors.gray[100],
+ },
+});
+
+export const getFooterButtonSize = (size) => {
+ return size === 'sm' ? 'sm' : 'md';
+};
+
+export const getSelectFontStyles = (size) => {
+ const baseFontStyles = {
+ lineHeight: typography.lineHeights.none,
+ };
+
+ const sizeStyles = {
+ sm: {
+ ...baseFontStyles,
+ fontSize: getFontSize(size),
+ },
+ md: {
+ ...baseFontStyles,
+ fontSize: getFontSize(size),
+ },
+ lg: {
+ ...baseFontStyles,
+ fontSize: getFontSize(size),
+ },
+ };
+
+ return sizeStyles[size];
+};
+
+export const getSelectPadding = (size) => {
+ const paddingStyles = {
+ sm: {
+ padding: `${spacing.sm} ${spacing.xsm}`,
+ },
+ md: {
+ padding: `${spacing.sm} ${spacing.md}`,
+ },
+ lg: {
+ padding: `${spacing.md} ${spacing.sm}`,
+ },
+ };
+
+ return paddingStyles[size];
+};
+
+export const getSearchPadding = (size) => {
+ const paddingStyles = {
+ sm: {
+ padding: `${spacing.xxsm} ${spacing.xsm}`,
+ },
+ md: {
+ padding: `${spacing.xsm} ${spacing.xsm}`,
+ },
+ lg: {
+ padding: `${spacing.xsm} ${spacing.xsm}`,
+ },
+ };
+
+ return paddingStyles[size];
+};
+
+export const getSelectStyle = (props: SelectStyleProps) => {
+ const { isDisabled, isReadOnly, fontSize, isOpen } = props;
+
+ const baseStyle = {
+ borderRadius: radius.md,
+ border: `1px solid ${colors.gray[200]}`,
+ fontFamily: typography.fonts.body,
+ color: isDisabled ? colors.gray[300] : colors.black,
+ cursor: isDisabled || isReadOnly ? 'not-allowed' : 'pointer',
+ backgroundColor: isDisabled ? colors.gray[100] : 'initial',
+
+ '&::placeholder': {
+ color: colors.gray[400],
+ },
+
+ // Open Styles
+ ...(isOpen
+ ? {
+ borderColor: colors.violet[300],
+ boxShadow: `0px 0px 4px 0px rgba(83, 63, 209, 0.5)`,
+ outline: 'none',
+ }
+ : {}),
+
+ // Hover Styles
+ ...(isDisabled || isReadOnly || isOpen
+ ? {}
+ : {
+ '&:hover': {
+ borderColor: colors.violet[200],
+ outline: `${borders['1px']} ${colors.violet[200]}`,
+ },
+ }),
+ };
+
+ const fontStyles = getSelectFontStyles(fontSize);
+ const paddingStyles = getSelectPadding(fontSize);
+
+ return {
+ ...baseStyle,
+ ...fontStyles,
+ ...paddingStyles,
+ };
+};
diff --git a/datahub-web-react/src/alchemy-components/components/Switch/Switch.stories.tsx b/datahub-web-react/src/alchemy-components/components/Switch/Switch.stories.tsx
new file mode 100644
index 00000000000000..7bb4ee2397cc63
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/components/Switch/Switch.stories.tsx
@@ -0,0 +1,169 @@
+import React from 'react';
+
+import type { Meta, StoryObj } from '@storybook/react';
+import { BADGE } from '@geometricpanda/storybook-addon-badges';
+
+import { GridList } from '@components/.docs/mdx-components';
+
+import { Switch, switchDefaults } from './Switch';
+import { AVAILABLE_ICONS } from '../Icon';
+
+const meta = {
+ title: 'Forms / Switch',
+ component: Switch,
+
+ // Display Properties
+ parameters: {
+ layout: 'centered',
+ badges: [BADGE.STABLE, 'readyForDesignReview'],
+ docs: {
+ subtitle: 'A component that is used to get user input in the state of a toggle.',
+ },
+ },
+
+ // Component-level argTypes
+ argTypes: {
+ label: {
+ description: 'Label for the Switch.',
+ table: {
+ defaultValue: { summary: switchDefaults.label },
+ },
+ control: {
+ type: 'text',
+ },
+ },
+ labelPosition: {
+ description: 'The position of the label relative to the Switch.',
+ options: ['left', 'top'],
+ table: {
+ defaultValue: { summary: switchDefaults.labelPosition },
+ },
+ control: {
+ type: 'select',
+ },
+ },
+ icon: {
+ description: 'The icon to display in the Switch Slider.',
+ type: 'string',
+ options: AVAILABLE_ICONS,
+ table: {
+ defaultValue: { summary: 'undefined' },
+ },
+ control: {
+ type: 'select',
+ },
+ },
+ colorScheme: {
+ description: 'The color of the Switch.',
+ options: ['violet', 'green', 'red', 'blue', 'gray'],
+ table: {
+ defaultValue: { summary: switchDefaults.colorScheme },
+ },
+ control: {
+ type: 'select',
+ },
+ },
+ size: {
+ description: 'The size of the Button.',
+ options: ['sm', 'md', 'lg', 'xl'],
+ table: {
+ defaultValue: { summary: switchDefaults.size },
+ },
+ control: {
+ type: 'select',
+ },
+ },
+ isSquare: {
+ description: 'Whether the Switch is square in shape.',
+ table: {
+ defaultValue: { summary: switchDefaults?.isSquare?.toString() },
+ },
+ control: {
+ type: 'boolean',
+ },
+ },
+ isChecked: {
+ description: 'Whether the Switch is checked.',
+ table: {
+ defaultValue: { summary: switchDefaults?.isChecked?.toString() },
+ },
+ control: {
+ type: 'boolean',
+ },
+ },
+ isDisabled: {
+ description: 'Whether the Switch is in disabled state.',
+ table: {
+ defaultValue: { summary: switchDefaults?.isDisabled?.toString() },
+ },
+ control: {
+ type: 'boolean',
+ },
+ },
+ isRequired: {
+ description: 'Whether the Switch is a required field.',
+ table: {
+ defaultValue: { summary: switchDefaults?.isRequired?.toString() },
+ },
+ control: {
+ type: 'boolean',
+ },
+ },
+ },
+
+ // Define defaults
+ args: {
+ label: switchDefaults.label,
+ labelPosition: switchDefaults.labelPosition,
+ icon: switchDefaults.icon,
+ colorScheme: switchDefaults.colorScheme,
+ size: switchDefaults.size,
+ isSquare: switchDefaults.isSquare,
+ isChecked: switchDefaults.isChecked,
+ isDisabled: switchDefaults.isDisabled,
+ isRequired: switchDefaults.isRequired,
+ },
+} satisfies Meta;
+
+export default meta;
+
+type Story = StoryObj;
+
+export const sandbox: Story = {
+ tags: ['dev'],
+ render: (props) => ,
+};
+
+export const sizes = () => (
+
+
+
+
+
+
+);
+
+export const colors = () => (
+
+
+
+
+
+
+
+);
+
+export const states = () => (
+
+
+
+
+
+);
+
+export const types = () => (
+
+
+
+
+);
diff --git a/datahub-web-react/src/alchemy-components/components/Switch/Switch.tsx b/datahub-web-react/src/alchemy-components/components/Switch/Switch.tsx
new file mode 100644
index 00000000000000..18a01386562ee9
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/components/Switch/Switch.tsx
@@ -0,0 +1,74 @@
+import { Tooltip } from '@components';
+import React, { useEffect, useState } from 'react';
+import { IconContainer, Label, Required, Slider, StyledIcon, StyledInput, SwitchContainer } from './components';
+import { SwitchProps } from './types';
+
+export const switchDefaults: SwitchProps = {
+ label: 'Label',
+ labelPosition: 'left',
+ colorScheme: 'violet',
+ size: 'md',
+ isSquare: false,
+ isChecked: false,
+ isDisabled: false,
+ isRequired: false,
+};
+
+export const Switch = ({
+ label = switchDefaults.label,
+ labelPosition = switchDefaults.labelPosition,
+ icon, // undefined by default
+ colorScheme = switchDefaults.colorScheme,
+ size = switchDefaults.size,
+ isSquare = switchDefaults.isSquare,
+ isChecked = switchDefaults.isChecked,
+ isDisabled = switchDefaults.isDisabled,
+ isRequired = switchDefaults.isRequired,
+ labelHoverText,
+ disabledHoverText,
+ labelStyle,
+ ...props
+}: SwitchProps) => {
+ const [checked, setChecked] = useState(isChecked);
+
+ useEffect(() => {
+ setChecked(isChecked);
+ }, [isChecked]);
+
+ const id = props.id || `switchToggle-${label}`;
+
+ return (
+
+
+
+ {label} {isRequired && * }
+
+
+ setChecked(!checked)}
+ customSize={size}
+ disabled={isDisabled}
+ colorScheme={colorScheme || 'violet'}
+ aria-labelledby={id}
+ aria-checked={checked}
+ {...props}
+ />
+
+
+
+ {icon && (
+
+ )}
+
+
+
+
+ );
+};
diff --git a/datahub-web-react/src/alchemy-components/components/Switch/components.ts b/datahub-web-react/src/alchemy-components/components/Switch/components.ts
new file mode 100644
index 00000000000000..1586c1cf9f32fd
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/components/Switch/components.ts
@@ -0,0 +1,118 @@
+import styled from 'styled-components';
+
+import { borders, colors, shadows, spacing, transition } from '@components/theme';
+import { ColorOptions, SizeOptions } from '@components/theme/config';
+
+import { Icon } from '../Icon';
+
+import { formLabelTextStyles } from '../commonStyles';
+
+import {
+ getIconTransformPositionLeft,
+ getIconTransformPositionTop,
+ getInputHeight,
+ getSliderTransformPosition,
+ getToggleSize,
+} from './utils';
+
+import type { SwitchLabelPosition } from './types';
+
+export const Label = styled.div({
+ ...formLabelTextStyles,
+ display: 'flex',
+ alignItems: 'flex-start',
+});
+
+export const SwitchContainer = styled.label<{ labelPosition: SwitchLabelPosition; isDisabled?: boolean }>(
+ ({ labelPosition, isDisabled }) => ({
+ display: 'flex',
+ flexDirection: labelPosition === 'top' ? 'column' : 'row',
+ alignItems: labelPosition === 'top' ? 'flex-start' : 'center',
+ gap: spacing.sm,
+ cursor: isDisabled ? 'not-allowed' : 'pointer',
+ width: 'max-content',
+ }),
+);
+
+export const Slider = styled.div<{ size?: SizeOptions; isSquare?: boolean; isDisabled?: boolean }>(
+ ({ size, isSquare, isDisabled }) => ({
+ '&:before': {
+ transition: `${transition.duration.normal} all`,
+ content: '""',
+ position: 'absolute',
+ minWidth: getToggleSize(size || 'md', 'slider'), // sliders width and height must be same
+ minHeight: getToggleSize(size || 'md', 'slider'),
+ borderRadius: !isSquare ? '35px' : '0px',
+ top: '50%',
+ left: spacing.xxsm,
+ transform: 'translate(0, -50%)',
+ backgroundColor: !isDisabled ? colors.white : colors.gray[200],
+ boxShadow: `
+ 0px 1px 2px 0px rgba(16, 24, 40, 0.06),
+ 0px 1px 3px 0px rgba(16, 24, 40, 0.12)
+ `,
+ },
+ borderRadius: !isSquare ? '32px' : '0px',
+ minWidth: getToggleSize(size || 'md', 'input'),
+ minHeight: getInputHeight(size || 'md'),
+ }),
+ {
+ display: 'flex',
+ justifyContent: 'center',
+ alignItems: 'center',
+ position: 'relative',
+
+ backgroundColor: colors.gray[100],
+ padding: spacing.xxsm,
+ transition: `${transition.duration.normal} all`,
+ boxSizing: 'content-box',
+ },
+);
+
+export const Required = styled.span({
+ color: colors.red[500],
+ marginLeft: spacing.xxsm,
+});
+
+export const StyledInput = styled.input<{
+ customSize?: SizeOptions;
+ disabled?: boolean;
+ colorScheme: ColorOptions;
+ checked?: boolean;
+}>`
+ opacity: 0;
+ position: absolute;
+
+ &:checked + ${Slider} {
+ background-color: ${(props) => (!props.disabled ? colors[props.colorScheme][500] : colors.gray[100])};
+
+ &:before {
+ transform: ${({ customSize }) => getSliderTransformPosition(customSize || 'md')};
+ }
+ }
+
+ &:focus-within + ${Slider} {
+ border-color: ${(props) => (props.checked ? colors[props.colorScheme][200] : 'transparent')};
+ outline: ${(props) => (props.checked ? `${borders['2px']} ${colors[props.colorScheme][200]}` : 'none')};
+ box-shadow: ${(props) => (props.checked ? shadows.xs : 'none')};
+ }
+`;
+
+export const StyledIcon = styled(Icon)<{ checked?: boolean; size: SizeOptions }>(
+ ({ checked, size }) => ({
+ left: getIconTransformPositionLeft(size, checked || false),
+ top: getIconTransformPositionTop(size),
+ }),
+ {
+ transition: `${transition.duration.normal} all`,
+ position: 'absolute',
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ color: colors.gray[500],
+ },
+);
+
+export const IconContainer = styled.div({
+ position: 'relative',
+});
diff --git a/datahub-web-react/src/alchemy-components/components/Switch/index.ts b/datahub-web-react/src/alchemy-components/components/Switch/index.ts
new file mode 100644
index 00000000000000..0c48d2964887ec
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/components/Switch/index.ts
@@ -0,0 +1,2 @@
+export { Switch, switchDefaults } from './Switch';
+export type { SwitchProps } from './types';
diff --git a/datahub-web-react/src/alchemy-components/components/Switch/types.ts b/datahub-web-react/src/alchemy-components/components/Switch/types.ts
new file mode 100644
index 00000000000000..e15c0f81b4a392
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/components/Switch/types.ts
@@ -0,0 +1,21 @@
+import { ColorOptions, SizeOptions } from '@components/theme/config';
+import { InputHTMLAttributes } from 'react';
+import { CSSProperties } from 'styled-components';
+import { IconNames } from '../Icon';
+
+export type SwitchLabelPosition = 'left' | 'top';
+
+export interface SwitchProps extends Omit, 'size'> {
+ label: string;
+ labelPosition?: SwitchLabelPosition;
+ icon?: IconNames;
+ colorScheme?: ColorOptions;
+ size?: SizeOptions;
+ isSquare?: boolean;
+ isChecked?: boolean;
+ isDisabled?: boolean;
+ isRequired?: boolean;
+ labelHoverText?: string;
+ disabledHoverText?: string;
+ labelStyle?: CSSProperties;
+}
diff --git a/datahub-web-react/src/alchemy-components/components/Switch/utils.ts b/datahub-web-react/src/alchemy-components/components/Switch/utils.ts
new file mode 100644
index 00000000000000..c0365baa348183
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/components/Switch/utils.ts
@@ -0,0 +1,97 @@
+import { SizeOptions } from '@components/theme/config';
+
+const sliderSize = {
+ sm: '14px',
+ md: '16px',
+ lg: '18px',
+ xl: '20px',
+};
+
+const inputSize = {
+ sm: '35px',
+ md: '40px',
+ lg: '45px',
+ xl: '50px',
+};
+
+const translateSize = {
+ sm: '22px',
+ md: '24px',
+ lg: '26px',
+ xl: '28px',
+};
+
+const iconTransformPositionLeft = {
+ sm: {
+ checked: '5.5px',
+ unchecked: '-16.5px',
+ },
+ md: {
+ checked: '5px',
+ unchecked: '-19px',
+ },
+ lg: {
+ checked: '4.5px',
+ unchecked: '-21.5px',
+ },
+ xl: {
+ checked: '4px',
+ unchecked: '-24px',
+ },
+};
+
+const iconTransformPositionTop = {
+ sm: '-6px',
+ md: '-7px',
+ lg: '-8px',
+ xl: '-9px',
+};
+
+export const getToggleSize = (size: SizeOptions, mode: 'slider' | 'input'): string => {
+ if (size === 'sm') return mode === 'slider' ? sliderSize.sm : inputSize.sm;
+ if (size === 'md') return mode === 'slider' ? sliderSize.md : inputSize.md;
+ if (size === 'lg') return mode === 'slider' ? sliderSize.lg : inputSize.lg;
+ return mode === 'slider' ? sliderSize.xl : inputSize.xl; // xl
+};
+
+export const getInputHeight = (size: SizeOptions) => {
+ if (size === 'sm') return sliderSize.sm;
+ if (size === 'md') return sliderSize.md;
+ if (size === 'lg') return sliderSize.lg;
+ return sliderSize.xl; // xl
+};
+
+export const getSliderTransformPosition = (size: SizeOptions): string => {
+ if (size === 'sm') return `translate(${translateSize.sm}, -50%)`;
+ if (size === 'md') return `translate(${translateSize.md}, -50%)`;
+ if (size === 'lg') return `translate(${translateSize.lg}, -50%)`;
+ return `translate(${translateSize.xl}, -50%)`; // xl
+};
+
+export const getIconTransformPositionLeft = (size: SizeOptions, checked: boolean): string => {
+ if (size === 'sm') {
+ if (checked) return iconTransformPositionLeft.sm.checked;
+ return iconTransformPositionLeft.sm.unchecked;
+ }
+
+ if (size === 'md') {
+ if (checked) return iconTransformPositionLeft.md.checked;
+ return iconTransformPositionLeft.md.unchecked;
+ }
+
+ if (size === 'lg') {
+ if (checked) return iconTransformPositionLeft.lg.checked;
+ return iconTransformPositionLeft.lg.unchecked;
+ }
+
+ // xl
+ if (checked) return iconTransformPositionLeft.xl.checked;
+ return iconTransformPositionLeft.xl.unchecked;
+};
+
+export const getIconTransformPositionTop = (size: SizeOptions): string => {
+ if (size === 'sm') return iconTransformPositionTop.sm;
+ if (size === 'md') return iconTransformPositionTop.md;
+ if (size === 'lg') return iconTransformPositionTop.lg;
+ return iconTransformPositionTop.xl; // xl
+};
diff --git a/datahub-web-react/src/alchemy-components/components/Table/Table.stories.tsx b/datahub-web-react/src/alchemy-components/components/Table/Table.stories.tsx
new file mode 100644
index 00000000000000..3a36b658978066
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/components/Table/Table.stories.tsx
@@ -0,0 +1,162 @@
+import { BADGE } from '@geometricpanda/storybook-addon-badges';
+import { Meta, StoryObj } from '@storybook/react';
+import React from 'react';
+import { Table, tableDefaults } from '.';
+
+// Auto Docs
+const meta = {
+ title: 'Lists & Tables / Table',
+ component: Table,
+
+ // Display Properties
+ parameters: {
+ layout: 'padded',
+ badges: [BADGE.STABLE, 'readyForDesignReview'],
+ docs: {
+ subtitle: 'This component allows users to render a table with different columns and their data',
+ },
+ },
+
+ // Component-level argTypes
+ argTypes: {
+ columns: {
+ description: 'Array of column objects for the table header.',
+ control: 'object',
+ table: {
+ defaultValue: { summary: JSON.stringify(tableDefaults.columns) },
+ },
+ },
+ data: {
+ description: 'Array of data rows for the table body.',
+ control: 'object',
+ table: {
+ defaultValue: { summary: JSON.stringify(tableDefaults.data) },
+ },
+ },
+ showHeader: {
+ description: 'Whether to show the table header.',
+ control: 'boolean',
+ table: {
+ defaultValue: { summary: tableDefaults.showHeader?.toString() },
+ },
+ },
+ isLoading: {
+ description: 'Whether the table is in loading state.',
+ control: 'boolean',
+ table: {
+ defaultValue: { summary: tableDefaults.isLoading?.toString() },
+ },
+ },
+ isScrollable: {
+ description: 'Whether the table is scrollable.',
+ control: 'boolean',
+ table: {
+ defaultValue: { summary: tableDefaults.isScrollable?.toString() },
+ },
+ },
+ maxHeight: {
+ description: 'Maximum height of the table container.',
+ control: 'text',
+ table: {
+ defaultValue: { summary: tableDefaults.maxHeight },
+ },
+ },
+ },
+
+ // Define defaults
+ args: {
+ columns: [
+ { title: 'Column 1', key: 'column1', dataIndex: 'column1' },
+ { title: 'Column 2', key: 'column2', dataIndex: 'column2' },
+ ],
+ data: [
+ { column1: 'Row 1 Col 1', column2: 'Row 1 Col 2' },
+ { column1: 'Row 2 Col 1', column2: 'Row 2 Col 2' },
+ ],
+ showHeader: tableDefaults.showHeader,
+ isLoading: tableDefaults.isLoading,
+ isScrollable: tableDefaults.isScrollable,
+ maxHeight: tableDefaults.maxHeight,
+ },
+} satisfies Meta>;
+
+export default meta;
+
+// Stories
+
+type Story = StoryObj;
+
+// Basic story is what is displayed 1st in storybook & is used as the code sandbox
+// Pass props to this so that it can be customized via the UI props panel
+export const sandbox: Story = {
+ tags: ['dev'],
+ render: (props) => ,
+};
+
+export const withScroll = () => (
+
+);
+
+export const withCustomColumnWidths = () => (
+
+);
+
+export const withColumnSorting = () => (
+ a.column1.localeCompare(b.column1),
+ },
+ { title: 'Column 2', key: 'column2', dataIndex: 'column2' },
+ { title: 'Column 3', key: 'column3', dataIndex: 'column3', sorter: (a, b) => a.column3 - b.column3 },
+ ]}
+ data={[
+ { column1: 'Row 2 Col 1', column2: 'Row 2 Col 2', column3: 3 },
+ { column1: 'Row 1 Col 1', column2: 'Row 1 Col 2', column3: 2 },
+ { column1: 'Row 3 Col 1', column2: 'Row 3 Col 2', column3: 1 },
+ ]}
+ />
+);
+
+export const withoutHeader = () => (
+
+);
diff --git a/datahub-web-react/src/alchemy-components/components/Table/Table.tsx b/datahub-web-react/src/alchemy-components/components/Table/Table.tsx
new file mode 100644
index 00000000000000..11e598f8d4e0f7
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/components/Table/Table.tsx
@@ -0,0 +1,115 @@
+import { LoadingOutlined } from '@ant-design/icons';
+import { Text } from '@components';
+import React, { useState } from 'react';
+import {
+ BaseTable,
+ HeaderContainer,
+ LoadingContainer,
+ SortIcon,
+ SortIconsContainer,
+ TableCell,
+ TableContainer,
+ TableHeader,
+ TableHeaderCell,
+ TableRow,
+} from './components';
+import { TableProps } from './types';
+import { getSortedData, handleActiveSort, renderCell, SortingState } from './utils';
+
+export const tableDefaults: TableProps = {
+ columns: [],
+ data: [],
+ showHeader: true,
+ isLoading: false,
+ isScrollable: false,
+ maxHeight: '100%',
+};
+
+export const Table = ({
+ columns = tableDefaults.columns,
+ data = tableDefaults.data,
+ showHeader = tableDefaults.showHeader,
+ isLoading = tableDefaults.isLoading,
+ isScrollable = tableDefaults.isScrollable,
+ maxHeight = tableDefaults.maxHeight,
+ ...props
+}: TableProps) => {
+ const [sortColumn, setSortColumn] = useState(null);
+ const [sortOrder, setSortOrder] = useState(SortingState.ORIGINAL);
+
+ const sortedData = getSortedData(columns, data, sortColumn, sortOrder);
+
+ if (isLoading) {
+ return (
+
+
+ Loading data...
+
+ );
+ }
+
+ return (
+
+
+ {showHeader && (
+
+
+ {columns.map((column) => (
+
+
+ {column.title}
+ {column.sorter && (
+
+ column.sorter &&
+ handleActiveSort(
+ column.key,
+ sortColumn,
+ setSortColumn,
+ setSortOrder,
+ )
+ }
+ >
+
+
+
+ )}
+
+
+ ))}
+
+
+ )}
+
+ {sortedData.map((row, index) => (
+
+ {columns.map((column) => {
+ return (
+
+ {renderCell(column, row, index)}
+
+ );
+ })}
+
+ ))}
+
+
+
+ );
+};
diff --git a/datahub-web-react/src/alchemy-components/components/Table/components.ts b/datahub-web-react/src/alchemy-components/components/Table/components.ts
new file mode 100644
index 00000000000000..8908256a81ddf2
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/components/Table/components.ts
@@ -0,0 +1,94 @@
+import { Icon } from '@components';
+import { colors, radius, spacing, typography } from '@src/alchemy-components/theme';
+import { AlignmentOptions } from '@src/alchemy-components/theme/config';
+import styled from 'styled-components';
+
+export const TableContainer = styled.div<{ isScrollable?: boolean; maxHeight?: string }>(
+ ({ isScrollable, maxHeight }) => ({
+ borderRadius: radius.lg,
+ border: `1px solid ${colors.gray[1400]}`,
+ overflow: isScrollable ? 'auto' : 'hidden',
+ width: '100%',
+ maxHeight: maxHeight || '100%',
+ }),
+);
+
+export const BaseTable = styled.table({
+ borderCollapse: 'collapse',
+ width: '100%',
+});
+
+export const TableHeader = styled.thead({
+ backgroundColor: colors.gray[1500],
+ borderRadius: radius.lg,
+ position: 'sticky',
+ top: 0,
+ zIndex: 100,
+});
+
+export const TableHeaderCell = styled.th<{ width?: string }>(({ width }) => ({
+ padding: `${spacing.sm} ${spacing.md}`,
+ color: colors.gray[600],
+ fontSize: typography.fontSizes.sm,
+ fontWeight: typography.fontWeights.medium,
+ textAlign: 'start',
+ width: width || 'auto',
+}));
+
+export const HeaderContainer = styled.div({
+ display: 'flex',
+ alignItems: 'center',
+ gap: spacing.sm,
+});
+
+export const TableRow = styled.tr({
+ '&:last-child': {
+ '& td': {
+ borderBottom: 'none',
+ },
+ },
+
+ '& td:first-child': {
+ fontWeight: typography.fontWeights.medium,
+ color: colors.gray[600],
+ },
+});
+
+export const TableCell = styled.td<{ width?: string; alignment?: AlignmentOptions }>(({ width, alignment }) => ({
+ padding: spacing.md,
+ borderBottom: `1px solid ${colors.gray[1400]}`,
+ color: colors.gray[1700],
+ fontSize: typography.fontSizes.md,
+ fontWeight: typography.fontWeights.normal,
+ overflow: 'hidden',
+ textOverflow: 'ellipsis',
+ whiteSpace: 'nowrap',
+ maxWidth: width || 'unset',
+ textAlign: alignment || 'left',
+}));
+
+export const SortIconsContainer = styled.div({
+ display: 'flex',
+ flexDirection: 'column',
+});
+
+export const SortIcon = styled(Icon)<{ isActive?: boolean }>(({ isActive }) => ({
+ margin: '-3px',
+ stroke: isActive ? colors.violet[600] : undefined,
+
+ ':hover': {
+ cursor: 'pointer',
+ },
+}));
+
+export const LoadingContainer = styled.div({
+ display: 'flex',
+ flexDirection: 'column',
+ alignItems: 'center',
+ justifyContent: 'center',
+ height: '100%',
+ width: '100%',
+ gap: spacing.sm,
+ color: colors.violet[700],
+ fontSize: typography.fontSizes['3xl'],
+});
diff --git a/datahub-web-react/src/alchemy-components/components/Table/index.ts b/datahub-web-react/src/alchemy-components/components/Table/index.ts
new file mode 100644
index 00000000000000..986f467da74b8c
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/components/Table/index.ts
@@ -0,0 +1,2 @@
+export { Table, tableDefaults } from './Table';
+export type { Column, TableProps } from './types';
diff --git a/datahub-web-react/src/alchemy-components/components/Table/types.ts b/datahub-web-react/src/alchemy-components/components/Table/types.ts
new file mode 100644
index 00000000000000..b3e0357d5cf147
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/components/Table/types.ts
@@ -0,0 +1,21 @@
+import { AlignmentOptions } from '@src/alchemy-components/theme/config';
+import { TableHTMLAttributes } from 'react';
+
+export interface Column {
+ title: string;
+ key: string;
+ dataIndex?: string;
+ render?: (record: T, index: number) => React.ReactNode;
+ width?: string;
+ sorter?: (a: T, b: T) => number;
+ alignment?: AlignmentOptions;
+}
+
+export interface TableProps extends TableHTMLAttributes {
+ columns: Column[];
+ data: T[];
+ showHeader?: boolean;
+ isLoading?: boolean;
+ isScrollable?: boolean;
+ maxHeight?: string;
+}
diff --git a/datahub-web-react/src/alchemy-components/components/Table/utils.ts b/datahub-web-react/src/alchemy-components/components/Table/utils.ts
new file mode 100644
index 00000000000000..c76494d32ca633
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/components/Table/utils.ts
@@ -0,0 +1,73 @@
+import { Column } from './types';
+
+export enum SortingState {
+ ASCENDING = 'ascending',
+ DESCENDING = 'descending',
+ ORIGINAL = 'original',
+}
+
+export const handleActiveSort = (
+ key: string,
+ sortColumn: string | null,
+ setSortColumn: React.Dispatch>,
+ setSortOrder: React.Dispatch>,
+) => {
+ if (sortColumn === key) {
+ // Toggle sort order
+ setSortOrder((prevOrder) => {
+ if (prevOrder === SortingState.ASCENDING) return SortingState.DESCENDING;
+ if (prevOrder === SortingState.DESCENDING) return SortingState.ORIGINAL;
+ return SortingState.ASCENDING;
+ });
+ } else {
+ // Set new column and default sort order
+ setSortColumn(key);
+ setSortOrder(SortingState.ASCENDING);
+ }
+};
+
+export const getSortedData = (
+ columns: Column[],
+ data: T[],
+ sortColumn: string | null,
+ sortOrder: SortingState,
+) => {
+ if (sortOrder === SortingState.ORIGINAL || !sortColumn) {
+ return data;
+ }
+
+ const activeColumn = columns.find((column) => column.key === sortColumn);
+
+ // Sort based on the order and column sorter
+ if (activeColumn && activeColumn.sorter) {
+ return data.slice().sort((a, b) => {
+ return sortOrder === SortingState.ASCENDING ? activeColumn.sorter!(a, b) : activeColumn.sorter!(b, a);
+ });
+ }
+
+ return data;
+};
+
+export const renderCell = (column: Column, row: T, index: number) => {
+ const { render, dataIndex } = column;
+
+ let cellData;
+
+ if (dataIndex) {
+ cellData = row[dataIndex];
+
+ if (typeof dataIndex === 'string') {
+ cellData = dataIndex.split('.').reduce((acc, prop) => acc && acc[prop], row);
+ }
+
+ if (Array.isArray(dataIndex)) {
+ cellData = dataIndex.reduce((acc, prop) => acc && acc[prop], row);
+ }
+ }
+
+ if (render) {
+ return render(row, index);
+ }
+
+ return cellData;
+};
diff --git a/datahub-web-react/src/alchemy-components/components/Text/Text.stories.tsx b/datahub-web-react/src/alchemy-components/components/Text/Text.stories.tsx
new file mode 100644
index 00000000000000..c82d468aaa08ce
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/components/Text/Text.stories.tsx
@@ -0,0 +1,100 @@
+import React from 'react';
+
+import type { Meta, StoryObj, StoryFn } from '@storybook/react';
+import { BADGE } from '@geometricpanda/storybook-addon-badges';
+
+import { VerticalFlexGrid } from '@components/.docs/mdx-components';
+import { Text, textDefaults } from '.';
+
+// Auto Docs
+const meta = {
+ title: 'Typography / Text',
+ component: Text,
+
+ // Display Properties
+ parameters: {
+ layout: 'centered',
+ badges: [BADGE.STABLE, 'readyForDesignReview'],
+ docs: {
+ subtitle: 'Used to render text and paragraphs within an interface.',
+ },
+ },
+
+ // Component-level argTypes
+ argTypes: {
+ children: {
+ description: 'The content to display within the heading.',
+ table: {
+ type: { summary: 'string' },
+ },
+ },
+ type: {
+ description: 'The type of text to display.',
+ table: {
+ defaultValue: { summary: textDefaults.type },
+ },
+ },
+ size: {
+ description: 'Override the size of the text.',
+ table: {
+ defaultValue: { summary: `${textDefaults.size}` },
+ },
+ },
+ color: {
+ description: 'Override the color of the text.',
+ table: {
+ defaultValue: { summary: textDefaults.color },
+ },
+ },
+ weight: {
+ description: 'Override the weight of the heading.',
+ table: {
+ defaultValue: { summary: textDefaults.weight },
+ },
+ },
+ },
+
+ // Define default args
+ args: {
+ children:
+ 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas aliquet nulla id felis vehicula, et posuere dui dapibus. Nullam rhoncus massa non tortor convallis, in blandit turpis rutrum. Morbi tempus velit mauris, at mattis metus mattis sed. Nunc molestie efficitur lectus, vel mollis eros.',
+ type: textDefaults.type,
+ size: textDefaults.size,
+ color: textDefaults.color,
+ weight: textDefaults.weight,
+ },
+} satisfies Meta;
+
+export default meta;
+
+// Stories
+
+type Story = StoryObj;
+
+// Basic story is what is displayed 1st in storybook
+// Pass props to this so that it can be customized via the UI props panel
+export const sandbox: Story = {
+ tags: ['dev'],
+ render: (props) => {props.children} ,
+};
+
+export const sizes: StoryFn = (props: any) => (
+
+ {props.children}
+ {props.children}
+ {props.children}
+ {props.children}
+ {props.children}
+ {props.children}
+ {props.children}
+ {props.children}
+
+);
+
+export const withLink = () => (
+
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas aliquet nulla id felis vehicula, et posuere
+ dui dapibus. Nullam rhoncus massa non tortor convallis , in blandit turpis rutrum. Morbi tempus
+ velit mauris, at mattis metus mattis sed. Nunc molestie efficitur lectus, vel mollis eros.
+
+);
diff --git a/datahub-web-react/src/alchemy-components/components/Text/Text.tsx b/datahub-web-react/src/alchemy-components/components/Text/Text.tsx
new file mode 100644
index 00000000000000..89122afbfcc8bf
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/components/Text/Text.tsx
@@ -0,0 +1,33 @@
+import React from 'react';
+
+import { TextProps } from './types';
+import { P, Div, Span } from './components';
+
+export const textDefaults: TextProps = {
+ type: 'p',
+ color: 'inherit',
+ size: 'md',
+ weight: 'normal',
+};
+
+export const Text = ({
+ type = textDefaults.type,
+ color = textDefaults.color,
+ size = textDefaults.size,
+ weight = textDefaults.weight,
+ children,
+ ...props
+}: TextProps) => {
+ const sharedProps = { size, color, weight, ...props };
+
+ switch (type) {
+ case 'p':
+ return {children}
;
+ case 'div':
+ return {children}
;
+ case 'span':
+ return {children} ;
+ default:
+ return {children}
;
+ }
+};
diff --git a/datahub-web-react/src/alchemy-components/components/Text/components.ts b/datahub-web-react/src/alchemy-components/components/Text/components.ts
new file mode 100644
index 00000000000000..1d48497f39c9c8
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/components/Text/components.ts
@@ -0,0 +1,50 @@
+import styled from 'styled-components';
+
+import { typography, colors } from '@components/theme';
+import { getColor, getFontSize } from '@components/theme/utils';
+import { TextProps } from './types';
+
+// Text Styles
+const textStyles = {
+ fontSize: typography.fontSizes.md,
+ lineHeight: typography.lineHeights.md,
+ fontWeight: typography.fontWeights.normal,
+};
+
+// Default styles
+const baseStyles = {
+ fontFamily: typography.fonts.body,
+ margin: 0,
+
+ '& a': {
+ color: colors.violet[400],
+ textDecoration: 'none',
+ transition: 'color 0.15s ease',
+
+ '&:hover': {
+ color: colors.violet[500],
+ },
+ },
+};
+
+// Prop Driven Styles
+const propStyles = (props, isText = false) => {
+ const styles = {} as any;
+ if (props.size) styles.fontSize = getFontSize(props.size);
+ if (props.color) styles.color = getColor(props.color);
+ if (props.weight) styles.fontWeight = typography.fontWeights[props.weight];
+ if (isText) styles.lineHeight = typography.lineHeights[props.size || 'md'];
+ return styles;
+};
+
+export const P = styled.p({ ...baseStyles, ...textStyles }, (props: TextProps) => ({
+ ...propStyles(props as TextProps, true),
+}));
+
+export const Span = styled.span({ ...baseStyles, ...textStyles }, (props: TextProps) => ({
+ ...propStyles(props as TextProps, true),
+}));
+
+export const Div = styled.div({ ...baseStyles, ...textStyles }, (props: TextProps) => ({
+ ...propStyles(props as TextProps, true),
+}));
diff --git a/datahub-web-react/src/alchemy-components/components/Text/index.ts b/datahub-web-react/src/alchemy-components/components/Text/index.ts
new file mode 100644
index 00000000000000..d4240105173d48
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/components/Text/index.ts
@@ -0,0 +1,2 @@
+export { Text, textDefaults } from './Text';
+export type { TextProps } from './types';
diff --git a/datahub-web-react/src/alchemy-components/components/Text/types.ts b/datahub-web-react/src/alchemy-components/components/Text/types.ts
new file mode 100644
index 00000000000000..6a41929da12a9b
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/components/Text/types.ts
@@ -0,0 +1,9 @@
+import { HTMLAttributes } from 'react';
+import type { FontSizeOptions, FontColorOptions, FontWeightOptions } from '@components/theme/config';
+
+export interface TextProps extends HTMLAttributes {
+ type?: 'span' | 'p' | 'div';
+ size?: FontSizeOptions;
+ color?: FontColorOptions;
+ weight?: FontWeightOptions;
+}
diff --git a/datahub-web-react/src/alchemy-components/components/TextArea/TextArea.stories.tsx b/datahub-web-react/src/alchemy-components/components/TextArea/TextArea.stories.tsx
new file mode 100644
index 00000000000000..b244eefa6f2073
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/components/TextArea/TextArea.stories.tsx
@@ -0,0 +1,159 @@
+import React from 'react';
+
+import type { Meta, StoryObj } from '@storybook/react';
+import { BADGE } from '@geometricpanda/storybook-addon-badges';
+
+import { GridList } from '@components/.docs/mdx-components';
+
+import { TextArea, textAreaDefaults } from './TextArea';
+import { AVAILABLE_ICONS } from '../Icon';
+
+// Auto Docs
+const meta = {
+ title: 'Forms / Text Area',
+ component: TextArea,
+
+ // Display Properties
+ parameters: {
+ layout: 'centered',
+ badges: [BADGE.STABLE, 'readyForDesignReview'],
+ docs: {
+ subtitle: 'A component that is used to get user input in a text area field.',
+ },
+ },
+
+ // Component-level argTypes
+ argTypes: {
+ label: {
+ description: 'Label for the TextArea.',
+ table: {
+ defaultValue: { summary: textAreaDefaults.label },
+ },
+ control: {
+ type: 'text',
+ },
+ },
+ placeholder: {
+ description: 'Placeholder for the Text Area.',
+ table: {
+ defaultValue: { summary: textAreaDefaults.placeholder },
+ },
+ control: {
+ type: 'text',
+ },
+ },
+ icon: {
+ description: 'The icon to display in the Text Area.',
+ type: 'string',
+ options: AVAILABLE_ICONS,
+ table: {
+ defaultValue: { summary: 'undefined' },
+ },
+ control: {
+ type: 'select',
+ },
+ },
+ error: {
+ description: 'Enforce error state on the TextArea.',
+ table: {
+ defaultValue: { summary: textAreaDefaults.error },
+ },
+ control: {
+ type: 'text',
+ },
+ },
+ warning: {
+ description: 'Enforce warning state on the TextArea.',
+ table: {
+ defaultValue: { summary: textAreaDefaults.warning },
+ },
+ control: {
+ type: 'text',
+ },
+ },
+ isSuccess: {
+ description: 'Enforce success state on the TextArea.',
+ table: {
+ defaultValue: { summary: textAreaDefaults?.isSuccess?.toString() },
+ },
+ control: {
+ type: 'boolean',
+ },
+ },
+ isDisabled: {
+ description: 'Enforce disabled state on the TextArea.',
+ table: {
+ defaultValue: { summary: textAreaDefaults?.isDisabled?.toString() },
+ },
+ control: {
+ type: 'boolean',
+ },
+ },
+ isInvalid: {
+ description: 'Enforce invalid state on the TextArea.',
+ table: {
+ defaultValue: { summary: textAreaDefaults?.isInvalid?.toString() },
+ },
+ control: {
+ type: 'boolean',
+ },
+ },
+ isReadOnly: {
+ description: 'Enforce read only state on the TextArea.',
+ table: {
+ defaultValue: { summary: textAreaDefaults?.isReadOnly?.toString() },
+ },
+ control: {
+ type: 'boolean',
+ },
+ },
+ isRequired: {
+ description: 'Enforce required state on the TextArea.',
+ table: {
+ defaultValue: { summary: textAreaDefaults?.isRequired?.toString() },
+ },
+ control: {
+ type: 'boolean',
+ },
+ },
+ },
+
+ // Define defaults
+ args: {
+ label: textAreaDefaults.label,
+ placeholder: textAreaDefaults.placeholder,
+ icon: textAreaDefaults.icon,
+ error: textAreaDefaults.error,
+ warning: textAreaDefaults.warning,
+ isSuccess: textAreaDefaults.isSuccess,
+ isDisabled: textAreaDefaults.isDisabled,
+ isInvalid: textAreaDefaults.isInvalid,
+ isReadOnly: textAreaDefaults.isReadOnly,
+ isRequired: textAreaDefaults.isRequired,
+ },
+} satisfies Meta;
+
+export default meta;
+
+type Story = StoryObj;
+
+export const sandbox: Story = {
+ tags: ['dev'],
+ render: (props) => ,
+};
+
+export const status = () => (
+
+
+
+
+
+);
+
+export const states = () => (
+
+
+
+
+
+);
diff --git a/datahub-web-react/src/alchemy-components/components/TextArea/TextArea.tsx b/datahub-web-react/src/alchemy-components/components/TextArea/TextArea.tsx
new file mode 100644
index 00000000000000..bf7de3520f9e5f
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/components/TextArea/TextArea.tsx
@@ -0,0 +1,80 @@
+import React from 'react';
+
+import { TextAreaProps } from './types';
+
+import {
+ ErrorMessage,
+ Label,
+ Required,
+ StyledIcon,
+ StyledStatusIcon,
+ TextAreaContainer,
+ TextAreaField,
+ TextAreaWrapper,
+ WarningMessage,
+} from './components';
+
+export const textAreaDefaults: TextAreaProps = {
+ label: 'Label',
+ placeholder: 'Placeholder',
+ error: '',
+ warning: '',
+ isSuccess: false,
+ isDisabled: false,
+ isInvalid: false,
+ isReadOnly: false,
+ isRequired: false,
+};
+
+export const TextArea = ({
+ label = textAreaDefaults.label,
+ placeholder = textAreaDefaults.placeholder,
+ icon, // default undefined
+ error = textAreaDefaults.error,
+ warning = textAreaDefaults.warning,
+ isSuccess = textAreaDefaults.isSuccess,
+ isDisabled = textAreaDefaults.isDisabled,
+ isInvalid = textAreaDefaults.isInvalid,
+ isReadOnly = textAreaDefaults.isReadOnly,
+ isRequired = textAreaDefaults.isRequired,
+ ...props
+}: TextAreaProps) => {
+ // Invalid state is always true if error is present
+ let invalid = isInvalid;
+ if (error) invalid = true;
+
+ // Input base props
+
+ const textAreaBaseProps = {
+ label,
+ isSuccess,
+ error,
+ warning,
+ isDisabled,
+ isInvalid: invalid,
+ };
+
+ return (
+
+
+ {label} {isRequired && * }
+
+
+ {icon && }
+
+ {isSuccess && }
+ {invalid && }
+ {warning && }
+
+ {invalid && error && {error} }
+ {warning && {warning} }
+
+ );
+};
diff --git a/datahub-web-react/src/alchemy-components/components/TextArea/components.ts b/datahub-web-react/src/alchemy-components/components/TextArea/components.ts
new file mode 100644
index 00000000000000..dd208d3b232f16
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/components/TextArea/components.ts
@@ -0,0 +1,106 @@
+import styled from 'styled-components';
+
+import theme, { colors, radius, borders, spacing, typography, sizes } from '@components/theme';
+import { getStatusColors } from '@components/theme/utils';
+
+import { Icon, IconNames } from '../Icon';
+
+import { formLabelTextStyles, inputValueTextStyles, inputPlaceholderTextStyles } from '../commonStyles';
+
+import { TextAreaProps } from './types';
+
+const minHeight = '100px';
+
+const defaultFlexStyles = {
+ display: 'flex',
+ justifyContent: 'space-between',
+ alignItems: 'flex-start',
+};
+
+const defaultMessageStyles = {
+ marginTop: spacing.xxsm,
+ fontSize: typography.fontSizes.sm,
+};
+
+export const TextAreaWrapper = styled.div({
+ ...defaultFlexStyles,
+ flexDirection: 'column',
+ width: '100%',
+});
+
+export const StyledIcon = styled(Icon)({
+ minWidth: '16px',
+ paddingLeft: spacing.sm,
+ marginTop: spacing.sm,
+});
+
+export const TextAreaContainer = styled.div(
+ ({ isSuccess, warning, isDisabled, isInvalid }: TextAreaProps) => ({
+ border: `${borders['1px']} ${getStatusColors(isSuccess, warning, isInvalid)}`,
+ backgroundColor: isDisabled ? colors.gray[100] : colors.white,
+ }),
+ {
+ ...defaultFlexStyles,
+ position: 'relative',
+ minWidth: '160px',
+ minHeight,
+ width: sizes.full,
+ borderRadius: radius.md,
+ flex: 1,
+ color: colors.gray[400], // first icon color
+
+ '&:focus-within': {
+ borderColor: colors.violet[200],
+ outline: `${borders['1px']} ${colors.violet[200]}`,
+ },
+ },
+);
+
+export const TextAreaField = styled.textarea<{ icon?: IconNames }>(({ icon }) => ({
+ padding: `${spacing.sm} ${spacing.md}`,
+ borderRadius: radius.md,
+ border: borders.none,
+ width: '100%',
+ minHeight,
+
+ ...inputValueTextStyles(),
+
+ // Account for icon placement
+ ...(icon && {
+ paddingLeft: spacing.xsm,
+ }),
+
+ '&:focus': {
+ outline: 'none',
+ },
+
+ '&::placeholder': {
+ ...inputPlaceholderTextStyles,
+ },
+}));
+
+export const Label = styled.div({
+ ...formLabelTextStyles,
+ marginBottom: spacing.xxsm,
+ textAlign: 'left',
+});
+
+export const Required = styled.span({
+ color: colors.red[500],
+});
+
+export const ErrorMessage = styled.div({
+ ...defaultMessageStyles,
+ color: theme.semanticTokens.colors.error,
+});
+
+export const WarningMessage = styled.div({
+ ...defaultMessageStyles,
+ color: theme.semanticTokens.colors.warning,
+});
+
+export const StyledStatusIcon = styled(Icon)({
+ position: 'absolute',
+ top: spacing.sm,
+ right: spacing.sm,
+});
diff --git a/datahub-web-react/src/alchemy-components/components/TextArea/index.ts b/datahub-web-react/src/alchemy-components/components/TextArea/index.ts
new file mode 100644
index 00000000000000..f367b1a5851a44
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/components/TextArea/index.ts
@@ -0,0 +1,2 @@
+export { TextArea, textAreaDefaults } from './TextArea';
+export type { TextAreaProps } from './types';
diff --git a/datahub-web-react/src/alchemy-components/components/TextArea/types.ts b/datahub-web-react/src/alchemy-components/components/TextArea/types.ts
new file mode 100644
index 00000000000000..a6edd05e8bd096
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/components/TextArea/types.ts
@@ -0,0 +1,15 @@
+import { TextareaHTMLAttributes } from 'react';
+import { IconNames } from '../Icon';
+
+export interface TextAreaProps extends TextareaHTMLAttributes {
+ label: string;
+ placeholder?: string;
+ icon?: IconNames;
+ error?: string;
+ warning?: string;
+ isSuccess?: boolean;
+ isDisabled?: boolean;
+ isInvalid?: boolean;
+ isReadOnly?: boolean;
+ isRequired?: boolean;
+}
diff --git a/datahub-web-react/src/alchemy-components/components/Tooltip/Tooltip.tsx b/datahub-web-react/src/alchemy-components/components/Tooltip/Tooltip.tsx
new file mode 100644
index 00000000000000..03a2da4382c282
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/components/Tooltip/Tooltip.tsx
@@ -0,0 +1,6 @@
+import { Tooltip, TooltipProps } from 'antd';
+import * as React from 'react';
+
+export default function DataHubTooltip(props: TooltipProps & React.RefAttributes) {
+ return ;
+}
diff --git a/datahub-web-react/src/alchemy-components/components/Tooltip/index.ts b/datahub-web-react/src/alchemy-components/components/Tooltip/index.ts
new file mode 100644
index 00000000000000..49ab72ce023868
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/components/Tooltip/index.ts
@@ -0,0 +1 @@
+export { default as Tooltip } from './Tooltip';
diff --git a/datahub-web-react/src/alchemy-components/components/commonStyles.ts b/datahub-web-react/src/alchemy-components/components/commonStyles.ts
new file mode 100644
index 00000000000000..04069f5950bc40
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/components/commonStyles.ts
@@ -0,0 +1,23 @@
+import { colors, typography } from '@components/theme';
+
+export const INPUT_MAX_HEIGHT = '40px';
+
+export const formLabelTextStyles = {
+ fontWeight: typography.fontWeights.normal,
+ fontSize: typography.fontSizes.md,
+ color: colors.gray[600],
+};
+
+export const inputValueTextStyles = (size = 'md') => ({
+ fontFamily: typography.fonts.body,
+ fontWeight: typography.fontWeights.normal,
+ fontSize: typography.fontSizes[size],
+ color: colors.gray[700],
+});
+
+export const inputPlaceholderTextStyles = {
+ fontFamily: typography.fonts.body,
+ fontWeight: typography.fontWeights.normal,
+ fontSize: typography.fontSizes.md,
+ color: colors.gray[400],
+};
diff --git a/datahub-web-react/src/alchemy-components/components/dataviz/utils.ts b/datahub-web-react/src/alchemy-components/components/dataviz/utils.ts
new file mode 100644
index 00000000000000..15e2783d2905d5
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/components/dataviz/utils.ts
@@ -0,0 +1,11 @@
+// Number Abbreviations
+export const abbreviateNumber = (str) => {
+ const number = parseFloat(str);
+ if (Number.isNaN(number)) return str;
+ if (number < 1000) return number;
+ const abbreviations = ['K', 'M', 'B', 'T'];
+ const index = Math.floor(Math.log10(number) / 3);
+ const suffix = abbreviations[index - 1];
+ const shortNumber = number / 10 ** (index * 3);
+ return `${shortNumber}${suffix}`;
+};
diff --git a/datahub-web-react/src/alchemy-components/index.ts b/datahub-web-react/src/alchemy-components/index.ts
new file mode 100644
index 00000000000000..8ef4f73f4408ff
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/index.ts
@@ -0,0 +1,23 @@
+// example usage: import { colors } from '@components/theme';
+export * from './theme';
+
+// example usage: import { Button } from '@components';
+export * from './components/Avatar';
+export * from './components/Badge';
+export * from './components/BarChart';
+export * from './components/Button';
+export * from './components/Card';
+export * from './components/Checkbox';
+export * from './components/Heading';
+export * from './components/Icon';
+export * from './components/Input';
+export * from './components/LineChart';
+export * from './components/PageTitle';
+export * from './components/Pills';
+export * from './components/Popover';
+export * from './components/Select';
+export * from './components/Switch';
+export * from './components/Table';
+export * from './components/Text';
+export * from './components/TextArea';
+export * from './components/Tooltip';
diff --git a/datahub-web-react/src/alchemy-components/theme/config/constants.ts b/datahub-web-react/src/alchemy-components/theme/config/constants.ts
new file mode 100644
index 00000000000000..84e55c78f85be0
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/theme/config/constants.ts
@@ -0,0 +1 @@
+export const DEFAULT_VALUE = 500;
diff --git a/datahub-web-react/src/alchemy-components/theme/config/index.ts b/datahub-web-react/src/alchemy-components/theme/config/index.ts
new file mode 100644
index 00000000000000..4fbce9564d5fc1
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/theme/config/index.ts
@@ -0,0 +1,2 @@
+export * from './constants';
+export * from './types';
diff --git a/datahub-web-react/src/alchemy-components/theme/config/types.ts b/datahub-web-react/src/alchemy-components/theme/config/types.ts
new file mode 100644
index 00000000000000..79ba2e27018f76
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/theme/config/types.ts
@@ -0,0 +1,47 @@
+// General types
+export type SizeOptions = 'xs' | 'sm' | 'md' | 'lg' | 'xl';
+
+// Color types
+export interface Color {
+ 100: string;
+ 200: string;
+ 300: string;
+ 400: string;
+ 500: string;
+ 600: string;
+ 700: string;
+ 800: string;
+ 900: string;
+}
+export type ColorOptions = 'white' | 'black' | 'violet' | 'green' | 'red' | 'blue' | 'gray' | 'yellow';
+export type MiscColorOptions = 'transparent' | 'current' | 'inherit';
+export type ColorShadeOptions = 'light' | 'default' | 'dark';
+
+// Typography types
+export type FontSizeOptions = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl' | '4xl';
+export type FontWeightOptions = 'normal' | 'medium' | 'semiBold' | 'bold';
+export type FontColorOptions = MiscColorOptions | ColorOptions;
+
+// Border radius types
+export type BorderRadiusOptions = 'none' | 'sm' | 'md' | 'lg' | 'xl' | 'full';
+
+// Box shadow types
+export type BoxShadowOptions = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | 'inner' | 'outline' | 'dropdown' | 'none';
+
+// Spacing types
+export type SpacingOptions = 'none' | 'sm' | 'md' | 'lg' | 'xl' | '3xl' | '4xl';
+
+// Transform types
+export type RotationOptions = '0' | '90' | '180' | '270';
+
+// Variant types
+export type VariantOptions = 'filled' | 'outline';
+
+// Alignment types
+export type AlignmentOptions = 'left' | 'right' | 'center' | 'justify';
+
+// Avatar Size options
+export type AvatarSizeOptions = 'sm' | 'md' | 'lg' | 'default';
+
+// Icon Alignment types
+export type IconAlignmentOptions = 'horizontal' | 'vertical';
diff --git a/datahub-web-react/src/alchemy-components/theme/foundations/blur.ts b/datahub-web-react/src/alchemy-components/theme/foundations/blur.ts
new file mode 100644
index 00000000000000..88f117f9076798
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/theme/foundations/blur.ts
@@ -0,0 +1,12 @@
+const blur = {
+ none: 0,
+ sm: '4px',
+ base: '8px',
+ md: '12px',
+ lg: '16px',
+ xl: '24px',
+ '2xl': '40px',
+ '3xl': '64px',
+};
+
+export default blur;
diff --git a/datahub-web-react/src/alchemy-components/theme/foundations/borders.ts b/datahub-web-react/src/alchemy-components/theme/foundations/borders.ts
new file mode 100644
index 00000000000000..202fd1f8c3e4f6
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/theme/foundations/borders.ts
@@ -0,0 +1,9 @@
+const borders = {
+ none: 0,
+ '1px': '1px solid',
+ '2px': '2px solid',
+ '4px': '4px solid',
+ '8px': '8px solid',
+};
+
+export default borders;
diff --git a/datahub-web-react/src/alchemy-components/theme/foundations/breakpoints.ts b/datahub-web-react/src/alchemy-components/theme/foundations/breakpoints.ts
new file mode 100644
index 00000000000000..722cc30ee3fd5e
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/theme/foundations/breakpoints.ts
@@ -0,0 +1,10 @@
+const breakpoints = {
+ base: '0em',
+ sm: '30em',
+ md: '48em',
+ lg: '62em',
+ xl: '80em',
+ '2xl': '96em',
+};
+
+export default breakpoints;
diff --git a/datahub-web-react/src/alchemy-components/theme/foundations/colors.ts b/datahub-web-react/src/alchemy-components/theme/foundations/colors.ts
new file mode 100644
index 00000000000000..760127a8b0f781
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/theme/foundations/colors.ts
@@ -0,0 +1,98 @@
+const colors = {
+ transparent: 'transparent',
+ current: 'currentColor',
+ white: '#ffffff',
+ black: '#000000',
+
+ gray: {
+ 100: '#EBECF0',
+ 200: '#CFD1DA',
+ 300: '#A9ADBD',
+ 400: '#81879F',
+ 500: '#5B6282', // primary value
+ 600: '#374066',
+ 700: '#2F3657',
+ 800: '#272D48',
+ 900: '#231A58',
+ 1000: '#F1F3FD',
+ 1100: '#F1FBFE',
+ 1200: '#FBF3EF',
+ 1300: '#F7FBF4',
+ 1400: '#E9EAEE',
+ 1500: '#F9FAFC',
+ 1600: '#F5F6FA',
+ 1700: '#5F6685',
+ 1800: '#8088A3',
+ },
+
+ violet: {
+ 100: '#CAC3F1',
+ 200: '#B0A7EA',
+ 300: '#8C7EE0',
+ 400: '#7565DA',
+ 500: '#533FD1', // primary value
+ 600: '#4C39BE',
+ 700: '#3B2D94',
+ 800: '#2E2373',
+ 900: '#231A58',
+ 1000: '#E5E2F8',
+ },
+
+ green: {
+ 100: '#d5e9c9',
+ 200: '#c0deaf',
+ 300: '#a4cf8a',
+ 400: '#92c573',
+ 500: '#77b750', // primary value
+ 600: '#6ca749',
+ 700: '#548239',
+ 800: '#41652c',
+ 900: '#324d22',
+ 1000: '#0D7543',
+ 1100: '#E1F0D6',
+ },
+
+ red: {
+ 100: '#f6d5d5',
+ 200: '#f2c1c1',
+ 300: '#eca5a5',
+ 400: '#e99393',
+ 500: '#e37878', // primary value
+ 600: '#cf6d6d',
+ 700: '#a15555',
+ 800: '#7d4242',
+ 900: '#5f3232',
+ 1000: '#C4360B',
+ 1100: '#F3DACE',
+ },
+
+ blue: {
+ 100: '#eff8fc',
+ 200: '#e6f5fb',
+ 300: '#ccebf6',
+ 400: '#5abde1', // primary value
+ 500: '#51aacb',
+ 600: '#4897b4',
+ 700: '#448ea9',
+ 800: '#367187',
+ 900: '#285565',
+ 1000: '#09739A',
+ 1100: '#CCEBF6',
+ },
+
+ yellow: {
+ 100: '#fcedc7',
+ 200: '#fae4ab',
+ 300: '#f8d785',
+ 400: '#f6d06d',
+ 500: '#eeae09', // primary value
+ 600: '#deb242',
+ 700: '#ad8b34',
+ 800: '#866c28',
+ 900: '#66521f',
+ 1000: '#C77100',
+ 1100: '#FCEDC7',
+ },
+};
+
+export default colors;
diff --git a/datahub-web-react/src/alchemy-components/theme/foundations/index.ts b/datahub-web-react/src/alchemy-components/theme/foundations/index.ts
new file mode 100644
index 00000000000000..e4c81d77f07557
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/theme/foundations/index.ts
@@ -0,0 +1,27 @@
+import blur from './blur';
+import borders from './borders';
+import breakpoints from './breakpoints';
+import colors from './colors';
+import radius from './radius';
+import shadows from './shadows';
+import sizes from './sizes';
+import spacing from './spacing';
+import transform from './transform';
+import transition from './transition';
+import typography from './typography';
+import zIndices from './zIndex';
+
+export const foundations = {
+ blur,
+ borders,
+ breakpoints,
+ colors,
+ radius,
+ shadows,
+ sizes,
+ spacing,
+ transform,
+ transition,
+ typography,
+ zIndices,
+};
diff --git a/datahub-web-react/src/alchemy-components/theme/foundations/radius.ts b/datahub-web-react/src/alchemy-components/theme/foundations/radius.ts
new file mode 100644
index 00000000000000..aa17688a4328a5
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/theme/foundations/radius.ts
@@ -0,0 +1,9 @@
+const radius = {
+ none: '0',
+ sm: '4px',
+ md: '8px',
+ lg: '12px',
+ full: '100%',
+};
+
+export default radius;
diff --git a/datahub-web-react/src/alchemy-components/theme/foundations/shadows.ts b/datahub-web-react/src/alchemy-components/theme/foundations/shadows.ts
new file mode 100644
index 00000000000000..e4cc920dd82e56
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/theme/foundations/shadows.ts
@@ -0,0 +1,16 @@
+import { BoxShadowOptions } from '../config';
+
+const shadows: Record = {
+ xs: '0px 1px 2px 0px rgba(0, 0, 0, 0.05)',
+ sm: '0 4px 4px 0 rgba(0 0 0 / 0.25)',
+ md: '0 8px 8px 4px rgba(0 0 0 / 0.25)',
+ lg: '0 12px 12px 8px rgba(0 0 0 / 0.25)',
+ xl: '0 16px 16px 12px rgba(0 0 0 / 0.25)',
+ '2xl': '0 24px 24px 16px rgba(0 0 0 / 0.25)',
+ inner: 'inset 0 2px 4px 0 rgba(0 0 0 / 0.06)',
+ outline: '0 0 0 3px rgba(66, 153, 225, 0.5)',
+ none: 'none',
+ dropdown: '0px 0px 14px 0px #00000026',
+};
+
+export default shadows;
diff --git a/datahub-web-react/src/alchemy-components/theme/foundations/sizes.ts b/datahub-web-react/src/alchemy-components/theme/foundations/sizes.ts
new file mode 100644
index 00000000000000..c1050e328b1097
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/theme/foundations/sizes.ts
@@ -0,0 +1,7 @@
+const sizes = {
+ max: 'max-content',
+ min: 'min-content',
+ full: '100%',
+};
+
+export default sizes;
diff --git a/datahub-web-react/src/alchemy-components/theme/foundations/spacing.ts b/datahub-web-react/src/alchemy-components/theme/foundations/spacing.ts
new file mode 100644
index 00000000000000..8adb1bd1769015
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/theme/foundations/spacing.ts
@@ -0,0 +1,12 @@
+const spacing = {
+ none: '0px',
+ xxsm: '4px',
+ xsm: '8px',
+ sm: '12px',
+ md: '16px',
+ lg: '24px',
+ xlg: '32px',
+ xxlg: '48px',
+};
+
+export default spacing;
diff --git a/datahub-web-react/src/alchemy-components/theme/foundations/transform.ts b/datahub-web-react/src/alchemy-components/theme/foundations/transform.ts
new file mode 100644
index 00000000000000..1065250dc191e1
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/theme/foundations/transform.ts
@@ -0,0 +1,10 @@
+const transform = {
+ rotate: {
+ none: 'rotate(0deg)',
+ 90: 'rotate(90deg)',
+ 180: 'rotate(180deg)',
+ 270: 'rotate(270deg)',
+ },
+};
+
+export default transform;
diff --git a/datahub-web-react/src/alchemy-components/theme/foundations/transition.ts b/datahub-web-react/src/alchemy-components/theme/foundations/transition.ts
new file mode 100644
index 00000000000000..83c541b9e52f0f
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/theme/foundations/transition.ts
@@ -0,0 +1,32 @@
+const transitionProperty = {
+ common: `background-color, border-color, color, fill,
+ stroke, opacity, box-shadow, transform`,
+ colors: 'background-color, border-color, color, fill, stroke',
+ dimensions: 'width, height',
+ position: 'left, right, top, bottom',
+ background: 'background-color, background-image, background-position',
+};
+
+const transitionTimingFunction = {
+ 'ease-in': 'cubic-bezier(0.4, 0, 1, 1)',
+ 'ease-out': 'cubic-bezier(0, 0, 0.2, 1)',
+ 'ease-in-out': 'cubic-bezier(0.4, 0, 0.2, 1)',
+};
+
+const transitionDuration = {
+ 'ultra-fast': '50ms',
+ faster: '100ms',
+ fast: '150ms',
+ normal: '200ms',
+ slow: '300ms',
+ slower: '400ms',
+ 'ultra-slow': '500ms',
+};
+
+const transition = {
+ property: transitionProperty,
+ easing: transitionTimingFunction,
+ duration: transitionDuration,
+};
+
+export default transition;
diff --git a/datahub-web-react/src/alchemy-components/theme/foundations/typography.ts b/datahub-web-react/src/alchemy-components/theme/foundations/typography.ts
new file mode 100644
index 00000000000000..3b63e678558e4a
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/theme/foundations/typography.ts
@@ -0,0 +1,52 @@
+const typography = {
+ letterSpacings: {
+ tighter: '-2px',
+ tight: '-1px',
+ normal: '0',
+ wide: '1px',
+ wider: '2px',
+ widest: '4px',
+ },
+
+ lineHeights: {
+ normal: 'normal',
+ none: 1,
+ xs: '16px',
+ sm: '20px',
+ md: '24px',
+ lg: '28px',
+ xl: '32px',
+ '2xl': '36px',
+ '3xl': '40px',
+ '4xl': '44px',
+ },
+
+ fontWeights: {
+ normal: 400, // regular
+ medium: 500,
+ semiBold: 600,
+ bold: 700,
+ },
+
+ fonts: {
+ heading: `'Mulish', -apple-system, BlinkMacSystemFont,
+ 'Segoe UI', Helvetica, Arial, sans-serif`,
+ body: `'Mulish', -apple-system, BlinkMacSystemFont,
+ 'Segoe UI', Helvetica, Arial, sans-serif`,
+ mono: `SFMono-Regular, Menlo, Monaco, Consolas,
+ 'Liberation Mono', 'Courier New', monospace`,
+ },
+
+ fontSizes: {
+ xs: '10px',
+ sm: '12px',
+ md: '14px', // default body text size
+ lg: '16px',
+ xl: '18px',
+ '2xl': '20px',
+ '3xl': '22px',
+ '4xl': '24px',
+ },
+};
+
+export default typography;
diff --git a/datahub-web-react/src/alchemy-components/theme/foundations/zIndex.ts b/datahub-web-react/src/alchemy-components/theme/foundations/zIndex.ts
new file mode 100644
index 00000000000000..318783d77e4e58
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/theme/foundations/zIndex.ts
@@ -0,0 +1,17 @@
+const zIndices = {
+ hide: -1,
+ auto: 'auto',
+ base: 0,
+ docked: 10,
+ dropdown: 1000,
+ sticky: 1100,
+ banner: 1200,
+ overlay: 1300,
+ modal: 1400,
+ popover: 1500,
+ skipLink: 1600,
+ toast: 1700,
+ tooltip: 1800,
+};
+
+export default zIndices;
diff --git a/datahub-web-react/src/alchemy-components/theme/index.ts b/datahub-web-react/src/alchemy-components/theme/index.ts
new file mode 100644
index 00000000000000..65bad35f827cc8
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/theme/index.ts
@@ -0,0 +1,30 @@
+import * as config from './config';
+import * as utils from './utils';
+
+import { foundations } from './foundations';
+import { semanticTokens } from './semantic-tokens';
+
+const theme = {
+ semanticTokens,
+ ...foundations,
+ config,
+ utils,
+};
+
+export const {
+ colors,
+ spacing,
+ radius,
+ shadows,
+ typography,
+ breakpoints,
+ zIndices,
+ transform,
+ transition,
+ sizes,
+ borders,
+ blur,
+} = theme;
+
+export type Theme = typeof theme;
+export default theme;
diff --git a/datahub-web-react/src/alchemy-components/theme/semantic-tokens.ts b/datahub-web-react/src/alchemy-components/theme/semantic-tokens.ts
new file mode 100644
index 00000000000000..29bd20d11f8544
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/theme/semantic-tokens.ts
@@ -0,0 +1,21 @@
+import { foundations } from './foundations';
+
+const { colors } = foundations;
+
+export const semanticTokens = {
+ colors: {
+ 'body-text': colors.gray[800],
+ 'body-bg': colors.white,
+ 'border-color': colors.gray[200],
+ 'inverse-text': colors.white,
+ 'subtle-bg': colors.gray[100],
+ 'subtle-text': colors.gray[600],
+ 'placeholder-color': colors.gray[500],
+ primary: colors.violet[500],
+ secondary: colors.blue[500],
+ error: colors.red[500],
+ success: colors.green[500],
+ warning: colors.yellow[500],
+ info: colors.blue[500],
+ },
+};
diff --git a/datahub-web-react/src/alchemy-components/theme/utils.ts b/datahub-web-react/src/alchemy-components/theme/utils.ts
new file mode 100644
index 00000000000000..4c20ca39f06eed
--- /dev/null
+++ b/datahub-web-react/src/alchemy-components/theme/utils.ts
@@ -0,0 +1,62 @@
+/*
+ Theme Utils that can be used anywhere in the app
+*/
+
+import { FontSizeOptions, ColorOptions, MiscColorOptions, RotationOptions, DEFAULT_VALUE } from './config';
+import { foundations } from './foundations';
+import { semanticTokens } from './semantic-tokens';
+
+const { colors, typography, transform } = foundations;
+/*
+ Get the color value for a given color
+ Falls back to `color.black` if the color is not found
+ @param color - the color to get the value for
+*/
+export const getColor = (color?: MiscColorOptions | ColorOptions, value: number | string = DEFAULT_VALUE) => {
+ if (!color) return colors.black;
+ if (color === 'inherit' || color === 'transparent' || color === 'current') return colors;
+ if (color === 'white') return colors.white;
+ if (color === 'black') return colors.black;
+ const colorValue = colors[color];
+ if (!colorValue) return colors.black;
+ return colors[color][value];
+};
+
+/*
+ Get the font size value for a given size
+ @param size - the size of the font
+*/
+export const getFontSize = (size?: FontSizeOptions) => {
+ let sizeValue = size || '';
+ if (!size) sizeValue = 'md';
+ return typography.fontSizes[sizeValue];
+};
+
+/*
+ Get the rotation transform value for a given rotation
+ @param r - the rotation to get the transform value for
+*/
+export const getRotationTransform = (rotate?: RotationOptions) => {
+ if (!rotate) return '';
+ return transform.rotate[rotate || '0'];
+};
+
+/**
+ * Get the status color depending on the flags that are true
+ * @param {string} [error] - Error definition, if any.
+ * @param {boolean} [isSuccess] - Boolean flag indicating success.
+ * @param {string} [warning] - Warning definition, if any.
+ * @returns {string} - The status color based on the provided flags.
+ */
+export const getStatusColors = (isSuccess?: boolean, warning?: string, isInvalid?: boolean): string => {
+ if (isInvalid) {
+ return colors.red[600];
+ }
+ if (isSuccess) {
+ return colors.green[600];
+ }
+ if (warning) {
+ return colors.yellow[600];
+ }
+ return semanticTokens.colors['border-color'];
+};
diff --git a/datahub-web-react/src/app/ingest/source/builder/sources.json b/datahub-web-react/src/app/ingest/source/builder/sources.json
index 776b6703895c35..102cce0f491e36 100644
--- a/datahub-web-react/src/app/ingest/source/builder/sources.json
+++ b/datahub-web-react/src/app/ingest/source/builder/sources.json
@@ -181,7 +181,7 @@
"displayName": "Athena",
"description": "Import Schemas, Tables, Views, and lineage to S3 from Athena.",
"docsUrl": "https://datahubproject.io/docs/generated/ingestion/sources/athena/",
- "recipe": "source:\n type: athena\n config:\n # Coordinates\n aws_region: my_aws_region\n work_group: primary\n\n # Options\n s3_staging_dir: \"s3://my_staging_athena_results_bucket/results/\""
+ "recipe": "source:\n type: athena\n config:\n # AWS Keys (Optional - Required only if local aws credentials are not set)\n username: aws_access_key_id\n password: aws_secret_access_key\n # Coordinates\n aws_region: my_aws_region\n work_group: primary\n\n # Options\n s3_staging_dir: \"s3://my_staging_athena_results_bucket/results/\""
},
{
"urn": "urn:li:dataPlatform:clickhouse",
diff --git a/datahub-web-react/src/fonts/Mulish-Black.ttf b/datahub-web-react/src/fonts/Mulish-Black.ttf
new file mode 100644
index 00000000000000..7c45457b315e1d
Binary files /dev/null and b/datahub-web-react/src/fonts/Mulish-Black.ttf differ
diff --git a/datahub-web-react/src/fonts/Mulish-BlackItalic.ttf b/datahub-web-react/src/fonts/Mulish-BlackItalic.ttf
new file mode 100644
index 00000000000000..7eda815bc44e32
Binary files /dev/null and b/datahub-web-react/src/fonts/Mulish-BlackItalic.ttf differ
diff --git a/datahub-web-react/src/fonts/Mulish-Bold.ttf b/datahub-web-react/src/fonts/Mulish-Bold.ttf
new file mode 100644
index 00000000000000..0c1eed735a9a8e
Binary files /dev/null and b/datahub-web-react/src/fonts/Mulish-Bold.ttf differ
diff --git a/datahub-web-react/src/fonts/Mulish-BoldItalic.ttf b/datahub-web-react/src/fonts/Mulish-BoldItalic.ttf
new file mode 100644
index 00000000000000..6f08e1512b7882
Binary files /dev/null and b/datahub-web-react/src/fonts/Mulish-BoldItalic.ttf differ
diff --git a/datahub-web-react/src/fonts/Mulish-ExtraBold.ttf b/datahub-web-react/src/fonts/Mulish-ExtraBold.ttf
new file mode 100644
index 00000000000000..bf116994f6797e
Binary files /dev/null and b/datahub-web-react/src/fonts/Mulish-ExtraBold.ttf differ
diff --git a/datahub-web-react/src/fonts/Mulish-ExtraBoldItalic.ttf b/datahub-web-react/src/fonts/Mulish-ExtraBoldItalic.ttf
new file mode 100644
index 00000000000000..33ae716cefaf15
Binary files /dev/null and b/datahub-web-react/src/fonts/Mulish-ExtraBoldItalic.ttf differ
diff --git a/datahub-web-react/src/fonts/Mulish-ExtraLight.ttf b/datahub-web-react/src/fonts/Mulish-ExtraLight.ttf
new file mode 100644
index 00000000000000..7964001697e4b8
Binary files /dev/null and b/datahub-web-react/src/fonts/Mulish-ExtraLight.ttf differ
diff --git a/datahub-web-react/src/fonts/Mulish-ExtraLightItalic.ttf b/datahub-web-react/src/fonts/Mulish-ExtraLightItalic.ttf
new file mode 100644
index 00000000000000..3e57b5451caa5c
Binary files /dev/null and b/datahub-web-react/src/fonts/Mulish-ExtraLightItalic.ttf differ
diff --git a/datahub-web-react/src/fonts/Mulish-Italic.ttf b/datahub-web-react/src/fonts/Mulish-Italic.ttf
new file mode 100644
index 00000000000000..a42d8759e0b48b
Binary files /dev/null and b/datahub-web-react/src/fonts/Mulish-Italic.ttf differ
diff --git a/datahub-web-react/src/fonts/Mulish-Light.ttf b/datahub-web-react/src/fonts/Mulish-Light.ttf
new file mode 100644
index 00000000000000..f4d91c3b92f6ad
Binary files /dev/null and b/datahub-web-react/src/fonts/Mulish-Light.ttf differ
diff --git a/datahub-web-react/src/fonts/Mulish-LightItalic.ttf b/datahub-web-react/src/fonts/Mulish-LightItalic.ttf
new file mode 100644
index 00000000000000..a4712b16769c04
Binary files /dev/null and b/datahub-web-react/src/fonts/Mulish-LightItalic.ttf differ
diff --git a/datahub-web-react/src/fonts/Mulish-Medium.ttf b/datahub-web-react/src/fonts/Mulish-Medium.ttf
new file mode 100644
index 00000000000000..be50c59750cdfb
Binary files /dev/null and b/datahub-web-react/src/fonts/Mulish-Medium.ttf differ
diff --git a/datahub-web-react/src/fonts/Mulish-MediumItalic.ttf b/datahub-web-react/src/fonts/Mulish-MediumItalic.ttf
new file mode 100644
index 00000000000000..01cf746fd2db03
Binary files /dev/null and b/datahub-web-react/src/fonts/Mulish-MediumItalic.ttf differ
diff --git a/datahub-web-react/src/fonts/Mulish-Regular.ttf b/datahub-web-react/src/fonts/Mulish-Regular.ttf
new file mode 100644
index 00000000000000..0971518b2d645c
Binary files /dev/null and b/datahub-web-react/src/fonts/Mulish-Regular.ttf differ
diff --git a/datahub-web-react/src/fonts/Mulish-SemiBold.ttf b/datahub-web-react/src/fonts/Mulish-SemiBold.ttf
new file mode 100644
index 00000000000000..9ac1fc8f29d59b
Binary files /dev/null and b/datahub-web-react/src/fonts/Mulish-SemiBold.ttf differ
diff --git a/datahub-web-react/src/fonts/Mulish-SemiBoldItalic.ttf b/datahub-web-react/src/fonts/Mulish-SemiBoldItalic.ttf
new file mode 100644
index 00000000000000..71b3dc84be2f7e
Binary files /dev/null and b/datahub-web-react/src/fonts/Mulish-SemiBoldItalic.ttf differ
diff --git a/datahub-web-react/tsconfig.json b/datahub-web-react/tsconfig.json
index 56361d52b21c3f..ebdff3429acd23 100644
--- a/datahub-web-react/tsconfig.json
+++ b/datahub-web-react/tsconfig.json
@@ -1,5 +1,6 @@
{
"compilerOptions": {
+ "baseUrl": ".",
"target": "es2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
@@ -13,10 +14,33 @@
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
- "isolatedModules": true,
+ "isolatedModules": false,
"noEmit": true,
"jsx": "react-jsx",
- "types": ["vitest/globals"]
+ "types": ["vitest/globals"],
+ "paths": {
+ "@src/*": ["./src/*"],
+ "@app/*": ["./src/app/*"],
+ "@conf/*": ["./src/conf/*"],
+ "@components": ["./src/alchemy-components"],
+ "@components/*": ["./src/alchemy-components/*"],
+ "@graphql/*": ["./src/graphql/*"],
+ "@graphql-mock/*": ["./src/graphql-mock/*"],
+ "@images/*": ["./src/images/*"],
+ "@providers/*": ["./src/providers/*"],
+ "@utils/*": ["./src/utils/*"],
+ "@app/entityV1/*": ["./src/app/entity/*"],
+ "@app/entityV2/*": ["./src/app/entityV2/*"],
+ "@app/searchV2/*": ["./src/app/searchV2/*"],
+ "@app/domainV2/*": ["./src/app/domainV2/*"],
+ "@app/glossaryV2/*": ["./src/app/glossaryV2/*"],
+ "@app/homeV2/*": ["./src/app/homeV2/*"],
+ "@app/lineageV2/*": ["./src/app/lineageV2/*"],
+ "@app/previewV2/*": ["./src/app/previewV2/*"],
+ "@app/sharedV2/*": ["./src/app/sharedV2/*"],
+ "@types": ["./src/types.generated.ts"],
+ "@images": ["./src/images"]
+ }
},
- "include": ["src", "src/conf/theme/styled-components.d.ts", "vite.config.ts", ".eslintrc.js"]
+ "include": ["src", "src/conf/theme/styled-components.d.ts", "vite.config.ts", ".eslintrc.js", "functions"]
}
diff --git a/datahub-web-react/vite.config.ts b/datahub-web-react/vite.config.ts
index 683b37974c85a1..2532b24067754d 100644
--- a/datahub-web-react/vite.config.ts
+++ b/datahub-web-react/vite.config.ts
@@ -97,5 +97,33 @@ export default defineConfig(({ mode }) => {
exclude: [],
},
},
+ resolve: {
+ alias: {
+ // Root Directories
+ '@src': path.resolve(__dirname, '/src'),
+ '@app': path.resolve(__dirname, '/src/app'),
+ '@conf': path.resolve(__dirname, '/src/conf'),
+ '@components': path.resolve(__dirname, 'src/alchemy-components'),
+ '@graphql': path.resolve(__dirname, 'src/graphql'),
+ '@graphql-mock': path.resolve(__dirname, 'src/graphql-mock'),
+ '@images': path.resolve(__dirname, 'src/images'),
+ '@providers': path.resolve(__dirname, 'src/providers'),
+ '@utils': path.resolve(__dirname, 'src/utils'),
+
+ // App Specific Directories
+ '@app/entityV1': path.resolve(__dirname, 'src/app/entity'),
+ '@app/entityV2': path.resolve(__dirname, 'src/app/entityV2'),
+ '@app/searchV2': path.resolve(__dirname, 'src/app/searchV2'),
+ '@app/domainV2': path.resolve(__dirname, 'src/app/domainV2'),
+ '@app/glossaryV2': path.resolve(__dirname, 'src/app/glossaryV2'),
+ '@app/homeV2': path.resolve(__dirname, 'src/app/homeV2'),
+ '@app/lineageV2': path.resolve(__dirname, 'src/app/lineageV2'),
+ '@app/previewV2': path.resolve(__dirname, 'src/app/previewV2'),
+ '@app/sharedV2': path.resolve(__dirname, 'src/app/sharedV2'),
+
+ // Specific Files
+ '@types': path.resolve(__dirname, 'src/types.generated.ts'),
+ },
+ },
};
});
diff --git a/datahub-web-react/yarn.lock b/datahub-web-react/yarn.lock
index ddda98d7f83268..13b5bbf638c15e 100644
--- a/datahub-web-react/yarn.lock
+++ b/datahub-web-react/yarn.lock
@@ -7,6 +7,11 @@
resolved "https://registry.yarnpkg.com/@adobe/css-tools/-/css-tools-4.3.2.tgz#a6abc715fb6884851fca9dad37fc34739a04fd11"
integrity sha512-DA5a1C0gD/pLOvhv33YMrbf2FK3oUzwNl9oOJqE4XVjuEtt6XIakRcsd7eLiOSPkp1kTRQGICTA8cKra/vFbjw==
+"@adobe/css-tools@^4.4.0":
+ version "4.4.1"
+ resolved "https://registry.yarnpkg.com/@adobe/css-tools/-/css-tools-4.4.1.tgz#2447a230bfe072c1659e6815129c03cf170710e3"
+ integrity sha512-12WGKBQzjUAI4ayyF4IAtfw2QR/IDoqk6jTddXDhtYTJF9ASmoE1zst7cVtP0aL/F1jUJL5r+JxKXKEgHNbEUQ==
+
"@ampproject/remapping@^2.2.0":
version "2.2.1"
resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.2.1.tgz#99e8e11851128b8702cd57c33684f1d0f260b630"
@@ -195,6 +200,15 @@
"@babel/highlight" "^7.23.4"
chalk "^2.4.2"
+"@babel/code-frame@^7.25.9", "@babel/code-frame@^7.26.0":
+ version "7.26.2"
+ resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.26.2.tgz#4b5fab97d33338eff916235055f0ebc21e573a85"
+ integrity sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==
+ dependencies:
+ "@babel/helper-validator-identifier" "^7.25.9"
+ js-tokens "^4.0.0"
+ picocolors "^1.0.0"
+
"@babel/compat-data@^7.20.5", "@babel/compat-data@^7.22.5":
version "7.22.5"
resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.22.5.tgz#b1f6c86a02d85d2dd3368a2b67c09add8cd0c255"
@@ -205,6 +219,11 @@
resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.23.5.tgz#ffb878728bb6bdcb6f4510aa51b1be9afb8cfd98"
integrity sha512-uU27kfDRlhfKl+w1U6vp16IuvSLtjAxdArVXPa9BvLkrr7CYIsxH5adpHObeAGY/41+syctUWOZ140a2Rvkgjw==
+"@babel/compat-data@^7.25.9":
+ version "7.26.3"
+ resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.26.3.tgz#99488264a56b2aded63983abd6a417f03b92ed02"
+ integrity sha512-nHIxvKPniQXpmQLb0vhY3VaFb3S0YrTAwpOWJZh1wn3oJPjJk9Asva204PsBdmAE8vpzfHudT8DB0scYvy9q0g==
+
"@babel/core@^7.14.0":
version "7.22.5"
resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.22.5.tgz#d67d9747ecf26ee7ecd3ebae1ee22225fe902a89"
@@ -247,6 +266,27 @@
json5 "^2.2.3"
semver "^6.3.1"
+"@babel/core@^7.18.9":
+ version "7.26.0"
+ resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.26.0.tgz#d78b6023cc8f3114ccf049eb219613f74a747b40"
+ integrity sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==
+ dependencies:
+ "@ampproject/remapping" "^2.2.0"
+ "@babel/code-frame" "^7.26.0"
+ "@babel/generator" "^7.26.0"
+ "@babel/helper-compilation-targets" "^7.25.9"
+ "@babel/helper-module-transforms" "^7.26.0"
+ "@babel/helpers" "^7.26.0"
+ "@babel/parser" "^7.26.0"
+ "@babel/template" "^7.25.9"
+ "@babel/traverse" "^7.25.9"
+ "@babel/types" "^7.26.0"
+ convert-source-map "^2.0.0"
+ debug "^4.1.0"
+ gensync "^1.0.0-beta.2"
+ json5 "^2.2.3"
+ semver "^6.3.1"
+
"@babel/core@^7.22.9":
version "7.23.5"
resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.23.5.tgz#6e23f2acbcb77ad283c5ed141f824fd9f70101c7"
@@ -298,6 +338,17 @@
"@jridgewell/trace-mapping" "^0.3.17"
jsesc "^2.5.1"
+"@babel/generator@^7.26.0":
+ version "7.26.3"
+ resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.26.3.tgz#ab8d4360544a425c90c248df7059881f4b2ce019"
+ integrity sha512-6FF/urZvD0sTeO7k6/B15pMLC4CHUv1426lzr3N01aHJTl046uCAh9LXW/fzeXXjPNCJ6iABW5XaWOsIZB93aQ==
+ dependencies:
+ "@babel/parser" "^7.26.3"
+ "@babel/types" "^7.26.3"
+ "@jridgewell/gen-mapping" "^0.3.5"
+ "@jridgewell/trace-mapping" "^0.3.25"
+ jsesc "^3.0.2"
+
"@babel/helper-annotate-as-pure@^7.0.0", "@babel/helper-annotate-as-pure@^7.22.5":
version "7.22.5"
resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.22.5.tgz#e7f06737b197d580a01edf75d97e2c8be99d3882"
@@ -327,6 +378,17 @@
lru-cache "^5.1.1"
semver "^6.3.1"
+"@babel/helper-compilation-targets@^7.25.9":
+ version "7.25.9"
+ resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.9.tgz#55af025ce365be3cdc0c1c1e56c6af617ce88875"
+ integrity sha512-j9Db8Suy6yV/VHa4qzrj9yZfZxhLWQdVnRlXxmKLYlhWUVB1sB2G5sxuWYXk/whHD9iW76PmNzxZ4UCnTQTVEQ==
+ dependencies:
+ "@babel/compat-data" "^7.25.9"
+ "@babel/helper-validator-option" "^7.25.9"
+ browserslist "^4.24.0"
+ lru-cache "^5.1.1"
+ semver "^6.3.1"
+
"@babel/helper-create-class-features-plugin@^7.18.6":
version "7.22.5"
resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.22.5.tgz#2192a1970ece4685fbff85b48da2c32fcb130b7c"
@@ -396,6 +458,14 @@
dependencies:
"@babel/types" "^7.22.15"
+"@babel/helper-module-imports@^7.25.9":
+ version "7.25.9"
+ resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz#e7f8d20602ebdbf9ebbea0a0751fb0f2a4141715"
+ integrity sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==
+ dependencies:
+ "@babel/traverse" "^7.25.9"
+ "@babel/types" "^7.25.9"
+
"@babel/helper-module-transforms@^7.22.5":
version "7.22.5"
resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.22.5.tgz#0f65daa0716961b6e96b164034e737f60a80d2ef"
@@ -432,6 +502,15 @@
"@babel/helper-split-export-declaration" "^7.22.6"
"@babel/helper-validator-identifier" "^7.22.20"
+"@babel/helper-module-transforms@^7.26.0":
+ version "7.26.0"
+ resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz#8ce54ec9d592695e58d84cd884b7b5c6a2fdeeae"
+ integrity sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==
+ dependencies:
+ "@babel/helper-module-imports" "^7.25.9"
+ "@babel/helper-validator-identifier" "^7.25.9"
+ "@babel/traverse" "^7.25.9"
+
"@babel/helper-optimise-call-expression@^7.22.5":
version "7.22.5"
resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.22.5.tgz#f21531a9ccbff644fdd156b4077c16ff0c3f609e"
@@ -494,6 +573,11 @@
resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz#9478c707febcbbe1ddb38a3d91a2e054ae622d83"
integrity sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==
+"@babel/helper-string-parser@^7.25.9":
+ version "7.25.9"
+ resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz#1aabb72ee72ed35789b4bbcad3ca2862ce614e8c"
+ integrity sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==
+
"@babel/helper-validator-identifier@^7.22.20":
version "7.22.20"
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz#c4ae002c61d2879e724581d96665583dbc1dc0e0"
@@ -504,6 +588,11 @@
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.5.tgz#9544ef6a33999343c8740fa51350f30eeaaaf193"
integrity sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ==
+"@babel/helper-validator-identifier@^7.25.9":
+ version "7.25.9"
+ resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz#24b64e2c3ec7cd3b3c547729b8d16871f22cbdc7"
+ integrity sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==
+
"@babel/helper-validator-option@^7.22.15":
version "7.23.5"
resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz#907a3fbd4523426285365d1206c423c4c5520307"
@@ -514,6 +603,11 @@
resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.22.5.tgz#de52000a15a177413c8234fa3a8af4ee8102d0ac"
integrity sha512-R3oB6xlIVKUnxNUxbmgq7pKjxpru24zlimpE8WK47fACIlM0II/Hm1RS8IaOI7NgCr6LNS+jl5l75m20npAziw==
+"@babel/helper-validator-option@^7.25.9":
+ version "7.25.9"
+ resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz#86e45bd8a49ab7e03f276577f96179653d41da72"
+ integrity sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==
+
"@babel/helpers@^7.22.5":
version "7.22.5"
resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.22.5.tgz#74bb4373eb390d1ceed74a15ef97767e63120820"
@@ -541,6 +635,14 @@
"@babel/traverse" "^7.23.5"
"@babel/types" "^7.23.5"
+"@babel/helpers@^7.26.0":
+ version "7.26.0"
+ resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.26.0.tgz#30e621f1eba5aa45fe6f4868d2e9154d884119a4"
+ integrity sha512-tbhNuIxNcVb21pInl3ZSjksLCvgdZy9KwJ8brv993QtIVKJBBkYXz4q4ZbAv31GdnC+R90np23L5FbEBlthAEw==
+ dependencies:
+ "@babel/template" "^7.25.9"
+ "@babel/types" "^7.26.0"
+
"@babel/highlight@^7.22.5":
version "7.22.5"
resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.22.5.tgz#aa6c05c5407a67ebce408162b7ede789b4d22031"
@@ -574,6 +676,13 @@
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.23.0.tgz#da950e622420bf96ca0d0f2909cdddac3acd8719"
integrity sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==
+"@babel/parser@^7.25.9", "@babel/parser@^7.26.0", "@babel/parser@^7.26.3":
+ version "7.26.3"
+ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.26.3.tgz#8c51c5db6ddf08134af1ddbacf16aaab48bac234"
+ integrity sha512-WJ/CvmY8Mea8iDXo6a7RK2wbmJITT5fN3BEkRuFlxVyNx8jOKIIhmC4fSkTcPcf8JyavbBwIe6OpiCOBXt/IcA==
+ dependencies:
+ "@babel/types" "^7.26.3"
+
"@babel/plugin-proposal-class-properties@^7.0.0":
version "7.18.6"
resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.18.6.tgz#b110f59741895f7ec21a6fff696ec46265c446a3"
@@ -823,6 +932,13 @@
dependencies:
regenerator-runtime "^0.13.11"
+"@babel/runtime@^7.17.8", "@babel/runtime@^7.23.9":
+ version "7.26.0"
+ resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.26.0.tgz#8600c2f595f277c60815256418b85356a65173c1"
+ integrity sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==
+ dependencies:
+ regenerator-runtime "^0.14.0"
+
"@babel/template@^7.18.10", "@babel/template@^7.22.15":
version "7.22.15"
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.22.15.tgz#09576efc3830f0430f4548ef971dde1350ef2f38"
@@ -841,7 +957,16 @@
"@babel/parser" "^7.22.5"
"@babel/types" "^7.22.5"
-"@babel/traverse@>=7.23.2", "@babel/traverse@^7.1.6", "@babel/traverse@^7.14.0", "@babel/traverse@^7.16.8", "@babel/traverse@^7.22.5", "@babel/traverse@^7.23.2", "@babel/traverse@^7.23.5", "@babel/traverse@^7.4.5":
+"@babel/template@^7.25.9":
+ version "7.25.9"
+ resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.25.9.tgz#ecb62d81a8a6f5dc5fe8abfc3901fc52ddf15016"
+ integrity sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==
+ dependencies:
+ "@babel/code-frame" "^7.25.9"
+ "@babel/parser" "^7.25.9"
+ "@babel/types" "^7.25.9"
+
+"@babel/traverse@>=7.23.2", "@babel/traverse@^7.1.6", "@babel/traverse@^7.14.0", "@babel/traverse@^7.16.8", "@babel/traverse@^7.18.9", "@babel/traverse@^7.22.5", "@babel/traverse@^7.23.2", "@babel/traverse@^7.23.5", "@babel/traverse@^7.25.9", "@babel/traverse@^7.4.5":
version "7.23.5"
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.23.5.tgz#f546bf9aba9ef2b042c0e00d245990c15508e7ec"
integrity sha512-czx7Xy5a6sapWWRx61m1Ke1Ra4vczu1mCTtJam5zRTBOonfdJ+S/B6HYmGYu3fJtr8GGET3si6IhgWVBhJ/m8w==
@@ -875,6 +1000,14 @@
"@babel/helper-validator-identifier" "^7.22.20"
to-fast-properties "^2.0.0"
+"@babel/types@^7.18.9", "@babel/types@^7.25.9", "@babel/types@^7.26.0", "@babel/types@^7.26.3":
+ version "7.26.3"
+ resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.26.3.tgz#37e79830f04c2b5687acc77db97fbc75fb81f3c0"
+ integrity sha512-vN5p+1kl59GVKMvTHt55NzzmYVxprfJD+ql7U9NFIfKCBkYE55LYtS+WtPlaYOyzydrKI8Nezd+aZextrd+FMA==
+ dependencies:
+ "@babel/helper-string-parser" "^7.25.9"
+ "@babel/helper-validator-identifier" "^7.25.9"
+
"@babel/types@^7.21.3":
version "7.23.6"
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.23.6.tgz#be33fdb151e1f5a56877d704492c240fc71c7ccd"
@@ -918,6 +1051,17 @@
"@emotion/weak-memoize" "^0.3.0"
stylis "4.1.3"
+"@emotion/cache@^11.11.0":
+ version "11.13.5"
+ resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-11.13.5.tgz#e78dad0489e1ed7572507ba8ed9d2130529e4266"
+ integrity sha512-Z3xbtJ+UcK76eWkagZ1onvn/wAVb1GOMuR15s30Fm2wrMgC7jzpnO2JZXr4eujTTqoQFUrZIw/rT0c6Zzjca1g==
+ dependencies:
+ "@emotion/memoize" "^0.9.0"
+ "@emotion/sheet" "^1.4.0"
+ "@emotion/utils" "^1.4.2"
+ "@emotion/weak-memoize" "^0.4.0"
+ stylis "4.2.0"
+
"@emotion/css@^11.1.3":
version "11.10.5"
resolved "https://registry.yarnpkg.com/@emotion/css/-/css-11.10.5.tgz#ca01bb83ce60517bc3a5c01d27ccf552fed84d9d"
@@ -958,6 +1102,11 @@
resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.8.0.tgz#f580f9beb67176fa57aae70b08ed510e1b18980f"
integrity sha512-G/YwXTkv7Den9mXDO7AhLWkE3q+I92B+VqAE+dYG4NGPaHZGvt3G8Q0p9vmE+sq7rTGphUbAvmQ9YpbfMQGGlA==
+"@emotion/memoize@^0.9.0":
+ version "0.9.0"
+ resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.9.0.tgz#745969d649977776b43fc7648c556aaa462b4102"
+ integrity sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==
+
"@emotion/react@^11.9.0":
version "11.10.5"
resolved "https://registry.yarnpkg.com/@emotion/react/-/react-11.10.5.tgz#95fff612a5de1efa9c0d535384d3cfa115fe175d"
@@ -988,6 +1137,11 @@
resolved "https://registry.yarnpkg.com/@emotion/sheet/-/sheet-1.2.1.tgz#0767e0305230e894897cadb6c8df2c51e61a6c2c"
integrity sha512-zxRBwl93sHMsOj4zs+OslQKg/uhF38MB+OMKoCrVuS0nyTkqnau+BM3WGEoOptg9Oz45T/aIGs1qbVAsEFo3nA==
+"@emotion/sheet@^1.4.0":
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/@emotion/sheet/-/sheet-1.4.0.tgz#c9299c34d248bc26e82563735f78953d2efca83c"
+ integrity sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==
+
"@emotion/styled@^11.3.0":
version "11.10.5"
resolved "https://registry.yarnpkg.com/@emotion/styled/-/styled-11.10.5.tgz#1fe7bf941b0909802cb826457e362444e7e96a79"
@@ -1025,121 +1179,251 @@
resolved "https://registry.yarnpkg.com/@emotion/utils/-/utils-1.2.0.tgz#9716eaccbc6b5ded2ea5a90d65562609aab0f561"
integrity sha512-sn3WH53Kzpw8oQ5mgMmIzzyAaH2ZqFEbozVVBSYp538E06OSE6ytOp7pRAjNQR+Q/orwqdQYJSe2m3hCOeznkw==
+"@emotion/utils@^1.4.2":
+ version "1.4.2"
+ resolved "https://registry.yarnpkg.com/@emotion/utils/-/utils-1.4.2.tgz#6df6c45881fcb1c412d6688a311a98b7f59c1b52"
+ integrity sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==
+
"@emotion/weak-memoize@^0.3.0":
version "0.3.0"
resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.3.0.tgz#ea89004119dc42db2e1dba0f97d553f7372f6fcb"
integrity sha512-AHPmaAx+RYfZz0eYu6Gviiagpmiyw98ySSlQvCUhVGDRtDFe4DBS0x1bSjdF3gqUDYOczB+yYvBTtEylYSdRhg==
+"@emotion/weak-memoize@^0.4.0":
+ version "0.4.0"
+ resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz#5e13fac887f08c44f76b0ccaf3370eb00fec9bb6"
+ integrity sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==
+
+"@esbuild/aix-ppc64@0.24.0":
+ version "0.24.0"
+ resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.24.0.tgz#b57697945b50e99007b4c2521507dc613d4a648c"
+ integrity sha512-WtKdFM7ls47zkKHFVzMz8opM7LkcsIp9amDUBIAWirg70RM71WRSjdILPsY5Uv1D42ZpUfaPILDlfactHgsRkw==
+
"@esbuild/android-arm64@0.18.20":
version "0.18.20"
resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz#984b4f9c8d0377443cc2dfcef266d02244593622"
integrity sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==
+"@esbuild/android-arm64@0.24.0":
+ version "0.24.0"
+ resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.24.0.tgz#1add7e0af67acefd556e407f8497e81fddad79c0"
+ integrity sha512-Vsm497xFM7tTIPYK9bNTYJyF/lsP590Qc1WxJdlB6ljCbdZKU9SY8i7+Iin4kyhV/KV5J2rOKsBQbB77Ab7L/w==
+
"@esbuild/android-arm@0.18.20":
version "0.18.20"
resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.18.20.tgz#fedb265bc3a589c84cc11f810804f234947c3682"
integrity sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==
+"@esbuild/android-arm@0.24.0":
+ version "0.24.0"
+ resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.24.0.tgz#ab7263045fa8e090833a8e3c393b60d59a789810"
+ integrity sha512-arAtTPo76fJ/ICkXWetLCc9EwEHKaeya4vMrReVlEIUCAUncH7M4bhMQ+M9Vf+FFOZJdTNMXNBrWwW+OXWpSew==
+
"@esbuild/android-x64@0.18.20":
version "0.18.20"
resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.18.20.tgz#35cf419c4cfc8babe8893d296cd990e9e9f756f2"
integrity sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==
+"@esbuild/android-x64@0.24.0":
+ version "0.24.0"
+ resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.24.0.tgz#e8f8b196cfdfdd5aeaebbdb0110983460440e705"
+ integrity sha512-t8GrvnFkiIY7pa7mMgJd7p8p8qqYIz1NYiAoKc75Zyv73L3DZW++oYMSHPRarcotTKuSs6m3hTOa5CKHaS02TQ==
+
"@esbuild/darwin-arm64@0.18.20":
version "0.18.20"
resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz#08172cbeccf95fbc383399a7f39cfbddaeb0d7c1"
integrity sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==
+"@esbuild/darwin-arm64@0.24.0":
+ version "0.24.0"
+ resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.24.0.tgz#2d0d9414f2acbffd2d86e98253914fca603a53dd"
+ integrity sha512-CKyDpRbK1hXwv79soeTJNHb5EiG6ct3efd/FTPdzOWdbZZfGhpbcqIpiD0+vwmpu0wTIL97ZRPZu8vUt46nBSw==
+
"@esbuild/darwin-x64@0.18.20":
version "0.18.20"
resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz#d70d5790d8bf475556b67d0f8b7c5bdff053d85d"
integrity sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==
+"@esbuild/darwin-x64@0.24.0":
+ version "0.24.0"
+ resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.24.0.tgz#33087aab31a1eb64c89daf3d2cf8ce1775656107"
+ integrity sha512-rgtz6flkVkh58od4PwTRqxbKH9cOjaXCMZgWD905JOzjFKW+7EiUObfd/Kav+A6Gyud6WZk9w+xu6QLytdi2OA==
+
"@esbuild/freebsd-arm64@0.18.20":
version "0.18.20"
resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz#98755cd12707f93f210e2494d6a4b51b96977f54"
integrity sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==
+"@esbuild/freebsd-arm64@0.24.0":
+ version "0.24.0"
+ resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.0.tgz#bb76e5ea9e97fa3c753472f19421075d3a33e8a7"
+ integrity sha512-6Mtdq5nHggwfDNLAHkPlyLBpE5L6hwsuXZX8XNmHno9JuL2+bg2BX5tRkwjyfn6sKbxZTq68suOjgWqCicvPXA==
+
"@esbuild/freebsd-x64@0.18.20":
version "0.18.20"
resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz#c1eb2bff03915f87c29cece4c1a7fa1f423b066e"
integrity sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==
+"@esbuild/freebsd-x64@0.24.0":
+ version "0.24.0"
+ resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.24.0.tgz#e0e2ce9249fdf6ee29e5dc3d420c7007fa579b93"
+ integrity sha512-D3H+xh3/zphoX8ck4S2RxKR6gHlHDXXzOf6f/9dbFt/NRBDIE33+cVa49Kil4WUjxMGW0ZIYBYtaGCa2+OsQwQ==
+
"@esbuild/linux-arm64@0.18.20":
version "0.18.20"
resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz#bad4238bd8f4fc25b5a021280c770ab5fc3a02a0"
integrity sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==
+"@esbuild/linux-arm64@0.24.0":
+ version "0.24.0"
+ resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.24.0.tgz#d1b2aa58085f73ecf45533c07c82d81235388e75"
+ integrity sha512-TDijPXTOeE3eaMkRYpcy3LarIg13dS9wWHRdwYRnzlwlA370rNdZqbcp0WTyyV/k2zSxfko52+C7jU5F9Tfj1g==
+
"@esbuild/linux-arm@0.18.20":
version "0.18.20"
resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz#3e617c61f33508a27150ee417543c8ab5acc73b0"
integrity sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==
+"@esbuild/linux-arm@0.24.0":
+ version "0.24.0"
+ resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.24.0.tgz#8e4915df8ea3e12b690a057e77a47b1d5935ef6d"
+ integrity sha512-gJKIi2IjRo5G6Glxb8d3DzYXlxdEj2NlkixPsqePSZMhLudqPhtZ4BUrpIuTjJYXxvF9njql+vRjB2oaC9XpBw==
+
"@esbuild/linux-ia32@0.18.20":
version "0.18.20"
resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz#699391cccba9aee6019b7f9892eb99219f1570a7"
integrity sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==
+"@esbuild/linux-ia32@0.24.0":
+ version "0.24.0"
+ resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.24.0.tgz#8200b1110666c39ab316572324b7af63d82013fb"
+ integrity sha512-K40ip1LAcA0byL05TbCQ4yJ4swvnbzHscRmUilrmP9Am7//0UjPreh4lpYzvThT2Quw66MhjG//20mrufm40mA==
+
"@esbuild/linux-loong64@0.18.20":
version "0.18.20"
resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz#e6fccb7aac178dd2ffb9860465ac89d7f23b977d"
integrity sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==
+"@esbuild/linux-loong64@0.24.0":
+ version "0.24.0"
+ resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.24.0.tgz#6ff0c99cf647504df321d0640f0d32e557da745c"
+ integrity sha512-0mswrYP/9ai+CU0BzBfPMZ8RVm3RGAN/lmOMgW4aFUSOQBjA31UP8Mr6DDhWSuMwj7jaWOT0p0WoZ6jeHhrD7g==
+
"@esbuild/linux-mips64el@0.18.20":
version "0.18.20"
resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz#eeff3a937de9c2310de30622a957ad1bd9183231"
integrity sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==
+"@esbuild/linux-mips64el@0.24.0":
+ version "0.24.0"
+ resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.24.0.tgz#3f720ccd4d59bfeb4c2ce276a46b77ad380fa1f3"
+ integrity sha512-hIKvXm0/3w/5+RDtCJeXqMZGkI2s4oMUGj3/jM0QzhgIASWrGO5/RlzAzm5nNh/awHE0A19h/CvHQe6FaBNrRA==
+
"@esbuild/linux-ppc64@0.18.20":
version "0.18.20"
resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz#2f7156bde20b01527993e6881435ad79ba9599fb"
integrity sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==
+"@esbuild/linux-ppc64@0.24.0":
+ version "0.24.0"
+ resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.24.0.tgz#9d6b188b15c25afd2e213474bf5f31e42e3aa09e"
+ integrity sha512-HcZh5BNq0aC52UoocJxaKORfFODWXZxtBaaZNuN3PUX3MoDsChsZqopzi5UupRhPHSEHotoiptqikjN/B77mYQ==
+
"@esbuild/linux-riscv64@0.18.20":
version "0.18.20"
resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz#6628389f210123d8b4743045af8caa7d4ddfc7a6"
integrity sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==
+"@esbuild/linux-riscv64@0.24.0":
+ version "0.24.0"
+ resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.24.0.tgz#f989fdc9752dfda286c9cd87c46248e4dfecbc25"
+ integrity sha512-bEh7dMn/h3QxeR2KTy1DUszQjUrIHPZKyO6aN1X4BCnhfYhuQqedHaa5MxSQA/06j3GpiIlFGSsy1c7Gf9padw==
+
"@esbuild/linux-s390x@0.18.20":
version "0.18.20"
resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz#255e81fb289b101026131858ab99fba63dcf0071"
integrity sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==
+"@esbuild/linux-s390x@0.24.0":
+ version "0.24.0"
+ resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.24.0.tgz#29ebf87e4132ea659c1489fce63cd8509d1c7319"
+ integrity sha512-ZcQ6+qRkw1UcZGPyrCiHHkmBaj9SiCD8Oqd556HldP+QlpUIe2Wgn3ehQGVoPOvZvtHm8HPx+bH20c9pvbkX3g==
+
"@esbuild/linux-x64@0.18.20":
version "0.18.20"
resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz#c7690b3417af318a9b6f96df3031a8865176d338"
integrity sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==
+"@esbuild/linux-x64@0.24.0":
+ version "0.24.0"
+ resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.24.0.tgz#4af48c5c0479569b1f359ffbce22d15f261c0cef"
+ integrity sha512-vbutsFqQ+foy3wSSbmjBXXIJ6PL3scghJoM8zCL142cGaZKAdCZHyf+Bpu/MmX9zT9Q0zFBVKb36Ma5Fzfa8xA==
+
"@esbuild/netbsd-x64@0.18.20":
version "0.18.20"
resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz#30e8cd8a3dded63975e2df2438ca109601ebe0d1"
integrity sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==
+"@esbuild/netbsd-x64@0.24.0":
+ version "0.24.0"
+ resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.24.0.tgz#1ae73d23cc044a0ebd4f198334416fb26c31366c"
+ integrity sha512-hjQ0R/ulkO8fCYFsG0FZoH+pWgTTDreqpqY7UnQntnaKv95uP5iW3+dChxnx7C3trQQU40S+OgWhUVwCjVFLvg==
+
+"@esbuild/openbsd-arm64@0.24.0":
+ version "0.24.0"
+ resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.0.tgz#5d904a4f5158c89859fd902c427f96d6a9e632e2"
+ integrity sha512-MD9uzzkPQbYehwcN583yx3Tu5M8EIoTD+tUgKF982WYL9Pf5rKy9ltgD0eUgs8pvKnmizxjXZyLt0z6DC3rRXg==
+
"@esbuild/openbsd-x64@0.18.20":
version "0.18.20"
resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz#7812af31b205055874c8082ea9cf9ab0da6217ae"
integrity sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==
+"@esbuild/openbsd-x64@0.24.0":
+ version "0.24.0"
+ resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.24.0.tgz#4c8aa88c49187c601bae2971e71c6dc5e0ad1cdf"
+ integrity sha512-4ir0aY1NGUhIC1hdoCzr1+5b43mw99uNwVzhIq1OY3QcEwPDO3B7WNXBzaKY5Nsf1+N11i1eOfFcq+D/gOS15Q==
+
"@esbuild/sunos-x64@0.18.20":
version "0.18.20"
resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz#d5c275c3b4e73c9b0ecd38d1ca62c020f887ab9d"
integrity sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==
+"@esbuild/sunos-x64@0.24.0":
+ version "0.24.0"
+ resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.24.0.tgz#8ddc35a0ea38575fa44eda30a5ee01ae2fa54dd4"
+ integrity sha512-jVzdzsbM5xrotH+W5f1s+JtUy1UWgjU0Cf4wMvffTB8m6wP5/kx0KiaLHlbJO+dMgtxKV8RQ/JvtlFcdZ1zCPA==
+
"@esbuild/win32-arm64@0.18.20":
version "0.18.20"
resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz#73bc7f5a9f8a77805f357fab97f290d0e4820ac9"
integrity sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==
+"@esbuild/win32-arm64@0.24.0":
+ version "0.24.0"
+ resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.24.0.tgz#6e79c8543f282c4539db684a207ae0e174a9007b"
+ integrity sha512-iKc8GAslzRpBytO2/aN3d2yb2z8XTVfNV0PjGlCxKo5SgWmNXx82I/Q3aG1tFfS+A2igVCY97TJ8tnYwpUWLCA==
+
"@esbuild/win32-ia32@0.18.20":
version "0.18.20"
resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz#ec93cbf0ef1085cc12e71e0d661d20569ff42102"
integrity sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==
+"@esbuild/win32-ia32@0.24.0":
+ version "0.24.0"
+ resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.24.0.tgz#057af345da256b7192d18b676a02e95d0fa39103"
+ integrity sha512-vQW36KZolfIudCcTnaTpmLQ24Ha1RjygBo39/aLkM2kmjkWmZGEJ5Gn9l5/7tzXA42QGIoWbICfg6KLLkIw6yw==
+
"@esbuild/win32-x64@0.18.20":
version "0.18.20"
resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz#786c5f41f043b07afb1af37683d7c33668858f6d"
integrity sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==
+"@esbuild/win32-x64@0.24.0":
+ version "0.24.0"
+ resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.24.0.tgz#168ab1c7e1c318b922637fad8f339d48b01e1244"
+ integrity sha512-7IAFPrjSQIJrGsK6flwg7NFmwBoSTyF3rl7If0hNUFQU4ilTsEPL6GuMuU9BfIWVVGuRnuIidkSMC+c0Otu8IA==
+
"@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.4.0":
version "4.4.0"
resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59"
@@ -1172,6 +1456,16 @@
resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.43.0.tgz#559ca3d9ddbd6bf907ad524320a0d14b85586af0"
integrity sha512-s2UHCoiXfxMvmfzqoN+vrQ84ahUSYde9qNO1MdxmoEhyHWsfmwOpFlwYV+ePJEVc7gFnATGUi376WowX1N7tFg==
+"@fontsource/mulish@^5.0.16":
+ version "5.1.0"
+ resolved "https://registry.yarnpkg.com/@fontsource/mulish/-/mulish-5.1.0.tgz#e5cfb25d3dc83e0f4f0c828708b304bf33c9f7a4"
+ integrity sha512-dETFYrY9aGE1aD8g3xSkpyAir2YDjrnYsujZrZj1C/3gv3iKlr2MUQYUFRTCdcnoFLMXpq2izDW/644OGgYsPQ==
+
+"@geometricpanda/storybook-addon-badges@^2.0.2":
+ version "2.0.5"
+ resolved "https://registry.yarnpkg.com/@geometricpanda/storybook-addon-badges/-/storybook-addon-badges-2.0.5.tgz#eea1c27df8956eddeca612a82e5606034232da95"
+ integrity sha512-FH56ly6ZhltjyKQWxUKORP67BxhL9FMJRByS5lqKZpeP8J2MMsMXG7eQmFXKcZGQORfVQye+1uYYWXweDOiFTQ==
+
"@graphql-codegen/add@^2.0.2":
version "2.0.2"
resolved "https://registry.yarnpkg.com/@graphql-codegen/add/-/add-2.0.2.tgz#4acbb95be9ebb859a3cebfe7132fdf49ffe06dd8"
@@ -1654,6 +1948,14 @@
dependencies:
"@sinclair/typebox" "^0.27.8"
+"@joshwooding/vite-plugin-react-docgen-typescript@0.4.2":
+ version "0.4.2"
+ resolved "https://registry.yarnpkg.com/@joshwooding/vite-plugin-react-docgen-typescript/-/vite-plugin-react-docgen-typescript-0.4.2.tgz#c2591d2d7b02160341672d6bf3cc248dd60f2530"
+ integrity sha512-feQ+ntr+8hbVudnsTUapiMN9q8T90XA1d5jn9QzY09sNoj4iD9wi0PY1vsBFTda4ZjEaxRK9S81oarR2nj7TFQ==
+ dependencies:
+ magic-string "^0.27.0"
+ react-docgen-typescript "^2.2.2"
+
"@jridgewell/gen-mapping@^0.3.0", "@jridgewell/gen-mapping@^0.3.2":
version "0.3.3"
resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz#7e02e6eb5df901aaedb08514203b096614024098"
@@ -1663,21 +1965,45 @@
"@jridgewell/sourcemap-codec" "^1.4.10"
"@jridgewell/trace-mapping" "^0.3.9"
+"@jridgewell/gen-mapping@^0.3.5":
+ version "0.3.5"
+ resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz#dcce6aff74bdf6dad1a95802b69b04a2fcb1fb36"
+ integrity sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==
+ dependencies:
+ "@jridgewell/set-array" "^1.2.1"
+ "@jridgewell/sourcemap-codec" "^1.4.10"
+ "@jridgewell/trace-mapping" "^0.3.24"
+
"@jridgewell/resolve-uri@3.1.0":
version "3.1.0"
resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz#2203b118c157721addfe69d47b70465463066d78"
integrity sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==
+"@jridgewell/resolve-uri@^3.1.0":
+ version "3.1.2"
+ resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6"
+ integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==
+
"@jridgewell/set-array@^1.0.1":
version "1.1.2"
resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72"
integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==
+"@jridgewell/set-array@^1.2.1":
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.2.1.tgz#558fb6472ed16a4c850b889530e6b36438c49280"
+ integrity sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==
+
"@jridgewell/sourcemap-codec@1.4.14", "@jridgewell/sourcemap-codec@^1.4.10":
version "1.4.14"
resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24"
integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==
+"@jridgewell/sourcemap-codec@^1.4.13", "@jridgewell/sourcemap-codec@^1.4.14", "@jridgewell/sourcemap-codec@^1.5.0":
+ version "1.5.0"
+ resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz#3188bcb273a414b0d215fd22a58540b989b9409a"
+ integrity sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==
+
"@jridgewell/sourcemap-codec@^1.4.15":
version "1.4.15"
resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32"
@@ -1691,6 +2017,14 @@
"@jridgewell/resolve-uri" "3.1.0"
"@jridgewell/sourcemap-codec" "1.4.14"
+"@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25":
+ version "0.3.25"
+ resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz#15f190e98895f3fc23276ee14bc76b675c2e50f0"
+ integrity sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==
+ dependencies:
+ "@jridgewell/resolve-uri" "^3.1.0"
+ "@jridgewell/sourcemap-codec" "^1.4.14"
+
"@linaria/core@3.0.0-beta.13":
version "3.0.0-beta.13"
resolved "https://registry.yarnpkg.com/@linaria/core/-/core-3.0.0-beta.13.tgz#049c5be5faa67e341e413a0f6b641d5d78d91056"
@@ -1719,6 +2053,13 @@
refractor "^3.3.1"
unist-util-visit "^2.0.3"
+"@mdx-js/react@^3.0.0":
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/@mdx-js/react/-/react-3.1.0.tgz#c4522e335b3897b9a845db1dbdd2f966ae8fb0ed"
+ integrity sha512-QjHtSaoameoalGnKDT3FoIl4+9RwyTmo9ZJGBdLOks/YOiWHoRDI3PUwEzOE7kEmGcV3AFcp9K6dYu9rEuKLAQ==
+ dependencies:
+ "@types/mdx" "^2.0.0"
+
"@monaco-editor/loader@^1.2.0":
version "1.2.0"
resolved "https://registry.yarnpkg.com/@monaco-editor/loader/-/loader-1.2.0.tgz#373fad69973384624e3d9b60eefd786461a76acd"
@@ -1753,6 +2094,36 @@
resolved "https://registry.yarnpkg.com/@mui/core-downloads-tracker/-/core-downloads-tracker-5.10.15.tgz#490f3dea5327c892f063496a0219c48301da0fa0"
integrity sha512-xFcS0LpdF0Q1qJrrNsYUv9PU+ovvhCEPTOMw2jcpEFtl3CA87dLpvztORR5oE2UBFjWF7qLQLOwboQU1+xC7Cw==
+"@mui/core-downloads-tracker@^5.16.9":
+ version "5.16.9"
+ resolved "https://registry.yarnpkg.com/@mui/core-downloads-tracker/-/core-downloads-tracker-5.16.9.tgz#df68e6b685857d48cab5ed73ec2bf47fdd48d252"
+ integrity sha512-ue3j79XJ56+F6DlTtFTM+n//5AvNENOvl3MFruZZP5iZzz+hOq6WBwnr+YxiMlr+kvmMHuHxgOHFdPR8+mElDw==
+
+"@mui/icons-material@^5.15.21":
+ version "5.16.9"
+ resolved "https://registry.yarnpkg.com/@mui/icons-material/-/icons-material-5.16.9.tgz#0f754ef0b943e3bea78343cd2861e8d95a114617"
+ integrity sha512-nnOJIqan6FS6zEsLX3vf8LZ4vXpZjP5xfCFezeXmqfQConypCOZG4nangoVwKwROlas7b6/bqOdacFUb/HuM/g==
+ dependencies:
+ "@babel/runtime" "^7.23.9"
+
+"@mui/material@^5.15.21":
+ version "5.16.9"
+ resolved "https://registry.yarnpkg.com/@mui/material/-/material-5.16.9.tgz#d0d3e2e9882d11d94171298e2d7ab38db736ce1f"
+ integrity sha512-XC0oHFm7mrWV0tvhed9uv/o6kLNClnLj1eo/ufuKbj+rgk47ek8Y6HjHe3cGvMn4Bcq8KyoQPgzdwqvS2ZzYrA==
+ dependencies:
+ "@babel/runtime" "^7.23.9"
+ "@mui/core-downloads-tracker" "^5.16.9"
+ "@mui/system" "^5.16.8"
+ "@mui/types" "^7.2.15"
+ "@mui/utils" "^5.16.8"
+ "@popperjs/core" "^2.11.8"
+ "@types/react-transition-group" "^4.4.10"
+ clsx "^2.1.0"
+ csstype "^3.1.3"
+ prop-types "^15.8.1"
+ react-is "^18.3.1"
+ react-transition-group "^4.4.5"
+
"@mui/material@^5.8.5":
version "5.10.15"
resolved "https://registry.yarnpkg.com/@mui/material/-/material-5.10.15.tgz#37345f5a3d71c662703af7b5be0cca229b2a1416"
@@ -1780,6 +2151,15 @@
"@mui/utils" "^5.10.15"
prop-types "^15.8.1"
+"@mui/private-theming@^5.16.8":
+ version "5.16.8"
+ resolved "https://registry.yarnpkg.com/@mui/private-theming/-/private-theming-5.16.8.tgz#7914996caaf6eedc59914aeab83dcd2d4e4da1ec"
+ integrity sha512-3Vl9yFVLU6T3CFtxRMQTcJ60Ijv7wxQi4yjH92+9YXcsqvVspeIYoocqNoIV/1bXGYfyWu5zrCmwQVHaGY7bug==
+ dependencies:
+ "@babel/runtime" "^7.23.9"
+ "@mui/utils" "^5.16.8"
+ prop-types "^15.8.1"
+
"@mui/styled-engine@^5.10.14":
version "5.10.14"
resolved "https://registry.yarnpkg.com/@mui/styled-engine/-/styled-engine-5.10.14.tgz#4395198a1919254a3edabf6e8fc8d43c9c59b5c3"
@@ -1790,6 +2170,16 @@
csstype "^3.1.1"
prop-types "^15.8.1"
+"@mui/styled-engine@^5.16.8":
+ version "5.16.8"
+ resolved "https://registry.yarnpkg.com/@mui/styled-engine/-/styled-engine-5.16.8.tgz#b8ca35f93f503a51d0759a05475bfd28e10757ea"
+ integrity sha512-OFdgFf8JczSRs0kvWGdSn0ZeXxWrY0LITDPJ/nAtLEvUUTyrlFaO4il3SECX8ruzvf1VnAxHx4M/4mX9oOn9yA==
+ dependencies:
+ "@babel/runtime" "^7.23.9"
+ "@emotion/cache" "^11.11.0"
+ csstype "^3.1.3"
+ prop-types "^15.8.1"
+
"@mui/system@^5.10.15":
version "5.10.15"
resolved "https://registry.yarnpkg.com/@mui/system/-/system-5.10.15.tgz#4bb58d1d1a531137559b775038a18d6050d9ee57"
@@ -1804,11 +2194,30 @@
csstype "^3.1.1"
prop-types "^15.8.1"
+"@mui/system@^5.16.8":
+ version "5.16.8"
+ resolved "https://registry.yarnpkg.com/@mui/system/-/system-5.16.8.tgz#e5010d76cd2fdcc403ad3f98abfba99d330055ad"
+ integrity sha512-L32TaFDFpGIi1g6ysRtmhc9zDgrlxDXu3NlrGE8gAsQw/ziHrPdr0PNr20O0POUshA1q14W4dNZ/z0Nx2F9lhA==
+ dependencies:
+ "@babel/runtime" "^7.23.9"
+ "@mui/private-theming" "^5.16.8"
+ "@mui/styled-engine" "^5.16.8"
+ "@mui/types" "^7.2.15"
+ "@mui/utils" "^5.16.8"
+ clsx "^2.1.0"
+ csstype "^3.1.3"
+ prop-types "^15.8.1"
+
"@mui/types@^7.2.1":
version "7.2.1"
resolved "https://registry.yarnpkg.com/@mui/types/-/types-7.2.1.tgz#1eb2bc182c595029884047f2525ad4dbefea318e"
integrity sha512-c5mSM7ivD8EsqK6HUi9hQPr5V7TJ/IRThUQ9nWNYPdhCGriTSQV4vL6DflT99LkM+wLiIS1rVjphpEWxERep7A==
+"@mui/types@^7.2.15":
+ version "7.2.19"
+ resolved "https://registry.yarnpkg.com/@mui/types/-/types-7.2.19.tgz#c941954dd24393fdce5f07830d44440cf4ab6c80"
+ integrity sha512-6XpZEM/Q3epK9RN8ENoXuygnqUQxE+siN/6rGRi2iwJPgBUR25mphYQ9ZI87plGh58YoZ5pp40bFvKYOCDJ3tA==
+
"@mui/utils@^5.10.15":
version "5.10.15"
resolved "https://registry.yarnpkg.com/@mui/utils/-/utils-5.10.15.tgz#54fc1b373508d20dd5568070b2dcc0818e6bebba"
@@ -1820,6 +2229,18 @@
prop-types "^15.8.1"
react-is "^18.2.0"
+"@mui/utils@^5.16.8":
+ version "5.16.8"
+ resolved "https://registry.yarnpkg.com/@mui/utils/-/utils-5.16.8.tgz#e44acf38d446d361347c46b3e81ae366f615f37b"
+ integrity sha512-P/yb7BSWallQUeiNGxb+TM8epHteIUC8gzNTdPV2VfKhVY/EnGliHgt5np0GPkjQ7EzwDi/+gBevrAJtf+K94A==
+ dependencies:
+ "@babel/runtime" "^7.23.9"
+ "@mui/types" "^7.2.15"
+ "@types/prop-types" "^15.7.12"
+ clsx "^2.1.1"
+ prop-types "^15.8.1"
+ react-is "^18.3.1"
+
"@nodelib/fs.scandir@2.1.5":
version "2.1.5"
resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5"
@@ -1880,6 +2301,11 @@
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.6.tgz#cee20bd55e68a1720bdab363ecf0c821ded4cd45"
integrity sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw==
+"@popperjs/core@^2.11.8":
+ version "2.11.8"
+ resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.8.tgz#6b79032e760a0899cd4204710beede972a3a185f"
+ integrity sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==
+
"@rc-component/portal@^1.0.0-6", "@rc-component/portal@^1.0.0-8", "@rc-component/portal@^1.0.2":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@rc-component/portal/-/portal-1.1.0.tgz#6b94450d2c2b00d50b141bd7a0be23bd96503dbe"
@@ -2866,6 +3292,15 @@
resolved "https://registry.yarnpkg.com/@repeaterjs/repeater/-/repeater-3.0.5.tgz#b77571685410217a548a9c753aa3cdfc215bfc78"
integrity sha512-l3YHBLAol6d/IKnB9LhpD0cEZWAoe3eFKUyTYWmFmCO2Q/WOckxLQAUyMZWwZV2M/m3+4vgRoaolFqaII82/TA==
+"@rollup/pluginutils@^5.0.2":
+ version "5.1.3"
+ resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-5.1.3.tgz#3001bf1a03f3ad24457591f2c259c8e514e0dbdf"
+ integrity sha512-Pnsb6f32CD2W3uCaLZIzDmeFyQ2b8UWMFI7xtwUezpcGBDVDW6y9XgAWIlARiGAo6eNF5FK5aQTr0LFyNyqq5A==
+ dependencies:
+ "@types/estree" "^1.0.0"
+ estree-walker "^2.0.2"
+ picomatch "^4.0.2"
+
"@rollup/pluginutils@^5.0.4":
version "5.0.5"
resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-5.0.5.tgz#bbb4c175e19ebfeeb8c132c2eea0ecb89941a66c"
@@ -2890,6 +3325,259 @@
resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.27.8.tgz#6667fac16c436b5434a387a34dedb013198f6e6e"
integrity sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==
+"@storybook/addon-actions@8.4.7":
+ version "8.4.7"
+ resolved "https://registry.yarnpkg.com/@storybook/addon-actions/-/addon-actions-8.4.7.tgz#210c6bb5a7e17c3664c300b4b69b6243ec34b9cd"
+ integrity sha512-mjtD5JxcPuW74T6h7nqMxWTvDneFtokg88p6kQ5OnC1M259iAXb//yiSZgu/quunMHPCXSiqn4FNOSgASTSbsA==
+ dependencies:
+ "@storybook/global" "^5.0.0"
+ "@types/uuid" "^9.0.1"
+ dequal "^2.0.2"
+ polished "^4.2.2"
+ uuid "^9.0.0"
+
+"@storybook/addon-backgrounds@8.4.7":
+ version "8.4.7"
+ resolved "https://registry.yarnpkg.com/@storybook/addon-backgrounds/-/addon-backgrounds-8.4.7.tgz#56856bdafc5a2ba18cc19422320883c9e8f66c1c"
+ integrity sha512-I4/aErqtFiazcoWyKafOAm3bLpxTj6eQuH/woSbk1Yx+EzN+Dbrgx1Updy8//bsNtKkcrXETITreqHC+a57DHQ==
+ dependencies:
+ "@storybook/global" "^5.0.0"
+ memoizerific "^1.11.3"
+ ts-dedent "^2.0.0"
+
+"@storybook/addon-controls@8.4.7":
+ version "8.4.7"
+ resolved "https://registry.yarnpkg.com/@storybook/addon-controls/-/addon-controls-8.4.7.tgz#0c2ace0c7056248577f08f90471f29e861b485be"
+ integrity sha512-377uo5IsJgXLnQLJixa47+11V+7Wn9KcDEw+96aGCBCfLbWNH8S08tJHHnSu+jXg9zoqCAC23MetntVp6LetHA==
+ dependencies:
+ "@storybook/global" "^5.0.0"
+ dequal "^2.0.2"
+ ts-dedent "^2.0.0"
+
+"@storybook/addon-docs@8.4.7":
+ version "8.4.7"
+ resolved "https://registry.yarnpkg.com/@storybook/addon-docs/-/addon-docs-8.4.7.tgz#556515da1049f97023427301e11ecb52d0b9dbe7"
+ integrity sha512-NwWaiTDT5puCBSUOVuf6ME7Zsbwz7Y79WF5tMZBx/sLQ60vpmJVQsap6NSjvK1Ravhc21EsIXqemAcBjAWu80w==
+ dependencies:
+ "@mdx-js/react" "^3.0.0"
+ "@storybook/blocks" "8.4.7"
+ "@storybook/csf-plugin" "8.4.7"
+ "@storybook/react-dom-shim" "8.4.7"
+ react "^16.8.0 || ^17.0.0 || ^18.0.0"
+ react-dom "^16.8.0 || ^17.0.0 || ^18.0.0"
+ ts-dedent "^2.0.0"
+
+"@storybook/addon-essentials@^8.1.11":
+ version "8.4.7"
+ resolved "https://registry.yarnpkg.com/@storybook/addon-essentials/-/addon-essentials-8.4.7.tgz#381c74230d1b1a209d5fdc017d241c016b98affe"
+ integrity sha512-+BtZHCBrYtQKILtejKxh0CDRGIgTl9PumfBOKRaihYb4FX1IjSAxoV/oo/IfEjlkF5f87vouShWsRa8EUauFDw==
+ dependencies:
+ "@storybook/addon-actions" "8.4.7"
+ "@storybook/addon-backgrounds" "8.4.7"
+ "@storybook/addon-controls" "8.4.7"
+ "@storybook/addon-docs" "8.4.7"
+ "@storybook/addon-highlight" "8.4.7"
+ "@storybook/addon-measure" "8.4.7"
+ "@storybook/addon-outline" "8.4.7"
+ "@storybook/addon-toolbars" "8.4.7"
+ "@storybook/addon-viewport" "8.4.7"
+ ts-dedent "^2.0.0"
+
+"@storybook/addon-highlight@8.4.7":
+ version "8.4.7"
+ resolved "https://registry.yarnpkg.com/@storybook/addon-highlight/-/addon-highlight-8.4.7.tgz#06b9752977e38884007e9446f9a2b0c04c873229"
+ integrity sha512-whQIDBd3PfVwcUCrRXvCUHWClXe9mQ7XkTPCdPo4B/tZ6Z9c6zD8JUHT76ddyHivixFLowMnA8PxMU6kCMAiNw==
+ dependencies:
+ "@storybook/global" "^5.0.0"
+
+"@storybook/addon-interactions@^8.1.11":
+ version "8.4.7"
+ resolved "https://registry.yarnpkg.com/@storybook/addon-interactions/-/addon-interactions-8.4.7.tgz#d34545db5ea6f03a5499ad6742c3317fb9e02d55"
+ integrity sha512-fnufT3ym8ht3HHUIRVXAH47iOJW/QOb0VSM+j269gDuvyDcY03D1civCu1v+eZLGaXPKJ8vtjr0L8zKQ/4P0JQ==
+ dependencies:
+ "@storybook/global" "^5.0.0"
+ "@storybook/instrumenter" "8.4.7"
+ "@storybook/test" "8.4.7"
+ polished "^4.2.2"
+ ts-dedent "^2.2.0"
+
+"@storybook/addon-links@^8.1.11":
+ version "8.4.7"
+ resolved "https://registry.yarnpkg.com/@storybook/addon-links/-/addon-links-8.4.7.tgz#c38b2b63c3b0308adacff4b0758464a0657154c4"
+ integrity sha512-L/1h4dMeMKF+MM0DanN24v5p3faNYbbtOApMgg7SlcBT/tgo3+cAjkgmNpYA8XtKnDezm+T2mTDhB8mmIRZpIQ==
+ dependencies:
+ "@storybook/csf" "^0.1.11"
+ "@storybook/global" "^5.0.0"
+ ts-dedent "^2.0.0"
+
+"@storybook/addon-measure@8.4.7":
+ version "8.4.7"
+ resolved "https://registry.yarnpkg.com/@storybook/addon-measure/-/addon-measure-8.4.7.tgz#9d556ba34b57c13ad8d00bd953b27ec405a64d23"
+ integrity sha512-QfvqYWDSI5F68mKvafEmZic3SMiK7zZM8VA0kTXx55hF/+vx61Mm0HccApUT96xCXIgmwQwDvn9gS4TkX81Dmw==
+ dependencies:
+ "@storybook/global" "^5.0.0"
+ tiny-invariant "^1.3.1"
+
+"@storybook/addon-onboarding@^8.1.11":
+ version "8.4.7"
+ resolved "https://registry.yarnpkg.com/@storybook/addon-onboarding/-/addon-onboarding-8.4.7.tgz#212a5e27db1ee8440a2dd0d5c67ac29f0e6efda5"
+ integrity sha512-FdC2NV60VNYeMxf6DVe0qV9ucSBAzMh1//C0Qqwq8CcjthMbmKlVZ7DqbVsbIHKnFaSCaUC88eR5olAfMaauCQ==
+ dependencies:
+ react-confetti "^6.1.0"
+
+"@storybook/addon-outline@8.4.7":
+ version "8.4.7"
+ resolved "https://registry.yarnpkg.com/@storybook/addon-outline/-/addon-outline-8.4.7.tgz#8a35fe519dd639bb287a370da2222e6ffdce4020"
+ integrity sha512-6LYRqUZxSodmAIl8icr585Oi8pmzbZ90aloZJIpve+dBAzo7ydYrSQxxoQEVltXbKf3VeVcrs64ouAYqjisMYA==
+ dependencies:
+ "@storybook/global" "^5.0.0"
+ ts-dedent "^2.0.0"
+
+"@storybook/addon-toolbars@8.4.7":
+ version "8.4.7"
+ resolved "https://registry.yarnpkg.com/@storybook/addon-toolbars/-/addon-toolbars-8.4.7.tgz#b898d4deaf6f5a58f3b70bd8d136cd4ec2844b79"
+ integrity sha512-OSfdv5UZs+NdGB+nZmbafGUWimiweJ/56gShlw8Neo/4jOJl1R3rnRqqY7MYx8E4GwoX+i3GF5C3iWFNQqlDcw==
+
+"@storybook/addon-viewport@8.4.7":
+ version "8.4.7"
+ resolved "https://registry.yarnpkg.com/@storybook/addon-viewport/-/addon-viewport-8.4.7.tgz#e65c53608f52149c06347b395487960605fc4805"
+ integrity sha512-hvczh/jjuXXcOogih09a663sRDDSATXwbE866al1DXgbDFraYD/LxX/QDb38W9hdjU9+Qhx8VFIcNWoMQns5HQ==
+ dependencies:
+ memoizerific "^1.11.3"
+
+"@storybook/blocks@8.4.7", "@storybook/blocks@^8.1.11":
+ version "8.4.7"
+ resolved "https://registry.yarnpkg.com/@storybook/blocks/-/blocks-8.4.7.tgz#ee17f59dd52d11c97c39b0f6b03957085a80ad95"
+ integrity sha512-+QH7+JwXXXIyP3fRCxz/7E2VZepAanXJM7G8nbR3wWsqWgrRp4Wra6MvybxAYCxU7aNfJX5c+RW84SNikFpcIA==
+ dependencies:
+ "@storybook/csf" "^0.1.11"
+ "@storybook/icons" "^1.2.12"
+ ts-dedent "^2.0.0"
+
+"@storybook/builder-vite@8.4.7", "@storybook/builder-vite@^8.1.11":
+ version "8.4.7"
+ resolved "https://registry.yarnpkg.com/@storybook/builder-vite/-/builder-vite-8.4.7.tgz#3d6d542fa1f46fce5ee7a159dc8491cb4421254d"
+ integrity sha512-LovyXG5VM0w7CovI/k56ZZyWCveQFVDl0m7WwetpmMh2mmFJ+uPQ35BBsgTvTfc8RHi+9Q3F58qP1MQSByXi9g==
+ dependencies:
+ "@storybook/csf-plugin" "8.4.7"
+ browser-assert "^1.2.1"
+ ts-dedent "^2.0.0"
+
+"@storybook/components@8.4.7":
+ version "8.4.7"
+ resolved "https://registry.yarnpkg.com/@storybook/components/-/components-8.4.7.tgz#09eeffa07aa672ad3966ca1764a43003731b1d30"
+ integrity sha512-uyJIcoyeMWKAvjrG9tJBUCKxr2WZk+PomgrgrUwejkIfXMO76i6jw9BwLa0NZjYdlthDv30r9FfbYZyeNPmF0g==
+
+"@storybook/core@8.4.7":
+ version "8.4.7"
+ resolved "https://registry.yarnpkg.com/@storybook/core/-/core-8.4.7.tgz#af9cbb3f26f0b6c98c679a134ce776c202570d66"
+ integrity sha512-7Z8Z0A+1YnhrrSXoKKwFFI4gnsLbWzr8fnDCU6+6HlDukFYh8GHRcZ9zKfqmy6U3hw2h8H5DrHsxWfyaYUUOoA==
+ dependencies:
+ "@storybook/csf" "^0.1.11"
+ better-opn "^3.0.2"
+ browser-assert "^1.2.1"
+ esbuild "^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0"
+ esbuild-register "^3.5.0"
+ jsdoc-type-pratt-parser "^4.0.0"
+ process "^0.11.10"
+ recast "^0.23.5"
+ semver "^7.6.2"
+ util "^0.12.5"
+ ws "^8.2.3"
+
+"@storybook/csf-plugin@8.4.7":
+ version "8.4.7"
+ resolved "https://registry.yarnpkg.com/@storybook/csf-plugin/-/csf-plugin-8.4.7.tgz#0117c872b05bf033eec089ab0224e0fab01da810"
+ integrity sha512-Fgogplu4HImgC+AYDcdGm1rmL6OR1rVdNX1Be9C/NEXwOCpbbBwi0BxTf/2ZxHRk9fCeaPEcOdP5S8QHfltc1g==
+ dependencies:
+ unplugin "^1.3.1"
+
+"@storybook/csf@^0.1.11":
+ version "0.1.12"
+ resolved "https://registry.yarnpkg.com/@storybook/csf/-/csf-0.1.12.tgz#1dcfa0f398a69b834c563884b5f747db3d5a81df"
+ integrity sha512-9/exVhabisyIVL0VxTCxo01Tdm8wefIXKXfltAPTSr8cbLn5JAxGQ6QV3mjdecLGEOucfoVhAKtJfVHxEK1iqw==
+ dependencies:
+ type-fest "^2.19.0"
+
+"@storybook/global@^5.0.0":
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/@storybook/global/-/global-5.0.0.tgz#b793d34b94f572c1d7d9e0f44fac4e0dbc9572ed"
+ integrity sha512-FcOqPAXACP0I3oJ/ws6/rrPT9WGhu915Cg8D02a9YxLo0DE9zI+a9A5gRGvmQ09fiWPukqI8ZAEoQEdWUKMQdQ==
+
+"@storybook/icons@^1.2.12":
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/@storybook/icons/-/icons-1.3.0.tgz#a5c1460fb15a7260e0b638ab86163f7347a0061e"
+ integrity sha512-Nz/UzeYQdUZUhacrPyfkiiysSjydyjgg/p0P9HxB4p/WaJUUjMAcaoaLgy3EXx61zZJ3iD36WPuDkZs5QYrA0A==
+
+"@storybook/instrumenter@8.4.7":
+ version "8.4.7"
+ resolved "https://registry.yarnpkg.com/@storybook/instrumenter/-/instrumenter-8.4.7.tgz#5a37876fee8f828241a1e7fd76891c6effc1805a"
+ integrity sha512-k6NSD3jaRCCHAFtqXZ7tw8jAzD/yTEWXGya+REgZqq5RCkmJ+9S4Ytp/6OhQMPtPFX23gAuJJzTQVLcCr+gjRg==
+ dependencies:
+ "@storybook/global" "^5.0.0"
+ "@vitest/utils" "^2.1.1"
+
+"@storybook/manager-api@8.4.7", "@storybook/manager-api@^8.1.11":
+ version "8.4.7"
+ resolved "https://registry.yarnpkg.com/@storybook/manager-api/-/manager-api-8.4.7.tgz#4e13debf645c9300d7d6d49195e720d0c7ecd261"
+ integrity sha512-ELqemTviCxAsZ5tqUz39sDmQkvhVAvAgiplYy9Uf15kO0SP2+HKsCMzlrm2ue2FfkUNyqbDayCPPCB0Cdn/mpQ==
+
+"@storybook/preview-api@8.4.7":
+ version "8.4.7"
+ resolved "https://registry.yarnpkg.com/@storybook/preview-api/-/preview-api-8.4.7.tgz#85e01a97f4182b974581765d725f6c7a7d190013"
+ integrity sha512-0QVQwHw+OyZGHAJEXo6Knx+6/4er7n2rTDE5RYJ9F2E2Lg42E19pfdLlq2Jhoods2Xrclo3wj6GWR//Ahi39Eg==
+
+"@storybook/react-dom-shim@8.4.7":
+ version "8.4.7"
+ resolved "https://registry.yarnpkg.com/@storybook/react-dom-shim/-/react-dom-shim-8.4.7.tgz#f0dd5bbf2fc185def72d9d08a11c8de22f152c2a"
+ integrity sha512-6bkG2jvKTmWrmVzCgwpTxwIugd7Lu+2btsLAqhQSzDyIj2/uhMNp8xIMr/NBDtLgq3nomt9gefNa9xxLwk/OMg==
+
+"@storybook/react-vite@^8.1.11":
+ version "8.4.7"
+ resolved "https://registry.yarnpkg.com/@storybook/react-vite/-/react-vite-8.4.7.tgz#1a755596d65551c77850361da76df47027687664"
+ integrity sha512-iiY9iLdMXhDnilCEVxU6vQsN72pW3miaf0WSenOZRyZv3HdbpgOxI0qapOS0KCyRUnX9vTlmrSPTMchY4cAeOg==
+ dependencies:
+ "@joshwooding/vite-plugin-react-docgen-typescript" "0.4.2"
+ "@rollup/pluginutils" "^5.0.2"
+ "@storybook/builder-vite" "8.4.7"
+ "@storybook/react" "8.4.7"
+ find-up "^5.0.0"
+ magic-string "^0.30.0"
+ react-docgen "^7.0.0"
+ resolve "^1.22.8"
+ tsconfig-paths "^4.2.0"
+
+"@storybook/react@8.4.7":
+ version "8.4.7"
+ resolved "https://registry.yarnpkg.com/@storybook/react/-/react-8.4.7.tgz#e2cf62b3c1d8e4bfe5eff82ced07ec473d4e4fd1"
+ integrity sha512-nQ0/7i2DkaCb7dy0NaT95llRVNYWQiPIVuhNfjr1mVhEP7XD090p0g7eqUmsx8vfdHh2BzWEo6CoBFRd3+EXxw==
+ dependencies:
+ "@storybook/components" "8.4.7"
+ "@storybook/global" "^5.0.0"
+ "@storybook/manager-api" "8.4.7"
+ "@storybook/preview-api" "8.4.7"
+ "@storybook/react-dom-shim" "8.4.7"
+ "@storybook/theming" "8.4.7"
+
+"@storybook/test@8.4.7", "@storybook/test@^8.1.11":
+ version "8.4.7"
+ resolved "https://registry.yarnpkg.com/@storybook/test/-/test-8.4.7.tgz#7f58f2cdf3a6d810bf3ff4e0e2fee634040c678f"
+ integrity sha512-AhvJsu5zl3uG40itSQVuSy5WByp3UVhS6xAnme4FWRwgSxhvZjATJ3AZkkHWOYjnnk+P2/sbz/XuPli1FVCWoQ==
+ dependencies:
+ "@storybook/csf" "^0.1.11"
+ "@storybook/global" "^5.0.0"
+ "@storybook/instrumenter" "8.4.7"
+ "@testing-library/dom" "10.4.0"
+ "@testing-library/jest-dom" "6.5.0"
+ "@testing-library/user-event" "14.5.2"
+ "@vitest/expect" "2.0.5"
+ "@vitest/spy" "2.0.5"
+
+"@storybook/theming@8.4.7", "@storybook/theming@^8.1.11":
+ version "8.4.7"
+ resolved "https://registry.yarnpkg.com/@storybook/theming/-/theming-8.4.7.tgz#c308f6a883999bd35e87826738ab8a76515932b5"
+ integrity sha512-99rgLEjf7iwfSEmdqlHkSG3AyLcK0sfExcr0jnc6rLiAkBhzuIsvcHjjUwkR210SOCgXqBPW0ZA6uhnuyppHLw==
+
"@svgmoji/blob@^3.2.0":
version "3.2.0"
resolved "https://registry.yarnpkg.com/@svgmoji/blob/-/blob-3.2.0.tgz#62a0ab1ba22a0d27f23cb38aacf6d4fb13123dfb"
@@ -3017,6 +3705,20 @@
"@svgr/hast-util-to-babel-ast" "8.0.0"
svg-parser "^2.0.4"
+"@testing-library/dom@10.4.0":
+ version "10.4.0"
+ resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-10.4.0.tgz#82a9d9462f11d240ecadbf406607c6ceeeff43a8"
+ integrity sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==
+ dependencies:
+ "@babel/code-frame" "^7.10.4"
+ "@babel/runtime" "^7.12.5"
+ "@types/aria-query" "^5.0.1"
+ aria-query "5.3.0"
+ chalk "^4.1.0"
+ dom-accessibility-api "^0.5.9"
+ lz-string "^1.5.0"
+ pretty-format "^27.0.2"
+
"@testing-library/dom@^8.0.0":
version "8.20.1"
resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-8.20.1.tgz#2e52a32e46fc88369eef7eef634ac2a192decd9f"
@@ -3031,6 +3733,19 @@
lz-string "^1.5.0"
pretty-format "^27.0.2"
+"@testing-library/jest-dom@6.5.0":
+ version "6.5.0"
+ resolved "https://registry.yarnpkg.com/@testing-library/jest-dom/-/jest-dom-6.5.0.tgz#50484da3f80fb222a853479f618a9ce5c47bfe54"
+ integrity sha512-xGGHpBXYSHUUr6XsKBfs85TWlYKpTc37cSBBVrXcib2MkHLboWlkClhWF37JKlDb9KEq3dHs+f2xR7XJEWGBxA==
+ dependencies:
+ "@adobe/css-tools" "^4.4.0"
+ aria-query "^5.0.0"
+ chalk "^3.0.0"
+ css.escape "^1.5.1"
+ dom-accessibility-api "^0.6.3"
+ lodash "^4.17.21"
+ redent "^3.0.0"
+
"@testing-library/jest-dom@^6.1.4":
version "6.1.4"
resolved "https://registry.yarnpkg.com/@testing-library/jest-dom/-/jest-dom-6.1.4.tgz#cf0835c33bc5ef00befb9e672b1e3e6a710e30e3"
@@ -3054,6 +3769,11 @@
"@testing-library/dom" "^8.0.0"
"@types/react-dom" "<18.0.0"
+"@testing-library/user-event@14.5.2":
+ version "14.5.2"
+ resolved "https://registry.yarnpkg.com/@testing-library/user-event/-/user-event-14.5.2.tgz#db7257d727c891905947bd1c1a99da20e03c2ebd"
+ integrity sha512-YAh82Wh4TIrxYLmfGcixwD18oIjyC1pFQC2Y01F2lzV2HTMiYrI0nze0FD0ocB//CKS/7jIUgae+adPqxK5yCQ==
+
"@tommoor/remove-markdown@^0.3.2":
version "0.3.2"
resolved "https://registry.yarnpkg.com/@tommoor/remove-markdown/-/remove-markdown-0.3.2.tgz#5288ddd0e26b6b173e76ebb31c94653b0dcff45d"
@@ -3080,6 +3800,17 @@
"@types/babel__template" "*"
"@types/babel__traverse" "*"
+"@types/babel__core@^7.18.0":
+ version "7.20.5"
+ resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.20.5.tgz#3df15f27ba85319caa07ba08d0721889bb39c017"
+ integrity sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==
+ dependencies:
+ "@babel/parser" "^7.20.7"
+ "@babel/types" "^7.20.7"
+ "@types/babel__generator" "*"
+ "@types/babel__template" "*"
+ "@types/babel__traverse" "*"
+
"@types/babel__generator@*":
version "7.6.2"
resolved "https://registry.yarnpkg.com/@types/babel__generator/-/babel__generator-7.6.2.tgz#f3d71178e187858f7c45e30380f8f1b7415a12d8"
@@ -3102,6 +3833,13 @@
dependencies:
"@babel/types" "^7.3.0"
+"@types/babel__traverse@^7.18.0":
+ version "7.20.6"
+ resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.20.6.tgz#8dc9f0ae0f202c08d8d4dab648912c8d6038e3f7"
+ integrity sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==
+ dependencies:
+ "@babel/types" "^7.20.7"
+
"@types/chai-subset@^1.3.3":
version "1.3.4"
resolved "https://registry.yarnpkg.com/@types/chai-subset/-/chai-subset-1.3.4.tgz#7938fa929dd12db451457e4d6faa27bcd599a729"
@@ -3192,6 +3930,11 @@
resolved "https://registry.yarnpkg.com/@types/direction/-/direction-1.0.0.tgz#6a0962feade8502f9e986e87abe1130b611b13be"
integrity sha512-et1wmqXm/5smJ8lTJfBnwD12/2Y7eVJLKbuaRT0h2xaKAoo1h8Dz2Io22GObDLFwxY1ddXRTLH3Gq5v44Fl/2w==
+"@types/doctrine@^0.0.9":
+ version "0.0.9"
+ resolved "https://registry.yarnpkg.com/@types/doctrine/-/doctrine-0.0.9.tgz#d86a5f452a15e3e3113b99e39616a9baa0f9863f"
+ integrity sha512-eOIHzCUSH7SMfonMG1LsC2f8vxBFtho6NGBznK41R84YzPuvSBzrhEps33IsQiOW9+VL6NQ9DbjQJznk/S4uRA==
+
"@types/dompurify@^2.3.3":
version "2.3.3"
resolved "https://registry.yarnpkg.com/@types/dompurify/-/dompurify-2.3.3.tgz#c24c92f698f77ed9cc9d9fa7888f90cf2bfaa23f"
@@ -3278,6 +4021,11 @@
dependencies:
"@types/unist" "*"
+"@types/mdx@^2.0.0":
+ version "2.0.13"
+ resolved "https://registry.yarnpkg.com/@types/mdx/-/mdx-2.0.13.tgz#68f6877043d377092890ff5b298152b0a21671bd"
+ integrity sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw==
+
"@types/min-document@^2.19.0":
version "2.19.0"
resolved "https://registry.yarnpkg.com/@types/min-document/-/min-document-2.19.0.tgz#4f9919e789917c00de967a2c38fa8d234cbcd7d6"
@@ -3325,6 +4073,11 @@
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.5.tgz#5f19d2b85a98e9558036f6a3cacc8819420f05cf"
integrity sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==
+"@types/prop-types@^15.7.12":
+ version "15.7.14"
+ resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.14.tgz#1433419d73b2a7ebfc6918dcefd2ec0d5cd698f2"
+ integrity sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==
+
"@types/query-string@^6.3.0":
version "6.3.0"
resolved "https://registry.yarnpkg.com/@types/query-string/-/query-string-6.3.0.tgz#b6fa172a01405abcaedac681118e78429d62ea39"
@@ -3390,6 +4143,13 @@
"@types/history" "*"
"@types/react" "*"
+"@types/react-transition-group@^4.4.10":
+ version "4.4.11"
+ resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.11.tgz#d963253a611d757de01ebb241143b1017d5d63d5"
+ integrity sha512-RM05tAniPZ5DZPzzNFP+DmrcOdD0efDUxMy3145oljWSl3x9ZV5vhme98gTxFrj2lhXvmGNnUiuDyJgY9IKkNA==
+ dependencies:
+ "@types/react" "*"
+
"@types/react-transition-group@^4.4.5":
version "4.4.5"
resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.5.tgz#aae20dcf773c5aa275d5b9f7cdbca638abc5e416"
@@ -3429,6 +4189,11 @@
dependencies:
"@types/prismjs" "*"
+"@types/resolve@^1.20.2":
+ version "1.20.6"
+ resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-1.20.6.tgz#e6e60dad29c2c8c206c026e6dd8d6d1bdda850b8"
+ integrity sha512-A4STmOXPhMUtHH+S6ymgE2GiBSMqf4oTvcQZMcHzokuTLVYzXTB8ttjcgxOVaAp2lGwEdzZ0J+cRbbeevQj1UQ==
+
"@types/scheduler@*":
version "0.16.1"
resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.1.tgz#18845205e86ff0038517aab7a18a62a6b9f71275"
@@ -3485,6 +4250,11 @@
resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.3.tgz#9c088679876f374eb5983f150d4787aa6fb32d7e"
integrity sha512-FvUupuM3rlRsRtCN+fDudtmytGO6iHJuuRKS1Ss0pG5z8oX0diNEw94UEL7hgDbpN94rgaK5R7sWm6RrSkZuAQ==
+"@types/uuid@^9.0.1":
+ version "9.0.8"
+ resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-9.0.8.tgz#7545ba4fc3c003d6c756f651f3bf163d8f0f29ba"
+ integrity sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==
+
"@types/ws@^8.0.0":
version "8.5.10"
resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.10.tgz#4acfb517970853fa6574a3a6886791d04a396787"
@@ -3734,6 +4504,14 @@
d3-shape "^1.2.0"
prop-types "^15.6.2"
+"@visx/gradient@^3.3.0":
+ version "3.12.0"
+ resolved "https://registry.yarnpkg.com/@visx/gradient/-/gradient-3.12.0.tgz#4898241cdd78ff3003dae584787a4b2dffff1eb7"
+ integrity sha512-QRatjjdUEPbcp4pqRca1JlChpAnmmIAO3r3ZscLK7D1xEIANlIjzjl3uNgrmseYmBAYyPCcJH8Zru07R97ovOg==
+ dependencies:
+ "@types/react" "*"
+ prop-types "^15.5.7"
+
"@visx/grid@3.2.0":
version "3.2.0"
resolved "https://registry.yarnpkg.com/@visx/grid/-/grid-3.2.0.tgz#1db936e0c20bf45d2e8280f8c70b62982c67fb59"
@@ -3945,6 +4723,30 @@
"@vitest/utils" "0.34.6"
chai "^4.3.10"
+"@vitest/expect@2.0.5":
+ version "2.0.5"
+ resolved "https://registry.yarnpkg.com/@vitest/expect/-/expect-2.0.5.tgz#f3745a6a2c18acbea4d39f5935e913f40d26fa86"
+ integrity sha512-yHZtwuP7JZivj65Gxoi8upUN2OzHTi3zVfjwdpu2WrvCZPLwsJ2Ey5ILIPccoW23dd/zQBlJ4/dhi7DWNyXCpA==
+ dependencies:
+ "@vitest/spy" "2.0.5"
+ "@vitest/utils" "2.0.5"
+ chai "^5.1.1"
+ tinyrainbow "^1.2.0"
+
+"@vitest/pretty-format@2.0.5":
+ version "2.0.5"
+ resolved "https://registry.yarnpkg.com/@vitest/pretty-format/-/pretty-format-2.0.5.tgz#91d2e6d3a7235c742e1a6cc50e7786e2f2979b1e"
+ integrity sha512-h8k+1oWHfwTkyTkb9egzwNMfJAEx4veaPSnMeKbVSjp4euqGSbQlm5+6VHwTr7u4FJslVVsUG5nopCaAYdOmSQ==
+ dependencies:
+ tinyrainbow "^1.2.0"
+
+"@vitest/pretty-format@2.1.8":
+ version "2.1.8"
+ resolved "https://registry.yarnpkg.com/@vitest/pretty-format/-/pretty-format-2.1.8.tgz#88f47726e5d0cf4ba873d50c135b02e4395e2bca"
+ integrity sha512-9HiSZ9zpqNLKlbIDRWOnAWqgcA7xu+8YxXSekhr0Ykab7PAYFkhkwoqVArPOtJhPmYeE2YHgKZlj3CP36z2AJQ==
+ dependencies:
+ tinyrainbow "^1.2.0"
+
"@vitest/runner@0.34.6":
version "0.34.6"
resolved "https://registry.yarnpkg.com/@vitest/runner/-/runner-0.34.6.tgz#6f43ca241fc96b2edf230db58bcde5b974b8dcaf"
@@ -3970,6 +4772,13 @@
dependencies:
tinyspy "^2.1.1"
+"@vitest/spy@2.0.5":
+ version "2.0.5"
+ resolved "https://registry.yarnpkg.com/@vitest/spy/-/spy-2.0.5.tgz#590fc07df84a78b8e9dd976ec2090920084a2b9f"
+ integrity sha512-c/jdthAhvJdpfVuaexSrnawxZz6pywlTPe84LUB2m/4t3rl2fTo9NFGBG4oWgaD+FTgDDV8hJ/nibT7IfH3JfA==
+ dependencies:
+ tinyspy "^3.0.0"
+
"@vitest/utils@0.34.6":
version "0.34.6"
resolved "https://registry.yarnpkg.com/@vitest/utils/-/utils-0.34.6.tgz#38a0a7eedddb8e7291af09a2409cb8a189516968"
@@ -3979,6 +4788,25 @@
loupe "^2.3.6"
pretty-format "^29.5.0"
+"@vitest/utils@2.0.5":
+ version "2.0.5"
+ resolved "https://registry.yarnpkg.com/@vitest/utils/-/utils-2.0.5.tgz#6f8307a4b6bc6ceb9270007f73c67c915944e926"
+ integrity sha512-d8HKbqIcya+GR67mkZbrzhS5kKhtp8dQLcmRZLGTscGVg7yImT82cIrhtn2L8+VujWcy6KZweApgNmPsTAO/UQ==
+ dependencies:
+ "@vitest/pretty-format" "2.0.5"
+ estree-walker "^3.0.3"
+ loupe "^3.1.1"
+ tinyrainbow "^1.2.0"
+
+"@vitest/utils@^2.1.1":
+ version "2.1.8"
+ resolved "https://registry.yarnpkg.com/@vitest/utils/-/utils-2.1.8.tgz#f8ef85525f3362ebd37fd25d268745108d6ae388"
+ integrity sha512-dwSoui6djdwbfFmIgbIjX2ZhIoG7Ex/+xpxyiEgIGzjliY8xGkcpITKTlp6B4MgtGkF2ilvm97cPM96XZaAgcA==
+ dependencies:
+ "@vitest/pretty-format" "2.1.8"
+ loupe "^3.1.2"
+ tinyrainbow "^1.2.0"
+
"@whatwg-node/events@^0.0.3":
version "0.0.3"
resolved "https://registry.yarnpkg.com/@whatwg-node/events/-/events-0.0.3.tgz#13a65dd4f5893f55280f766e29ae48074927acad"
@@ -4085,6 +4913,11 @@ acorn@^8.10.0, acorn@^8.9.0:
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.11.2.tgz#ca0d78b51895be5390a5903c5b3bdcdaf78ae40b"
integrity sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==
+acorn@^8.14.0:
+ version "8.14.0"
+ resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.14.0.tgz#063e2c70cac5fb4f6467f0b11152e04c682795b0"
+ integrity sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==
+
acorn@^8.8.0:
version "8.9.0"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.9.0.tgz#78a16e3b2bcc198c10822786fa6679e245db5b59"
@@ -4245,7 +5078,7 @@ aria-query@5.1.3:
dependencies:
deep-equal "^2.0.5"
-aria-query@^5.0.0, aria-query@^5.1.3:
+aria-query@5.3.0, aria-query@^5.0.0, aria-query@^5.1.3:
version "5.3.0"
resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.3.0.tgz#650c569e41ad90b51b3d7df5e5eed1c7549c103e"
integrity sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==
@@ -4331,11 +5164,23 @@ assertion-error@^1.1.0:
resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-1.1.0.tgz#e60b6b0e8f301bd97e5375215bda406c85118c0b"
integrity sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==
+assertion-error@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-2.0.1.tgz#f641a196b335690b1070bf00b6e7593fec190bf7"
+ integrity sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==
+
ast-types-flow@^0.0.7:
version "0.0.7"
resolved "https://registry.yarnpkg.com/ast-types-flow/-/ast-types-flow-0.0.7.tgz#f70b735c6bca1a5c9c22d982c3e39e7feba3bdad"
integrity sha1-9wtzXGvKGlycItmCw+Oef+ujva0=
+ast-types@^0.16.1:
+ version "0.16.1"
+ resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.16.1.tgz#7a9da1617c9081bc121faafe91711b4c8bb81da2"
+ integrity sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==
+ dependencies:
+ tslib "^2.0.1"
+
astral-regex@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31"
@@ -4366,6 +5211,13 @@ available-typed-arrays@^1.0.5:
resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz#92f95616501069d07d10edb2fc37d3e1c65123b7"
integrity sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==
+available-typed-arrays@^1.0.7:
+ version "1.0.7"
+ resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz#a5cc375d6a03c2efc87a553f3e0b1522def14846"
+ integrity sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==
+ dependencies:
+ possible-typed-array-names "^1.0.0"
+
axe-core@^4.6.2:
version "4.7.2"
resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.7.2.tgz#040a7342b20765cb18bb50b628394c21bccc17a0"
@@ -4478,6 +5330,13 @@ base64-js@^1.3.1:
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
+better-opn@^3.0.2:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/better-opn/-/better-opn-3.0.2.tgz#f96f35deaaf8f34144a4102651babcf00d1d8817"
+ integrity sha512-aVNobHnJqLiUelTaHat9DZ1qM2w0C0Eym4LPI/3JxOnSokGVdsl1T1kN7TFvsEAD8G47A6VKQ0TVHqbBnYMJlQ==
+ dependencies:
+ open "^8.0.4"
+
binary-extensions@^2.0.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d"
@@ -4524,6 +5383,11 @@ braces@~3.0.2:
dependencies:
fill-range "^7.0.1"
+browser-assert@^1.2.1:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/browser-assert/-/browser-assert-1.2.1.tgz#9aaa5a2a8c74685c2ae05bfe46efd606f068c200"
+ integrity sha512-nfulgvOR6S4gt9UKCeGJOuSGBPGiFT6oQ/2UBnvTY/5aQ1PnksW72fhZkM30DzoRRv2WpwZf1vHHEr3mtuXIWQ==
+
browserslist@^4.21.3:
version "4.21.9"
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.21.9.tgz#e11bdd3c313d7e2a9e87e8b4b0c7872b13897635"
@@ -4544,6 +5408,16 @@ browserslist@^4.21.9:
node-releases "^2.0.13"
update-browserslist-db "^1.0.13"
+browserslist@^4.24.0:
+ version "4.24.2"
+ resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.24.2.tgz#f5845bc91069dbd55ee89faf9822e1d885d16580"
+ integrity sha512-ZIc+Q62revdMcqC6aChtW4jz3My3klmCO1fEmINZY/8J3EpBg5/A/D0AKmBveUh6pgoeycoMkVMko84tuYS+Gg==
+ dependencies:
+ caniuse-lite "^1.0.30001669"
+ electron-to-chromium "^1.5.41"
+ node-releases "^2.0.18"
+ update-browserslist-db "^1.1.1"
+
bser@2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/bser/-/bser-2.1.1.tgz#e6787da20ece9d07998533cfd9de6f5c38f4bc05"
@@ -4576,6 +5450,14 @@ cac@^6.7.14:
resolved "https://registry.yarnpkg.com/cac/-/cac-6.7.14.tgz#804e1e6f506ee363cb0e3ccbb09cad5dd9870959"
integrity sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==
+call-bind-apply-helpers@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.0.tgz#33127b42608972f76812a501d69db5d8ce404979"
+ integrity sha512-CCKAP2tkPau7D3GE8+V8R6sQubA9R5foIzGp+85EXCVSCivuxBNAWqcpn72PKYiIcqoViv/kcUDpaEIMBVi1lQ==
+ dependencies:
+ es-errors "^1.3.0"
+ function-bind "^1.1.2"
+
call-bind@^1.0.0, call-bind@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c"
@@ -4593,6 +5475,16 @@ call-bind@^1.0.4:
get-intrinsic "^1.2.1"
set-function-length "^1.1.1"
+call-bind@^1.0.7:
+ version "1.0.8"
+ resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.8.tgz#0736a9660f537e3388826f440d5ec45f744eaa4c"
+ integrity sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==
+ dependencies:
+ call-bind-apply-helpers "^1.0.0"
+ es-define-property "^1.0.0"
+ get-intrinsic "^1.2.4"
+ set-function-length "^1.2.2"
+
callsites@^3.0.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73"
@@ -4631,6 +5523,11 @@ caniuse-lite@^1.0.30001541:
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001559.tgz#95a982440d3d314c471db68d02664fb7536c5a30"
integrity sha512-cPiMKZgqgkg5LY3/ntGeLFUpi6tzddBNS58A4tnTgQw1zON7u2sZMU7SzOeVH4tj20++9ggL+V6FDOFMTaFFYA==
+caniuse-lite@^1.0.30001669:
+ version "1.0.30001687"
+ resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001687.tgz#d0ac634d043648498eedf7a3932836beba90ebae"
+ integrity sha512-0S/FDhf4ZiqrTUiQ39dKeUjYRjkv7lOZU1Dgif2rIqrTzX/1wV2hfKu9TOm1IHkdSijfLswxTFzl/cvir+SLSQ==
+
capital-case@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/capital-case/-/capital-case-1.0.4.tgz#9d130292353c9249f6b00fa5852bee38a717e669"
@@ -4663,6 +5560,17 @@ chai@^4.3.10:
pathval "^1.1.1"
type-detect "^4.0.8"
+chai@^5.1.1:
+ version "5.1.2"
+ resolved "https://registry.yarnpkg.com/chai/-/chai-5.1.2.tgz#3afbc340b994ae3610ca519a6c70ace77ad4378d"
+ integrity sha512-aGtmf24DW6MLHHG5gCx4zaI3uBq3KRtxeVs0DjFH6Z0rDNbsvTxFASFvdj79pxjxZ8/5u3PIiN3IwEIQkiiuPw==
+ dependencies:
+ assertion-error "^2.0.1"
+ check-error "^2.1.1"
+ deep-eql "^5.0.1"
+ loupe "^3.1.0"
+ pathval "^2.0.0"
+
chalk@^2.0.0, chalk@^2.4.2:
version "2.4.2"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424"
@@ -4770,6 +5678,11 @@ check-error@^1.0.3:
dependencies:
get-func-name "^2.0.2"
+check-error@^2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/check-error/-/check-error-2.1.1.tgz#87eb876ae71ee388fa0471fe423f494be1d96ccc"
+ integrity sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==
+
chokidar@^3.5.3:
version "3.5.3"
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd"
@@ -4862,6 +5775,11 @@ clsx@^1.2.1:
resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.2.1.tgz#0ddc4a20a549b59c93a4116bb26f5294ca17dc12"
integrity sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==
+clsx@^2.1.0, clsx@^2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.1.tgz#eed397c9fd8bd882bfb18deab7102049a2f32999"
+ integrity sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==
+
codemirror@^5.62.0:
version "5.65.10"
resolved "https://registry.yarnpkg.com/codemirror/-/codemirror-5.65.10.tgz#4276a93b8534ce91f14b733ba9a1ac949666eac9"
@@ -5097,6 +6015,11 @@ csstype@^3.0.2, csstype@^3.0.6, csstype@^3.0.7, csstype@^3.1.0, csstype@^3.1.1:
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.1.tgz#841b532c45c758ee546a11d5bd7b7b473c8c30b9"
integrity sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==
+csstype@^3.1.3:
+ version "3.1.3"
+ resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81"
+ integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==
+
"d3-array@2 - 3", "d3-array@2.10.0 - 3":
version "3.2.4"
resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-3.2.4.tgz#15fec33b237f97ac5d7c986dc77da273a8ed0bb5"
@@ -5267,6 +6190,11 @@ deep-eql@^4.1.3:
dependencies:
type-detect "^4.0.0"
+deep-eql@^5.0.1:
+ version "5.0.2"
+ resolved "https://registry.yarnpkg.com/deep-eql/-/deep-eql-5.0.2.tgz#4b756d8d770a9257300825d52a2c2cff99c3a341"
+ integrity sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==
+
deep-equal@^2.0.5:
version "2.2.2"
resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-2.2.2.tgz#9b2635da569a13ba8e1cc159c2f744071b115daa"
@@ -5317,6 +6245,20 @@ define-data-property@^1.0.1, define-data-property@^1.1.1:
gopd "^1.0.1"
has-property-descriptors "^1.0.0"
+define-data-property@^1.1.4:
+ version "1.1.4"
+ resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e"
+ integrity sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==
+ dependencies:
+ es-define-property "^1.0.0"
+ es-errors "^1.3.0"
+ gopd "^1.0.1"
+
+define-lazy-prop@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz#3f7ae421129bcaaac9bc74905c98a0009ec9ee7f"
+ integrity sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==
+
define-properties@^1.1.3, define-properties@^1.1.4:
version "1.1.4"
resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.4.tgz#0b14d7bd7fbeb2f3572c3a7eda80ea5d57fb05b1"
@@ -5344,7 +6286,7 @@ dependency-graph@^0.11.0:
resolved "https://registry.yarnpkg.com/dependency-graph/-/dependency-graph-0.11.0.tgz#ac0ce7ed68a54da22165a85e97a01d53f5eb2e27"
integrity sha512-JeMq7fEshyepOWDfcfHK06N3MhyPhz++vtqWhMT5O9A3K42rdsEDpfdVqjaqaAhsw6a+ZqeDvQVtD0hFHQWrzg==
-dequal@^2.0.3:
+dequal@^2.0.2, dequal@^2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be"
integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==
@@ -5405,6 +6347,11 @@ dom-accessibility-api@^0.5.6, dom-accessibility-api@^0.5.9:
resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz#5a7429e6066eb3664d911e33fb0e45de8eb08453"
integrity sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==
+dom-accessibility-api@^0.6.3:
+ version "0.6.3"
+ resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz#993e925cc1d73f2c662e7d75dd5a5445259a8fd8"
+ integrity sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==
+
dom-align@^1.7.0:
version "1.12.2"
resolved "https://registry.yarnpkg.com/dom-align/-/dom-align-1.12.2.tgz#0f8164ebd0c9c21b0c790310493cd855892acd4b"
@@ -5485,6 +6432,11 @@ electron-to-chromium@^1.4.535:
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.576.tgz#0c6940fdc0d60f7e34bd742b29d8fa847c9294d1"
integrity sha512-yXsZyXJfAqzWk1WKryr0Wl0MN2D47xodPvEEwlVePBnhU5E7raevLQR+E6b9JAD3GfL/7MbAL9ZtWQQPcLx7wA==
+electron-to-chromium@^1.5.41:
+ version "1.5.71"
+ resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.71.tgz#d8b5dba1e55b320f2f4e9b1ca80738f53fcfec2b"
+ integrity sha512-dB68l59BI75W1BUGVTAEJy45CEVuEGy9qPVVQ8pnHyHMn36PLPPoE1mjLH+lo9rKulO3HC2OhbACI/8tCqJBcA==
+
emoji-regex@^8.0.0:
version "8.0.0"
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
@@ -5576,6 +6528,16 @@ es-abstract@^1.19.0, es-abstract@^1.20.4:
string.prototype.trimstart "^1.0.5"
unbox-primitive "^1.0.2"
+es-define-property@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.1.tgz#983eb2f9a6724e9303f61addf011c72e09e0b0fa"
+ integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==
+
+es-errors@^1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f"
+ integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==
+
es-get-iterator@^1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/es-get-iterator/-/es-get-iterator-1.1.3.tgz#3ef87523c5d464d41084b2c3c9c214f1199763d6"
@@ -5607,6 +6569,43 @@ es-to-primitive@^1.2.1:
is-date-object "^1.0.1"
is-symbol "^1.0.2"
+esbuild-register@^3.5.0:
+ version "3.6.0"
+ resolved "https://registry.yarnpkg.com/esbuild-register/-/esbuild-register-3.6.0.tgz#cf270cfa677baebbc0010ac024b823cbf723a36d"
+ integrity sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==
+ dependencies:
+ debug "^4.3.4"
+
+"esbuild@^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0":
+ version "0.24.0"
+ resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.24.0.tgz#f2d470596885fcb2e91c21eb3da3b3c89c0b55e7"
+ integrity sha512-FuLPevChGDshgSicjisSooU0cemp/sGXR841D5LHMB7mTVOmsEHcAxaH3irL53+8YDIeVNQEySh4DaYU/iuPqQ==
+ optionalDependencies:
+ "@esbuild/aix-ppc64" "0.24.0"
+ "@esbuild/android-arm" "0.24.0"
+ "@esbuild/android-arm64" "0.24.0"
+ "@esbuild/android-x64" "0.24.0"
+ "@esbuild/darwin-arm64" "0.24.0"
+ "@esbuild/darwin-x64" "0.24.0"
+ "@esbuild/freebsd-arm64" "0.24.0"
+ "@esbuild/freebsd-x64" "0.24.0"
+ "@esbuild/linux-arm" "0.24.0"
+ "@esbuild/linux-arm64" "0.24.0"
+ "@esbuild/linux-ia32" "0.24.0"
+ "@esbuild/linux-loong64" "0.24.0"
+ "@esbuild/linux-mips64el" "0.24.0"
+ "@esbuild/linux-ppc64" "0.24.0"
+ "@esbuild/linux-riscv64" "0.24.0"
+ "@esbuild/linux-s390x" "0.24.0"
+ "@esbuild/linux-x64" "0.24.0"
+ "@esbuild/netbsd-x64" "0.24.0"
+ "@esbuild/openbsd-arm64" "0.24.0"
+ "@esbuild/openbsd-x64" "0.24.0"
+ "@esbuild/sunos-x64" "0.24.0"
+ "@esbuild/win32-arm64" "0.24.0"
+ "@esbuild/win32-ia32" "0.24.0"
+ "@esbuild/win32-x64" "0.24.0"
+
esbuild@^0.18.10:
version "0.18.20"
resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.18.20.tgz#4709f5a34801b43b799ab7d6d82f7284a9b7a7a6"
@@ -5640,6 +6639,11 @@ escalade@^3.1.1:
resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40"
integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==
+escalade@^3.2.0:
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5"
+ integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==
+
escape-html@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
@@ -5858,6 +6862,11 @@ espree@^9.5.2:
acorn-jsx "^5.3.2"
eslint-visitor-keys "^3.4.1"
+esprima@~4.0.0:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71"
+ integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==
+
esquery@^1.4.2:
version "1.5.0"
resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.5.0.tgz#6ce17738de8577694edd7361c57182ac8cb0db0b"
@@ -5887,6 +6896,13 @@ estree-walker@^2.0.2:
resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac"
integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==
+estree-walker@^3.0.3:
+ version "3.0.3"
+ resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-3.0.3.tgz#67c3e549ec402a487b4fc193d1953a524752340d"
+ integrity sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==
+ dependencies:
+ "@types/estree" "^1.0.0"
+
esutils@^2.0.2:
version "2.0.3"
resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64"
@@ -6221,6 +7237,17 @@ get-intrinsic@^1.2.0, get-intrinsic@^1.2.1, get-intrinsic@^1.2.2:
has-symbols "^1.0.3"
hasown "^2.0.0"
+get-intrinsic@^1.2.4:
+ version "1.2.4"
+ resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.4.tgz#e385f5a4b5227d449c3eabbad05494ef0abbeadd"
+ integrity sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==
+ dependencies:
+ es-errors "^1.3.0"
+ function-bind "^1.1.2"
+ has-proto "^1.0.1"
+ has-symbols "^1.0.3"
+ hasown "^2.0.0"
+
get-symbol-description@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.0.0.tgz#7fdb81c900101fbd564dd5f1a30af5aadc1e58d6"
@@ -6387,6 +7414,13 @@ has-property-descriptors@^1.0.0:
dependencies:
get-intrinsic "^1.1.1"
+has-property-descriptors@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz#963ed7d071dc7bf5f084c5bfbe0d1b6222586854"
+ integrity sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==
+ dependencies:
+ es-define-property "^1.0.0"
+
has-proto@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.1.tgz#1885c1305538958aff469fef37937c22795408e0"
@@ -6404,6 +7438,13 @@ has-tostringtag@^1.0.0:
dependencies:
has-symbols "^1.0.2"
+has-tostringtag@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz#2cdc42d40bef2e5b4eeab7c01a73c54ce7ab5abc"
+ integrity sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==
+ dependencies:
+ has-symbols "^1.0.3"
+
has@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796"
@@ -6418,6 +7459,13 @@ hasown@^2.0.0:
dependencies:
function-bind "^1.1.2"
+hasown@^2.0.2:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003"
+ integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==
+ dependencies:
+ function-bind "^1.1.2"
+
hast-to-hyperscript@^9.0.0:
version "9.0.1"
resolved "https://registry.yarnpkg.com/hast-to-hyperscript/-/hast-to-hyperscript-9.0.1.tgz#9b67fd188e4c81e8ad66f803855334173920218d"
@@ -6769,7 +7817,7 @@ is-alphanumerical@^1.0.0:
is-alphabetical "^1.0.0"
is-decimal "^1.0.0"
-is-arguments@^1.1.1:
+is-arguments@^1.0.4, is-arguments@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.1.1.tgz#15b3f88fda01f2a97fec84ca761a560f123efa9b"
integrity sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==
@@ -6827,6 +7875,13 @@ is-core-module@^2.11.0, is-core-module@^2.9.0:
dependencies:
has "^1.0.3"
+is-core-module@^2.13.0:
+ version "2.15.1"
+ resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.15.1.tgz#a7363a25bee942fefab0de13bf6aa372c82dcc37"
+ integrity sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==
+ dependencies:
+ hasown "^2.0.2"
+
is-date-object@^1.0.1:
version "1.0.4"
resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.4.tgz#550cfcc03afada05eea3dd30981c7b09551f73e5"
@@ -6844,7 +7899,7 @@ is-decimal@^1.0.0:
resolved "https://registry.yarnpkg.com/is-decimal/-/is-decimal-1.0.4.tgz#65a3a5958a1c5b63a706e1b333d7cd9f630d3fa5"
integrity sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==
-is-docker@^2.0.0:
+is-docker@^2.0.0, is-docker@^2.1.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.2.1.tgz#33eeabe23cfe86f14bde4408a02c0cfb853acdaa"
integrity sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==
@@ -6878,6 +7933,13 @@ is-fullwidth-code-point@^3.0.0:
resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d"
integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==
+is-generator-function@^1.0.7:
+ version "1.0.10"
+ resolved "https://registry.yarnpkg.com/is-generator-function/-/is-generator-function-1.0.10.tgz#f1558baf1ac17e0deea7c0415c438351ff2b3c72"
+ integrity sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==
+ dependencies:
+ has-tostringtag "^1.0.0"
+
is-glob@4.0.3, is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1:
version "4.0.3"
resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084"
@@ -6999,6 +8061,13 @@ is-typed-array@^1.1.10:
dependencies:
which-typed-array "^1.1.11"
+is-typed-array@^1.1.3:
+ version "1.1.13"
+ resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.13.tgz#d6c5ca56df62334959322d7d7dd1cca50debe229"
+ integrity sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==
+ dependencies:
+ which-typed-array "^1.1.14"
+
is-unc-path@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/is-unc-path/-/is-unc-path-1.0.0.tgz#d731e8898ed090a12c352ad2eaed5095ad322c9d"
@@ -7048,7 +8117,7 @@ is-windows@^1.0.1:
resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d"
integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==
-is-wsl@^2.1.1:
+is-wsl@^2.1.1, is-wsl@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271"
integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==
@@ -7127,6 +8196,11 @@ js-yaml@^4.0.0, js-yaml@^4.1.0:
dependencies:
argparse "^2.0.1"
+jsdoc-type-pratt-parser@^4.0.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-4.1.0.tgz#ff6b4a3f339c34a6c188cbf50a16087858d22113"
+ integrity sha512-Hicd6JK5Njt2QB6XYFS7ok9e37O8AYk3jTcppG4YVQnYjOemymvTcmc7OWsmq/Qqj5TdRFO5/x/tIPmBeRtGHg==
+
jsdom@^22.1.0:
version "22.1.0"
resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-22.1.0.tgz#0fca6d1a37fbeb7f4aac93d1090d782c56b611c8"
@@ -7161,6 +8235,11 @@ jsesc@^2.5.1:
resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4"
integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==
+jsesc@^3.0.2:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-3.0.2.tgz#bb8b09a6597ba426425f2e4a07245c3d00b9343e"
+ integrity sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==
+
json-parse-even-better-errors@^2.3.0:
version "2.3.1"
resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d"
@@ -7388,6 +8467,11 @@ loupe@^2.3.6:
dependencies:
get-func-name "^2.0.1"
+loupe@^3.1.0, loupe@^3.1.1, loupe@^3.1.2:
+ version "3.1.2"
+ resolved "https://registry.yarnpkg.com/loupe/-/loupe-3.1.2.tgz#c86e0696804a02218f2206124c45d8b15291a240"
+ integrity sha512-23I4pFZHmAemUnz8WZXbYRSKYj801VDaNv9ETuMh7IrMc7VuVVSo+Z9iLE3ni30+U48iDWfi30d3twAXBYmnCg==
+
lower-case-first@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/lower-case-first/-/lower-case-first-2.0.2.tgz#64c2324a2250bf7c37c5901e76a5b5309301160b"
@@ -7429,6 +8513,20 @@ lz-string@^1.5.0:
resolved "https://registry.yarnpkg.com/lz-string/-/lz-string-1.5.0.tgz#c1ab50f77887b712621201ba9fd4e3a6ed099941"
integrity sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==
+magic-string@^0.27.0:
+ version "0.27.0"
+ resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.27.0.tgz#e4a3413b4bab6d98d2becffd48b4a257effdbbf3"
+ integrity sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA==
+ dependencies:
+ "@jridgewell/sourcemap-codec" "^1.4.13"
+
+magic-string@^0.30.0:
+ version "0.30.14"
+ resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.14.tgz#e9bb29870b81cfc1ec3cc656552f5a7fcbf19077"
+ integrity sha512-5c99P1WKTed11ZC0HMJOj6CDIue6F8ySu+bJL+85q1zBEIY8IklrJ1eiKC2NDRh3Ct3FcvmJPyQHb9erXMTJNw==
+ dependencies:
+ "@jridgewell/sourcemap-codec" "^1.5.0"
+
magic-string@^0.30.1:
version "0.30.5"
resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.5.tgz#1994d980bd1c8835dc6e78db7cbd4ae4f24746f9"
@@ -7459,6 +8557,11 @@ map-cache@^0.2.0:
resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf"
integrity sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=
+map-or-similar@^1.5.0:
+ version "1.5.0"
+ resolved "https://registry.yarnpkg.com/map-or-similar/-/map-or-similar-1.5.0.tgz#6de2653174adfb5d9edc33c69d3e92a1b76faf08"
+ integrity sha512-0aF7ZmVon1igznGI4VS30yugpduQW3y3GkcgGJOp7d8x8QrizhigUxjI/m2UojsXXto+jLAH3KSz+xOJTiORjg==
+
markdown-table@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/markdown-table/-/markdown-table-2.0.0.tgz#194a90ced26d31fe753d8b9434430214c011865b"
@@ -7599,6 +8702,13 @@ mdurl@^1.0.0:
resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e"
integrity sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4=
+memoizerific@^1.11.3:
+ version "1.11.3"
+ resolved "https://registry.yarnpkg.com/memoizerific/-/memoizerific-1.11.3.tgz#7c87a4646444c32d75438570905f2dbd1b1a805a"
+ integrity sha512-/EuHYwAPdLtXwAwSZkh/Gutery6pD2KYd44oQLhAvQp/50mpyduZh8Q7PYHXTCJ+wuXxt7oij2LXyIJOOYFPog==
+ dependencies:
+ map-or-similar "^1.5.0"
+
merge2@^1.3.0, merge2@^1.4.1:
version "1.4.1"
resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae"
@@ -7704,7 +8814,7 @@ min-document@^2.19.0:
dependencies:
dom-walk "^0.1.0"
-min-indent@^1.0.0:
+min-indent@^1.0.0, min-indent@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869"
integrity sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==
@@ -7863,6 +8973,11 @@ node-releases@^2.0.13:
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.13.tgz#d5ed1627c23e3461e819b02e57b75e4899b1c81d"
integrity sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==
+node-releases@^2.0.18:
+ version "2.0.18"
+ resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.18.tgz#f010e8d35e2fe8d6b2944f03f70213ecedc4ca3f"
+ integrity sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==
+
normalize-path@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9"
@@ -8001,6 +9116,15 @@ open@^7.3.1:
is-docker "^2.0.0"
is-wsl "^2.1.1"
+open@^8.0.4:
+ version "8.4.2"
+ resolved "https://registry.yarnpkg.com/open/-/open-8.4.2.tgz#5b5ffe2a8f793dcd2aad73e550cb87b59cb084f9"
+ integrity sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==
+ dependencies:
+ define-lazy-prop "^2.0.0"
+ is-docker "^2.1.1"
+ is-wsl "^2.2.0"
+
optimism@^0.16.0:
version "0.16.1"
resolved "https://registry.yarnpkg.com/optimism/-/optimism-0.16.1.tgz#7c8efc1f3179f18307b887e18c15c5b7133f6e7d"
@@ -8236,16 +9360,31 @@ pathval@^1.1.1:
resolved "https://registry.yarnpkg.com/pathval/-/pathval-1.1.1.tgz#8534e77a77ce7ac5a2512ea21e0fdb8fcf6c3d8d"
integrity sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==
+pathval@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/pathval/-/pathval-2.0.0.tgz#7e2550b422601d4f6b8e26f1301bc8f15a741a25"
+ integrity sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==
+
picocolors@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c"
integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==
+picocolors@^1.1.0:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b"
+ integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==
+
picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1:
version "2.3.1"
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
+picomatch@^4.0.2:
+ version "4.0.2"
+ resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.2.tgz#77c742931e8f3b8820946c76cd0c1f13730d1dab"
+ integrity sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==
+
pify@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231"
@@ -8260,6 +9399,18 @@ pkg-types@^1.0.3:
mlly "^1.2.0"
pathe "^1.1.0"
+polished@^4.2.2:
+ version "4.3.1"
+ resolved "https://registry.yarnpkg.com/polished/-/polished-4.3.1.tgz#5a00ae32715609f83d89f6f31d0f0261c6170548"
+ integrity sha512-OBatVyC/N7SCW/FaDHrSd+vn0o5cS855TOmYi4OkdWUMSJCET/xip//ch8xGUvtr3i44X9LVyWwQlRMTN3pwSA==
+ dependencies:
+ "@babel/runtime" "^7.17.8"
+
+possible-typed-array-names@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz#89bb63c6fada2c3e90adc4a647beeeb39cc7bf8f"
+ integrity sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==
+
postcss-value-parser@^4.0.2:
version "4.2.0"
resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514"
@@ -8315,6 +9466,11 @@ prismjs@^1.22.0, prismjs@^1.27.0, prismjs@~1.23.0:
resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.29.0.tgz#f113555a8fa9b57c35e637bba27509dcf802dd12"
integrity sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==
+process@^0.11.10:
+ version "0.11.10"
+ resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182"
+ integrity sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==
+
promise@^7.1.1:
version "7.3.1"
resolved "https://registry.yarnpkg.com/promise/-/promise-7.3.1.tgz#064b72602b18f90f29192b8b1bc418ffd1ebd3bf"
@@ -8331,7 +9487,7 @@ prop-types@15.7.2:
object-assign "^4.1.1"
react-is "^16.8.1"
-prop-types@^15.5.10, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1:
+prop-types@^15.5.10, prop-types@^15.5.7, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1:
version "15.8.1"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
@@ -8968,6 +10124,42 @@ react-color@^2.19.3:
reactcss "^1.2.0"
tinycolor2 "^1.4.1"
+react-confetti@^6.1.0:
+ version "6.1.0"
+ resolved "https://registry.yarnpkg.com/react-confetti/-/react-confetti-6.1.0.tgz#03dc4340d955acd10b174dbf301f374a06e29ce6"
+ integrity sha512-7Ypx4vz0+g8ECVxr88W9zhcQpbeujJAVqL14ZnXJ3I23mOI9/oBVTQ3dkJhUmB0D6XOtCZEM6N0Gm9PMngkORw==
+ dependencies:
+ tween-functions "^1.2.0"
+
+react-docgen-typescript@^2.2.2:
+ version "2.2.2"
+ resolved "https://registry.yarnpkg.com/react-docgen-typescript/-/react-docgen-typescript-2.2.2.tgz#4611055e569edc071204aadb20e1c93e1ab1659c"
+ integrity sha512-tvg2ZtOpOi6QDwsb3GZhOjDkkX0h8Z2gipvTg6OVMUyoYoURhEiRNePT8NZItTVCDh39JJHnLdfCOkzoLbFnTg==
+
+react-docgen@^7.0.0:
+ version "7.1.0"
+ resolved "https://registry.yarnpkg.com/react-docgen/-/react-docgen-7.1.0.tgz#4b41e557dab939a5157be09ee532fd09c07d99fc"
+ integrity sha512-APPU8HB2uZnpl6Vt/+0AFoVYgSRtfiP6FLrZgPPTDmqSb2R4qZRbgd0A3VzIFxDt5e+Fozjx79WjLWnF69DK8g==
+ dependencies:
+ "@babel/core" "^7.18.9"
+ "@babel/traverse" "^7.18.9"
+ "@babel/types" "^7.18.9"
+ "@types/babel__core" "^7.18.0"
+ "@types/babel__traverse" "^7.18.0"
+ "@types/doctrine" "^0.0.9"
+ "@types/resolve" "^1.20.2"
+ doctrine "^3.0.0"
+ resolve "^1.22.1"
+ strip-indent "^4.0.0"
+
+"react-dom@^16.8.0 || ^17.0.0 || ^18.0.0":
+ version "18.3.1"
+ resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.3.1.tgz#c2265d79511b57d479b3dd3fdfa51536494c5cb4"
+ integrity sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==
+ dependencies:
+ loose-envify "^1.1.0"
+ scheduler "^0.23.2"
+
react-dom@^17.0.0:
version "17.0.2"
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-17.0.2.tgz#ecffb6845e3ad8dbfcdc498f0d0a939736502c23"
@@ -9040,6 +10232,11 @@ react-is@^18.0.0, react-is@^18.2.0:
resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b"
integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==
+react-is@^18.3.1:
+ version "18.3.1"
+ resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.3.1.tgz#e83557dc12eae63a99e003a46388b1dcbb44db7e"
+ integrity sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==
+
react-js-cron@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/react-js-cron/-/react-js-cron-2.1.0.tgz#ce88a3260617222d8e1dc51534bb6606088304fc"
@@ -9157,6 +10354,13 @@ react-visibility-sensor@^5.1.1:
dependencies:
prop-types "^15.7.2"
+"react@^16.8.0 || ^17.0.0 || ^18.0.0":
+ version "18.3.1"
+ resolved "https://registry.yarnpkg.com/react/-/react-18.3.1.tgz#49ab892009c53933625bd16b2533fc754cab2891"
+ integrity sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==
+ dependencies:
+ loose-envify "^1.1.0"
+
react@^17.0.0:
version "17.0.2"
resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037"
@@ -9202,6 +10406,17 @@ readdirp@~3.6.0:
dependencies:
picomatch "^2.2.1"
+recast@^0.23.5:
+ version "0.23.9"
+ resolved "https://registry.yarnpkg.com/recast/-/recast-0.23.9.tgz#587c5d3a77c2cfcb0c18ccce6da4361528c2587b"
+ integrity sha512-Hx/BGIbwj+Des3+xy5uAtAbdCyqK9y9wbBcDFDYanLS9JnMqf7OeF87HQwUimE87OEc72mr6tkKUKMBBL+hF9Q==
+ dependencies:
+ ast-types "^0.16.1"
+ esprima "~4.0.0"
+ source-map "~0.6.1"
+ tiny-invariant "^1.3.3"
+ tslib "^2.0.1"
+
redent@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/redent/-/redent-3.0.0.tgz#e557b7998316bb53c9f1f56fa626352c6963059f"
@@ -9240,6 +10455,11 @@ regenerator-runtime@^0.13.11, regenerator-runtime@^0.13.4:
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9"
integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==
+regenerator-runtime@^0.14.0:
+ version "0.14.1"
+ resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz#356ade10263f685dda125100cd862c1db895327f"
+ integrity sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==
+
regexp.prototype.flags@^1.4.3:
version "1.4.3"
resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz#87cab30f80f66660181a3bb7bf5981a872b367ac"
@@ -9462,6 +10682,15 @@ resolve@^1.12.0, resolve@^1.19.0, resolve@^1.22.1:
path-parse "^1.0.7"
supports-preserve-symlinks-flag "^1.0.0"
+resolve@^1.22.8:
+ version "1.22.8"
+ resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d"
+ integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==
+ dependencies:
+ is-core-module "^2.13.0"
+ path-parse "^1.0.7"
+ supports-preserve-symlinks-flag "^1.0.0"
+
resolve@^2.0.0-next.4:
version "2.0.0-next.4"
resolved "https://registry.yarnpkg.com/resolve/-/resolve-2.0.0-next.4.tgz#3d37a113d6429f496ec4752d2a2e58efb1fd4660"
@@ -9606,6 +10835,13 @@ scheduler@^0.20.2:
loose-envify "^1.1.0"
object-assign "^4.1.1"
+scheduler@^0.23.2:
+ version "0.23.2"
+ resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.2.tgz#414ba64a3b282892e944cf2108ecc078d115cdc3"
+ integrity sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==
+ dependencies:
+ loose-envify "^1.1.0"
+
screenfull@^5.1.0:
version "5.2.0"
resolved "https://registry.yarnpkg.com/screenfull/-/screenfull-5.2.0.tgz#6533d524d30621fc1283b9692146f3f13a93d1ba"
@@ -9662,6 +10898,11 @@ semver@^7.5.4:
dependencies:
lru-cache "^6.0.0"
+semver@^7.6.2:
+ version "7.6.3"
+ resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.3.tgz#980f7b5550bc175fb4dc09403085627f9eb33143"
+ integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==
+
sentence-case@^3.0.4:
version "3.0.4"
resolved "https://registry.yarnpkg.com/sentence-case/-/sentence-case-3.0.4.tgz#3645a7b8c117c787fde8702056225bb62a45131f"
@@ -9686,6 +10927,18 @@ set-function-length@^1.1.1:
gopd "^1.0.1"
has-property-descriptors "^1.0.0"
+set-function-length@^1.2.2:
+ version "1.2.2"
+ resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449"
+ integrity sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==
+ dependencies:
+ define-data-property "^1.1.4"
+ es-errors "^1.3.0"
+ function-bind "^1.1.2"
+ get-intrinsic "^1.2.4"
+ gopd "^1.0.1"
+ has-property-descriptors "^1.0.2"
+
set-function-name@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/set-function-name/-/set-function-name-2.0.1.tgz#12ce38b7954310b9f61faa12701620a0c882793a"
@@ -9815,7 +11068,7 @@ source-map@^0.5.7:
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc"
integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=
-source-map@^0.6.1, source-map@~0.6.0:
+source-map@^0.6.1, source-map@~0.6.0, source-map@~0.6.1:
version "0.6.1"
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
@@ -9903,6 +11156,13 @@ stop-iteration-iterator@^1.0.0:
dependencies:
internal-slot "^1.0.4"
+storybook@^8.1.11:
+ version "8.4.7"
+ resolved "https://registry.yarnpkg.com/storybook/-/storybook-8.4.7.tgz#a3068787a58074cec1b4197eed1c4427ec644b3f"
+ integrity sha512-RP/nMJxiWyFc8EVMH5gp20ID032Wvk+Yr3lmKidoegto5Iy+2dVQnUoElZb2zpbVXNHWakGuAkfI0dY1Hfp/vw==
+ dependencies:
+ "@storybook/core" "8.4.7"
+
streamsearch@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-1.1.0.tgz#404dd1e2247ca94af554e841a8ef0eaa238da764"
@@ -10008,6 +11268,13 @@ strip-indent@^3.0.0:
dependencies:
min-indent "^1.0.0"
+strip-indent@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-4.0.0.tgz#b41379433dd06f5eae805e21d631e07ee670d853"
+ integrity sha512-mnVSV2l+Zv6BLpSD/8V87CW/y9EmmbYzGCIavsnsI6/nwn26DwffM/yztm30Z/I2DY9wdS3vXVCMnHDgZaVNoA==
+ dependencies:
+ min-indent "^1.0.1"
+
strip-json-comments@^3.1.0, strip-json-comments@^3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006"
@@ -10048,6 +11315,11 @@ stylis@4.1.3, stylis@^4.0.6:
resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.1.3.tgz#fd2fbe79f5fed17c55269e16ed8da14c84d069f7"
integrity sha512-GP6WDNWf+o403jrEp9c5jibKavrtLW+/qYGhFxFrG8maXhwTBI7gLLhiBb0o7uFccWN+EOS9aMO6cGHWAO07OA==
+stylis@4.2.0:
+ version "4.2.0"
+ resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.2.0.tgz#79daee0208964c8fe695a42fcffcac633a211a51"
+ integrity sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==
+
supports-color@^5.3.0, supports-color@^5.5.0:
version "5.5.0"
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f"
@@ -10129,6 +11401,11 @@ tiny-invariant@^1.0.2:
resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.1.0.tgz#634c5f8efdc27714b7f386c35e6760991d230875"
integrity sha512-ytxQvrb1cPc9WBEI/HSeYYoGD0kWnGEOR8RY6KomWLBVhqz0RgTwVO9dLrGz7dC+nN9llyI7OKAgRq8Vq4ZBSw==
+tiny-invariant@^1.3.1, tiny-invariant@^1.3.3:
+ version "1.3.3"
+ resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.3.3.tgz#46680b7a873a0d5d10005995eb90a70d74d60127"
+ integrity sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==
+
tiny-warning@^1.0.0, tiny-warning@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754"
@@ -10149,11 +11426,21 @@ tinypool@^0.7.0:
resolved "https://registry.yarnpkg.com/tinypool/-/tinypool-0.7.0.tgz#88053cc99b4a594382af23190c609d93fddf8021"
integrity sha512-zSYNUlYSMhJ6Zdou4cJwo/p7w5nmAH17GRfU/ui3ctvjXFErXXkruT4MWW6poDeXgCaIBlGLrfU6TbTXxyGMww==
+tinyrainbow@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/tinyrainbow/-/tinyrainbow-1.2.0.tgz#5c57d2fc0fb3d1afd78465c33ca885d04f02abb5"
+ integrity sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==
+
tinyspy@^2.1.1:
version "2.2.0"
resolved "https://registry.yarnpkg.com/tinyspy/-/tinyspy-2.2.0.tgz#9dc04b072746520b432f77ea2c2d17933de5d6ce"
integrity sha512-d2eda04AN/cPOR89F7Xv5bK/jrQEhmcLFe6HFldoeO9AJtps+fqEnh486vnT/8y4bw38pSyxDcTCAq+Ks2aJTg==
+tinyspy@^3.0.0:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/tinyspy/-/tinyspy-3.0.2.tgz#86dd3cf3d737b15adcf17d7887c84a75201df20a"
+ integrity sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==
+
title-case@^3.0.3:
version "3.0.3"
resolved "https://registry.yarnpkg.com/title-case/-/title-case-3.0.3.tgz#bc689b46f02e411f1d1e1d081f7c3deca0489982"
@@ -10217,6 +11504,11 @@ ts-api-utils@^1.0.1:
resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-1.0.3.tgz#f12c1c781d04427313dbac808f453f050e54a331"
integrity sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg==
+ts-dedent@^2.0.0, ts-dedent@^2.2.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/ts-dedent/-/ts-dedent-2.2.0.tgz#39e4bd297cd036292ae2394eb3412be63f563bb5"
+ integrity sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==
+
ts-easing@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/ts-easing/-/ts-easing-0.2.0.tgz#c8a8a35025105566588d87dbda05dd7fbfa5a4ec"
@@ -10244,6 +11536,15 @@ tsconfig-paths@^3.14.1:
minimist "^1.2.6"
strip-bom "^3.0.0"
+tsconfig-paths@^4.2.0:
+ version "4.2.0"
+ resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz#ef78e19039133446d244beac0fd6a1632e2d107c"
+ integrity sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==
+ dependencies:
+ json5 "^2.2.2"
+ minimist "^1.2.6"
+ strip-bom "^3.0.0"
+
tslib@^1.10.0, tslib@^1.8.1:
version "1.14.1"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
@@ -10254,6 +11555,11 @@ tslib@^2.0.0, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.3.0, tslib@^2.4.0:
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.1.tgz#0d0bfbaac2880b91e22df0768e55be9753a5b17e"
integrity sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==
+tslib@^2.0.1:
+ version "2.8.1"
+ resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f"
+ integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==
+
tslib@^2.3.1, tslib@^2.5.0, tslib@^2.6.1, tslib@^2.6.2:
version "2.6.2"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae"
@@ -10298,6 +11604,11 @@ turndown@^7.1.1:
dependencies:
domino "^2.1.6"
+tween-functions@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/tween-functions/-/tween-functions-1.2.0.tgz#1ae3a50e7c60bb3def774eac707acbca73bbc3ff"
+ integrity sha512-PZBtLYcCLtEcjL14Fzb1gSxPBeL7nWvGhO5ZFPGqziCcr8uvHp0NDmdjBchp6KHL+tExcg0m3NISmKxhU394dA==
+
type-check@^0.4.0, type-check@~0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1"
@@ -10325,7 +11636,7 @@ type-fest@^1.2.0:
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-1.4.0.tgz#e9fb813fe3bf1744ec359d55d1affefa76f14be1"
integrity sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==
-type-fest@^2.0.0:
+type-fest@^2.0.0, type-fest@^2.19.0:
version "2.19.0"
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-2.19.0.tgz#88068015bb33036a598b952e55e9311a60fd3a9b"
integrity sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==
@@ -10438,6 +11749,14 @@ unixify@^1.0.0:
dependencies:
normalize-path "^2.1.1"
+unplugin@^1.3.1:
+ version "1.16.0"
+ resolved "https://registry.yarnpkg.com/unplugin/-/unplugin-1.16.0.tgz#ca0f248bf8798cd752dd02e5b381223b737cef72"
+ integrity sha512-5liCNPuJW8dqh3+DM6uNM2EI3MLLpCKp/KY+9pB5M2S2SR2qvvDHhKgBOaTWEbZTAws3CXfB0rKTIolWKL05VQ==
+ dependencies:
+ acorn "^8.14.0"
+ webpack-virtual-modules "^0.6.2"
+
update-browserslist-db@^1.0.11:
version "1.0.11"
resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz#9a2a641ad2907ae7b3616506f4b977851db5b940"
@@ -10454,6 +11773,14 @@ update-browserslist-db@^1.0.13:
escalade "^3.1.1"
picocolors "^1.0.0"
+update-browserslist-db@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz#80846fba1d79e82547fb661f8d141e0945755fe5"
+ integrity sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==
+ dependencies:
+ escalade "^3.2.0"
+ picocolors "^1.1.0"
+
upper-case-first@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/upper-case-first/-/upper-case-first-2.0.2.tgz#992c3273f882abd19d1e02894cc147117f844324"
@@ -10525,11 +11852,27 @@ util-deprecate@^1.0.1:
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=
+util@^0.12.5:
+ version "0.12.5"
+ resolved "https://registry.yarnpkg.com/util/-/util-0.12.5.tgz#5f17a6059b73db61a875668781a1c2b136bd6fbc"
+ integrity sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==
+ dependencies:
+ inherits "^2.0.3"
+ is-arguments "^1.0.4"
+ is-generator-function "^1.0.7"
+ is-typed-array "^1.1.3"
+ which-typed-array "^1.1.2"
+
uuid@^8.3.2:
version "8.3.2"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==
+uuid@^9.0.0:
+ version "9.0.1"
+ resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.1.tgz#e188d4c8853cc722220392c424cd637f32293f30"
+ integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==
+
value-equal@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/value-equal/-/value-equal-1.0.1.tgz#1e0b794c734c5c0cade179c437d356d931a34d6c"
@@ -10706,6 +12049,11 @@ webidl-conversions@^7.0.0:
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-7.0.0.tgz#256b4e1882be7debbf01d05f0aa2039778ea080a"
integrity sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==
+webpack-virtual-modules@^0.6.2:
+ version "0.6.2"
+ resolved "https://registry.yarnpkg.com/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz#057faa9065c8acf48f24cb57ac0e77739ab9a7e8"
+ integrity sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==
+
whatwg-encoding@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz#e7635f597fd87020858626805a2729fa7698ac53"
@@ -10771,6 +12119,17 @@ which-typed-array@^1.1.11, which-typed-array@^1.1.9:
gopd "^1.0.1"
has-tostringtag "^1.0.0"
+which-typed-array@^1.1.14, which-typed-array@^1.1.2:
+ version "1.1.16"
+ resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.16.tgz#db4db429c4706feca2f01677a144278e4a8c216b"
+ integrity sha512-g+N+GAWiRj66DngFwHvISJd+ITsyphZvD1vChfVg6cEdnzy53GzB3oy0fUNlvhz7H7+MiqhYr26qxQShCpKTTQ==
+ dependencies:
+ available-typed-arrays "^1.0.7"
+ call-bind "^1.0.7"
+ for-each "^0.3.3"
+ gopd "^1.0.1"
+ has-tostringtag "^1.0.2"
+
which@^2.0.1:
version "2.0.2"
resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1"
@@ -10824,6 +12183,11 @@ ws@^8.13.0:
resolved "https://registry.yarnpkg.com/ws/-/ws-8.13.0.tgz#9a9fb92f93cf41512a0735c8f4dd09b8a1211cd0"
integrity sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==
+ws@^8.2.3:
+ version "8.18.0"
+ resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.0.tgz#0d7505a6eafe2b0e712d232b42279f53bc289bbc"
+ integrity sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==
+
xml-name-validator@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-4.0.0.tgz#79a006e2e63149a8600f15430f0a4725d1524835"
diff --git a/docs-website/sidebars.js b/docs-website/sidebars.js
index 6ae50215c8166f..2f1ac047720978 100644
--- a/docs-website/sidebars.js
+++ b/docs-website/sidebars.js
@@ -239,7 +239,21 @@ module.exports = {
type: "doc",
id: "docs/posts",
},
- "docs/features/feature-guides/properties",
+ {
+ label: "Properties",
+ type: "category",
+ collapsed: true,
+ items: [
+ {
+ type: "doc",
+ id: "docs/features/feature-guides/properties/overview",
+ },
+ {
+ type: "doc",
+ id: "docs/features/feature-guides/properties/create-a-property",
+ },
+ ],
+ },
{
label: "Schema history",
type: "doc",
diff --git a/docs/api/tutorials/structured-properties.md b/docs/api/tutorials/structured-properties.md
index b606ce9a8e2455..95c89424e9ca7a 100644
--- a/docs/api/tutorials/structured-properties.md
+++ b/docs/api/tutorials/structured-properties.md
@@ -8,7 +8,7 @@ import TabItem from '@theme/TabItem';
Structured properties are a structured, named set of properties that can be attached to logical entities like Datasets, DataJobs, etc.
Structured properties have values that are types. Conceptually, they are like “field definitions”.
-Learn more about structured properties in the [Structured Properties Feature Guide](../../../docs/features/feature-guides/properties.md).
+Learn more about structured properties in the [Structured Properties Feature Guide](../../../docs/features/feature-guides/properties/overview.md).
### Goal Of This Guide
diff --git a/docs/features/feature-guides/compliance-forms/create-a-form.md b/docs/features/feature-guides/compliance-forms/create-a-form.md
index e97aaaa581777d..a768bb16e4f64c 100644
--- a/docs/features/feature-guides/compliance-forms/create-a-form.md
+++ b/docs/features/feature-guides/compliance-forms/create-a-form.md
@@ -175,11 +175,11 @@ Great question. We are working on Compliance Forms Analytics that will directly
### API Tutorials
-- [API Guides on Documentation Form](../../../api/tutorials/forms.md)
+- [Compliance Form API Guide](../../../api/tutorials/forms.md)
### Related Features
-- [DataHub Properties](../../feature-guides/properties.md)
+- [DataHub Structured Properties](../../feature-guides/properties/overview.md)
## Next Steps
diff --git a/docs/features/feature-guides/properties.md b/docs/features/feature-guides/properties.md
deleted file mode 100644
index abdb736ad2a429..00000000000000
--- a/docs/features/feature-guides/properties.md
+++ /dev/null
@@ -1,158 +0,0 @@
-import FeatureAvailability from '@site/src/components/FeatureAvailability';
-import Tabs from '@theme/Tabs';
-import TabItem from '@theme/TabItem';
-
-# About DataHub Properties
-
-
-DataHub Custom Properties and Structured Properties are powerful tools to collect meaningful metadata for Assets that might not perfectly fit into other Aspects within DataHub, such as Glossary Terms, Tags, etc. Both types can be found in an Asset's Properties tab:
-
-
-
-
-
-This guide will explain the differences and use cases of each property type.
-
-## What are Custom Properties and Structured Properties?
-Here are the differences between the two property types at a glance:
-
-| Custom Properties | Structured Properties |
-| --- | --- |
-| Map of key-value pairs stored as strings | Validated namespaces and data types |
-| Added to assets during ingestion and via API | Defined via YAML; created and added to assets via CLI |
-| No support for UI-based Edits | Support for UI-based edits |
-
-**Custom Properties** are key-value pairs of strings that capture additional information about assets that is not readily available in standard metadata fields. Custom Properties can be added to assets automatically during ingestion or programmatically via API and *cannot* be edited via the UI.
-
-
-
-Example of Custom Properties assigned to a Dataset
-
-**Structured Properties** are an extension of Custom Properties, providing a structured and validated way to attach metadata to DataHub Assets. Available as of v0.13.1, Structured Properties have a pre-defined type (Date, Integer, URN, String, etc.). They can be configured to only accept a specific set of allowed values, making it easier to ensure high levels of data quality and consistency. Structured Properties are defined via YAML, added to assets via CLI, and can be edited via the UI.
-
-
-
-Example of Structured Properties assigned to a Dataset
-
-## Use Cases for Custom Properties and Structured Properties
-**Custom Properties** are useful for capturing raw metadata from source systems during ingestion or programmatically via API. Some examples include:
-
-- GitHub file location of code which generated a dataset
-- Data encoding type
-- Account ID, cluster size, and region where a dataset is stored
-
-**Structured Properties** are useful for setting and enforcing standards of metadata collection, particularly in support of compliance and governance initiatives. Values can be added programmatically via API, then manually via the DataHub UI as necessary. Some examples include:
-
-- Deprecation Date
- - Type: Date, Single Select
- - Validation: Must be formatted as 'YYYY-MM-DD'
-- Data Retention Period
- - Type: String, Single Select
- - Validation: Adheres to allowed values "30 Days", "90 Days", "365 Days", or "Indefinite"
-- Consulted Compliance Officer, chosen from a list of DataHub users
- - Type: DataHub User, Multi-Select
- - Validation: Must be valid DataHub User URN
-
-By using Structured Properties, compliance and governance officers can ensure consistency in data collection across assets.
-
-## Creating, Assigning, and Editing Structured Properties
-
-Structured Properties are defined via YAML, then created and assigned to DataHub Assets via the DataHub CLI.
-
-Here's how we would define the above examples in YAML:
-
-
-
-
-```yaml
-- id: deprecation_date
- qualified_name: deprecation_date
- type: date # Supported types: date, string, number, urn, rich_text
- cardinality: SINGLE # Supported options: SINGLE, MULTIPLE
- display_name: Deprecation Date
- description: "Scheduled date when resource will be deprecated in the source system"
- entity_types: # Define which types of DataHub Assets the Property can be assigned to
- - dataset
-```
-
-
-
-
-```yaml
-- id: retention_period
- qualified_name: retention_period
- type: string # Supported types: date, string, number, urn, rich_text
- cardinality: SINGLE # Supported options: SINGLE, MULTIPLE
- display_name: Data Retention Period
- description: "Predetermined storage duration before being deleted or archived
- based on legal, regulatory, or organizational requirements"
- entity_types: # Define which types of DataHub Assets the Property can be assigned to
- - dataset
- allowed_values:
- - value: "30 Days"
- description: "Use this for datasets that are ephemeral and contain PII"
- - value: "90 Days"
- description: "Use this for datasets that drive monthly reporting but contain PII"
- - value: "365 Days"
- description: "Use this for non-sensitive data that can be retained for longer"
- - value: "Indefinite"
- description: "Use this for non-sensitive data that can be retained indefinitely"
-```
-
-
-
-
-```yaml
-- id: compliance_officer
- qualified_name: compliance_officer
- type: urn # Supported types: date, string, number, urn, rich_text
- cardinality: MULTIPLE # Supported options: SINGLE, MULTIPLE
- display_name: Consulted Compliance Officer(s)
- description: "Member(s) of the Compliance Team consulted/informed during audit"
- type_qualifier: # Define the type of Asset URNs to allow
- - corpuser
- - corpGroup
- entity_types: # Define which types of DataHub Assets the Property can be assigned to
- - dataset
-```
-
-
-
-
-:::note
-To learn more about creating and assigning Structured Properties via CLI, please see the [Create Structured Properties](/docs/api/tutorials/structured-properties.md) tutorial.
-:::
-
-Once a Structured Property is assigned to an Asset, Users with the `Edit Properties` Metadata Privilege will be able to change Structured Property values via the DataHub UI.
-
-
-
-Example of editing the value of a Structured Property via the UI
-
-### Videos
-
-**Deep Dive: UI-Editable Properties**
-
-
-VIDEO
-
-
-
-### API
-
-Please see the following API guides related to Custom and Structured Properties:
-
-- [Custom Properties API Guide](/docs/api/tutorials/structured-properties.md)
-- [Structured Properties API Guide](/docs/api/tutorials/structured-properties.md)
-
-
-## FAQ and Troubleshooting
-
-**Why can't I edit the value of a Structured Property from the DataHub UI?**
-1. Your version of DataHub does not support UI-based edits of Structured Properties. Confirm you are running DataHub v0.13.1 or later.
-2. You are attempting to edit a Custom Property, not a Structured Property. Confirm you are trying to edit a Structured Property, which will have an "Edit" button visible. Please note that Custom Properties are not eligible for UI-based edits to minimize overwrites during recurring ingestion.
-3. You do not have the necessary privileges. Confirm with your Admin that you have the `Edit Properties` Metadata Privilege.
-
-### Related Features
-
-- [Compliance Forms](compliance-forms/overview.md)
\ No newline at end of file
diff --git a/docs/features/feature-guides/properties/create-a-property.md b/docs/features/feature-guides/properties/create-a-property.md
new file mode 100644
index 00000000000000..2428f70a105516
--- /dev/null
+++ b/docs/features/feature-guides/properties/create-a-property.md
@@ -0,0 +1,261 @@
+---
+title: Create and Add a Structured Property
+---
+
+import FeatureAvailability from '@site/src/components/FeatureAvailability';
+
+# Create and Add a DataHub Structured Property
+
+
+This guide walks you through creating a Structured Property via the DataHub UI, including:
+
+1. Defining a new Structured Property
+2. Configuring display preferences for a Structured Property
+3. Adding a Structured Property to an Asset
+4. Adding a Structured Property to a Column
+
+:::note
+To learn more about creating and assigning Structured Properties via the CLI, please see the [Create Structured Properties](/docs/api/tutorials/structured-properties.md) tutorial.
+:::
+
+### Prerequisites
+
+To create, edit, or remove Structured Properties, you must have the **View Structured Properties** and **Manage Structured Properties** platform privileges.
+
+To add an existing Structured Property to an Asset, change its value, or remove it from an Asset, you must have the **Edit Properties** metadata privilege.
+
+## Define a New Structured Property
+
+From the navigation bar, go to **Govern** > **Structured Properties**.
+
+Click **+ Create** to start defining your Property.
+
+
+
+
+
+First, provide the following details:
+
+1. **Name and Description:** Clearly describe the purpose and meaning of the Structured Property so users understand its role and context.
+2. **Property Type:** Choose a type that best fits the metadata you want to collect. Available types include **Text**, **Number**, **Date**, **DataHub Entity**, or **Rich Text**. Choosing any of the "List" options allows multiple entries for the Property.
+3. **Allowed Values (Optional):** For **Text**, **Number**, and **DataHub Entity** types, define a set of allowed values to ensure consistent input across assets.
+4. **Applies To:** Specify which DataHub asset types (e.g., Datasets, Dashboards, Pipelines) the Structured Property can be associated with, ensuring relevance and precision.
+
+:::caution
+Once you you save a Structured Property, you **cannot** edit or remove Allowed Values. However, you can add additional Allowed Values.
+:::
+
+For example, imagine your organization wants to standardize how data assets (e.g., Datasets, Tasks, Pipelines) are categorized during their development cycle. By creating a **Lifecycle Stage** Structured Property, you can set a pre-defined list of allowed statuses, such as **Draft**, **Review**, and **Prod**, ensuring consistency and transparency.
+
+
+
+
+
+## Set Display Preferences for the Structured Property
+
+When defining a Structured Property, you can customize how it will be visible to DataHub users. By default, Structured Properties are visible in an Asset's **Properties** tab but can be conditionally configured with the following options:
+
+1. **Hide Property:**
+ Use this option if the Structured Property contains sensitive metadata that should not be visible to DataHub users via the UI. This ensures that only users with the necessary permissions can view or interact with the property values.
+
+2. **Customize Visibility:**
+ Decide where the Structured Property appears across the DataHub UI:
+ - **Asset Badge:** Display the property value as a badge on Assets to highlight key metadata.
+ - **Asset Sidebar:** Show the property in the Asset Sidebar for quick visibility while navigating an Asset.
+
+3. **Show in Search Filters:**
+ Enable this option to allow users to filter Assets by the values of this Structured Property. This improves discoverability and facilitates searches for Assets with specific attributes or classifications.
+
+4. **Show in Columns Table:**
+ Use this option to display the Structured Property value in the Dataset Schema view’s Columns Table. This is particularly useful for capturing field-level custom metadata and making it accessible alongside schema details.
+
+For the **Lifecycle Stage** example, imagine you want to allow users to filter by lifecycle status and view it at a glance during data discovery. To achieve this, you would enable **Show in Search Filters**, **Asset Badge**, and **Asset Sidebar**:
+
+
+
+
+
+## Add a Structured Property to an Asset
+
+Once a Structured Property has been defined, you can add it to the designated Asset Types.
+
+From an Asset's **Properties** tab, click the `+` button to see a drop-down list of all available Structured Properties. For example, you can now see **Lifecycle Stage** as an option for the `pet_profiles` Dataset:
+
+
+
+
+
+Continuing with the **Lifecycle Stage** example, designate the `pet_profiles` Dataset as being in **Prod**:
+
+
+
+
+
+After clicking **Save**, the **Lifecycle Stage** for `pet_profiles` will appear in the following sections of the Asset Page:
+
+1. Properties Tab
+2. Asset Badge
+3. Asset Sidebar
+
+
+
+
+
+:::info
+[**DataHub Compliance Forms**](../compliance-forms/overview.md) make it easy to update values for multiple Assets at once!
+:::
+
+### Edit or Remove a Structured Property from an Asset
+
+After a Structured Property has been added to an Asset, users can modify its value or remove the property entirely using the **More** menu:
+
+
+
+
+
+### Search for Assets by a Structured Property Value
+
+For Structured Properties that have **Show in Search Filters** enabled, users can filter search results based on allowed values.
+
+For example, with the **Lifecycle Stage** property enabled as a filter, users can find it under the **More** dropdown in the Search interface:
+
+
+
+
+
+From here, you can quickly narrow down Search results based on the desired stage, such as **Prod**:
+
+
+
+
+
+Notice how the **Prod** value is displayed prominently on the `pet_profiles` Asset Badge:
+
+
+
+
+
+## Add a Structured Property to a Column
+
+Structured Properties can be applied at the column level, providing deeper context for how individual dataset fields relate to business concepts or terminology. In this example, we’ll create a Structured Property called **Business Label** to help business users understand how dataset columns align with common terminology, acronyms, or key business concepts.
+
+### Define the Business Label Property
+
+Follow these steps to define and configure the **Business Label** Structured Property:
+
+1. **Property Details:**
+ - **Name:** Business Label
+ - **Description:** Provide a description to explain its purpose, such as:
+ *"A user-friendly name for a dataset column, helping business users understand its meaning."*
+ - **Property Type:** Select **Text**, allowing any valid string to be entered.
+ - **Applies To:** Set this property to apply exclusively to **Columns**.
+
+2. **Display Preferences:**
+ - By default, column-level Structured Properties will be enabled for **all columns** on **all datasets** within DataHub, accessible via the Column Sidebar.
+ - Optionally, enable **Show in Table Columns** to make the **Business Label** visible within the Columns Table on the dataset schema.
+
+
+
+
+
+:::caution
+While the column sidebar provides convenient access to assigned properties, adding too many Structured Properties can clutter the view. Limit the number of properties shown in the sidebar to maintain clarity and usability.
+:::
+
+Once configured, the **Business Label** Structured Property will automatically be added to all columns on dataset assets within DataHub.
+
+For example, after assigning the property, it will appear in two key areas of the `pet_profiles` Asset Page:
+
+1. **Columns Table:** The **Business Label** property and its populated values will be displayed directly within the Columns Table on the Dataset Schema, enabling users to view field-level metadata easily.
+2. **Column Sidebar:** Structured Properties configured for columns, including **Business Label**, will also appear in the column’s sidebar.
+
+By applying column-level Structured Properties like **Business Label**, you enhance data discoverability and provide business users with valuable insights while keeping the interface user-friendly.
+
+
+
+
+
+### Update the Business Label from the Column Sidebar
+
+When selecting a specific column in the UI, the **Business Label** Structured Property will be visible in the column’s sidebar. Users with appropriate permissions can view or update the value directly from this interface.
+
+
+
+
+
+This setup ensures that column-specific metadata, such as the **Business Label**, is accessible and actionable, helping business users better understand the dataset's structure and its alignment with key business concepts.
+
+## FAQ and Troubleshooting
+
+### Why can’t I change a Structured Property’s definition?
+
+Once a Structured Property has been defined, only certain aspects can be modified:
+
+**You can change:**
+- Title and description
+- Add new allowed values
+- Add new supported asset types
+- Update display preferences
+
+**You cannot change:**
+- The type of the Structured Property
+- Existing allowed values and their definitions
+
+### Why can't I configure a Structured Property to appear as an Asset Badge?
+- Only **Text** and **Number** types with allowed values can be configured as Asset Badges.
+- Only one Structured Property can be displayed as a Badge for a given Asset.
+
+### Why can't I filter Search Results by a Structured Property?
+- Verify that the Structured Property has been configured to appear in search filters.
+- Ensure the filter is relevant by checking if there are assets associated with the Structured Property's value in your search results. Try different search terms or relax other applied filters.
+
+### Why can't I add a Structured Property to an Asset?
+- Confirm you have the **Edit Properties** privilege.
+- Ensure the Structured Property has already been created and supports the type of Asset you're trying to modify.
+
+### API Tutorials
+
+- [Structured Properties API Guide](/docs/api/tutorials/structured-properties.md)
+
+### Related Features
+
+- [DataHub Compliance Forms](/docs/features/feature-guides/compliance-forms/overview.md)
\ No newline at end of file
diff --git a/docs/features/feature-guides/properties/overview.md b/docs/features/feature-guides/properties/overview.md
new file mode 100644
index 00000000000000..7637a3be53e0d8
--- /dev/null
+++ b/docs/features/feature-guides/properties/overview.md
@@ -0,0 +1,54 @@
+---
+title: Overview
+---
+
+import FeatureAvailability from '@site/src/components/FeatureAvailability';
+
+# About DataHub Structured Properties
+
+
+DataHub **Structured Properties** allow you to add custom, validated properties to any Entity type in DataHub. Using Structured Properties, you can enable data discovery and governance based on attributes unique to your organization.
+
+
+
+
+
+## What are Structured Properties?
+
+**Structured Properties** are a powerful way to customize your DataHub environment, enabling you to align metadata with your organization’s unique needs. By defining specific property types—such as Date, Integer, DataHub Asset, or Text—you can apply meaningful, context-aware attributes to your Assets. Validation rules, like restricting allowed values or enforcing specific formats, ensure consistency while giving you the flexibility to reflect your business’s terminology, workflows, and priorities.
+
+Structured Properties can be added to the following Asset Types:
+
+- Data Assets, such as Datasets, Columns, Tasks, Pipelines, Charts, Dashboards, and more.
+- DataHub Entities, such as Domains, Glossary Terms & Groups, and Data Products.
+
+### Key Features of Structured Properties:
+
+1. **Typed Fields:** Properties are explicitly typed, including options like Date, Integer, URN, or Text.
+2. **Allowed Values:** Enforce standards by restricting values to a specific format or a pre-defined list of acceptable inputs.
+3. **Targeted Application:** Structured Properties can be tailored to specific asset types—such as Datasets, Columns, or Dashboards—ensuring they align with your organization’s data management needs and usage context.
+
+### Display Settings
+
+Structured Properties offer several configuration options to enhance metadata management:
+
+- **Hide Property:** For use cases where property values should not be viewable by DataHub users.
+- **Show in Search Filters:** Enables users to filter for Assets based on specific property values, improving discoverability.
+- **Customize Visibility:** Allows you to control where the Structured Property appears, such as in the Asset Badge, Asset Sidebar, and/or a Dataset Schema view’s Columns Table.
+
+## Why Use Structured Properties?
+
+Structured Properties are especially useful for organizations that require:
+
+- **Customization:** Customize how your end-users find assets within DataHub.
+- **Governance and Compliance:** Collect metadata in a way that supports compliance with internal or external standards.
+
+
+
+
+
+By leveraging these configurations, teams can ensure their metadata adheres to organizational policies and improves the discoverability and usability of Data Assets.
+
+## Next Steps
+
+Now that you understand Structured Properties, you’re ready to [Create a Structured Property](create-a-property.md).
\ No newline at end of file
diff --git a/docs/how/updating-datahub.md b/docs/how/updating-datahub.md
index bcc89332cc1c1b..8ba83768512a5f 100644
--- a/docs/how/updating-datahub.md
+++ b/docs/how/updating-datahub.md
@@ -19,24 +19,28 @@ This file documents any backwards-incompatible changes in DataHub and assists pe
## Next
- #11560 - The PowerBI ingestion source configuration option include_workspace_name_in_dataset_urn determines whether the workspace name is included in the PowerBI dataset's URN. PowerBI allows to have identical name of semantic model and their tables across the workspace, It will overwrite the semantic model in-case of multi-workspace ingestion.
- Entity urn with `include_workspace_name_in_dataset_urn: false`
- ```
- urn:li:dataset:(urn:li:dataPlatform:powerbi,[.].,)
- ```
+ Entity urn with `include_workspace_name_in_dataset_urn: false`
- Entity urn with `include_workspace_name_in_dataset_urn: true`
- ```
- urn:li:dataset:(urn:li:dataPlatform:powerbi,[.]...,)
- ```
+ ```
+ urn:li:dataset:(urn:li:dataPlatform:powerbi,[.].,)
+ ```
+
+ Entity urn with `include_workspace_name_in_dataset_urn: true`
+
+ ```
+ urn:li:dataset:(urn:li:dataPlatform:powerbi,[.]...,)
+ ```
The config `include_workspace_name_in_dataset_urn` is default to `false` for backward compatiblity, However, we recommend enabling this flag after performing the necessary cleanup.
If stateful ingestion is enabled, running ingestion with the latest CLI version will handle the cleanup automatically. Otherwise, we recommend soft deleting all powerbi data via the DataHub CLI:
- `datahub delete --platform powerbi --soft` and then re-ingest with the latest CLI version, ensuring the `include_workspace_name_in_dataset_urn` configuration is set to true.
+ `datahub delete --platform powerbi --soft` and then re-ingest with the latest CLI version, ensuring the `include_workspace_name_in_dataset_urn` configuration is set to true.
- #11701: The Fivetran `sources_to_database` field is deprecated in favor of setting directly within `sources_to_platform_instance..database`.
- #11742: For PowerBi ingestion, `use_powerbi_email` is now enabled by default when extracting ownership information.
- #12056: The DataHub Airflow plugin no longer supports Airflow 2.1 and Airflow 2.2.
- #12056: The DataHub Airflow plugin now defaults to the v2 plugin implementation.
+- OpenAPI Update: PIT Keep Alive parameter added to scroll. NOTE: This parameter requires the `pointInTimeCreationEnabled` feature flag to be enabled and the `elasticSearch.implementation` configuration to be `elasticsearch`. This feature is not supported for OpenSearch at this time and the parameter will not be respected without both of these set.
+- OpenAPI Update 2: Previously there was an incorrectly marked parameter named `sort` on the generic list entities endpoint for v3. This parameter is deprecated and only supports a single string value while the documentation indicates it supports a list of strings. This documentation error has been fixed and the correct field, `sortCriteria`, is now documented which supports a list of strings.
### Breaking Changes
@@ -48,6 +52,9 @@ This file documents any backwards-incompatible changes in DataHub and assists pe
- #11619 - schema field/column paths can no longer be duplicated within the schema
- #11570 - The `DatahubClientConfig`'s server field no longer defaults to `http://localhost:8080`. Be sure to explicitly set this.
- #11570 - If a `datahub_api` is explicitly passed to a stateful ingestion config provider, it will be used. We previously ignored it if the pipeline context also had a graph object.
+- #11518 - DataHub Garbage Collection: Various entities that are soft-deleted (after 10d) or are timeseries _entities_ (dataprocess, execution requests) will be removed automatically using logic in the `datahub-gc` ingestion source.
+- #12020 - Removed `sql_parser` configuration from the Redash source, as Redash now exclusively uses the sqlglot-based parser for lineage extraction.
+- #12020 - Removed `datahub.utilities.sql_parser`, `datahub.utilities.sql_parser_base` and `datahub.utilities.sql_lineage_parser_impl` module along with `SqlLineageSQLParser` and `DefaultSQLParser`. Use `create_lineage_sql_parsed_result` from `datahub.sql_parsing.sqlglot_lineage` module instead.
- #11518 - DataHub Garbage Collection: Various entities that are soft-deleted
(after 10d) or are timeseries *entities* (dataprocess, execution requests)
will be removed automatically using logic in the `datahub-gc` ingestion
diff --git a/entity-registry/src/main/java/com/linkedin/metadata/aspect/batch/AspectsBatch.java b/entity-registry/src/main/java/com/linkedin/metadata/aspect/batch/AspectsBatch.java
index 30f5dce379a077..6ce6a9a5730385 100644
--- a/entity-registry/src/main/java/com/linkedin/metadata/aspect/batch/AspectsBatch.java
+++ b/entity-registry/src/main/java/com/linkedin/metadata/aspect/batch/AspectsBatch.java
@@ -28,10 +28,12 @@
public interface AspectsBatch {
Collection extends BatchItem> getItems();
+ Collection extends BatchItem> getInitialItems();
+
RetrieverContext getRetrieverContext();
/**
- * Returns MCP items. Could be patch, upsert, etc.
+ * Returns MCP items. Could be one of patch, upsert, etc.
*
* @return batch items
*/
@@ -160,13 +162,24 @@ static Stream applyMCLSideEffects(
}
default boolean containsDuplicateAspects() {
- return getItems().stream()
- .map(i -> String.format("%s_%s", i.getClass().getName(), i.hashCode()))
+ return getInitialItems().stream()
+ .map(i -> String.format("%s_%s", i.getClass().getSimpleName(), i.hashCode()))
.distinct()
.count()
!= getItems().size();
}
+ default Map> duplicateAspects() {
+ return getInitialItems().stream()
+ .collect(
+ Collectors.groupingBy(
+ i -> String.format("%s_%s", i.getClass().getSimpleName(), i.hashCode())))
+ .entrySet()
+ .stream()
+ .filter(entry -> entry.getValue() != null && entry.getValue().size() > 1)
+ .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
+ }
+
default Map> getUrnAspectsMap() {
return getItems().stream()
.map(aspect -> Pair.of(aspect.getUrn().toString(), aspect.getAspectName()))
diff --git a/entity-registry/src/main/java/com/linkedin/metadata/aspect/batch/BatchItem.java b/entity-registry/src/main/java/com/linkedin/metadata/aspect/batch/BatchItem.java
index a6dfbc277e12ec..7f0a849a0eda1d 100644
--- a/entity-registry/src/main/java/com/linkedin/metadata/aspect/batch/BatchItem.java
+++ b/entity-registry/src/main/java/com/linkedin/metadata/aspect/batch/BatchItem.java
@@ -23,4 +23,11 @@ public interface BatchItem extends ReadItem {
*/
@Nonnull
ChangeType getChangeType();
+
+ /**
+ * Determines if this item is a duplicate of another item in terms of the operation it represents
+ * to the database.Each implementation can define what constitutes a duplicate based on its
+ * specific fields which are persisted.
+ */
+ boolean isDatabaseDuplicateOf(BatchItem other);
}
diff --git a/entity-registry/src/main/java/com/linkedin/metadata/models/registry/PluginEntityRegistryLoader.java b/entity-registry/src/main/java/com/linkedin/metadata/models/registry/PluginEntityRegistryLoader.java
index 4f2e5a106ae792..531537852109b0 100644
--- a/entity-registry/src/main/java/com/linkedin/metadata/models/registry/PluginEntityRegistryLoader.java
+++ b/entity-registry/src/main/java/com/linkedin/metadata/models/registry/PluginEntityRegistryLoader.java
@@ -6,7 +6,6 @@
import com.linkedin.metadata.models.registry.config.LoadStatus;
import com.linkedin.util.Pair;
import java.io.File;
-import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.nio.file.Files;
@@ -204,8 +203,8 @@ private void loadOneRegistry(
loadResultBuilder.plugins(entityRegistry.getPluginFactory().getPluginLoadResult());
log.info("Loaded registry {} successfully", entityRegistry);
- } catch (RuntimeException | EntityRegistryException | IOException e) {
- log.debug("{}: Failed to load registry {} with {}", this, registryName, e.getMessage());
+ } catch (Exception | EntityRegistryException e) {
+ log.error("{}: Failed to load registry {} with {}", this, registryName, e.getMessage(), e);
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
e.printStackTrace(pw);
diff --git a/entity-registry/src/testFixtures/java/com/linkedin/test/metadata/aspect/batch/TestMCL.java b/entity-registry/src/testFixtures/java/com/linkedin/test/metadata/aspect/batch/TestMCL.java
index 7dd889c48b8747..6643a9de58562b 100644
--- a/entity-registry/src/testFixtures/java/com/linkedin/test/metadata/aspect/batch/TestMCL.java
+++ b/entity-registry/src/testFixtures/java/com/linkedin/test/metadata/aspect/batch/TestMCL.java
@@ -4,10 +4,12 @@
import com.linkedin.common.urn.Urn;
import com.linkedin.data.template.RecordTemplate;
import com.linkedin.events.metadata.ChangeType;
+import com.linkedin.metadata.aspect.batch.BatchItem;
import com.linkedin.metadata.aspect.batch.MCLItem;
import com.linkedin.metadata.models.AspectSpec;
import com.linkedin.metadata.models.EntitySpec;
import com.linkedin.mxe.MetadataChangeLog;
+import java.util.Objects;
import javax.annotation.Nonnull;
import lombok.Builder;
import lombok.Getter;
@@ -29,4 +31,23 @@ public class TestMCL implements MCLItem {
public String getAspectName() {
return getAspectSpec().getName();
}
+
+ @Override
+ public boolean isDatabaseDuplicateOf(BatchItem other) {
+ return equals(other);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ TestMCL testMCL = (TestMCL) o;
+ return Objects.equals(metadataChangeLog, testMCL.metadataChangeLog);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hashCode(metadataChangeLog);
+ }
}
diff --git a/entity-registry/src/testFixtures/java/com/linkedin/test/metadata/aspect/batch/TestMCP.java b/entity-registry/src/testFixtures/java/com/linkedin/test/metadata/aspect/batch/TestMCP.java
index e562390a959a74..5b714bdbf0b478 100644
--- a/entity-registry/src/testFixtures/java/com/linkedin/test/metadata/aspect/batch/TestMCP.java
+++ b/entity-registry/src/testFixtures/java/com/linkedin/test/metadata/aspect/batch/TestMCP.java
@@ -6,6 +6,7 @@
import com.linkedin.common.AuditStamp;
import com.linkedin.common.urn.Urn;
+import com.linkedin.data.template.DataTemplateUtil;
import com.linkedin.data.template.RecordTemplate;
import com.linkedin.events.metadata.ChangeType;
import com.linkedin.metadata.aspect.ReadItem;
@@ -21,6 +22,7 @@
import java.net.URISyntaxException;
import java.util.Collection;
import java.util.Map;
+import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
@@ -140,4 +142,40 @@ public Map getHeaders() {
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)))
.orElse(headers);
}
+
+ @Override
+ public boolean isDatabaseDuplicateOf(BatchItem other) {
+ return equals(other);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ TestMCP testMCP = (TestMCP) o;
+ return urn.equals(testMCP.urn)
+ && DataTemplateUtil.areEqual(recordTemplate, testMCP.recordTemplate)
+ && Objects.equals(systemAspect, testMCP.systemAspect)
+ && Objects.equals(previousSystemAspect, testMCP.previousSystemAspect)
+ && Objects.equals(auditStamp, testMCP.auditStamp)
+ && Objects.equals(changeType, testMCP.changeType)
+ && Objects.equals(metadataChangeProposal, testMCP.metadataChangeProposal);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = urn.hashCode();
+ result = 31 * result + Objects.hashCode(recordTemplate);
+ result = 31 * result + Objects.hashCode(systemAspect);
+ result = 31 * result + Objects.hashCode(previousSystemAspect);
+ result = 31 * result + Objects.hashCode(auditStamp);
+ result = 31 * result + Objects.hashCode(changeType);
+ result = 31 * result + Objects.hashCode(metadataChangeProposal);
+ return result;
+ }
}
diff --git a/gradle/coverage/python-coverage.gradle b/gradle/coverage/python-coverage.gradle
index 0ab921dfb21ffd..23d6e37387ed83 100644
--- a/gradle/coverage/python-coverage.gradle
+++ b/gradle/coverage/python-coverage.gradle
@@ -1,7 +1,7 @@
//coverage related args to be passed to pytest
ext.get_coverage_args = { test_name = "" ->
- def coverage_file_name = "pycov-${project.name}${test_name}.xml"
+ def coverage_file_name = "coverage-${project.name}${test_name}.xml"
/*
Tools that aggregate and analyse coverage tools search for the coverage result files. Keeping them under one folder
diff --git a/metadata-events/mxe-schemas/build.gradle b/metadata-events/mxe-schemas/build.gradle
index ab0ea8b649e9d4..6dfe69a420242f 100644
--- a/metadata-events/mxe-schemas/build.gradle
+++ b/metadata-events/mxe-schemas/build.gradle
@@ -25,7 +25,7 @@ task copyOriginalAvsc(type: Copy, dependsOn: generateAvroSchema) {
}
task renameNamespace(type: Exec, dependsOn: copyOriginalAvsc) {
- commandLine 'sh', './rename-namespace.sh'
+ commandLine 'bash', './rename-namespace.sh'
}
build.dependsOn renameNamespace
@@ -34,4 +34,4 @@ clean {
project.delete('src/main/pegasus')
project.delete('src/mainGeneratedAvroSchema/avro')
project.delete('src/renamed/avro')
-}
\ No newline at end of file
+}
diff --git a/metadata-ingestion-modules/gx-plugin/setup.py b/metadata-ingestion-modules/gx-plugin/setup.py
index e87bbded96584e..73d5d1a9a02f18 100644
--- a/metadata-ingestion-modules/gx-plugin/setup.py
+++ b/metadata-ingestion-modules/gx-plugin/setup.py
@@ -15,15 +15,6 @@ def get_long_description():
rest_common = {"requests", "requests_file"}
-# TODO: Can we move away from sqllineage and use sqlglot ??
-sqllineage_lib = {
- "sqllineage==1.3.8",
- # We don't have a direct dependency on sqlparse but it is a dependency of sqllineage.
- # There have previously been issues from not pinning sqlparse, so it's best to pin it.
- # Related: https://github.com/reata/sqllineage/issues/361 and https://github.com/reata/sqllineage/pull/360
- "sqlparse==0.4.4",
-}
-
_version: str = package_metadata["__version__"]
_self_pin = (
f"=={_version}"
@@ -43,8 +34,7 @@ def get_long_description():
# https://github.com/ipython/traitlets/issues/741
"traitlets<5.2.2",
*rest_common,
- *sqllineage_lib,
- f"acryl-datahub[datahub-rest]{_self_pin}",
+ f"acryl-datahub[datahub-rest,sql-parser]{_self_pin}",
}
mypy_stubs = {
diff --git a/metadata-ingestion-modules/gx-plugin/src/datahub_gx_plugin/action.py b/metadata-ingestion-modules/gx-plugin/src/datahub_gx_plugin/action.py
index 2ad301a38d0028..2d89d26997d1f3 100644
--- a/metadata-ingestion-modules/gx-plugin/src/datahub_gx_plugin/action.py
+++ b/metadata-ingestion-modules/gx-plugin/src/datahub_gx_plugin/action.py
@@ -34,8 +34,9 @@
)
from datahub.metadata.com.linkedin.pegasus2avro.common import DataPlatformInstance
from datahub.metadata.schema_classes import PartitionSpecClass, PartitionTypeClass
+from datahub.sql_parsing.sqlglot_lineage import create_lineage_sql_parsed_result
from datahub.utilities._markupsafe_compat import MARKUPSAFE_PATCHED
-from datahub.utilities.sql_parser import DefaultSQLParser
+from datahub.utilities.urns.dataset_urn import DatasetUrn
from great_expectations.checkpoint.actions import ValidationAction
from great_expectations.core.batch import Batch
from great_expectations.core.batch_spec import (
@@ -677,10 +678,23 @@ def get_dataset_partitions(self, batch_identifier, data_asset):
query=query,
customProperties=batchSpecProperties,
)
- try:
- tables = DefaultSQLParser(query).get_tables()
- except Exception as e:
- logger.warning(f"Sql parser failed on {query} with {e}")
+
+ data_platform = get_platform_from_sqlalchemy_uri(str(sqlalchemy_uri))
+ sql_parser_in_tables = create_lineage_sql_parsed_result(
+ query=query,
+ platform=data_platform,
+ env=self.env,
+ platform_instance=None,
+ default_db=None,
+ )
+ tables = [
+ DatasetUrn.from_string(table_urn).name
+ for table_urn in sql_parser_in_tables.in_tables
+ ]
+ if sql_parser_in_tables.debug_info.table_error:
+ logger.warning(
+ f"Sql parser failed on {query} with {sql_parser_in_tables.debug_info.table_error}"
+ )
tables = []
if len(set(tables)) != 1:
diff --git a/metadata-ingestion/docs/sources/athena/athena_recipe.yml b/metadata-ingestion/docs/sources/athena/athena_recipe.yml
index 540d8101737a32..c93047ffed9ffc 100644
--- a/metadata-ingestion/docs/sources/athena/athena_recipe.yml
+++ b/metadata-ingestion/docs/sources/athena/athena_recipe.yml
@@ -1,6 +1,11 @@
source:
type: athena
config:
+
+ # AWS Keys (Optional - Required only if local aws credentials are not set)
+ username: my_aws_access_key_id
+ password: my_aws_secret_access_key
+
# Coordinates
aws_region: my_aws_region
work_group: primary
diff --git a/metadata-ingestion/docs/sources/redash/redash.md b/metadata-ingestion/docs/sources/redash/redash.md
index 8f8c5c85496a09..f23a523cebc913 100644
--- a/metadata-ingestion/docs/sources/redash/redash.md
+++ b/metadata-ingestion/docs/sources/redash/redash.md
@@ -1,5 +1,2 @@
-Note! The integration can use an SQL parser to try to parse the tables the chart depends on. This parsing is disabled by default,
-but can be enabled by setting `parse_table_names_from_sql: true`. The default parser is based on the [`sqllineage`](https://pypi.org/project/sqllineage/) package.
-As this package doesn't officially support all the SQL dialects that Redash supports, the result might not be correct. You can, however, implement a
-custom parser and take it into use by setting the `sql_parser` configuration value. A custom SQL parser must inherit from `datahub.utilities.sql_parser.SQLParser`
-and must be made available to Datahub by ,for example, installing it. The configuration then needs to be set to `module_name.ClassName` of the parser.
+Note! The integration can use an SQL parser to try to parse the tables the chart depends on. This parsing is disabled by default,
+but can be enabled by setting `parse_table_names_from_sql: true`. The parser is based on the [`sqlglot`](https://pypi.org/project/sqlglot/) package.
diff --git a/metadata-ingestion/setup.py b/metadata-ingestion/setup.py
index 5ae5438e212c5b..415871d30175f8 100644
--- a/metadata-ingestion/setup.py
+++ b/metadata-ingestion/setup.py
@@ -159,14 +159,6 @@
| classification_lib
)
-sqllineage_lib = {
- "sqllineage==1.3.8",
- # We don't have a direct dependency on sqlparse but it is a dependency of sqllineage.
- # There have previously been issues from not pinning sqlparse, so it's best to pin it.
- # Related: https://github.com/reata/sqllineage/issues/361 and https://github.com/reata/sqllineage/pull/360
- "sqlparse==0.4.4",
-}
-
aws_common = {
# AWS Python SDK
"boto3",
@@ -216,7 +208,6 @@
"sqlalchemy-redshift>=0.8.3",
"GeoAlchemy2",
"redshift-connector>=2.1.0",
- *sqllineage_lib,
*path_spec_common,
}
@@ -464,9 +455,7 @@
# It's technically wrong for packages to depend on setuptools. However, it seems mlflow does it anyways.
"setuptools",
},
- "mode": {"requests", "python-liquid", "tenacity>=8.0.1"}
- | sqllineage_lib
- | sqlglot_lib,
+ "mode": {"requests", "python-liquid", "tenacity>=8.0.1"} | sqlglot_lib,
"mongodb": {"pymongo[srv]>=3.11", "packaging"},
"mssql": sql_common | mssql_common,
"mssql-odbc": sql_common | mssql_common | {"pyodbc"},
@@ -482,7 +471,7 @@
| pyhive_common
| {"psycopg2-binary", "pymysql>=1.0.2"},
"pulsar": {"requests"},
- "redash": {"redash-toolbelt", "sql-metadata"} | sqllineage_lib,
+ "redash": {"redash-toolbelt", "sql-metadata"} | sqlglot_lib,
"redshift": sql_common
| redshift_common
| usage_common
@@ -503,9 +492,7 @@
"slack": slack,
"superset": superset_common,
"preset": superset_common,
- # FIXME: I don't think tableau uses sqllineage anymore so we should be able
- # to remove that dependency.
- "tableau": {"tableauserverclient>=0.24.0"} | sqllineage_lib | sqlglot_lib,
+ "tableau": {"tableauserverclient>=0.24.0"} | sqlglot_lib,
"teradata": sql_common
| usage_common
| sqlglot_lib
@@ -527,9 +514,9 @@
),
"powerbi-report-server": powerbi_report_server,
"vertica": sql_common | {"vertica-sqlalchemy-dialect[vertica-python]==0.0.8.2"},
- "unity-catalog": databricks | sql_common | sqllineage_lib,
+ "unity-catalog": databricks | sql_common,
# databricks is alias for unity-catalog and needs to be kept in sync
- "databricks": databricks | sql_common | sqllineage_lib,
+ "databricks": databricks | sql_common,
"fivetran": snowflake_common | bigquery_common | sqlglot_lib,
"qlik-sense": sqlglot_lib | {"requests", "websocket-client"},
"sigma": sqlglot_lib | {"requests"},
diff --git a/metadata-ingestion/src/datahub/cli/cli_utils.py b/metadata-ingestion/src/datahub/cli/cli_utils.py
index 1b9cccb1cbc215..f80181192ba583 100644
--- a/metadata-ingestion/src/datahub/cli/cli_utils.py
+++ b/metadata-ingestion/src/datahub/cli/cli_utils.py
@@ -327,6 +327,8 @@ def _ensure_valid_gms_url_acryl_cloud(url: str) -> str:
url = f"{url}/gms"
elif url.endswith("acryl.io/"):
url = f"{url}gms"
+ if url.endswith("acryl.io/api/gms"):
+ url = url.replace("acryl.io/api/gms", "acryl.io/gms")
return url
diff --git a/metadata-ingestion/src/datahub/ingestion/api/incremental_properties_helper.py b/metadata-ingestion/src/datahub/ingestion/api/incremental_properties_helper.py
new file mode 100644
index 00000000000000..151b0c72a6c2de
--- /dev/null
+++ b/metadata-ingestion/src/datahub/ingestion/api/incremental_properties_helper.py
@@ -0,0 +1,69 @@
+import logging
+from typing import Iterable, Optional
+
+from pydantic.fields import Field
+
+from datahub.configuration.common import ConfigModel
+from datahub.emitter.mce_builder import set_aspect
+from datahub.emitter.mcp import MetadataChangeProposalWrapper
+from datahub.ingestion.api.source_helpers import create_dataset_props_patch_builder
+from datahub.ingestion.api.workunit import MetadataWorkUnit
+from datahub.metadata.schema_classes import (
+ DatasetPropertiesClass,
+ MetadataChangeEventClass,
+ SystemMetadataClass,
+)
+
+logger = logging.getLogger(__name__)
+
+
+def convert_dataset_properties_to_patch(
+ urn: str,
+ aspect: DatasetPropertiesClass,
+ system_metadata: Optional[SystemMetadataClass],
+) -> MetadataWorkUnit:
+ patch_builder = create_dataset_props_patch_builder(urn, aspect, system_metadata)
+ mcp = next(iter(patch_builder.build()))
+ return MetadataWorkUnit(id=MetadataWorkUnit.generate_workunit_id(mcp), mcp_raw=mcp)
+
+
+def auto_incremental_properties(
+ incremental_properties: bool,
+ stream: Iterable[MetadataWorkUnit],
+) -> Iterable[MetadataWorkUnit]:
+ if not incremental_properties:
+ yield from stream
+ return # early exit
+
+ for wu in stream:
+ urn = wu.get_urn()
+
+ if isinstance(wu.metadata, MetadataChangeEventClass):
+ properties_aspect = wu.get_aspect_of_type(DatasetPropertiesClass)
+ set_aspect(wu.metadata, None, DatasetPropertiesClass)
+ if len(wu.metadata.proposedSnapshot.aspects) > 0:
+ yield wu
+
+ if properties_aspect:
+ yield convert_dataset_properties_to_patch(
+ urn, properties_aspect, wu.metadata.systemMetadata
+ )
+ elif isinstance(wu.metadata, MetadataChangeProposalWrapper) and isinstance(
+ wu.metadata.aspect, DatasetPropertiesClass
+ ):
+ properties_aspect = wu.metadata.aspect
+ if properties_aspect:
+ yield convert_dataset_properties_to_patch(
+ urn, properties_aspect, wu.metadata.systemMetadata
+ )
+ else:
+ yield wu
+
+
+# TODO: Use this in SQLCommonConfig. Currently only used in snowflake
+class IncrementalPropertiesConfigMixin(ConfigModel):
+ incremental_properties: bool = Field(
+ default=False,
+ description="When enabled, emits dataset properties as incremental to existing dataset properties "
+ "in DataHub. When disabled, re-states dataset properties on each run.",
+ )
diff --git a/metadata-ingestion/src/datahub/ingestion/api/source_helpers.py b/metadata-ingestion/src/datahub/ingestion/api/source_helpers.py
index 8511f8529ac125..0c86e1cf47203f 100644
--- a/metadata-ingestion/src/datahub/ingestion/api/source_helpers.py
+++ b/metadata-ingestion/src/datahub/ingestion/api/source_helpers.py
@@ -32,6 +32,7 @@
SchemaFieldClass,
SchemaMetadataClass,
StatusClass,
+ SystemMetadataClass,
TimeWindowSizeClass,
)
from datahub.metadata.urns import DatasetUrn, GlossaryTermUrn, TagUrn, Urn
@@ -65,9 +66,10 @@ def auto_workunit(
def create_dataset_props_patch_builder(
dataset_urn: str,
dataset_properties: DatasetPropertiesClass,
+ system_metadata: Optional[SystemMetadataClass] = None,
) -> DatasetPatchBuilder:
"""Creates a patch builder with a table's or view's attributes and dataset properties"""
- patch_builder = DatasetPatchBuilder(dataset_urn)
+ patch_builder = DatasetPatchBuilder(dataset_urn, system_metadata)
patch_builder.set_display_name(dataset_properties.name)
patch_builder.set_description(dataset_properties.description)
patch_builder.set_created(dataset_properties.created)
diff --git a/metadata-ingestion/src/datahub/ingestion/sink/datahub_rest.py b/metadata-ingestion/src/datahub/ingestion/sink/datahub_rest.py
index 5b4d3fe38ecd97..1bb07ea8462279 100644
--- a/metadata-ingestion/src/datahub/ingestion/sink/datahub_rest.py
+++ b/metadata-ingestion/src/datahub/ingestion/sink/datahub_rest.py
@@ -65,11 +65,11 @@ class DatahubRestSinkConfig(DatahubClientConfig):
mode: RestSinkMode = _DEFAULT_REST_SINK_MODE
# These only apply in async modes.
- max_threads: int = _DEFAULT_REST_SINK_MAX_THREADS
- max_pending_requests: int = 2000
+ max_threads: pydantic.PositiveInt = _DEFAULT_REST_SINK_MAX_THREADS
+ max_pending_requests: pydantic.PositiveInt = 2000
# Only applies in async batch mode.
- max_per_batch: int = 100
+ max_per_batch: pydantic.PositiveInt = 100
@dataclasses.dataclass
diff --git a/metadata-ingestion/src/datahub/ingestion/source/abs/source.py b/metadata-ingestion/src/datahub/ingestion/source/abs/source.py
index 66f268799b2f1f..ad2bc36cf558b5 100644
--- a/metadata-ingestion/src/datahub/ingestion/source/abs/source.py
+++ b/metadata-ingestion/src/datahub/ingestion/source/abs/source.py
@@ -201,6 +201,10 @@ def get_fields(self, table_data: TableData, path_spec: PathSpec) -> List:
).infer_schema(file)
elif extension == ".json":
fields = json.JsonInferrer().infer_schema(file)
+ elif extension == ".jsonl":
+ fields = json.JsonInferrer(
+ max_rows=self.source_config.max_rows, format="jsonl"
+ ).infer_schema(file)
elif extension == ".avro":
fields = avro.AvroInferrer().infer_schema(file)
else:
diff --git a/metadata-ingestion/src/datahub/ingestion/source/gc/datahub_gc.py b/metadata-ingestion/src/datahub/ingestion/source/gc/datahub_gc.py
index 52807ca2a3f026..814f65ecb45cf0 100644
--- a/metadata-ingestion/src/datahub/ingestion/source/gc/datahub_gc.py
+++ b/metadata-ingestion/src/datahub/ingestion/source/gc/datahub_gc.py
@@ -153,11 +153,6 @@ def get_workunits_internal(
self.truncate_indices()
except Exception as e:
self.report.failure("While trying to truncate indices ", exc=e)
- if self.dataprocess_cleanup:
- try:
- yield from self.dataprocess_cleanup.get_workunits_internal()
- except Exception as e:
- self.report.failure("While trying to cleanup data process ", exc=e)
if self.soft_deleted_entities_cleanup:
try:
self.soft_deleted_entities_cleanup.cleanup_soft_deleted_entities()
@@ -170,6 +165,11 @@ def get_workunits_internal(
self.execution_request_cleanup.run()
except Exception as e:
self.report.failure("While trying to cleanup execution request ", exc=e)
+ if self.dataprocess_cleanup:
+ try:
+ yield from self.dataprocess_cleanup.get_workunits_internal()
+ except Exception as e:
+ self.report.failure("While trying to cleanup data process ", exc=e)
yield from []
def truncate_indices(self) -> None:
diff --git a/metadata-ingestion/src/datahub/ingestion/source/gc/soft_deleted_entity_cleanup.py b/metadata-ingestion/src/datahub/ingestion/source/gc/soft_deleted_entity_cleanup.py
index 3b367cdea58134..bb4ab753543b7b 100644
--- a/metadata-ingestion/src/datahub/ingestion/source/gc/soft_deleted_entity_cleanup.py
+++ b/metadata-ingestion/src/datahub/ingestion/source/gc/soft_deleted_entity_cleanup.py
@@ -60,7 +60,7 @@ class SoftDeletedEntitiesCleanupConfig(ConfigModel):
description="Query to filter entities",
)
limit_entities_delete: Optional[int] = Field(
- 10000, description="Max number of entities to delete."
+ 25000, description="Max number of entities to delete."
)
runtime_limit_seconds: Optional[int] = Field(
diff --git a/metadata-ingestion/src/datahub/ingestion/source/mode.py b/metadata-ingestion/src/datahub/ingestion/source/mode.py
index e24cba9b193d31..c1ab9271ce13ae 100644
--- a/metadata-ingestion/src/datahub/ingestion/source/mode.py
+++ b/metadata-ingestion/src/datahub/ingestion/source/mode.py
@@ -18,7 +18,6 @@
from requests.adapters import HTTPAdapter, Retry
from requests.exceptions import ConnectionError
from requests.models import HTTPBasicAuth, HTTPError
-from sqllineage.runner import LineageRunner
from tenacity import retry_if_exception_type, stop_after_attempt, wait_exponential
import datahub.emitter.mce_builder as builder
@@ -820,28 +819,6 @@ def _get_definition(self, definition_name):
)
return None
- @lru_cache(maxsize=None)
- def _get_source_from_query(self, raw_query: str) -> set:
- query = self._replace_definitions(raw_query)
- parser = LineageRunner(query)
- source_paths = set()
- try:
- for table in parser.source_tables:
- sources = str(table).split(".")
- source_schema, source_table = sources[-2], sources[-1]
- if source_schema == "":
- source_schema = str(self.config.default_schema)
-
- source_paths.add(f"{source_schema}.{source_table}")
- except Exception as e:
- self.report.report_failure(
- title="Failed to Extract Lineage From Query",
- message="Unable to retrieve lineage from Mode query.",
- context=f"Query: {raw_query}, Error: {str(e)}",
- )
-
- return source_paths
-
def _get_datasource_urn(
self,
platform: str,
diff --git a/metadata-ingestion/src/datahub/ingestion/source/redash.py b/metadata-ingestion/src/datahub/ingestion/source/redash.py
index 581e32d29dceaf..f11d1944029ebb 100644
--- a/metadata-ingestion/src/datahub/ingestion/source/redash.py
+++ b/metadata-ingestion/src/datahub/ingestion/source/redash.py
@@ -2,7 +2,7 @@
import math
import sys
from dataclasses import dataclass, field
-from typing import Dict, Iterable, List, Optional, Set, Type
+from typing import Dict, Iterable, List, Optional, Set
import dateutil.parser as dp
from packaging import version
@@ -22,7 +22,6 @@
platform_name,
support_status,
)
-from datahub.ingestion.api.registry import import_path
from datahub.ingestion.api.source import Source, SourceCapability, SourceReport
from datahub.ingestion.api.workunit import MetadataWorkUnit
from datahub.metadata.com.linkedin.pegasus2avro.common import (
@@ -39,9 +38,9 @@
ChartTypeClass,
DashboardInfoClass,
)
+from datahub.sql_parsing.sqlglot_lineage import create_lineage_sql_parsed_result
from datahub.utilities.lossy_collections import LossyDict, LossyList
from datahub.utilities.perf_timer import PerfTimer
-from datahub.utilities.sql_parser_base import SQLParser
from datahub.utilities.threaded_iterator_executor import ThreadedIteratorExecutor
logger = logging.getLogger(__name__)
@@ -270,10 +269,6 @@ class RedashConfig(ConfigModel):
parse_table_names_from_sql: bool = Field(
default=False, description="See note below."
)
- sql_parser: str = Field(
- default="datahub.utilities.sql_parser.DefaultSQLParser",
- description="custom SQL parser. See note below for details.",
- )
env: str = Field(
default=DEFAULT_ENV,
@@ -354,7 +349,6 @@ def __init__(self, ctx: PipelineContext, config: RedashConfig):
self.api_page_limit = self.config.api_page_limit or math.inf
self.parse_table_names_from_sql = self.config.parse_table_names_from_sql
- self.sql_parser_path = self.config.sql_parser
logger.info(
f"Running Redash ingestion with parse_table_names_from_sql={self.parse_table_names_from_sql}"
@@ -380,31 +374,6 @@ def create(cls, config_dict: dict, ctx: PipelineContext) -> Source:
config = RedashConfig.parse_obj(config_dict)
return cls(ctx, config)
- @classmethod
- def _import_sql_parser_cls(cls, sql_parser_path: str) -> Type[SQLParser]:
- assert "." in sql_parser_path, "sql_parser-path must contain a ."
- parser_cls = import_path(sql_parser_path)
-
- if not issubclass(parser_cls, SQLParser):
- raise ValueError(f"must be derived from {SQLParser}; got {parser_cls}")
- return parser_cls
-
- @classmethod
- def _get_sql_table_names(cls, sql: str, sql_parser_path: str) -> List[str]:
- parser_cls = cls._import_sql_parser_cls(sql_parser_path)
-
- try:
- sql_table_names: List[str] = parser_cls(sql).get_tables()
- except Exception as e:
- logger.warning(f"Sql parser failed on {sql} with {e}")
- return []
-
- # Remove quotes from table names
- sql_table_names = [t.replace('"', "") for t in sql_table_names]
- sql_table_names = [t.replace("`", "") for t in sql_table_names]
-
- return sql_table_names
-
def _get_chart_data_source(self, data_source_id: Optional[int] = None) -> Dict:
url = f"/api/data_sources/{data_source_id}"
resp = self.client._get(url).json()
@@ -441,14 +410,6 @@ def _get_database_name_based_on_datasource(
return database_name
- def _construct_datalineage_urn(
- self, platform: str, database_name: str, sql_table_name: str
- ) -> str:
- full_dataset_name = get_full_qualified_name(
- platform, database_name, sql_table_name
- )
- return builder.make_dataset_urn(platform, full_dataset_name, self.config.env)
-
def _get_datasource_urns(
self, data_source: Dict, sql_query_data: Dict = {}
) -> Optional[List[str]]:
@@ -464,34 +425,23 @@ def _get_datasource_urns(
# Getting table lineage from SQL parsing
if self.parse_table_names_from_sql and data_source_syntax == "sql":
dataset_urns = list()
- try:
- sql_table_names = self._get_sql_table_names(
- query, self.sql_parser_path
- )
- except Exception as e:
+ sql_parser_in_tables = create_lineage_sql_parsed_result(
+ query=query,
+ platform=platform,
+ env=self.config.env,
+ platform_instance=None,
+ default_db=database_name,
+ )
+ # make sure dataset_urns is not empty list
+ dataset_urns = sql_parser_in_tables.in_tables
+ if sql_parser_in_tables.debug_info.table_error:
self.report.queries_problem_parsing.add(str(query_id))
self.error(
logger,
"sql-parsing",
- f"exception {e} in parsing query-{query_id}-datasource-{data_source_id}",
+ f"exception {sql_parser_in_tables.debug_info.table_error} in parsing query-{query_id}-datasource-{data_source_id}",
)
- sql_table_names = []
- for sql_table_name in sql_table_names:
- try:
- dataset_urns.append(
- self._construct_datalineage_urn(
- platform, database_name, sql_table_name
- )
- )
- except Exception:
- self.report.queries_problem_parsing.add(str(query_id))
- self.warn(
- logger,
- "data-urn-invalid",
- f"Problem making URN for {sql_table_name} parsed from query {query_id}",
- )
- # make sure dataset_urns is not empty list
return dataset_urns if len(dataset_urns) > 0 else None
else:
diff --git a/metadata-ingestion/src/datahub/ingestion/source/redshift/config.py b/metadata-ingestion/src/datahub/ingestion/source/redshift/config.py
index 2ff73323a14e35..cad48eaf1c2375 100644
--- a/metadata-ingestion/src/datahub/ingestion/source/redshift/config.py
+++ b/metadata-ingestion/src/datahub/ingestion/source/redshift/config.py
@@ -159,6 +159,7 @@ class RedshiftConfig(
description="Whether to extract column level lineage. This config works with rest-sink only.",
)
+ # TODO - use DatasetPropertiesConfigMixin instead
patch_custom_properties: bool = Field(
default=True,
description="Whether to patch custom properties on existing datasets rather than replace.",
diff --git a/metadata-ingestion/src/datahub/ingestion/source/redshift/redshift.py b/metadata-ingestion/src/datahub/ingestion/source/redshift/redshift.py
index 06cbb7fbae27cc..49f7941563c1a7 100644
--- a/metadata-ingestion/src/datahub/ingestion/source/redshift/redshift.py
+++ b/metadata-ingestion/src/datahub/ingestion/source/redshift/redshift.py
@@ -831,6 +831,8 @@ def gen_dataset_workunits(
customProperties=custom_properties,
)
if self.config.patch_custom_properties:
+ # TODO: use auto_incremental_properties workunit processor instead
+ # Deprecate use of patch_custom_properties
patch_builder = create_dataset_props_patch_builder(
dataset_urn, dataset_properties
)
diff --git a/metadata-ingestion/src/datahub/ingestion/source/snowflake/snowflake_config.py b/metadata-ingestion/src/datahub/ingestion/source/snowflake/snowflake_config.py
index c30a26fbbd02cc..1d1cc3c2af4f08 100644
--- a/metadata-ingestion/src/datahub/ingestion/source/snowflake/snowflake_config.py
+++ b/metadata-ingestion/src/datahub/ingestion/source/snowflake/snowflake_config.py
@@ -16,6 +16,9 @@
from datahub.configuration.time_window_config import BaseTimeWindowConfig
from datahub.configuration.validate_field_removal import pydantic_removed_field
from datahub.configuration.validate_field_rename import pydantic_renamed_field
+from datahub.ingestion.api.incremental_properties_helper import (
+ IncrementalPropertiesConfigMixin,
+)
from datahub.ingestion.glossary.classification_mixin import (
ClassificationSourceConfigMixin,
)
@@ -188,6 +191,7 @@ class SnowflakeV2Config(
StatefulUsageConfigMixin,
StatefulProfilingConfigMixin,
ClassificationSourceConfigMixin,
+ IncrementalPropertiesConfigMixin,
):
include_usage_stats: bool = Field(
default=True,
diff --git a/metadata-ingestion/src/datahub/ingestion/source/snowflake/snowflake_v2.py b/metadata-ingestion/src/datahub/ingestion/source/snowflake/snowflake_v2.py
index 538841018067e2..c3a7912c40e8ee 100644
--- a/metadata-ingestion/src/datahub/ingestion/source/snowflake/snowflake_v2.py
+++ b/metadata-ingestion/src/datahub/ingestion/source/snowflake/snowflake_v2.py
@@ -17,6 +17,9 @@
support_status,
)
from datahub.ingestion.api.incremental_lineage_helper import auto_incremental_lineage
+from datahub.ingestion.api.incremental_properties_helper import (
+ auto_incremental_properties,
+)
from datahub.ingestion.api.source import (
CapabilityReport,
MetadataWorkUnitProcessor,
@@ -446,6 +449,9 @@ def get_workunit_processors(self) -> List[Optional[MetadataWorkUnitProcessor]]:
functools.partial(
auto_incremental_lineage, self.config.incremental_lineage
),
+ functools.partial(
+ auto_incremental_properties, self.config.incremental_properties
+ ),
StaleEntityRemovalHandler.create(
self, self.config, self.ctx
).workunit_processor,
diff --git a/metadata-ingestion/src/datahub/ingestion/source/unity/source.py b/metadata-ingestion/src/datahub/ingestion/source/unity/source.py
index f758746193cd83..9d9a746580f939 100644
--- a/metadata-ingestion/src/datahub/ingestion/source/unity/source.py
+++ b/metadata-ingestion/src/datahub/ingestion/source/unity/source.py
@@ -556,6 +556,8 @@ def process_table(self, table: Table, schema: Schema) -> Iterable[MetadataWorkUn
)
if table_props:
+ # TODO: use auto_incremental_properties workunit processor instead
+ # Consider enabling incremental_properties by default
patch_builder = create_dataset_props_patch_builder(dataset_urn, table_props)
for patch_mcp in patch_builder.build():
yield MetadataWorkUnit(
diff --git a/metadata-ingestion/src/datahub/ingestion/source/unity/usage.py b/metadata-ingestion/src/datahub/ingestion/source/unity/usage.py
index 8c42ac81b98cf5..718818d9b347bf 100644
--- a/metadata-ingestion/src/datahub/ingestion/source/unity/usage.py
+++ b/metadata-ingestion/src/datahub/ingestion/source/unity/usage.py
@@ -7,7 +7,6 @@
import pyspark
from databricks.sdk.service.sql import QueryStatementType
-from sqllineage.runner import LineageRunner
from datahub.emitter.mcp import MetadataChangeProposalWrapper
from datahub.ingestion.api.source_helpers import auto_empty_dataset_usage_statistics
@@ -22,7 +21,9 @@
from datahub.ingestion.source.unity.report import UnityCatalogReport
from datahub.ingestion.source.usage.usage_common import UsageAggregator
from datahub.metadata.schema_classes import OperationClass
+from datahub.sql_parsing.sqlglot_lineage import create_lineage_sql_parsed_result
from datahub.sql_parsing.sqlglot_utils import get_query_fingerprint
+from datahub.utilities.urns.dataset_urn import DatasetUrn
logger = logging.getLogger(__name__)
@@ -48,6 +49,7 @@ class UnityCatalogUsageExtractor:
proxy: UnityCatalogApiProxy
table_urn_builder: Callable[[TableReference], str]
user_urn_builder: Callable[[str], str]
+ platform: str = "databricks"
def __post_init__(self):
self.usage_aggregator = UsageAggregator[TableReference](self.config)
@@ -173,7 +175,7 @@ def _parse_query(
self, query: Query, table_map: TableMap
) -> Optional[QueryTableInfo]:
with self.report.usage_perf_report.sql_parsing_timer:
- table_info = self._parse_query_via_lineage_runner(query.query_text)
+ table_info = self._parse_query_via_sqlglot(query.query_text)
if table_info is None and query.statement_type == QueryStatementType.SELECT:
with self.report.usage_perf_report.spark_sql_parsing_timer:
table_info = self._parse_query_via_spark_sql_plan(query.query_text)
@@ -191,26 +193,33 @@ def _parse_query(
),
)
- def _parse_query_via_lineage_runner(self, query: str) -> Optional[StringTableInfo]:
+ def _parse_query_via_sqlglot(self, query: str) -> Optional[StringTableInfo]:
try:
- runner = LineageRunner(query)
+ sql_parser_in_tables = create_lineage_sql_parsed_result(
+ query=query,
+ default_db=None,
+ platform=self.platform,
+ env=self.config.env,
+ platform_instance=None,
+ )
+
return GenericTableInfo(
source_tables=[
- self._parse_sqllineage_table(table)
- for table in runner.source_tables
+ self._parse_sqlglot_table(table)
+ for table in sql_parser_in_tables.in_tables
],
target_tables=[
- self._parse_sqllineage_table(table)
- for table in runner.target_tables
+ self._parse_sqlglot_table(table)
+ for table in sql_parser_in_tables.out_tables
],
)
except Exception as e:
- logger.info(f"Could not parse query via lineage runner, {query}: {e!r}")
+ logger.info(f"Could not parse query via sqlglot, {query}: {e!r}")
return None
@staticmethod
- def _parse_sqllineage_table(sqllineage_table: object) -> str:
- full_table_name = str(sqllineage_table)
+ def _parse_sqlglot_table(table_urn: str) -> str:
+ full_table_name = DatasetUrn.from_string(table_urn).name
default_schema = "."
if full_table_name.startswith(default_schema):
return full_table_name[len(default_schema) :]
diff --git a/metadata-ingestion/src/datahub/utilities/partition_executor.py b/metadata-ingestion/src/datahub/utilities/partition_executor.py
index 4d873d8f74bd8e..542889f2f90e29 100644
--- a/metadata-ingestion/src/datahub/utilities/partition_executor.py
+++ b/metadata-ingestion/src/datahub/utilities/partition_executor.py
@@ -268,7 +268,7 @@ def __init__(
self.process_batch = process_batch
self.min_process_interval = min_process_interval
self.read_from_pending_interval = read_from_pending_interval
- assert self.max_workers > 1
+ assert self.max_workers >= 1
self._state_lock = threading.Lock()
self._executor = ThreadPoolExecutor(
diff --git a/metadata-ingestion/src/datahub/utilities/sql_lineage_parser_impl.py b/metadata-ingestion/src/datahub/utilities/sql_lineage_parser_impl.py
deleted file mode 100644
index 5a8802c7a0a49c..00000000000000
--- a/metadata-ingestion/src/datahub/utilities/sql_lineage_parser_impl.py
+++ /dev/null
@@ -1,160 +0,0 @@
-import contextlib
-import logging
-import re
-import unittest
-import unittest.mock
-from typing import Dict, List, Optional, Set
-
-from sqllineage.core.holders import Column, SQLLineageHolder
-from sqllineage.exceptions import SQLLineageException
-
-from datahub.utilities.sql_parser_base import SQLParser, SqlParserException
-
-with contextlib.suppress(ImportError):
- import sqlparse
- from networkx import DiGraph
- from sqllineage.core import LineageAnalyzer
-
- import datahub.utilities.sqllineage_patch
-logger = logging.getLogger(__name__)
-
-
-class SqlLineageSQLParserImpl(SQLParser):
- _DATE_SWAP_TOKEN = "__d_a_t_e"
- _HOUR_SWAP_TOKEN = "__h_o_u_r"
- _TIMESTAMP_SWAP_TOKEN = "__t_i_m_e_s_t_a_m_p"
- _DATA_SWAP_TOKEN = "__d_a_t_a"
- _ADMIN_SWAP_TOKEN = "__a_d_m_i_n"
- _MYVIEW_SQL_TABLE_NAME_TOKEN = "__my_view__.__sql_table_name__"
- _MYVIEW_LOOKER_TOKEN = "my_view.SQL_TABLE_NAME"
-
- def __init__(self, sql_query: str, use_raw_names: bool = False) -> None:
- super().__init__(sql_query)
- original_sql_query = sql_query
- self._use_raw_names = use_raw_names
-
- # SqlLineageParser makes mistakes on lateral flatten queries, use the prefix
- if "lateral flatten" in sql_query:
- sql_query = sql_query[: sql_query.find("lateral flatten")]
-
- # Replace reserved words that break SqlLineageParser
- self.token_to_original: Dict[str, str] = {
- self._DATE_SWAP_TOKEN: "date",
- self._HOUR_SWAP_TOKEN: "hour",
- self._TIMESTAMP_SWAP_TOKEN: "timestamp",
- self._DATA_SWAP_TOKEN: "data",
- self._ADMIN_SWAP_TOKEN: "admin",
- }
- for replacement, original in self.token_to_original.items():
- # Replace original tokens with replacement. Since table and column name can contain a hyphen('-'),
- # also prevent original tokens appearing as part of these names with a hyphen from getting substituted.
- sql_query = re.sub(
- rf"((? List[str]:
- result: List[str] = []
- if self._sql_holder is None:
- logger.error("sql holder not present so cannot get tables")
- return result
- for table in self._sql_holder.source_tables:
- table_normalized = re.sub(
- r"^.",
- "",
- (
- str(table)
- if not self._use_raw_names
- else f"{table.schema.raw_name}.{table.raw_name}"
- ),
- )
- result.append(str(table_normalized))
-
- # We need to revert TOKEN replacements
- for token, replacement in self.token_to_original.items():
- result = [replacement if c == token else c for c in result]
- result = [
- self._MYVIEW_LOOKER_TOKEN if c == self._MYVIEW_SQL_TABLE_NAME_TOKEN else c
- for c in result
- ]
-
- # Sort tables to make the list deterministic
- result.sort()
-
- return result
-
- def get_columns(self) -> List[str]:
- if self._sql_holder is None:
- raise SqlParserException("sql holder not present so cannot get columns")
- graph: DiGraph = self._sql_holder.graph # For mypy attribute checking
- column_nodes = [n for n in graph.nodes if isinstance(n, Column)]
- column_graph = graph.subgraph(column_nodes)
-
- target_columns = {column for column, deg in column_graph.out_degree if deg == 0}
-
- result: Set[str] = set()
- for column in target_columns:
- # Let's drop all the count(*) and similard columns which are expression actually if it does not have an alias
- if not any(ele in column.raw_name for ele in ["*", "(", ")"]):
- result.add(str(column.raw_name))
-
- # Reverting back all the previously renamed words which confuses the parser
- result = {"date" if c == self._DATE_SWAP_TOKEN else c for c in result}
- result = {
- "timestamp" if c == self._TIMESTAMP_SWAP_TOKEN else c for c in list(result)
- }
-
- # swap back renamed date column
- return list(result)
diff --git a/metadata-ingestion/src/datahub/utilities/sql_parser.py b/metadata-ingestion/src/datahub/utilities/sql_parser.py
deleted file mode 100644
index b88f8fd8c73029..00000000000000
--- a/metadata-ingestion/src/datahub/utilities/sql_parser.py
+++ /dev/null
@@ -1,94 +0,0 @@
-import logging
-import multiprocessing
-import traceback
-from multiprocessing import Process, Queue
-from typing import Any, List, Optional, Tuple
-
-from datahub.utilities.sql_lineage_parser_impl import SqlLineageSQLParserImpl
-from datahub.utilities.sql_parser_base import SQLParser
-
-logger = logging.getLogger(__name__)
-
-
-def sql_lineage_parser_impl_func_wrapper(
- queue: Optional[multiprocessing.Queue], sql_query: str, use_raw_names: bool = False
-) -> Optional[Tuple[List[str], List[str], Any]]:
- """
- The wrapper function that computes the tables and columns using the SqlLineageSQLParserImpl
- and puts the results on the shared IPC queue. This is used to isolate SqlLineageSQLParserImpl
- functionality in a separate process, and hence protect our sources from memory leaks originating in
- the sqllineage module.
- :param queue: The shared IPC queue on to which the results will be put.
- :param sql_query: The SQL query to extract the tables & columns from.
- :param use_raw_names: Parameter used to ignore sqllineage's default lowercasing.
- :return: None.
- """
- exception_details: Optional[Tuple[BaseException, str]] = None
- tables: List[str] = []
- columns: List[str] = []
- try:
- parser = SqlLineageSQLParserImpl(sql_query, use_raw_names)
- tables = parser.get_tables()
- columns = parser.get_columns()
- except BaseException as e:
- exc_msg = traceback.format_exc()
- exception_details = (e, exc_msg)
- logger.debug(exc_msg)
-
- if queue is not None:
- queue.put((tables, columns, exception_details))
- return None
- else:
- return (tables, columns, exception_details)
-
-
-class SqlLineageSQLParser(SQLParser):
- def __init__(
- self,
- sql_query: str,
- use_external_process: bool = False,
- use_raw_names: bool = False,
- ) -> None:
- super().__init__(sql_query, use_external_process)
- if use_external_process:
- self.tables, self.columns = self._get_tables_columns_process_wrapped(
- sql_query, use_raw_names
- )
- else:
- return_tuple = sql_lineage_parser_impl_func_wrapper(
- None, sql_query, use_raw_names
- )
- if return_tuple is not None:
- (
- self.tables,
- self.columns,
- some_exception,
- ) = return_tuple
-
- @staticmethod
- def _get_tables_columns_process_wrapped(
- sql_query: str, use_raw_names: bool = False
- ) -> Tuple[List[str], List[str]]:
- # Invoke sql_lineage_parser_impl_func_wrapper in a separate process to avoid
- # memory leaks from sqllineage module used by SqlLineageSQLParserImpl. This will help
- # shield our sources like lookml & redash, that need to parse a large number of SQL statements,
- # from causing significant memory leaks in the datahub cli during ingestion.
- queue: multiprocessing.Queue = Queue()
- process: multiprocessing.Process = Process(
- target=sql_lineage_parser_impl_func_wrapper,
- args=(queue, sql_query, use_raw_names),
- )
- process.start()
- tables, columns, exception_details = queue.get(block=True)
- if exception_details is not None:
- raise exception_details[0](f"Sub-process exception: {exception_details[1]}")
- return tables, columns
-
- def get_tables(self) -> List[str]:
- return self.tables
-
- def get_columns(self) -> List[str]:
- return self.columns
-
-
-DefaultSQLParser = SqlLineageSQLParser
diff --git a/metadata-ingestion/src/datahub/utilities/sql_parser_base.py b/metadata-ingestion/src/datahub/utilities/sql_parser_base.py
deleted file mode 100644
index 8fd5dfaf4978d1..00000000000000
--- a/metadata-ingestion/src/datahub/utilities/sql_parser_base.py
+++ /dev/null
@@ -1,21 +0,0 @@
-from abc import ABCMeta, abstractmethod
-from typing import List
-
-
-class SqlParserException(Exception):
- """Raised when sql parser fails"""
-
- pass
-
-
-class SQLParser(metaclass=ABCMeta):
- def __init__(self, sql_query: str, use_external_process: bool = True) -> None:
- self._sql_query = sql_query
-
- @abstractmethod
- def get_tables(self) -> List[str]:
- pass
-
- @abstractmethod
- def get_columns(self) -> List[str]:
- pass
diff --git a/metadata-ingestion/tests/integration/snowflake/snowflake_privatelink_golden.json b/metadata-ingestion/tests/integration/snowflake/snowflake_privatelink_golden.json
index d232ae710e8916..3040c6c4e9196f 100644
--- a/metadata-ingestion/tests/integration/snowflake/snowflake_privatelink_golden.json
+++ b/metadata-ingestion/tests/integration/snowflake/snowflake_privatelink_golden.json
@@ -138,27 +138,49 @@
{
"entityType": "dataset",
"entityUrn": "urn:li:dataset:(urn:li:dataPlatform:snowflake,instance1.test_db.test_schema.table_3,PROD)",
- "changeType": "UPSERT",
+ "changeType": "PATCH",
"aspectName": "datasetProperties",
"aspect": {
- "json": {
- "customProperties": {"CLUSTERING_KEY": "LINEAR(COL_1)"},
- "externalUrl": "https://app.abc12345.ap-south-1.privatelink.snowflakecomputing.com/#/data/databases/TEST_DB/schemas/TEST_SCHEMA/table/TABLE_3/",
- "name": "TABLE_3",
- "qualifiedName": "TEST_DB.TEST_SCHEMA.TABLE_3",
- "description": "Comment for Table",
- "created": {
- "time": 1623110400000
+ "json": [
+ {
+ "op": "add",
+ "path": "/name",
+ "value": "TABLE_3"
},
- "lastModified": {
- "time": 1623110400000
+ {
+ "op": "add",
+ "path": "/description",
+ "value": "Comment for Table"
},
- "tags": []
- }
+ {
+ "op": "add",
+ "path": "/created",
+ "value": {
+ "time": 1623110400000
+ }
+ },
+ {
+ "op": "add",
+ "path": "/lastModified",
+ "value": {
+ "time": 1623110400000
+ }
+ },
+ {
+ "op": "add",
+ "path": "/qualifiedName",
+ "value": "TEST_DB.TEST_SCHEMA.TABLE_3"
+ },
+ {
+ "op": "add",
+ "path": "/customProperties/CLUSTERING_KEY",
+ "value": "LINEAR(COL_1)"
+ }
+ ]
},
"systemMetadata": {
"lastObserved": 1654621200000,
- "runId": "snowflake-2022_06_07-17_00_00",
+ "runId": "snowflake-2022_06_07-17_00_00-ad3hnf",
"lastRunId": "no-run-id-provided"
}
},
@@ -567,27 +589,44 @@
{
"entityType": "dataset",
"entityUrn": "urn:li:dataset:(urn:li:dataPlatform:snowflake,instance1.test_db.test_schema.view_1,PROD)",
- "changeType": "UPSERT",
+ "changeType": "PATCH",
"aspectName": "datasetProperties",
"aspect": {
- "json": {
- "customProperties": {},
- "externalUrl": "https://app.abc12345.ap-south-1.privatelink.snowflakecomputing.com/#/data/databases/TEST_DB/schemas/TEST_SCHEMA/view/VIEW_1/",
- "name": "VIEW_1",
- "qualifiedName": "TEST_DB.TEST_SCHEMA.VIEW_1",
- "description": "Comment for View",
- "created": {
- "time": 1623110400000
+ "json": [
+ {
+ "op": "add",
+ "path": "/name",
+ "value": "VIEW_1"
},
- "lastModified": {
- "time": 1623110400000
+ {
+ "op": "add",
+ "path": "/description",
+ "value": "Comment for View"
},
- "tags": []
- }
+ {
+ "op": "add",
+ "path": "/created",
+ "value": {
+ "time": 1623110400000
+ }
+ },
+ {
+ "op": "add",
+ "path": "/lastModified",
+ "value": {
+ "time": 1623110400000
+ }
+ },
+ {
+ "op": "add",
+ "path": "/qualifiedName",
+ "value": "TEST_DB.TEST_SCHEMA.VIEW_1"
+ }
+ ]
},
"systemMetadata": {
"lastObserved": 1654621200000,
- "runId": "snowflake-2022_06_07-17_00_00",
+ "runId": "snowflake-2022_06_07-17_00_00-ad3hnf",
"lastRunId": "no-run-id-provided"
}
},
@@ -808,27 +847,49 @@
{
"entityType": "dataset",
"entityUrn": "urn:li:dataset:(urn:li:dataPlatform:snowflake,instance1.test_db.test_schema.table_1,PROD)",
- "changeType": "UPSERT",
+ "changeType": "PATCH",
"aspectName": "datasetProperties",
"aspect": {
- "json": {
- "customProperties": {"CLUSTERING_KEY": "LINEAR(COL_1)"},
- "externalUrl": "https://app.abc12345.ap-south-1.privatelink.snowflakecomputing.com/#/data/databases/TEST_DB/schemas/TEST_SCHEMA/table/TABLE_1/",
- "name": "TABLE_1",
- "qualifiedName": "TEST_DB.TEST_SCHEMA.TABLE_1",
- "description": "Comment for Table",
- "created": {
- "time": 1623110400000
+ "json": [
+ {
+ "op": "add",
+ "path": "/name",
+ "value": "TABLE_1"
},
- "lastModified": {
- "time": 1623110400000
+ {
+ "op": "add",
+ "path": "/description",
+ "value": "Comment for Table"
},
- "tags": []
- }
+ {
+ "op": "add",
+ "path": "/created",
+ "value": {
+ "time": 1623110400000
+ }
+ },
+ {
+ "op": "add",
+ "path": "/lastModified",
+ "value": {
+ "time": 1623110400000
+ }
+ },
+ {
+ "op": "add",
+ "path": "/qualifiedName",
+ "value": "TEST_DB.TEST_SCHEMA.TABLE_1"
+ },
+ {
+ "op": "add",
+ "path": "/customProperties/CLUSTERING_KEY",
+ "value": "LINEAR(COL_1)"
+ }
+ ]
},
"systemMetadata": {
"lastObserved": 1654621200000,
- "runId": "snowflake-2022_06_07-17_00_00",
+ "runId": "snowflake-2022_06_07-17_00_00-ad3hnf",
"lastRunId": "no-run-id-provided"
}
},
@@ -1473,27 +1534,49 @@
{
"entityType": "dataset",
"entityUrn": "urn:li:dataset:(urn:li:dataPlatform:snowflake,instance1.test_db.test_schema.table_10,PROD)",
- "changeType": "UPSERT",
+ "changeType": "PATCH",
"aspectName": "datasetProperties",
"aspect": {
- "json": {
- "customProperties": {"CLUSTERING_KEY": "LINEAR(COL_1)"},
- "externalUrl": "https://app.abc12345.ap-south-1.privatelink.snowflakecomputing.com/#/data/databases/TEST_DB/schemas/TEST_SCHEMA/table/TABLE_10/",
- "name": "TABLE_10",
- "qualifiedName": "TEST_DB.TEST_SCHEMA.TABLE_10",
- "description": "Comment for Table",
- "created": {
- "time": 1623110400000
+ "json": [
+ {
+ "op": "add",
+ "path": "/name",
+ "value": "TABLE_10"
},
- "lastModified": {
- "time": 1623110400000
+ {
+ "op": "add",
+ "path": "/description",
+ "value": "Comment for Table"
},
- "tags": []
- }
+ {
+ "op": "add",
+ "path": "/created",
+ "value": {
+ "time": 1623110400000
+ }
+ },
+ {
+ "op": "add",
+ "path": "/lastModified",
+ "value": {
+ "time": 1623110400000
+ }
+ },
+ {
+ "op": "add",
+ "path": "/qualifiedName",
+ "value": "TEST_DB.TEST_SCHEMA.TABLE_10"
+ },
+ {
+ "op": "add",
+ "path": "/customProperties/CLUSTERING_KEY",
+ "value": "LINEAR(COL_1)"
+ }
+ ]
},
"systemMetadata": {
"lastObserved": 1654621200000,
- "runId": "snowflake-2022_06_07-17_00_00",
+ "runId": "snowflake-2022_06_07-17_00_00-ad3hnf",
"lastRunId": "no-run-id-provided"
}
},
@@ -1712,54 +1795,98 @@
{
"entityType": "dataset",
"entityUrn": "urn:li:dataset:(urn:li:dataPlatform:snowflake,instance1.test_db.test_schema.table_5,PROD)",
- "changeType": "UPSERT",
+ "changeType": "PATCH",
"aspectName": "datasetProperties",
"aspect": {
- "json": {
- "customProperties": {"CLUSTERING_KEY": "LINEAR(COL_1)"},
- "externalUrl": "https://app.abc12345.ap-south-1.privatelink.snowflakecomputing.com/#/data/databases/TEST_DB/schemas/TEST_SCHEMA/table/TABLE_5/",
- "name": "TABLE_5",
- "qualifiedName": "TEST_DB.TEST_SCHEMA.TABLE_5",
- "description": "Comment for Table",
- "created": {
- "time": 1623110400000
+ "json": [
+ {
+ "op": "add",
+ "path": "/name",
+ "value": "TABLE_5"
},
- "lastModified": {
- "time": 1623110400000
+ {
+ "op": "add",
+ "path": "/description",
+ "value": "Comment for Table"
},
- "tags": []
- }
+ {
+ "op": "add",
+ "path": "/created",
+ "value": {
+ "time": 1623110400000
+ }
+ },
+ {
+ "op": "add",
+ "path": "/lastModified",
+ "value": {
+ "time": 1623110400000
+ }
+ },
+ {
+ "op": "add",
+ "path": "/qualifiedName",
+ "value": "TEST_DB.TEST_SCHEMA.TABLE_5"
+ },
+ {
+ "op": "add",
+ "path": "/customProperties/CLUSTERING_KEY",
+ "value": "LINEAR(COL_1)"
+ }
+ ]
},
"systemMetadata": {
"lastObserved": 1654621200000,
- "runId": "snowflake-2022_06_07-17_00_00",
+ "runId": "snowflake-2022_06_07-17_00_00-ad3hnf",
"lastRunId": "no-run-id-provided"
}
},
{
"entityType": "dataset",
"entityUrn": "urn:li:dataset:(urn:li:dataPlatform:snowflake,instance1.test_db.test_schema.table_2,PROD)",
- "changeType": "UPSERT",
+ "changeType": "PATCH",
"aspectName": "datasetProperties",
"aspect": {
- "json": {
- "customProperties": {"CLUSTERING_KEY": "LINEAR(COL_1)"},
- "externalUrl": "https://app.abc12345.ap-south-1.privatelink.snowflakecomputing.com/#/data/databases/TEST_DB/schemas/TEST_SCHEMA/table/TABLE_2/",
- "name": "TABLE_2",
- "qualifiedName": "TEST_DB.TEST_SCHEMA.TABLE_2",
- "description": "Comment for Table",
- "created": {
- "time": 1623110400000
+ "json": [
+ {
+ "op": "add",
+ "path": "/name",
+ "value": "TABLE_2"
},
- "lastModified": {
- "time": 1623110400000
+ {
+ "op": "add",
+ "path": "/description",
+ "value": "Comment for Table"
},
- "tags": []
- }
+ {
+ "op": "add",
+ "path": "/created",
+ "value": {
+ "time": 1623110400000
+ }
+ },
+ {
+ "op": "add",
+ "path": "/lastModified",
+ "value": {
+ "time": 1623110400000
+ }
+ },
+ {
+ "op": "add",
+ "path": "/qualifiedName",
+ "value": "TEST_DB.TEST_SCHEMA.TABLE_2"
+ },
+ {
+ "op": "add",
+ "path": "/customProperties/CLUSTERING_KEY",
+ "value": "LINEAR(COL_1)"
+ }
+ ]
},
"systemMetadata": {
"lastObserved": 1654621200000,
- "runId": "snowflake-2022_06_07-17_00_00",
+ "runId": "snowflake-2022_06_07-17_00_00-ad3hnf",
"lastRunId": "no-run-id-provided"
}
},
@@ -2301,27 +2428,49 @@
{
"entityType": "dataset",
"entityUrn": "urn:li:dataset:(urn:li:dataPlatform:snowflake,instance1.test_db.test_schema.table_6,PROD)",
- "changeType": "UPSERT",
+ "changeType": "PATCH",
"aspectName": "datasetProperties",
"aspect": {
- "json": {
- "customProperties": {"CLUSTERING_KEY": "LINEAR(COL_1)"},
- "externalUrl": "https://app.abc12345.ap-south-1.privatelink.snowflakecomputing.com/#/data/databases/TEST_DB/schemas/TEST_SCHEMA/table/TABLE_6/",
- "name": "TABLE_6",
- "qualifiedName": "TEST_DB.TEST_SCHEMA.TABLE_6",
- "description": "Comment for Table",
- "created": {
- "time": 1623110400000
+ "json": [
+ {
+ "op": "add",
+ "path": "/name",
+ "value": "TABLE_6"
},
- "lastModified": {
- "time": 1623110400000
+ {
+ "op": "add",
+ "path": "/description",
+ "value": "Comment for Table"
},
- "tags": []
- }
+ {
+ "op": "add",
+ "path": "/created",
+ "value": {
+ "time": 1623110400000
+ }
+ },
+ {
+ "op": "add",
+ "path": "/lastModified",
+ "value": {
+ "time": 1623110400000
+ }
+ },
+ {
+ "op": "add",
+ "path": "/qualifiedName",
+ "value": "TEST_DB.TEST_SCHEMA.TABLE_6"
+ },
+ {
+ "op": "add",
+ "path": "/customProperties/CLUSTERING_KEY",
+ "value": "LINEAR(COL_1)"
+ }
+ ]
},
"systemMetadata": {
"lastObserved": 1654621200000,
- "runId": "snowflake-2022_06_07-17_00_00",
+ "runId": "snowflake-2022_06_07-17_00_00-ad3hnf",
"lastRunId": "no-run-id-provided"
}
},
@@ -2621,27 +2770,49 @@
{
"entityType": "dataset",
"entityUrn": "urn:li:dataset:(urn:li:dataPlatform:snowflake,instance1.test_db.test_schema.table_7,PROD)",
- "changeType": "UPSERT",
+ "changeType": "PATCH",
"aspectName": "datasetProperties",
"aspect": {
- "json": {
- "customProperties": {"CLUSTERING_KEY": "LINEAR(COL_1)"},
- "externalUrl": "https://app.abc12345.ap-south-1.privatelink.snowflakecomputing.com/#/data/databases/TEST_DB/schemas/TEST_SCHEMA/table/TABLE_7/",
- "name": "TABLE_7",
- "qualifiedName": "TEST_DB.TEST_SCHEMA.TABLE_7",
- "description": "Comment for Table",
- "created": {
- "time": 1623110400000
+ "json": [
+ {
+ "op": "add",
+ "path": "/name",
+ "value": "TABLE_7"
},
- "lastModified": {
- "time": 1623110400000
+ {
+ "op": "add",
+ "path": "/description",
+ "value": "Comment for Table"
},
- "tags": []
- }
+ {
+ "op": "add",
+ "path": "/created",
+ "value": {
+ "time": 1623110400000
+ }
+ },
+ {
+ "op": "add",
+ "path": "/lastModified",
+ "value": {
+ "time": 1623110400000
+ }
+ },
+ {
+ "op": "add",
+ "path": "/qualifiedName",
+ "value": "TEST_DB.TEST_SCHEMA.TABLE_7"
+ },
+ {
+ "op": "add",
+ "path": "/customProperties/CLUSTERING_KEY",
+ "value": "LINEAR(COL_1)"
+ }
+ ]
},
"systemMetadata": {
"lastObserved": 1654621200000,
- "runId": "snowflake-2022_06_07-17_00_00",
+ "runId": "snowflake-2022_06_07-17_00_00-ad3hnf",
"lastRunId": "no-run-id-provided"
}
},
@@ -2664,27 +2835,49 @@
{
"entityType": "dataset",
"entityUrn": "urn:li:dataset:(urn:li:dataPlatform:snowflake,instance1.test_db.test_schema.table_4,PROD)",
- "changeType": "UPSERT",
+ "changeType": "PATCH",
"aspectName": "datasetProperties",
"aspect": {
- "json": {
- "customProperties": {"CLUSTERING_KEY": "LINEAR(COL_1)"},
- "externalUrl": "https://app.abc12345.ap-south-1.privatelink.snowflakecomputing.com/#/data/databases/TEST_DB/schemas/TEST_SCHEMA/table/TABLE_4/",
- "name": "TABLE_4",
- "qualifiedName": "TEST_DB.TEST_SCHEMA.TABLE_4",
- "description": "Comment for Table",
- "created": {
- "time": 1623110400000
+ "json": [
+ {
+ "op": "add",
+ "path": "/name",
+ "value": "TABLE_4"
},
- "lastModified": {
- "time": 1623110400000
+ {
+ "op": "add",
+ "path": "/description",
+ "value": "Comment for Table"
},
- "tags": []
- }
+ {
+ "op": "add",
+ "path": "/created",
+ "value": {
+ "time": 1623110400000
+ }
+ },
+ {
+ "op": "add",
+ "path": "/lastModified",
+ "value": {
+ "time": 1623110400000
+ }
+ },
+ {
+ "op": "add",
+ "path": "/qualifiedName",
+ "value": "TEST_DB.TEST_SCHEMA.TABLE_4"
+ },
+ {
+ "op": "add",
+ "path": "/customProperties/CLUSTERING_KEY",
+ "value": "LINEAR(COL_1)"
+ }
+ ]
},
"systemMetadata": {
"lastObserved": 1654621200000,
- "runId": "snowflake-2022_06_07-17_00_00",
+ "runId": "snowflake-2022_06_07-17_00_00-ad3hnf",
"lastRunId": "no-run-id-provided"
}
},
@@ -3162,27 +3355,49 @@
{
"entityType": "dataset",
"entityUrn": "urn:li:dataset:(urn:li:dataPlatform:snowflake,instance1.test_db.test_schema.table_8,PROD)",
- "changeType": "UPSERT",
+ "changeType": "PATCH",
"aspectName": "datasetProperties",
"aspect": {
- "json": {
- "customProperties": {"CLUSTERING_KEY": "LINEAR(COL_1)"},
- "externalUrl": "https://app.abc12345.ap-south-1.privatelink.snowflakecomputing.com/#/data/databases/TEST_DB/schemas/TEST_SCHEMA/table/TABLE_8/",
- "name": "TABLE_8",
- "qualifiedName": "TEST_DB.TEST_SCHEMA.TABLE_8",
- "description": "Comment for Table",
- "created": {
- "time": 1623110400000
+ "json": [
+ {
+ "op": "add",
+ "path": "/name",
+ "value": "TABLE_8"
},
- "lastModified": {
- "time": 1623110400000
+ {
+ "op": "add",
+ "path": "/description",
+ "value": "Comment for Table"
},
- "tags": []
- }
+ {
+ "op": "add",
+ "path": "/created",
+ "value": {
+ "time": 1623110400000
+ }
+ },
+ {
+ "op": "add",
+ "path": "/lastModified",
+ "value": {
+ "time": 1623110400000
+ }
+ },
+ {
+ "op": "add",
+ "path": "/qualifiedName",
+ "value": "TEST_DB.TEST_SCHEMA.TABLE_8"
+ },
+ {
+ "op": "add",
+ "path": "/customProperties/CLUSTERING_KEY",
+ "value": "LINEAR(COL_1)"
+ }
+ ]
},
"systemMetadata": {
"lastObserved": 1654621200000,
- "runId": "snowflake-2022_06_07-17_00_00",
+ "runId": "snowflake-2022_06_07-17_00_00-ad3hnf",
"lastRunId": "no-run-id-provided"
}
},
@@ -3302,27 +3517,49 @@
{
"entityType": "dataset",
"entityUrn": "urn:li:dataset:(urn:li:dataPlatform:snowflake,instance1.test_db.test_schema.table_9,PROD)",
- "changeType": "UPSERT",
+ "changeType": "PATCH",
"aspectName": "datasetProperties",
"aspect": {
- "json": {
- "customProperties": {"CLUSTERING_KEY": "LINEAR(COL_1)"},
- "externalUrl": "https://app.abc12345.ap-south-1.privatelink.snowflakecomputing.com/#/data/databases/TEST_DB/schemas/TEST_SCHEMA/table/TABLE_9/",
- "name": "TABLE_9",
- "qualifiedName": "TEST_DB.TEST_SCHEMA.TABLE_9",
- "description": "Comment for Table",
- "created": {
- "time": 1623110400000
+ "json": [
+ {
+ "op": "add",
+ "path": "/name",
+ "value": "TABLE_9"
},
- "lastModified": {
- "time": 1623110400000
+ {
+ "op": "add",
+ "path": "/description",
+ "value": "Comment for Table"
},
- "tags": []
- }
+ {
+ "op": "add",
+ "path": "/created",
+ "value": {
+ "time": 1623110400000
+ }
+ },
+ {
+ "op": "add",
+ "path": "/lastModified",
+ "value": {
+ "time": 1623110400000
+ }
+ },
+ {
+ "op": "add",
+ "path": "/qualifiedName",
+ "value": "TEST_DB.TEST_SCHEMA.TABLE_9"
+ },
+ {
+ "op": "add",
+ "path": "/customProperties/CLUSTERING_KEY",
+ "value": "LINEAR(COL_1)"
+ }
+ ]
},
"systemMetadata": {
"lastObserved": 1654621200000,
- "runId": "snowflake-2022_06_07-17_00_00",
+ "runId": "snowflake-2022_06_07-17_00_00-ad3hnf",
"lastRunId": "no-run-id-provided"
}
},
@@ -3607,27 +3844,44 @@
{
"entityType": "dataset",
"entityUrn": "urn:li:dataset:(urn:li:dataPlatform:snowflake,instance1.test_db.test_schema.view_2,PROD)",
- "changeType": "UPSERT",
+ "changeType": "PATCH",
"aspectName": "datasetProperties",
"aspect": {
- "json": {
- "customProperties": {},
- "externalUrl": "https://app.abc12345.ap-south-1.privatelink.snowflakecomputing.com/#/data/databases/TEST_DB/schemas/TEST_SCHEMA/view/VIEW_2/",
- "name": "VIEW_2",
- "qualifiedName": "TEST_DB.TEST_SCHEMA.VIEW_2",
- "description": "Comment for View",
- "created": {
- "time": 1623110400000
+ "json": [
+ {
+ "op": "add",
+ "path": "/name",
+ "value": "VIEW_2"
},
- "lastModified": {
- "time": 1623110400000
+ {
+ "op": "add",
+ "path": "/description",
+ "value": "Comment for View"
},
- "tags": []
- }
+ {
+ "op": "add",
+ "path": "/created",
+ "value": {
+ "time": 1623110400000
+ }
+ },
+ {
+ "op": "add",
+ "path": "/lastModified",
+ "value": {
+ "time": 1623110400000
+ }
+ },
+ {
+ "op": "add",
+ "path": "/qualifiedName",
+ "value": "TEST_DB.TEST_SCHEMA.VIEW_2"
+ }
+ ]
},
"systemMetadata": {
"lastObserved": 1654621200000,
- "runId": "snowflake-2022_06_07-17_00_00",
+ "runId": "snowflake-2022_06_07-17_00_00-ad3hnf",
"lastRunId": "no-run-id-provided"
}
},
diff --git a/metadata-ingestion/tests/integration/snowflake/test_snowflake.py b/metadata-ingestion/tests/integration/snowflake/test_snowflake.py
index ca694b02cff010..1d7470d24f7689 100644
--- a/metadata-ingestion/tests/integration/snowflake/test_snowflake.py
+++ b/metadata-ingestion/tests/integration/snowflake/test_snowflake.py
@@ -187,7 +187,9 @@ def test_snowflake_basic(pytestconfig, tmp_path, mock_time, mock_datahub_graph):
@freeze_time(FROZEN_TIME)
-def test_snowflake_private_link(pytestconfig, tmp_path, mock_time, mock_datahub_graph):
+def test_snowflake_private_link_and_incremental_mcps(
+ pytestconfig, tmp_path, mock_time, mock_datahub_graph
+):
test_resources_dir = pytestconfig.rootpath / "tests/integration/snowflake"
# Run the metadata ingestion pipeline.
@@ -218,6 +220,7 @@ def test_snowflake_private_link(pytestconfig, tmp_path, mock_time, mock_datahub_
include_usage_stats=False,
format_sql_queries=True,
incremental_lineage=False,
+ incremental_properties=True,
include_operational_stats=False,
platform_instance="instance1",
start_time=datetime(2022, 6, 6, 0, 0, 0, 0).replace(
diff --git a/metadata-ingestion/tests/unit/cli/test_cli_utils.py b/metadata-ingestion/tests/unit/cli/test_cli_utils.py
index af3a184d97e41c..c9693c75d96fe9 100644
--- a/metadata-ingestion/tests/unit/cli/test_cli_utils.py
+++ b/metadata-ingestion/tests/unit/cli/test_cli_utils.py
@@ -66,6 +66,10 @@ def test_fixup_gms_url():
assert cli_utils.fixup_gms_url("http://localhost:8080") == "http://localhost:8080"
assert cli_utils.fixup_gms_url("http://localhost:8080/") == "http://localhost:8080"
assert cli_utils.fixup_gms_url("http://abc.acryl.io") == "https://abc.acryl.io/gms"
+ assert (
+ cli_utils.fixup_gms_url("http://abc.acryl.io/api/gms")
+ == "https://abc.acryl.io/gms"
+ )
def test_guess_frontend_url_from_gms_url():
diff --git a/metadata-ingestion/tests/unit/test_redash_source.py b/metadata-ingestion/tests/unit/test_redash_source.py
index 2982fe76c4d4e7..32ab200847dc6c 100644
--- a/metadata-ingestion/tests/unit/test_redash_source.py
+++ b/metadata-ingestion/tests/unit/test_redash_source.py
@@ -710,9 +710,9 @@ def test_get_chart_snapshot_parse_table_names_from_sql(mocked_data_source):
),
chartUrl="http://localhost:5000/queries/4#10",
inputs=[
- "urn:li:dataset:(urn:li:dataPlatform:mysql,Rfam.order_items,PROD)",
- "urn:li:dataset:(urn:li:dataPlatform:mysql,Rfam.orders,PROD)",
- "urn:li:dataset:(urn:li:dataPlatform:mysql,Rfam.staffs,PROD)",
+ "urn:li:dataset:(urn:li:dataPlatform:mysql,rfam.order_items,PROD)",
+ "urn:li:dataset:(urn:li:dataPlatform:mysql,rfam.orders,PROD)",
+ "urn:li:dataset:(urn:li:dataPlatform:mysql,rfam.staffs,PROD)",
],
type="PIE",
)
diff --git a/metadata-ingestion/tests/unit/utilities/test_partition_executor.py b/metadata-ingestion/tests/unit/utilities/test_partition_executor.py
index eba79eafce473b..ce211c2d618062 100644
--- a/metadata-ingestion/tests/unit/utilities/test_partition_executor.py
+++ b/metadata-ingestion/tests/unit/utilities/test_partition_executor.py
@@ -80,7 +80,8 @@ def task(id: str) -> str:
assert len(done_tasks) == 16
-def test_batch_partition_executor_sequential_key_execution():
+@pytest.mark.parametrize("max_workers", [1, 2, 10])
+def test_batch_partition_executor_sequential_key_execution(max_workers: int) -> None:
executing_tasks = set()
done_tasks = set()
done_task_batches = set()
@@ -99,7 +100,7 @@ def process_batch(batch):
done_task_batches.add(tuple(id for _, id in batch))
with BatchPartitionExecutor(
- max_workers=2,
+ max_workers=max_workers,
max_pending=10,
max_per_batch=2,
process_batch=process_batch,
diff --git a/metadata-ingestion/tests/unit/utilities/test_utilities.py b/metadata-ingestion/tests/unit/utilities/test_utilities.py
index 68da1bc1c01be2..91819bff41e629 100644
--- a/metadata-ingestion/tests/unit/utilities/test_utilities.py
+++ b/metadata-ingestion/tests/unit/utilities/test_utilities.py
@@ -1,8 +1,55 @@
import doctest
+import re
+from typing import List
+from datahub.sql_parsing.schema_resolver import SchemaResolver
+from datahub.sql_parsing.sqlglot_lineage import sqlglot_lineage
from datahub.utilities.delayed_iter import delayed_iter
from datahub.utilities.is_pytest import is_pytest_running
-from datahub.utilities.sql_parser import SqlLineageSQLParser
+from datahub.utilities.urns.dataset_urn import DatasetUrn
+
+
+class SqlLineageSQLParser:
+ """
+ It uses `sqlglot_lineage` to extract tables and columns, serving as a replacement for the `sqllineage` implementation, similar to BigQuery.
+ Reference: [BigQuery SQL Lineage Test](https://github.com/datahub-project/datahub/blob/master/metadata-ingestion/tests/unit/bigquery/test_bigquery_sql_lineage.py#L8).
+ """
+
+ _MYVIEW_SQL_TABLE_NAME_TOKEN = "__my_view__.__sql_table_name__"
+ _MYVIEW_LOOKER_TOKEN = "my_view.SQL_TABLE_NAME"
+
+ def __init__(self, sql_query: str, platform: str = "bigquery") -> None:
+ # SqlLineageParser lowercarese tablenames and we need to replace Looker specific token which should be uppercased
+ sql_query = re.sub(
+ rf"(\${{{self._MYVIEW_LOOKER_TOKEN}}})",
+ rf"{self._MYVIEW_SQL_TABLE_NAME_TOKEN}",
+ sql_query,
+ )
+ self.sql_query = sql_query
+ self.schema_resolver = SchemaResolver(platform=platform)
+ self.result = sqlglot_lineage(sql_query, self.schema_resolver)
+
+ def get_tables(self) -> List[str]:
+ ans = []
+ for urn in self.result.in_tables:
+ table_ref = DatasetUrn.from_string(urn)
+ ans.append(str(table_ref.name))
+
+ result = [
+ self._MYVIEW_LOOKER_TOKEN if c == self._MYVIEW_SQL_TABLE_NAME_TOKEN else c
+ for c in ans
+ ]
+ # Sort tables to make the list deterministic
+ result.sort()
+
+ return result
+
+ def get_columns(self) -> List[str]:
+ ans = []
+ for col_info in self.result.column_lineage or []:
+ for col_ref in col_info.upstreams:
+ ans.append(col_ref.column)
+ return ans
def test_delayed_iter():
@@ -121,7 +168,7 @@ def test_sqllineage_sql_parser_get_columns_with_alias_and_count_star():
columns_list = SqlLineageSQLParser(sql_query).get_columns()
columns_list.sort()
- assert columns_list == ["a", "b", "count", "test"]
+ assert columns_list == ["a", "b", "c"]
def test_sqllineage_sql_parser_get_columns_with_more_complex_join():
@@ -145,7 +192,7 @@ def test_sqllineage_sql_parser_get_columns_with_more_complex_join():
columns_list = SqlLineageSQLParser(sql_query).get_columns()
columns_list.sort()
- assert columns_list == ["bs", "pi", "pt", "pu", "v"]
+ assert columns_list == ["bs", "pi", "tt", "tt", "v"]
def test_sqllineage_sql_parser_get_columns_complex_query_with_union():
@@ -198,7 +245,7 @@ def test_sqllineage_sql_parser_get_columns_complex_query_with_union():
columns_list = SqlLineageSQLParser(sql_query).get_columns()
columns_list.sort()
- assert columns_list == ["c", "date", "e", "u", "x"]
+ assert columns_list == ["c", "c", "e", "e", "e", "e", "u", "u", "x", "x"]
def test_sqllineage_sql_parser_get_tables_from_templated_query():
@@ -239,7 +286,7 @@ def test_sqllineage_sql_parser_with_weird_lookml_query():
"""
columns_list = SqlLineageSQLParser(sql_query).get_columns()
columns_list.sort()
- assert columns_list == ["aliased_platform", "country", "date"]
+ assert columns_list == []
def test_sqllineage_sql_parser_tables_from_redash_query():
@@ -276,13 +323,7 @@ def test_sqllineage_sql_parser_tables_with_special_names():
"hour-table",
"timestamp-table",
]
- expected_columns = [
- "column-admin",
- "column-data",
- "column-date",
- "column-hour",
- "column-timestamp",
- ]
+ expected_columns: List[str] = []
assert sorted(SqlLineageSQLParser(sql_query).get_tables()) == expected_tables
assert sorted(SqlLineageSQLParser(sql_query).get_columns()) == expected_columns
diff --git a/metadata-io/metadata-io-api/src/main/java/com/linkedin/metadata/entity/EntityAspect.java b/metadata-io/metadata-io-api/src/main/java/com/linkedin/metadata/entity/EntityAspect.java
index 976db4133c0043..2b67d5e92f833c 100644
--- a/metadata-io/metadata-io-api/src/main/java/com/linkedin/metadata/entity/EntityAspect.java
+++ b/metadata-io/metadata-io-api/src/main/java/com/linkedin/metadata/entity/EntityAspect.java
@@ -52,6 +52,26 @@ public class EntityAspect {
private String createdFor;
+ @Override
+ public String toString() {
+ return "EntityAspect{"
+ + "urn='"
+ + urn
+ + '\''
+ + ", aspect='"
+ + aspect
+ + '\''
+ + ", version="
+ + version
+ + ", metadata='"
+ + metadata
+ + '\''
+ + ", systemMetadata='"
+ + systemMetadata
+ + '\''
+ + '}';
+ }
+
/**
* Provide a typed EntityAspect without breaking the existing public contract with generic types.
*/
@@ -144,6 +164,11 @@ public EnvelopedAspect toEnvelopedAspects() {
return envelopedAspect;
}
+ @Override
+ public String toString() {
+ return entityAspect.toString();
+ }
+
public static class EntitySystemAspectBuilder {
private EntityAspect.EntitySystemAspect build() {
diff --git a/metadata-io/metadata-io-api/src/main/java/com/linkedin/metadata/entity/ebean/batch/AspectsBatchImpl.java b/metadata-io/metadata-io-api/src/main/java/com/linkedin/metadata/entity/ebean/batch/AspectsBatchImpl.java
index c0d65640df2378..1af9fc1565a456 100644
--- a/metadata-io/metadata-io-api/src/main/java/com/linkedin/metadata/entity/ebean/batch/AspectsBatchImpl.java
+++ b/metadata-io/metadata-io-api/src/main/java/com/linkedin/metadata/entity/ebean/batch/AspectsBatchImpl.java
@@ -1,6 +1,7 @@
package com.linkedin.metadata.entity.ebean.batch;
import com.linkedin.common.AuditStamp;
+import com.linkedin.common.urn.Urn;
import com.linkedin.data.template.RecordTemplate;
import com.linkedin.events.metadata.ChangeType;
import com.linkedin.metadata.aspect.AspectRetriever;
@@ -15,7 +16,9 @@
import com.linkedin.metadata.models.EntitySpec;
import com.linkedin.mxe.MetadataChangeProposal;
import com.linkedin.util.Pair;
+import java.util.ArrayList;
import java.util.Collection;
+import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
@@ -29,12 +32,23 @@
import lombok.extern.slf4j.Slf4j;
@Slf4j
-@Getter
@Builder(toBuilder = true)
public class AspectsBatchImpl implements AspectsBatch {
@Nonnull private final Collection extends BatchItem> items;
- @Nonnull private final RetrieverContext retrieverContext;
+ @Nonnull private final Collection extends BatchItem> nonRepeatedItems;
+ @Getter @Nonnull private final RetrieverContext retrieverContext;
+
+ @Override
+ @Nonnull
+ public Collection extends BatchItem> getItems() {
+ return nonRepeatedItems;
+ }
+
+ @Override
+ public Collection extends BatchItem> getInitialItems() {
+ return items;
+ }
/**
* Convert patches to upserts, apply hooks at the aspect and batch level.
@@ -207,14 +221,32 @@ public AspectsBatchImplBuilder mcps(
return this;
}
+ private static List filterRepeats(Collection items) {
+ List result = new ArrayList<>();
+ Map, T> last = new HashMap<>();
+
+ for (T item : items) {
+ Pair urnAspect = Pair.of(item.getUrn(), item.getAspectName());
+ // Check if this item is a duplicate of the previous
+ if (!last.containsKey(urnAspect) || !item.isDatabaseDuplicateOf(last.get(urnAspect))) {
+ result.add(item);
+ }
+ last.put(urnAspect, item);
+ }
+
+ return result;
+ }
+
public AspectsBatchImpl build() {
+ this.nonRepeatedItems = filterRepeats(this.items);
+
ValidationExceptionCollection exceptions =
- AspectsBatch.validateProposed(this.items, this.retrieverContext);
+ AspectsBatch.validateProposed(this.nonRepeatedItems, this.retrieverContext);
if (!exceptions.isEmpty()) {
throw new IllegalArgumentException("Failed to validate MCP due to: " + exceptions);
}
- return new AspectsBatchImpl(this.items, this.retrieverContext);
+ return new AspectsBatchImpl(this.items, this.nonRepeatedItems, this.retrieverContext);
}
}
diff --git a/metadata-io/metadata-io-api/src/main/java/com/linkedin/metadata/entity/ebean/batch/ChangeItemImpl.java b/metadata-io/metadata-io-api/src/main/java/com/linkedin/metadata/entity/ebean/batch/ChangeItemImpl.java
index 6f45a36d1daf46..64263859e4aadb 100644
--- a/metadata-io/metadata-io-api/src/main/java/com/linkedin/metadata/entity/ebean/batch/ChangeItemImpl.java
+++ b/metadata-io/metadata-io-api/src/main/java/com/linkedin/metadata/entity/ebean/batch/ChangeItemImpl.java
@@ -3,11 +3,13 @@
import com.datahub.util.exception.ModelConversionException;
import com.linkedin.common.AuditStamp;
import com.linkedin.common.urn.Urn;
+import com.linkedin.data.template.DataTemplateUtil;
import com.linkedin.data.template.RecordTemplate;
import com.linkedin.data.template.StringMap;
import com.linkedin.events.metadata.ChangeType;
import com.linkedin.metadata.aspect.AspectRetriever;
import com.linkedin.metadata.aspect.SystemAspect;
+import com.linkedin.metadata.aspect.batch.BatchItem;
import com.linkedin.metadata.aspect.batch.ChangeMCP;
import com.linkedin.metadata.aspect.batch.MCPItem;
import com.linkedin.metadata.aspect.patch.template.common.GenericPatchTemplate;
@@ -269,6 +271,11 @@ private static RecordTemplate convertToRecordTemplate(
}
}
+ @Override
+ public boolean isDatabaseDuplicateOf(BatchItem other) {
+ return equals(other);
+ }
+
@Override
public boolean equals(Object o) {
if (this == o) {
@@ -280,13 +287,15 @@ public boolean equals(Object o) {
ChangeItemImpl that = (ChangeItemImpl) o;
return urn.equals(that.urn)
&& aspectName.equals(that.aspectName)
+ && changeType.equals(that.changeType)
&& Objects.equals(systemMetadata, that.systemMetadata)
- && recordTemplate.equals(that.recordTemplate);
+ && Objects.equals(auditStamp, that.auditStamp)
+ && DataTemplateUtil.areEqual(recordTemplate, that.recordTemplate);
}
@Override
public int hashCode() {
- return Objects.hash(urn, aspectName, systemMetadata, recordTemplate);
+ return Objects.hash(urn, aspectName, changeType, systemMetadata, auditStamp, recordTemplate);
}
@Override
diff --git a/metadata-io/metadata-io-api/src/main/java/com/linkedin/metadata/entity/ebean/batch/DeleteItemImpl.java b/metadata-io/metadata-io-api/src/main/java/com/linkedin/metadata/entity/ebean/batch/DeleteItemImpl.java
index 9c1ded284fa0bd..40bcb0fa8ed2d1 100644
--- a/metadata-io/metadata-io-api/src/main/java/com/linkedin/metadata/entity/ebean/batch/DeleteItemImpl.java
+++ b/metadata-io/metadata-io-api/src/main/java/com/linkedin/metadata/entity/ebean/batch/DeleteItemImpl.java
@@ -6,6 +6,7 @@
import com.linkedin.events.metadata.ChangeType;
import com.linkedin.metadata.aspect.AspectRetriever;
import com.linkedin.metadata.aspect.SystemAspect;
+import com.linkedin.metadata.aspect.batch.BatchItem;
import com.linkedin.metadata.aspect.batch.ChangeMCP;
import com.linkedin.metadata.entity.EntityApiUtils;
import com.linkedin.metadata.entity.EntityAspect;
@@ -115,6 +116,11 @@ public DeleteItemImpl build(AspectRetriever aspectRetriever) {
}
}
+ @Override
+ public boolean isDatabaseDuplicateOf(BatchItem other) {
+ return equals(other);
+ }
+
@Override
public boolean equals(Object o) {
if (this == o) {
diff --git a/metadata-io/metadata-io-api/src/main/java/com/linkedin/metadata/entity/ebean/batch/MCLItemImpl.java b/metadata-io/metadata-io-api/src/main/java/com/linkedin/metadata/entity/ebean/batch/MCLItemImpl.java
index a5afd4651ed2c4..85923a28a64be5 100644
--- a/metadata-io/metadata-io-api/src/main/java/com/linkedin/metadata/entity/ebean/batch/MCLItemImpl.java
+++ b/metadata-io/metadata-io-api/src/main/java/com/linkedin/metadata/entity/ebean/batch/MCLItemImpl.java
@@ -5,6 +5,7 @@
import com.linkedin.data.template.RecordTemplate;
import com.linkedin.events.metadata.ChangeType;
import com.linkedin.metadata.aspect.AspectRetriever;
+import com.linkedin.metadata.aspect.batch.BatchItem;
import com.linkedin.metadata.aspect.batch.MCLItem;
import com.linkedin.metadata.aspect.batch.MCPItem;
import com.linkedin.metadata.entity.AspectUtils;
@@ -158,6 +159,11 @@ private static Pair convertToRecordTemplate(
}
}
+ @Override
+ public boolean isDatabaseDuplicateOf(BatchItem other) {
+ return equals(other);
+ }
+
@Override
public boolean equals(Object o) {
if (this == o) {
diff --git a/metadata-io/metadata-io-api/src/main/java/com/linkedin/metadata/entity/ebean/batch/PatchItemImpl.java b/metadata-io/metadata-io-api/src/main/java/com/linkedin/metadata/entity/ebean/batch/PatchItemImpl.java
index ec0a8422e3c4a2..2543d99ac6af37 100644
--- a/metadata-io/metadata-io-api/src/main/java/com/linkedin/metadata/entity/ebean/batch/PatchItemImpl.java
+++ b/metadata-io/metadata-io-api/src/main/java/com/linkedin/metadata/entity/ebean/batch/PatchItemImpl.java
@@ -14,6 +14,7 @@
import com.linkedin.data.template.RecordTemplate;
import com.linkedin.events.metadata.ChangeType;
import com.linkedin.metadata.aspect.AspectRetriever;
+import com.linkedin.metadata.aspect.batch.BatchItem;
import com.linkedin.metadata.aspect.batch.MCPItem;
import com.linkedin.metadata.aspect.batch.PatchMCP;
import com.linkedin.metadata.aspect.patch.template.AspectTemplateEngine;
@@ -216,6 +217,11 @@ public static JsonPatch convertToJsonPatch(MetadataChangeProposal mcp) {
}
}
+ @Override
+ public boolean isDatabaseDuplicateOf(BatchItem other) {
+ return equals(other);
+ }
+
@Override
public boolean equals(Object o) {
if (this == o) {
@@ -228,12 +234,13 @@ public boolean equals(Object o) {
return urn.equals(that.urn)
&& aspectName.equals(that.aspectName)
&& Objects.equals(systemMetadata, that.systemMetadata)
+ && auditStamp.equals(that.auditStamp)
&& patch.equals(that.patch);
}
@Override
public int hashCode() {
- return Objects.hash(urn, aspectName, systemMetadata, patch);
+ return Objects.hash(urn, aspectName, systemMetadata, auditStamp, patch);
}
@Override
diff --git a/metadata-io/metadata-io-api/src/main/java/com/linkedin/metadata/entity/ebean/batch/ProposedItem.java b/metadata-io/metadata-io-api/src/main/java/com/linkedin/metadata/entity/ebean/batch/ProposedItem.java
index 88187ef159f233..370f1f6f073e65 100644
--- a/metadata-io/metadata-io-api/src/main/java/com/linkedin/metadata/entity/ebean/batch/ProposedItem.java
+++ b/metadata-io/metadata-io-api/src/main/java/com/linkedin/metadata/entity/ebean/batch/ProposedItem.java
@@ -4,6 +4,7 @@
import com.linkedin.common.urn.Urn;
import com.linkedin.data.template.RecordTemplate;
import com.linkedin.events.metadata.ChangeType;
+import com.linkedin.metadata.aspect.batch.BatchItem;
import com.linkedin.metadata.aspect.batch.MCPItem;
import com.linkedin.metadata.models.AspectSpec;
import com.linkedin.metadata.models.EntitySpec;
@@ -86,6 +87,32 @@ public ChangeType getChangeType() {
return metadataChangeProposal.getChangeType();
}
+ @Override
+ public boolean isDatabaseDuplicateOf(BatchItem other) {
+ return equals(other);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ ProposedItem that = (ProposedItem) o;
+ return metadataChangeProposal.equals(that.metadataChangeProposal)
+ && auditStamp.equals(that.auditStamp);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = metadataChangeProposal.hashCode();
+ result = 31 * result + auditStamp.hashCode();
+ return result;
+ }
+
public static class ProposedItemBuilder {
public ProposedItem build() {
// Ensure systemMetadata
diff --git a/metadata-io/metadata-io-api/src/test/java/com/linkedin/metadata/entity/ebean/batch/AspectsBatchImplTest.java b/metadata-io/metadata-io-api/src/test/java/com/linkedin/metadata/entity/ebean/batch/AspectsBatchImplTest.java
index 96f535f2295aa4..9f57d36f800de3 100644
--- a/metadata-io/metadata-io-api/src/test/java/com/linkedin/metadata/entity/ebean/batch/AspectsBatchImplTest.java
+++ b/metadata-io/metadata-io-api/src/test/java/com/linkedin/metadata/entity/ebean/batch/AspectsBatchImplTest.java
@@ -6,6 +6,7 @@
import static org.testng.Assert.assertEquals;
import com.google.common.collect.ImmutableList;
+import com.linkedin.common.AuditStamp;
import com.linkedin.common.FabricType;
import com.linkedin.common.Status;
import com.linkedin.common.urn.DataPlatformUrn;
@@ -220,6 +221,7 @@ public void toUpsertBatchItemsPatchItemTest() {
@Test
public void toUpsertBatchItemsProposedItemTest() {
+ AuditStamp auditStamp = AuditStampUtils.createDefaultAuditStamp();
List testItems =
List.of(
ProposedItem.builder()
@@ -239,7 +241,7 @@ public void toUpsertBatchItemsProposedItemTest() {
ByteString.copyString(
"{\"foo\":\"bar\"}", StandardCharsets.UTF_8)))
.setSystemMetadata(new SystemMetadata()))
- .auditStamp(AuditStampUtils.createDefaultAuditStamp())
+ .auditStamp(auditStamp)
.build(),
ProposedItem.builder()
.entitySpec(testRegistry.getEntitySpec(DATASET_ENTITY_NAME))
@@ -258,7 +260,7 @@ public void toUpsertBatchItemsProposedItemTest() {
ByteString.copyString(
"{\"foo\":\"bar\"}", StandardCharsets.UTF_8)))
.setSystemMetadata(new SystemMetadata()))
- .auditStamp(AuditStampUtils.createDefaultAuditStamp())
+ .auditStamp(auditStamp)
.build());
AspectsBatchImpl testBatch =
@@ -280,7 +282,7 @@ public void toUpsertBatchItemsProposedItemTest() {
testRegistry
.getEntitySpec(DATASET_ENTITY_NAME)
.getAspectSpec(STATUS_ASPECT_NAME))
- .auditStamp(AuditStampUtils.createDefaultAuditStamp())
+ .auditStamp(auditStamp)
.systemMetadata(testItems.get(0).getSystemMetadata().setVersion("1"))
.recordTemplate(new Status().setRemoved(false))
.build(mockAspectRetriever),
@@ -295,7 +297,7 @@ public void toUpsertBatchItemsProposedItemTest() {
testRegistry
.getEntitySpec(DATASET_ENTITY_NAME)
.getAspectSpec(STATUS_ASPECT_NAME))
- .auditStamp(AuditStampUtils.createDefaultAuditStamp())
+ .auditStamp(auditStamp)
.systemMetadata(testItems.get(1).getSystemMetadata().setVersion("1"))
.recordTemplate(new Status().setRemoved(false))
.build(mockAspectRetriever))),
diff --git a/metadata-io/src/main/java/com/linkedin/metadata/entity/EntityServiceImpl.java b/metadata-io/src/main/java/com/linkedin/metadata/entity/EntityServiceImpl.java
index bf3481205fb5ab..d14990f93d22d9 100644
--- a/metadata-io/src/main/java/com/linkedin/metadata/entity/EntityServiceImpl.java
+++ b/metadata-io/src/main/java/com/linkedin/metadata/entity/EntityServiceImpl.java
@@ -854,7 +854,7 @@ private List ingestAspectsToLocalDB(
boolean overwrite) {
if (inputBatch.containsDuplicateAspects()) {
- log.warn(String.format("Batch contains duplicates: %s", inputBatch));
+ log.warn("Batch contains duplicates: {}", inputBatch.duplicateAspects());
MetricUtils.counter(EntityServiceImpl.class, "batch_with_duplicate").inc();
}
@@ -968,39 +968,20 @@ private List ingestAspectsToLocalDB(
writeItem.getAspectSpec(),
databaseAspect);
- final UpdateAspectResult result;
/*
This condition is specifically for an older conditional write ingestAspectIfNotPresent()
overwrite is always true otherwise
*/
if (overwrite || databaseAspect == null) {
- result =
- Optional.ofNullable(
- ingestAspectToLocalDB(
- txContext, writeItem, databaseSystemAspect))
- .map(
- optResult ->
- optResult.toBuilder().request(writeItem).build())
- .orElse(null);
-
- } else {
- RecordTemplate oldValue = databaseSystemAspect.getRecordTemplate();
- SystemMetadata oldMetadata = databaseSystemAspect.getSystemMetadata();
- result =
- UpdateAspectResult.builder()
- .urn(writeItem.getUrn())
- .request(writeItem)
- .oldValue(oldValue)
- .newValue(oldValue)
- .oldSystemMetadata(oldMetadata)
- .newSystemMetadata(oldMetadata)
- .operation(MetadataAuditOperation.UPDATE)
- .auditStamp(writeItem.getAuditStamp())
- .maxVersion(databaseAspect.getVersion())
- .build();
+ return Optional.ofNullable(
+ ingestAspectToLocalDB(
+ txContext, writeItem, databaseSystemAspect))
+ .map(
+ optResult -> optResult.toBuilder().request(writeItem).build())
+ .orElse(null);
}
- return result;
+ return null;
})
.filter(Objects::nonNull)
.collect(Collectors.toList());
@@ -1051,7 +1032,8 @@ This condition is specifically for an older conditional write ingestAspectIfNotP
}
} else {
MetricUtils.counter(EntityServiceImpl.class, "batch_empty_transaction").inc();
- log.warn("Empty transaction detected. {}", inputBatch);
+ // This includes no-op batches. i.e. patch removing non-existent items
+ log.debug("Empty transaction detected");
}
return upsertResults;
@@ -1150,7 +1132,7 @@ public RecordTemplate ingestAspectIfNotPresent(
.build();
List ingested = ingestAspects(opContext, aspectsBatch, true, false);
- return ingested.stream().findFirst().get().getNewValue();
+ return ingested.stream().findFirst().map(UpdateAspectResult::getNewValue).orElse(null);
}
/**
@@ -2525,6 +2507,14 @@ private UpdateAspectResult ingestAspectToLocalDB(
@Nonnull final ChangeMCP writeItem,
@Nullable final EntityAspect.EntitySystemAspect databaseAspect) {
+ if (writeItem.getRecordTemplate() == null) {
+ log.error(
+ "Unexpected write of null aspect with name {}, urn {}",
+ writeItem.getAspectName(),
+ writeItem.getUrn());
+ return null;
+ }
+
// Set the "last run id" to be the run id provided with the new system metadata. This will be
// stored in index
// for all aspects that have a run id, regardless of whether they change.
@@ -2533,9 +2523,6 @@ private UpdateAspectResult ingestAspectToLocalDB(
.setLastRunId(writeItem.getSystemMetadata().getRunId(GetMode.NULL), SetMode.IGNORE_NULL);
// 2. Compare the latest existing and new.
- final RecordTemplate databaseValue =
- databaseAspect == null ? null : databaseAspect.getRecordTemplate();
-
final EntityAspect.EntitySystemAspect previousBatchAspect =
(EntityAspect.EntitySystemAspect) writeItem.getPreviousSystemAspect();
final RecordTemplate previousValue =
@@ -2544,45 +2531,94 @@ private UpdateAspectResult ingestAspectToLocalDB(
// 3. If there is no difference between existing and new, we just update
// the lastObserved in system metadata. RunId should stay as the original runId
if (previousValue != null
- && DataTemplateUtil.areEqual(databaseValue, writeItem.getRecordTemplate())) {
+ && DataTemplateUtil.areEqual(previousValue, writeItem.getRecordTemplate())) {
- SystemMetadata latestSystemMetadata = previousBatchAspect.getSystemMetadata();
- latestSystemMetadata.setLastObserved(writeItem.getSystemMetadata().getLastObserved());
- latestSystemMetadata.setLastRunId(
- writeItem.getSystemMetadata().getLastRunId(GetMode.NULL), SetMode.IGNORE_NULL);
-
- previousBatchAspect
- .getEntityAspect()
- .setSystemMetadata(RecordUtils.toJsonString(latestSystemMetadata));
-
- log.info(
- "Ingesting aspect with name {}, urn {}",
- previousBatchAspect.getAspectName(),
- previousBatchAspect.getUrn());
- aspectDao.saveAspect(txContext, previousBatchAspect.getEntityAspect(), false);
-
- // metrics
- aspectDao.incrementWriteMetrics(
- previousBatchAspect.getAspectName(),
- 1,
- previousBatchAspect.getMetadataRaw().getBytes(StandardCharsets.UTF_8).length);
+ Optional latestSystemMetadataDiff =
+ systemMetadataDiff(
+ txContext,
+ writeItem.getUrn(),
+ previousBatchAspect.getSystemMetadata(),
+ writeItem.getSystemMetadata(),
+ databaseAspect == null ? null : databaseAspect.getSystemMetadata());
+
+ if (latestSystemMetadataDiff.isPresent()) {
+ // Inserts & update order is not guaranteed, flush the insert for potential updates within
+ // same tx
+ if (databaseAspect == null && txContext != null) {
+ conditionalLogLevel(
+ txContext,
+ String.format(
+ "Flushing for systemMetadata update aspect with name %s, urn %s",
+ writeItem.getAspectName(), writeItem.getUrn()));
+ txContext.flush();
+ }
- return UpdateAspectResult.builder()
- .urn(writeItem.getUrn())
- .oldValue(previousValue)
- .newValue(previousValue)
- .oldSystemMetadata(previousBatchAspect.getSystemMetadata())
- .newSystemMetadata(latestSystemMetadata)
- .operation(MetadataAuditOperation.UPDATE)
- .auditStamp(writeItem.getAuditStamp())
- .maxVersion(0)
- .build();
+ conditionalLogLevel(
+ txContext,
+ String.format(
+ "Update aspect with name %s, urn %s, txContext: %s, databaseAspect: %s, newMetadata: %s newSystemMetadata: %s",
+ previousBatchAspect.getAspectName(),
+ previousBatchAspect.getUrn(),
+ txContext != null,
+ databaseAspect == null ? null : databaseAspect.getEntityAspect(),
+ previousBatchAspect.getEntityAspect().getMetadata(),
+ latestSystemMetadataDiff.get()));
+
+ aspectDao.saveAspect(
+ txContext,
+ previousBatchAspect.getUrnRaw(),
+ previousBatchAspect.getAspectName(),
+ previousBatchAspect.getMetadataRaw(),
+ previousBatchAspect.getCreatedBy(),
+ null,
+ previousBatchAspect.getCreatedOn(),
+ RecordUtils.toJsonString(latestSystemMetadataDiff.get()),
+ previousBatchAspect.getVersion(),
+ false);
+
+ // metrics
+ aspectDao.incrementWriteMetrics(
+ previousBatchAspect.getAspectName(),
+ 1,
+ previousBatchAspect.getMetadataRaw().getBytes(StandardCharsets.UTF_8).length);
+
+ return UpdateAspectResult.builder()
+ .urn(writeItem.getUrn())
+ .oldValue(previousValue)
+ .newValue(previousValue)
+ .oldSystemMetadata(previousBatchAspect.getSystemMetadata())
+ .newSystemMetadata(latestSystemMetadataDiff.get())
+ .operation(MetadataAuditOperation.UPDATE)
+ .auditStamp(writeItem.getAuditStamp())
+ .maxVersion(0)
+ .build();
+ } else {
+ MetricUtils.counter(EntityServiceImpl.class, "batch_with_noop_sysmetadata").inc();
+ return null;
+ }
}
// 4. Save the newValue as the latest version
- if (!DataTemplateUtil.areEqual(databaseValue, writeItem.getRecordTemplate())) {
- log.debug(
- "Ingesting aspect with name {}, urn {}", writeItem.getAspectName(), writeItem.getUrn());
+ if (writeItem.getRecordTemplate() != null
+ && !DataTemplateUtil.areEqual(previousValue, writeItem.getRecordTemplate())) {
+ conditionalLogLevel(
+ txContext,
+ String.format(
+ "Insert aspect with name %s, urn %s", writeItem.getAspectName(), writeItem.getUrn()));
+
+ // Inserts & update order is not guaranteed, flush the insert for potential updates within
+ // same tx
+ if (databaseAspect == null
+ && !ASPECT_LATEST_VERSION.equals(writeItem.getNextAspectVersion())
+ && txContext != null) {
+ conditionalLogLevel(
+ txContext,
+ String.format(
+ "Flushing for update aspect with name %s, urn %s",
+ writeItem.getAspectName(), writeItem.getUrn()));
+ txContext.flush();
+ }
+
String newValueStr = EntityApiUtils.toJsonAspect(writeItem.getRecordTemplate());
long versionOfOld =
aspectDao.saveLatestAspect(
@@ -2630,4 +2666,43 @@ private static boolean shouldAspectEmitChangeLog(@Nonnull final AspectSpec aspec
aspectSpec.getRelationshipFieldSpecs();
return relationshipFieldSpecs.stream().anyMatch(RelationshipFieldSpec::isLineageRelationship);
}
+
+ private static Optional systemMetadataDiff(
+ @Nullable TransactionContext txContext,
+ @Nonnull Urn urn,
+ @Nullable SystemMetadata previous,
+ @Nonnull SystemMetadata current,
+ @Nullable SystemMetadata database) {
+
+ SystemMetadata latestSystemMetadata = GenericRecordUtils.copy(previous, SystemMetadata.class);
+
+ latestSystemMetadata.setLastRunId(latestSystemMetadata.getRunId(), SetMode.REMOVE_IF_NULL);
+ latestSystemMetadata.setLastObserved(current.getLastObserved(), SetMode.IGNORE_NULL);
+ latestSystemMetadata.setRunId(current.getRunId(), SetMode.REMOVE_IF_NULL);
+
+ if (!DataTemplateUtil.areEqual(latestSystemMetadata, previous)
+ && !DataTemplateUtil.areEqual(latestSystemMetadata, database)) {
+
+ conditionalLogLevel(
+ txContext,
+ String.format(
+ "systemMetdataDiff urn %s, %s != %s AND %s",
+ urn,
+ RecordUtils.toJsonString(latestSystemMetadata),
+ previous == null ? null : RecordUtils.toJsonString(previous),
+ database == null ? null : RecordUtils.toJsonString(database)));
+
+ return Optional.of(latestSystemMetadata);
+ }
+
+ return Optional.empty();
+ }
+
+ private static void conditionalLogLevel(@Nullable TransactionContext txContext, String message) {
+ if (txContext != null && txContext.getFailedAttempts() > 1) {
+ log.warn(message);
+ } else {
+ log.debug(message);
+ }
+ }
}
diff --git a/metadata-io/src/main/java/com/linkedin/metadata/entity/TransactionContext.java b/metadata-io/src/main/java/com/linkedin/metadata/entity/TransactionContext.java
index 69f2f1c8981c03..6897c9152e9a25 100644
--- a/metadata-io/src/main/java/com/linkedin/metadata/entity/TransactionContext.java
+++ b/metadata-io/src/main/java/com/linkedin/metadata/entity/TransactionContext.java
@@ -66,4 +66,10 @@ public void commitAndContinue() {
}
success();
}
+
+ public void flush() {
+ if (tx != null) {
+ tx.flush();
+ }
+ }
}
diff --git a/metadata-io/src/main/java/com/linkedin/metadata/entity/cassandra/CassandraAspectDao.java b/metadata-io/src/main/java/com/linkedin/metadata/entity/cassandra/CassandraAspectDao.java
index 9e7387947a9547..a00482acda62e2 100644
--- a/metadata-io/src/main/java/com/linkedin/metadata/entity/cassandra/CassandraAspectDao.java
+++ b/metadata-io/src/main/java/com/linkedin/metadata/entity/cassandra/CassandraAspectDao.java
@@ -590,7 +590,7 @@ public long saveLatestAspect(
// Save oldValue as the largest version + 1
long largestVersion = ASPECT_LATEST_VERSION;
BatchStatement batch = BatchStatement.newInstance(BatchType.UNLOGGED);
- if (oldAspectMetadata != null && oldTime != null) {
+ if (!ASPECT_LATEST_VERSION.equals(nextVersion) && oldTime != null) {
largestVersion = nextVersion;
final EntityAspect aspect =
new EntityAspect(
@@ -616,7 +616,7 @@ public long saveLatestAspect(
newTime,
newActor,
newImpersonator);
- batch = batch.add(generateSaveStatement(aspect, oldAspectMetadata == null));
+ batch = batch.add(generateSaveStatement(aspect, ASPECT_LATEST_VERSION.equals(nextVersion)));
_cqlSession.execute(batch);
return largestVersion;
}
diff --git a/metadata-io/src/main/java/com/linkedin/metadata/entity/ebean/EbeanAspectDao.java b/metadata-io/src/main/java/com/linkedin/metadata/entity/ebean/EbeanAspectDao.java
index 6233bf5e0e35cf..729d0e61cb2c00 100644
--- a/metadata-io/src/main/java/com/linkedin/metadata/entity/ebean/EbeanAspectDao.java
+++ b/metadata-io/src/main/java/com/linkedin/metadata/entity/ebean/EbeanAspectDao.java
@@ -165,7 +165,7 @@ public long saveLatestAspect(
}
// Save oldValue as the largest version + 1
long largestVersion = ASPECT_LATEST_VERSION;
- if (oldAspectMetadata != null && oldTime != null) {
+ if (!ASPECT_LATEST_VERSION.equals(nextVersion) && oldTime != null) {
largestVersion = nextVersion;
saveAspect(
txContext,
@@ -191,7 +191,7 @@ public long saveLatestAspect(
newTime,
newSystemMetadata,
ASPECT_LATEST_VERSION,
- oldAspectMetadata == null);
+ ASPECT_LATEST_VERSION.equals(nextVersion));
return largestVersion;
}
diff --git a/metadata-io/src/test/java/com/linkedin/metadata/AspectGenerationUtils.java b/metadata-io/src/test/java/com/linkedin/metadata/AspectGenerationUtils.java
index 346a1eef845923..395c040f288111 100644
--- a/metadata-io/src/test/java/com/linkedin/metadata/AspectGenerationUtils.java
+++ b/metadata-io/src/test/java/com/linkedin/metadata/AspectGenerationUtils.java
@@ -34,19 +34,19 @@ public static SystemMetadata createSystemMetadata() {
}
@Nonnull
- public static SystemMetadata createSystemMetadata(long nextAspectVersion) {
+ public static SystemMetadata createSystemMetadata(int nextAspectVersion) {
return createSystemMetadata(
1625792689, "run-123", "run-123", String.valueOf(nextAspectVersion));
}
@Nonnull
- public static SystemMetadata createSystemMetadata(long lastObserved, @Nonnull String runId) {
+ public static SystemMetadata createSystemMetadata(int lastObserved, @Nonnull String runId) {
return createSystemMetadata(lastObserved, runId, runId, null);
}
@Nonnull
public static SystemMetadata createSystemMetadata(
- long lastObserved,
+ int lastObserved, // for test comparison must be int
@Nonnull String runId,
@Nonnull String lastRunId,
@Nullable String version) {
diff --git a/metadata-io/src/test/java/com/linkedin/metadata/entity/EbeanEntityServiceTest.java b/metadata-io/src/test/java/com/linkedin/metadata/entity/EbeanEntityServiceTest.java
index a1000fd02abfe1..aa42545fa0e46f 100644
--- a/metadata-io/src/test/java/com/linkedin/metadata/entity/EbeanEntityServiceTest.java
+++ b/metadata-io/src/test/java/com/linkedin/metadata/entity/EbeanEntityServiceTest.java
@@ -1,10 +1,6 @@
package com.linkedin.metadata.entity;
-import static com.linkedin.metadata.Constants.APP_SOURCE;
import static com.linkedin.metadata.Constants.CORP_USER_ENTITY_NAME;
-import static com.linkedin.metadata.Constants.DATASET_ENTITY_NAME;
-import static com.linkedin.metadata.Constants.GLOBAL_TAGS_ASPECT_NAME;
-import static com.linkedin.metadata.Constants.METADATA_TESTS_SOURCE;
import static com.linkedin.metadata.Constants.STATUS_ASPECT_NAME;
import static org.mockito.Mockito.mock;
import static org.testng.Assert.assertEquals;
@@ -12,36 +8,27 @@
import static org.testng.Assert.assertTrue;
import com.linkedin.common.AuditStamp;
-import com.linkedin.common.GlobalTags;
import com.linkedin.common.Status;
-import com.linkedin.common.TagAssociation;
-import com.linkedin.common.TagAssociationArray;
-import com.linkedin.common.urn.TagUrn;
import com.linkedin.common.urn.Urn;
import com.linkedin.common.urn.UrnUtils;
import com.linkedin.data.template.DataTemplateUtil;
import com.linkedin.data.template.RecordTemplate;
-import com.linkedin.data.template.StringMap;
import com.linkedin.entity.EnvelopedAspect;
import com.linkedin.identity.CorpUserInfo;
import com.linkedin.metadata.AspectGenerationUtils;
import com.linkedin.metadata.Constants;
import com.linkedin.metadata.EbeanTestUtils;
-import com.linkedin.metadata.aspect.patch.GenericJsonPatch;
-import com.linkedin.metadata.aspect.patch.PatchOperationType;
import com.linkedin.metadata.config.EbeanConfiguration;
import com.linkedin.metadata.config.PreProcessHooks;
import com.linkedin.metadata.entity.ebean.EbeanAspectDao;
import com.linkedin.metadata.entity.ebean.EbeanRetentionService;
import com.linkedin.metadata.entity.ebean.batch.AspectsBatchImpl;
import com.linkedin.metadata.entity.ebean.batch.ChangeItemImpl;
-import com.linkedin.metadata.entity.ebean.batch.PatchItemImpl;
import com.linkedin.metadata.event.EventProducer;
import com.linkedin.metadata.key.CorpUserKey;
import com.linkedin.metadata.models.registry.EntityRegistryException;
import com.linkedin.metadata.query.ListUrnsResult;
import com.linkedin.metadata.service.UpdateIndicesService;
-import com.linkedin.metadata.utils.AuditStampUtils;
import com.linkedin.metadata.utils.PegasusUtils;
import com.linkedin.mxe.MetadataChangeProposal;
import com.linkedin.mxe.SystemMetadata;
@@ -64,7 +51,6 @@
import java.util.concurrent.LinkedBlockingQueue;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
-import java.util.stream.Stream;
import org.apache.commons.lang3.tuple.Triple;
import org.testng.Assert;
import org.testng.annotations.BeforeMethod;
@@ -396,360 +382,6 @@ public void testSystemMetadataDuplicateKey() throws Exception {
"Expected version 0 with systemMeta version 3 accounting for the the collision");
}
- @Test
- public void testBatchDuplicate() throws Exception {
- Urn entityUrn = UrnUtils.getUrn("urn:li:corpuser:batchDuplicateTest");
- SystemMetadata systemMetadata = AspectGenerationUtils.createSystemMetadata();
- ChangeItemImpl item1 =
- ChangeItemImpl.builder()
- .urn(entityUrn)
- .aspectName(STATUS_ASPECT_NAME)
- .recordTemplate(new Status().setRemoved(true))
- .systemMetadata(systemMetadata.copy())
- .auditStamp(TEST_AUDIT_STAMP)
- .build(TestOperationContexts.emptyAspectRetriever(null));
- ChangeItemImpl item2 =
- ChangeItemImpl.builder()
- .urn(entityUrn)
- .aspectName(STATUS_ASPECT_NAME)
- .recordTemplate(new Status().setRemoved(false))
- .systemMetadata(systemMetadata.copy())
- .auditStamp(TEST_AUDIT_STAMP)
- .build(TestOperationContexts.emptyAspectRetriever(null));
- _entityServiceImpl.ingestAspects(
- opContext,
- AspectsBatchImpl.builder()
- .retrieverContext(opContext.getRetrieverContext().get())
- .items(List.of(item1, item2))
- .build(),
- false,
- true);
-
- // List aspects urns
- ListUrnsResult batch = _entityServiceImpl.listUrns(opContext, entityUrn.getEntityType(), 0, 2);
-
- assertEquals(batch.getStart().intValue(), 0);
- assertEquals(batch.getCount().intValue(), 1);
- assertEquals(batch.getTotal().intValue(), 1);
- assertEquals(batch.getEntities().size(), 1);
- assertEquals(entityUrn.toString(), batch.getEntities().get(0).toString());
-
- EnvelopedAspect envelopedAspect =
- _entityServiceImpl.getLatestEnvelopedAspect(
- opContext, CORP_USER_ENTITY_NAME, entityUrn, STATUS_ASPECT_NAME);
- assertEquals(
- envelopedAspect.getSystemMetadata().getVersion(),
- "2",
- "Expected version 2 accounting for duplicates");
- assertEquals(
- envelopedAspect.getValue().toString(),
- "{removed=false}",
- "Expected 2nd item to be the latest");
- }
-
- @Test
- public void testBatchPatchWithTrailingNoOp() throws Exception {
- Urn entityUrn =
- UrnUtils.getUrn(
- "urn:li:dataset:(urn:li:dataPlatform:snowflake,testBatchPatchWithTrailingNoOp,PROD)");
- TagUrn tag1 = TagUrn.createFromString("urn:li:tag:tag1");
- Urn tag2 = UrnUtils.getUrn("urn:li:tag:tag2");
- Urn tagOther = UrnUtils.getUrn("urn:li:tag:other");
-
- SystemMetadata systemMetadata = AspectGenerationUtils.createSystemMetadata();
-
- ChangeItemImpl initialAspectTag1 =
- ChangeItemImpl.builder()
- .urn(entityUrn)
- .aspectName(GLOBAL_TAGS_ASPECT_NAME)
- .recordTemplate(
- new GlobalTags()
- .setTags(new TagAssociationArray(new TagAssociation().setTag(tag1))))
- .systemMetadata(systemMetadata.copy())
- .auditStamp(TEST_AUDIT_STAMP)
- .build(TestOperationContexts.emptyAspectRetriever(null));
-
- PatchItemImpl patchAdd2 =
- PatchItemImpl.builder()
- .urn(entityUrn)
- .entitySpec(_testEntityRegistry.getEntitySpec(DATASET_ENTITY_NAME))
- .aspectName(GLOBAL_TAGS_ASPECT_NAME)
- .aspectSpec(
- _testEntityRegistry
- .getEntitySpec(DATASET_ENTITY_NAME)
- .getAspectSpec(GLOBAL_TAGS_ASPECT_NAME))
- .patch(
- GenericJsonPatch.builder()
- .arrayPrimaryKeys(Map.of("properties", List.of("tag")))
- .patch(List.of(tagPatchOp(PatchOperationType.ADD, tag2)))
- .build()
- .getJsonPatch())
- .auditStamp(AuditStampUtils.createDefaultAuditStamp())
- .build(_testEntityRegistry);
-
- PatchItemImpl patchRemoveNonExistent =
- PatchItemImpl.builder()
- .urn(entityUrn)
- .entitySpec(_testEntityRegistry.getEntitySpec(DATASET_ENTITY_NAME))
- .aspectName(GLOBAL_TAGS_ASPECT_NAME)
- .aspectSpec(
- _testEntityRegistry
- .getEntitySpec(DATASET_ENTITY_NAME)
- .getAspectSpec(GLOBAL_TAGS_ASPECT_NAME))
- .patch(
- GenericJsonPatch.builder()
- .arrayPrimaryKeys(Map.of("properties", List.of("tag")))
- .patch(List.of(tagPatchOp(PatchOperationType.REMOVE, tagOther)))
- .build()
- .getJsonPatch())
- .auditStamp(AuditStampUtils.createDefaultAuditStamp())
- .build(_testEntityRegistry);
-
- // establish base entity
- _entityServiceImpl.ingestAspects(
- opContext,
- AspectsBatchImpl.builder()
- .retrieverContext(opContext.getRetrieverContext().get())
- .items(List.of(initialAspectTag1))
- .build(),
- false,
- true);
-
- _entityServiceImpl.ingestAspects(
- opContext,
- AspectsBatchImpl.builder()
- .retrieverContext(opContext.getRetrieverContext().get())
- .items(List.of(patchAdd2, patchRemoveNonExistent))
- .build(),
- false,
- true);
-
- // List aspects urns
- ListUrnsResult batch = _entityServiceImpl.listUrns(opContext, entityUrn.getEntityType(), 0, 1);
-
- assertEquals(batch.getStart().intValue(), 0);
- assertEquals(batch.getCount().intValue(), 1);
- assertEquals(batch.getTotal().intValue(), 1);
- assertEquals(batch.getEntities().size(), 1);
- assertEquals(entityUrn.toString(), batch.getEntities().get(0).toString());
-
- EnvelopedAspect envelopedAspect =
- _entityServiceImpl.getLatestEnvelopedAspect(
- opContext, DATASET_ENTITY_NAME, entityUrn, GLOBAL_TAGS_ASPECT_NAME);
- assertEquals(
- envelopedAspect.getSystemMetadata().getVersion(),
- "3",
- "Expected version 3. 1 - Initial, + 1 add, 1 remove");
- assertEquals(
- new GlobalTags(envelopedAspect.getValue().data())
- .getTags().stream().map(TagAssociation::getTag).collect(Collectors.toSet()),
- Set.of(tag1, tag2),
- "Expected both tags");
- }
-
- @Test
- public void testBatchPatchAdd() throws Exception {
- Urn entityUrn =
- UrnUtils.getUrn("urn:li:dataset:(urn:li:dataPlatform:snowflake,testBatchPatchAdd,PROD)");
- TagUrn tag1 = TagUrn.createFromString("urn:li:tag:tag1");
- TagUrn tag2 = TagUrn.createFromString("urn:li:tag:tag2");
- TagUrn tag3 = TagUrn.createFromString("urn:li:tag:tag3");
-
- SystemMetadata systemMetadata = AspectGenerationUtils.createSystemMetadata();
-
- ChangeItemImpl initialAspectTag1 =
- ChangeItemImpl.builder()
- .urn(entityUrn)
- .aspectName(GLOBAL_TAGS_ASPECT_NAME)
- .recordTemplate(
- new GlobalTags()
- .setTags(new TagAssociationArray(new TagAssociation().setTag(tag1))))
- .systemMetadata(systemMetadata.copy())
- .auditStamp(TEST_AUDIT_STAMP)
- .build(TestOperationContexts.emptyAspectRetriever(null));
-
- PatchItemImpl patchAdd3 =
- PatchItemImpl.builder()
- .urn(entityUrn)
- .entitySpec(_testEntityRegistry.getEntitySpec(DATASET_ENTITY_NAME))
- .aspectName(GLOBAL_TAGS_ASPECT_NAME)
- .aspectSpec(
- _testEntityRegistry
- .getEntitySpec(DATASET_ENTITY_NAME)
- .getAspectSpec(GLOBAL_TAGS_ASPECT_NAME))
- .patch(
- GenericJsonPatch.builder()
- .arrayPrimaryKeys(Map.of("properties", List.of("tag")))
- .patch(List.of(tagPatchOp(PatchOperationType.ADD, tag3)))
- .build()
- .getJsonPatch())
- .auditStamp(AuditStampUtils.createDefaultAuditStamp())
- .build(_testEntityRegistry);
-
- PatchItemImpl patchAdd2 =
- PatchItemImpl.builder()
- .urn(entityUrn)
- .entitySpec(_testEntityRegistry.getEntitySpec(DATASET_ENTITY_NAME))
- .aspectName(GLOBAL_TAGS_ASPECT_NAME)
- .aspectSpec(
- _testEntityRegistry
- .getEntitySpec(DATASET_ENTITY_NAME)
- .getAspectSpec(GLOBAL_TAGS_ASPECT_NAME))
- .patch(
- GenericJsonPatch.builder()
- .arrayPrimaryKeys(Map.of("properties", List.of("tag")))
- .patch(List.of(tagPatchOp(PatchOperationType.ADD, tag2)))
- .build()
- .getJsonPatch())
- .auditStamp(AuditStampUtils.createDefaultAuditStamp())
- .build(_testEntityRegistry);
-
- PatchItemImpl patchAdd1 =
- PatchItemImpl.builder()
- .urn(entityUrn)
- .entitySpec(_testEntityRegistry.getEntitySpec(DATASET_ENTITY_NAME))
- .aspectName(GLOBAL_TAGS_ASPECT_NAME)
- .aspectSpec(
- _testEntityRegistry
- .getEntitySpec(DATASET_ENTITY_NAME)
- .getAspectSpec(GLOBAL_TAGS_ASPECT_NAME))
- .patch(
- GenericJsonPatch.builder()
- .arrayPrimaryKeys(Map.of("properties", List.of("tag")))
- .patch(List.of(tagPatchOp(PatchOperationType.ADD, tag1)))
- .build()
- .getJsonPatch())
- .auditStamp(AuditStampUtils.createDefaultAuditStamp())
- .build(_testEntityRegistry);
-
- // establish base entity
- _entityServiceImpl.ingestAspects(
- opContext,
- AspectsBatchImpl.builder()
- .retrieverContext(opContext.getRetrieverContext().get())
- .items(List.of(initialAspectTag1))
- .build(),
- false,
- true);
-
- _entityServiceImpl.ingestAspects(
- opContext,
- AspectsBatchImpl.builder()
- .retrieverContext(opContext.getRetrieverContext().get())
- .items(List.of(patchAdd3, patchAdd2, patchAdd1))
- .build(),
- false,
- true);
-
- // List aspects urns
- ListUrnsResult batch = _entityServiceImpl.listUrns(opContext, entityUrn.getEntityType(), 0, 1);
-
- assertEquals(batch.getStart().intValue(), 0);
- assertEquals(batch.getCount().intValue(), 1);
- assertEquals(batch.getTotal().intValue(), 1);
- assertEquals(batch.getEntities().size(), 1);
- assertEquals(entityUrn.toString(), batch.getEntities().get(0).toString());
-
- EnvelopedAspect envelopedAspect =
- _entityServiceImpl.getLatestEnvelopedAspect(
- opContext, DATASET_ENTITY_NAME, entityUrn, GLOBAL_TAGS_ASPECT_NAME);
- assertEquals(envelopedAspect.getSystemMetadata().getVersion(), "4", "Expected version 4");
- assertEquals(
- new GlobalTags(envelopedAspect.getValue().data())
- .getTags().stream().map(TagAssociation::getTag).collect(Collectors.toSet()),
- Set.of(tag1, tag2, tag3),
- "Expected all tags");
- }
-
- @Test
- public void testBatchPatchAddDuplicate() throws Exception {
- Urn entityUrn =
- UrnUtils.getUrn("urn:li:dataset:(urn:li:dataPlatform:snowflake,testBatchPatchAdd,PROD)");
- List initialTags =
- List.of(
- TagUrn.createFromString("urn:li:tag:__default_large_table"),
- TagUrn.createFromString("urn:li:tag:__default_low_queries"),
- TagUrn.createFromString("urn:li:tag:__default_low_changes"),
- TagUrn.createFromString("urn:li:tag:!10TB+ tables"))
- .stream()
- .map(tag -> new TagAssociation().setTag(tag))
- .collect(Collectors.toList());
- TagUrn tag2 = TagUrn.createFromString("urn:li:tag:$ 1TB+");
-
- SystemMetadata systemMetadata = AspectGenerationUtils.createSystemMetadata();
-
- SystemMetadata patchSystemMetadata = new SystemMetadata();
- patchSystemMetadata.setLastObserved(systemMetadata.getLastObserved() + 1);
- patchSystemMetadata.setProperties(new StringMap(Map.of(APP_SOURCE, METADATA_TESTS_SOURCE)));
-
- ChangeItemImpl initialAspectTag1 =
- ChangeItemImpl.builder()
- .urn(entityUrn)
- .aspectName(GLOBAL_TAGS_ASPECT_NAME)
- .recordTemplate(new GlobalTags().setTags(new TagAssociationArray(initialTags)))
- .systemMetadata(systemMetadata.copy())
- .auditStamp(TEST_AUDIT_STAMP)
- .build(TestOperationContexts.emptyAspectRetriever(null));
-
- PatchItemImpl patchAdd2 =
- PatchItemImpl.builder()
- .urn(entityUrn)
- .entitySpec(_testEntityRegistry.getEntitySpec(DATASET_ENTITY_NAME))
- .aspectName(GLOBAL_TAGS_ASPECT_NAME)
- .aspectSpec(
- _testEntityRegistry
- .getEntitySpec(DATASET_ENTITY_NAME)
- .getAspectSpec(GLOBAL_TAGS_ASPECT_NAME))
- .patch(
- GenericJsonPatch.builder()
- .arrayPrimaryKeys(Map.of("properties", List.of("tag")))
- .patch(List.of(tagPatchOp(PatchOperationType.ADD, tag2)))
- .build()
- .getJsonPatch())
- .systemMetadata(patchSystemMetadata)
- .auditStamp(AuditStampUtils.createDefaultAuditStamp())
- .build(_testEntityRegistry);
-
- // establish base entity
- _entityServiceImpl.ingestAspects(
- opContext,
- AspectsBatchImpl.builder()
- .retrieverContext(opContext.getRetrieverContext().get())
- .items(List.of(initialAspectTag1))
- .build(),
- false,
- true);
-
- _entityServiceImpl.ingestAspects(
- opContext,
- AspectsBatchImpl.builder()
- .retrieverContext(opContext.getRetrieverContext().get())
- .items(List.of(patchAdd2, patchAdd2)) // duplicate
- .build(),
- false,
- true);
-
- // List aspects urns
- ListUrnsResult batch = _entityServiceImpl.listUrns(opContext, entityUrn.getEntityType(), 0, 1);
-
- assertEquals(batch.getStart().intValue(), 0);
- assertEquals(batch.getCount().intValue(), 1);
- assertEquals(batch.getTotal().intValue(), 1);
- assertEquals(batch.getEntities().size(), 1);
- assertEquals(entityUrn.toString(), batch.getEntities().get(0).toString());
-
- EnvelopedAspect envelopedAspect =
- _entityServiceImpl.getLatestEnvelopedAspect(
- opContext, DATASET_ENTITY_NAME, entityUrn, GLOBAL_TAGS_ASPECT_NAME);
- assertEquals(envelopedAspect.getSystemMetadata().getVersion(), "3", "Expected version 3");
- assertEquals(
- new GlobalTags(envelopedAspect.getValue().data())
- .getTags().stream().map(TagAssociation::getTag).collect(Collectors.toSet()),
- Stream.concat(initialTags.stream().map(TagAssociation::getTag), Stream.of(tag2))
- .collect(Collectors.toSet()),
- "Expected all tags");
- }
-
@Test
public void dataGeneratorThreadingTest() {
DataGenerator dataGenerator = new DataGenerator(opContext, _entityServiceImpl);
@@ -976,14 +608,4 @@ public void run() {
}
}
}
-
- private static GenericJsonPatch.PatchOp tagPatchOp(PatchOperationType op, Urn tagUrn) {
- GenericJsonPatch.PatchOp patchOp = new GenericJsonPatch.PatchOp();
- patchOp.setOp(op.getValue());
- patchOp.setPath(String.format("/tags/%s", tagUrn));
- if (PatchOperationType.ADD.equals(op)) {
- patchOp.setValue(Map.of("tag", tagUrn.toString()));
- }
- return patchOp;
- }
}
diff --git a/metadata-io/src/test/java/com/linkedin/metadata/entity/EntityServiceTest.java b/metadata-io/src/test/java/com/linkedin/metadata/entity/EntityServiceTest.java
index 654c448fdec946..4c42815a80f3f1 100644
--- a/metadata-io/src/test/java/com/linkedin/metadata/entity/EntityServiceTest.java
+++ b/metadata-io/src/test/java/com/linkedin/metadata/entity/EntityServiceTest.java
@@ -11,14 +11,18 @@
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.linkedin.common.AuditStamp;
+import com.linkedin.common.GlobalTags;
import com.linkedin.common.Owner;
import com.linkedin.common.OwnerArray;
import com.linkedin.common.Ownership;
import com.linkedin.common.OwnershipType;
import com.linkedin.common.Status;
+import com.linkedin.common.TagAssociation;
+import com.linkedin.common.TagAssociationArray;
import com.linkedin.common.UrnArray;
import com.linkedin.common.VersionedUrn;
import com.linkedin.common.urn.CorpuserUrn;
+import com.linkedin.common.urn.TagUrn;
import com.linkedin.common.urn.TupleKey;
import com.linkedin.common.urn.Urn;
import com.linkedin.common.urn.UrnUtils;
@@ -42,8 +46,11 @@
import com.linkedin.metadata.aspect.CorpUserAspect;
import com.linkedin.metadata.aspect.CorpUserAspectArray;
import com.linkedin.metadata.aspect.VersionedAspect;
+import com.linkedin.metadata.aspect.patch.GenericJsonPatch;
+import com.linkedin.metadata.aspect.patch.PatchOperationType;
import com.linkedin.metadata.entity.ebean.batch.AspectsBatchImpl;
import com.linkedin.metadata.entity.ebean.batch.ChangeItemImpl;
+import com.linkedin.metadata.entity.ebean.batch.PatchItemImpl;
import com.linkedin.metadata.entity.restoreindices.RestoreIndicesArgs;
import com.linkedin.metadata.entity.validation.ValidationApiUtils;
import com.linkedin.metadata.entity.validation.ValidationException;
@@ -52,10 +59,12 @@
import com.linkedin.metadata.models.AspectSpec;
import com.linkedin.metadata.models.registry.EntityRegistry;
import com.linkedin.metadata.models.registry.EntityRegistryException;
+import com.linkedin.metadata.query.ListUrnsResult;
import com.linkedin.metadata.run.AspectRowSummary;
import com.linkedin.metadata.service.UpdateIndicesService;
import com.linkedin.metadata.snapshot.CorpUserSnapshot;
import com.linkedin.metadata.snapshot.Snapshot;
+import com.linkedin.metadata.utils.AuditStampUtils;
import com.linkedin.metadata.utils.EntityKeyUtils;
import com.linkedin.metadata.utils.GenericRecordUtils;
import com.linkedin.mxe.GenericAspect;
@@ -605,6 +614,9 @@ public void testReingestLineageAspect() throws Exception {
entityUrn,
_testEntityRegistry.getEntitySpec(entityUrn.getEntityType()).getKeyAspectSpec())));
+ SystemMetadata futureSystemMetadata = AspectGenerationUtils.createSystemMetadata(1);
+ futureSystemMetadata.setLastObserved(futureSystemMetadata.getLastObserved() + 1);
+
final MetadataChangeLog restateChangeLog = new MetadataChangeLog();
restateChangeLog.setEntityType(entityUrn.getEntityType());
restateChangeLog.setEntityUrn(entityUrn);
@@ -612,7 +624,7 @@ public void testReingestLineageAspect() throws Exception {
restateChangeLog.setAspectName(aspectName1);
restateChangeLog.setCreated(TEST_AUDIT_STAMP);
restateChangeLog.setAspect(aspect);
- restateChangeLog.setSystemMetadata(AspectGenerationUtils.createSystemMetadata(1));
+ restateChangeLog.setSystemMetadata(futureSystemMetadata);
restateChangeLog.setPreviousAspectValue(aspect);
restateChangeLog.setPreviousSystemMetadata(
simulatePullFromDB(initialSystemMetadata, SystemMetadata.class));
@@ -636,11 +648,7 @@ public void testReingestLineageAspect() throws Exception {
clearInvocations(_mockProducer);
_entityServiceImpl.ingestAspects(
- opContext,
- entityUrn,
- pairToIngest,
- TEST_AUDIT_STAMP,
- AspectGenerationUtils.createSystemMetadata());
+ opContext, entityUrn, pairToIngest, TEST_AUDIT_STAMP, futureSystemMetadata);
verify(_mockProducer, times(1))
.produceMetadataChangeLog(
@@ -682,6 +690,12 @@ public void testReingestLineageProposal() throws Exception {
initialChangeLog.setAspect(genericAspect);
initialChangeLog.setSystemMetadata(metadata1);
+ SystemMetadata futureSystemMetadata = AspectGenerationUtils.createSystemMetadata(1);
+ futureSystemMetadata.setLastObserved(futureSystemMetadata.getLastObserved() + 1);
+
+ MetadataChangeProposal mcp2 = new MetadataChangeProposal(mcp1.data().copy());
+ mcp2.getSystemMetadata().setLastObserved(futureSystemMetadata.getLastObserved());
+
final MetadataChangeLog restateChangeLog = new MetadataChangeLog();
restateChangeLog.setEntityType(entityUrn.getEntityType());
restateChangeLog.setEntityUrn(entityUrn);
@@ -689,7 +703,7 @@ public void testReingestLineageProposal() throws Exception {
restateChangeLog.setAspectName(aspectName1);
restateChangeLog.setCreated(TEST_AUDIT_STAMP);
restateChangeLog.setAspect(genericAspect);
- restateChangeLog.setSystemMetadata(AspectGenerationUtils.createSystemMetadata(1));
+ restateChangeLog.setSystemMetadata(futureSystemMetadata);
restateChangeLog.setPreviousAspectValue(genericAspect);
restateChangeLog.setPreviousSystemMetadata(simulatePullFromDB(metadata1, SystemMetadata.class));
@@ -706,7 +720,7 @@ public void testReingestLineageProposal() throws Exception {
// unless invocations are cleared
clearInvocations(_mockProducer);
- _entityServiceImpl.ingestProposal(opContext, mcp1, TEST_AUDIT_STAMP, false);
+ _entityServiceImpl.ingestProposal(opContext, mcp2, TEST_AUDIT_STAMP, false);
verify(_mockProducer, times(1))
.produceMetadataChangeLog(
@@ -1390,7 +1404,7 @@ public void testIngestSameAspect() throws AssertionError {
SystemMetadata metadata1 = AspectGenerationUtils.createSystemMetadata(1625792689, "run-123");
SystemMetadata metadata2 = AspectGenerationUtils.createSystemMetadata(1635792689, "run-456");
SystemMetadata metadata3 =
- AspectGenerationUtils.createSystemMetadata(1635792689, "run-123", "run-456", "1");
+ AspectGenerationUtils.createSystemMetadata(1635792689, "run-456", "run-123", "1");
List items =
List.of(
@@ -1482,6 +1496,9 @@ public void testIngestSameAspect() throws AssertionError {
assertTrue(
DataTemplateUtil.areEqual(
+ EntityApiUtils.parseSystemMetadata(readAspectDao2.getSystemMetadata()), metadata3),
+ String.format(
+ "Expected %s == %s",
EntityApiUtils.parseSystemMetadata(readAspectDao2.getSystemMetadata()), metadata3));
verify(_mockProducer, times(0))
@@ -2179,6 +2196,474 @@ public void testExists() throws Exception {
Set.of(existentUrn, noStatusUrn, softDeletedUrn));
}
+ @Test
+ public void testBatchDuplicate() throws Exception {
+ Urn entityUrn = UrnUtils.getUrn("urn:li:corpuser:batchDuplicateTest");
+ SystemMetadata systemMetadata = AspectGenerationUtils.createSystemMetadata();
+ ChangeItemImpl item1 =
+ ChangeItemImpl.builder()
+ .urn(entityUrn)
+ .aspectName(STATUS_ASPECT_NAME)
+ .recordTemplate(new Status().setRemoved(true))
+ .systemMetadata(systemMetadata.copy())
+ .auditStamp(TEST_AUDIT_STAMP)
+ .build(TestOperationContexts.emptyAspectRetriever(null));
+ ChangeItemImpl item2 =
+ ChangeItemImpl.builder()
+ .urn(entityUrn)
+ .aspectName(STATUS_ASPECT_NAME)
+ .recordTemplate(new Status().setRemoved(false))
+ .systemMetadata(systemMetadata.copy())
+ .auditStamp(TEST_AUDIT_STAMP)
+ .build(TestOperationContexts.emptyAspectRetriever(null));
+ _entityServiceImpl.ingestAspects(
+ opContext,
+ AspectsBatchImpl.builder()
+ .retrieverContext(opContext.getRetrieverContext().get())
+ .items(List.of(item1, item2))
+ .build(),
+ false,
+ true);
+
+ // List aspects urns
+ ListUrnsResult batch = _entityServiceImpl.listUrns(opContext, entityUrn.getEntityType(), 0, 2);
+
+ assertEquals(batch.getStart().intValue(), 0);
+ assertEquals(batch.getCount().intValue(), 1);
+ assertEquals(batch.getTotal().intValue(), 1);
+ assertEquals(batch.getEntities().size(), 1);
+ assertEquals(entityUrn.toString(), batch.getEntities().get(0).toString());
+
+ EnvelopedAspect envelopedAspect =
+ _entityServiceImpl.getLatestEnvelopedAspect(
+ opContext, CORP_USER_ENTITY_NAME, entityUrn, STATUS_ASPECT_NAME);
+ assertEquals(
+ envelopedAspect.getSystemMetadata().getVersion(),
+ "2",
+ "Expected version 2 after accounting for sequential duplicates");
+ assertEquals(
+ envelopedAspect.getValue().toString(),
+ "{removed=false}",
+ "Expected 2nd item to be the latest");
+ }
+
+ @Test
+ public void testBatchPatchWithTrailingNoOp() throws Exception {
+ Urn entityUrn =
+ UrnUtils.getUrn(
+ "urn:li:dataset:(urn:li:dataPlatform:snowflake,testBatchPatchWithTrailingNoOp,PROD)");
+ TagUrn tag1 = TagUrn.createFromString("urn:li:tag:tag1");
+ Urn tag2 = UrnUtils.getUrn("urn:li:tag:tag2");
+ Urn tagOther = UrnUtils.getUrn("urn:li:tag:other");
+
+ SystemMetadata systemMetadata = AspectGenerationUtils.createSystemMetadata();
+
+ ChangeItemImpl initialAspectTag1 =
+ ChangeItemImpl.builder()
+ .urn(entityUrn)
+ .aspectName(GLOBAL_TAGS_ASPECT_NAME)
+ .recordTemplate(
+ new GlobalTags()
+ .setTags(new TagAssociationArray(new TagAssociation().setTag(tag1))))
+ .systemMetadata(systemMetadata.copy())
+ .auditStamp(TEST_AUDIT_STAMP)
+ .build(TestOperationContexts.emptyAspectRetriever(null));
+
+ PatchItemImpl patchAdd2 =
+ PatchItemImpl.builder()
+ .urn(entityUrn)
+ .entitySpec(_testEntityRegistry.getEntitySpec(DATASET_ENTITY_NAME))
+ .aspectName(GLOBAL_TAGS_ASPECT_NAME)
+ .aspectSpec(
+ _testEntityRegistry
+ .getEntitySpec(DATASET_ENTITY_NAME)
+ .getAspectSpec(GLOBAL_TAGS_ASPECT_NAME))
+ .patch(
+ GenericJsonPatch.builder()
+ .arrayPrimaryKeys(Map.of("properties", List.of("tag")))
+ .patch(List.of(tagPatchOp(PatchOperationType.ADD, tag2)))
+ .build()
+ .getJsonPatch())
+ .auditStamp(AuditStampUtils.createDefaultAuditStamp())
+ .build(_testEntityRegistry);
+
+ PatchItemImpl patchRemoveNonExistent =
+ PatchItemImpl.builder()
+ .urn(entityUrn)
+ .entitySpec(_testEntityRegistry.getEntitySpec(DATASET_ENTITY_NAME))
+ .aspectName(GLOBAL_TAGS_ASPECT_NAME)
+ .aspectSpec(
+ _testEntityRegistry
+ .getEntitySpec(DATASET_ENTITY_NAME)
+ .getAspectSpec(GLOBAL_TAGS_ASPECT_NAME))
+ .patch(
+ GenericJsonPatch.builder()
+ .arrayPrimaryKeys(Map.of("properties", List.of("tag")))
+ .patch(List.of(tagPatchOp(PatchOperationType.REMOVE, tagOther)))
+ .build()
+ .getJsonPatch())
+ .auditStamp(AuditStampUtils.createDefaultAuditStamp())
+ .build(_testEntityRegistry);
+
+ // establish base entity
+ _entityServiceImpl.ingestAspects(
+ opContext,
+ AspectsBatchImpl.builder()
+ .retrieverContext(opContext.getRetrieverContext().get())
+ .items(List.of(initialAspectTag1))
+ .build(),
+ false,
+ true);
+
+ _entityServiceImpl.ingestAspects(
+ opContext,
+ AspectsBatchImpl.builder()
+ .retrieverContext(opContext.getRetrieverContext().get())
+ .items(List.of(patchAdd2, patchRemoveNonExistent))
+ .build(),
+ false,
+ true);
+
+ // List aspects urns
+ ListUrnsResult batch = _entityServiceImpl.listUrns(opContext, entityUrn.getEntityType(), 0, 1);
+
+ assertEquals(batch.getStart().intValue(), 0);
+ assertEquals(batch.getCount().intValue(), 1);
+ assertEquals(batch.getTotal().intValue(), 1);
+ assertEquals(batch.getEntities().size(), 1);
+ assertEquals(entityUrn.toString(), batch.getEntities().get(0).toString());
+
+ EnvelopedAspect envelopedAspect =
+ _entityServiceImpl.getLatestEnvelopedAspect(
+ opContext, DATASET_ENTITY_NAME, entityUrn, GLOBAL_TAGS_ASPECT_NAME);
+ assertEquals(
+ envelopedAspect.getSystemMetadata().getVersion(),
+ "2",
+ "Expected version 3. 1 - Initial, + 1 add, 1 remove");
+ assertEquals(
+ new GlobalTags(envelopedAspect.getValue().data())
+ .getTags().stream().map(TagAssociation::getTag).collect(Collectors.toSet()),
+ Set.of(tag1, tag2),
+ "Expected both tags");
+ }
+
+ @Test
+ public void testBatchPatchAdd() throws Exception {
+ Urn entityUrn =
+ UrnUtils.getUrn("urn:li:dataset:(urn:li:dataPlatform:snowflake,testBatchPatchAdd,PROD)");
+ TagUrn tag1 = TagUrn.createFromString("urn:li:tag:tag1");
+ TagUrn tag2 = TagUrn.createFromString("urn:li:tag:tag2");
+ TagUrn tag3 = TagUrn.createFromString("urn:li:tag:tag3");
+
+ SystemMetadata systemMetadata = AspectGenerationUtils.createSystemMetadata();
+
+ ChangeItemImpl initialAspectTag1 =
+ ChangeItemImpl.builder()
+ .urn(entityUrn)
+ .aspectName(GLOBAL_TAGS_ASPECT_NAME)
+ .recordTemplate(
+ new GlobalTags()
+ .setTags(new TagAssociationArray(new TagAssociation().setTag(tag1))))
+ .systemMetadata(systemMetadata.copy())
+ .auditStamp(TEST_AUDIT_STAMP)
+ .build(TestOperationContexts.emptyAspectRetriever(null));
+
+ PatchItemImpl patchAdd3 =
+ PatchItemImpl.builder()
+ .urn(entityUrn)
+ .entitySpec(_testEntityRegistry.getEntitySpec(DATASET_ENTITY_NAME))
+ .aspectName(GLOBAL_TAGS_ASPECT_NAME)
+ .aspectSpec(
+ _testEntityRegistry
+ .getEntitySpec(DATASET_ENTITY_NAME)
+ .getAspectSpec(GLOBAL_TAGS_ASPECT_NAME))
+ .patch(
+ GenericJsonPatch.builder()
+ .arrayPrimaryKeys(Map.of("properties", List.of("tag")))
+ .patch(List.of(tagPatchOp(PatchOperationType.ADD, tag3)))
+ .build()
+ .getJsonPatch())
+ .auditStamp(AuditStampUtils.createDefaultAuditStamp())
+ .build(_testEntityRegistry);
+
+ PatchItemImpl patchAdd2 =
+ PatchItemImpl.builder()
+ .urn(entityUrn)
+ .entitySpec(_testEntityRegistry.getEntitySpec(DATASET_ENTITY_NAME))
+ .aspectName(GLOBAL_TAGS_ASPECT_NAME)
+ .aspectSpec(
+ _testEntityRegistry
+ .getEntitySpec(DATASET_ENTITY_NAME)
+ .getAspectSpec(GLOBAL_TAGS_ASPECT_NAME))
+ .patch(
+ GenericJsonPatch.builder()
+ .arrayPrimaryKeys(Map.of("properties", List.of("tag")))
+ .patch(List.of(tagPatchOp(PatchOperationType.ADD, tag2)))
+ .build()
+ .getJsonPatch())
+ .auditStamp(AuditStampUtils.createDefaultAuditStamp())
+ .build(_testEntityRegistry);
+
+ PatchItemImpl patchAdd1 =
+ PatchItemImpl.builder()
+ .urn(entityUrn)
+ .entitySpec(_testEntityRegistry.getEntitySpec(DATASET_ENTITY_NAME))
+ .aspectName(GLOBAL_TAGS_ASPECT_NAME)
+ .aspectSpec(
+ _testEntityRegistry
+ .getEntitySpec(DATASET_ENTITY_NAME)
+ .getAspectSpec(GLOBAL_TAGS_ASPECT_NAME))
+ .patch(
+ GenericJsonPatch.builder()
+ .arrayPrimaryKeys(Map.of("properties", List.of("tag")))
+ .patch(List.of(tagPatchOp(PatchOperationType.ADD, tag1)))
+ .build()
+ .getJsonPatch())
+ .auditStamp(AuditStampUtils.createDefaultAuditStamp())
+ .build(_testEntityRegistry);
+
+ // establish base entity
+ _entityServiceImpl.ingestAspects(
+ opContext,
+ AspectsBatchImpl.builder()
+ .retrieverContext(opContext.getRetrieverContext().get())
+ .items(List.of(initialAspectTag1))
+ .build(),
+ false,
+ true);
+
+ _entityServiceImpl.ingestAspects(
+ opContext,
+ AspectsBatchImpl.builder()
+ .retrieverContext(opContext.getRetrieverContext().get())
+ .items(List.of(patchAdd3, patchAdd2, patchAdd1))
+ .build(),
+ false,
+ true);
+
+ // List aspects urns
+ ListUrnsResult batch = _entityServiceImpl.listUrns(opContext, entityUrn.getEntityType(), 0, 1);
+
+ assertEquals(batch.getStart().intValue(), 0);
+ assertEquals(batch.getCount().intValue(), 1);
+ assertEquals(batch.getTotal().intValue(), 1);
+ assertEquals(batch.getEntities().size(), 1);
+ assertEquals(entityUrn.toString(), batch.getEntities().get(0).toString());
+
+ EnvelopedAspect envelopedAspect =
+ _entityServiceImpl.getLatestEnvelopedAspect(
+ opContext, DATASET_ENTITY_NAME, entityUrn, GLOBAL_TAGS_ASPECT_NAME);
+ assertEquals(envelopedAspect.getSystemMetadata().getVersion(), "3", "Expected version 4");
+ assertEquals(
+ new GlobalTags(envelopedAspect.getValue().data())
+ .getTags().stream().map(TagAssociation::getTag).collect(Collectors.toSet()),
+ Set.of(tag1, tag2, tag3),
+ "Expected all tags");
+ }
+
+ @Test
+ public void testBatchPatchAddDuplicate() throws Exception {
+ Urn entityUrn =
+ UrnUtils.getUrn("urn:li:dataset:(urn:li:dataPlatform:snowflake,testBatchPatchAdd,PROD)");
+ List initialTags =
+ List.of(
+ TagUrn.createFromString("urn:li:tag:__default_large_table"),
+ TagUrn.createFromString("urn:li:tag:__default_low_queries"),
+ TagUrn.createFromString("urn:li:tag:__default_low_changes"),
+ TagUrn.createFromString("urn:li:tag:!10TB+ tables"))
+ .stream()
+ .map(tag -> new TagAssociation().setTag(tag))
+ .collect(Collectors.toList());
+ TagUrn tag2 = TagUrn.createFromString("urn:li:tag:$ 1TB+");
+
+ SystemMetadata systemMetadata = AspectGenerationUtils.createSystemMetadata();
+
+ SystemMetadata patchSystemMetadata = new SystemMetadata();
+ patchSystemMetadata.setLastObserved(systemMetadata.getLastObserved() + 1);
+ patchSystemMetadata.setProperties(new StringMap(Map.of(APP_SOURCE, METADATA_TESTS_SOURCE)));
+
+ ChangeItemImpl initialAspectTag1 =
+ ChangeItemImpl.builder()
+ .urn(entityUrn)
+ .aspectName(GLOBAL_TAGS_ASPECT_NAME)
+ .recordTemplate(new GlobalTags().setTags(new TagAssociationArray(initialTags)))
+ .systemMetadata(systemMetadata.copy())
+ .auditStamp(TEST_AUDIT_STAMP)
+ .build(TestOperationContexts.emptyAspectRetriever(null));
+
+ PatchItemImpl patchAdd2 =
+ PatchItemImpl.builder()
+ .urn(entityUrn)
+ .entitySpec(_testEntityRegistry.getEntitySpec(DATASET_ENTITY_NAME))
+ .aspectName(GLOBAL_TAGS_ASPECT_NAME)
+ .aspectSpec(
+ _testEntityRegistry
+ .getEntitySpec(DATASET_ENTITY_NAME)
+ .getAspectSpec(GLOBAL_TAGS_ASPECT_NAME))
+ .patch(
+ GenericJsonPatch.builder()
+ .arrayPrimaryKeys(Map.of("properties", List.of("tag")))
+ .patch(List.of(tagPatchOp(PatchOperationType.ADD, tag2)))
+ .build()
+ .getJsonPatch())
+ .systemMetadata(patchSystemMetadata)
+ .auditStamp(AuditStampUtils.createDefaultAuditStamp())
+ .build(_testEntityRegistry);
+
+ // establish base entity
+ _entityServiceImpl.ingestAspects(
+ opContext,
+ AspectsBatchImpl.builder()
+ .retrieverContext(opContext.getRetrieverContext().get())
+ .items(List.of(initialAspectTag1))
+ .build(),
+ false,
+ true);
+
+ _entityServiceImpl.ingestAspects(
+ opContext,
+ AspectsBatchImpl.builder()
+ .retrieverContext(opContext.getRetrieverContext().get())
+ .items(List.of(patchAdd2, patchAdd2)) // duplicate
+ .build(),
+ false,
+ true);
+
+ // List aspects urns
+ ListUrnsResult batch = _entityServiceImpl.listUrns(opContext, entityUrn.getEntityType(), 0, 1);
+
+ assertEquals(batch.getStart().intValue(), 0);
+ assertEquals(batch.getCount().intValue(), 1);
+ assertEquals(batch.getTotal().intValue(), 1);
+ assertEquals(batch.getEntities().size(), 1);
+ assertEquals(entityUrn.toString(), batch.getEntities().get(0).toString());
+
+ EnvelopedAspect envelopedAspect =
+ _entityServiceImpl.getLatestEnvelopedAspect(
+ opContext, DATASET_ENTITY_NAME, entityUrn, GLOBAL_TAGS_ASPECT_NAME);
+ assertEquals(envelopedAspect.getSystemMetadata().getVersion(), "2", "Expected version 2");
+ assertEquals(
+ new GlobalTags(envelopedAspect.getValue().data())
+ .getTags().stream().map(TagAssociation::getTag).collect(Collectors.toSet()),
+ Stream.concat(initialTags.stream().map(TagAssociation::getTag), Stream.of(tag2))
+ .collect(Collectors.toSet()),
+ "Expected all tags");
+ }
+
+ @Test
+ public void testPatchRemoveNonExistent() throws Exception {
+ Urn entityUrn =
+ UrnUtils.getUrn(
+ "urn:li:dataset:(urn:li:dataPlatform:snowflake,testPatchRemoveNonExistent,PROD)");
+ TagUrn tag1 = TagUrn.createFromString("urn:li:tag:tag1");
+
+ PatchItemImpl patchRemove =
+ PatchItemImpl.builder()
+ .urn(entityUrn)
+ .entitySpec(_testEntityRegistry.getEntitySpec(DATASET_ENTITY_NAME))
+ .aspectName(GLOBAL_TAGS_ASPECT_NAME)
+ .aspectSpec(
+ _testEntityRegistry
+ .getEntitySpec(DATASET_ENTITY_NAME)
+ .getAspectSpec(GLOBAL_TAGS_ASPECT_NAME))
+ .patch(
+ GenericJsonPatch.builder()
+ .arrayPrimaryKeys(Map.of("properties", List.of("tag")))
+ .patch(List.of(tagPatchOp(PatchOperationType.REMOVE, tag1)))
+ .build()
+ .getJsonPatch())
+ .auditStamp(AuditStampUtils.createDefaultAuditStamp())
+ .build(_testEntityRegistry);
+
+ List results =
+ _entityServiceImpl.ingestAspects(
+ opContext,
+ AspectsBatchImpl.builder()
+ .retrieverContext(opContext.getRetrieverContext().get())
+ .items(List.of(patchRemove))
+ .build(),
+ false,
+ true);
+
+ assertEquals(results.size(), 4, "Expected default aspects + empty globalTags");
+
+ // List aspects urns
+ ListUrnsResult batch = _entityServiceImpl.listUrns(opContext, entityUrn.getEntityType(), 0, 1);
+
+ assertEquals(batch.getStart().intValue(), 0);
+ assertEquals(batch.getCount().intValue(), 1);
+ assertEquals(batch.getTotal().intValue(), 1);
+ assertEquals(batch.getEntities().size(), 1);
+ assertEquals(entityUrn.toString(), batch.getEntities().get(0).toString());
+
+ EnvelopedAspect envelopedAspect =
+ _entityServiceImpl.getLatestEnvelopedAspect(
+ opContext, DATASET_ENTITY_NAME, entityUrn, GLOBAL_TAGS_ASPECT_NAME);
+ assertEquals(envelopedAspect.getSystemMetadata().getVersion(), "1", "Expected version 4");
+ assertEquals(
+ new GlobalTags(envelopedAspect.getValue().data())
+ .getTags().stream().map(TagAssociation::getTag).collect(Collectors.toSet()),
+ Set.of(),
+ "Expected empty tags");
+ }
+
+ @Test
+ public void testPatchAddNonExistent() throws Exception {
+ Urn entityUrn =
+ UrnUtils.getUrn(
+ "urn:li:dataset:(urn:li:dataPlatform:snowflake,testPatchAddNonExistent,PROD)");
+ TagUrn tag1 = TagUrn.createFromString("urn:li:tag:tag1");
+
+ PatchItemImpl patchAdd =
+ PatchItemImpl.builder()
+ .urn(entityUrn)
+ .entitySpec(_testEntityRegistry.getEntitySpec(DATASET_ENTITY_NAME))
+ .aspectName(GLOBAL_TAGS_ASPECT_NAME)
+ .aspectSpec(
+ _testEntityRegistry
+ .getEntitySpec(DATASET_ENTITY_NAME)
+ .getAspectSpec(GLOBAL_TAGS_ASPECT_NAME))
+ .patch(
+ GenericJsonPatch.builder()
+ .arrayPrimaryKeys(Map.of("properties", List.of("tag")))
+ .patch(List.of(tagPatchOp(PatchOperationType.ADD, tag1)))
+ .build()
+ .getJsonPatch())
+ .auditStamp(AuditStampUtils.createDefaultAuditStamp())
+ .build(_testEntityRegistry);
+
+ List results =
+ _entityServiceImpl.ingestAspects(
+ opContext,
+ AspectsBatchImpl.builder()
+ .retrieverContext(opContext.getRetrieverContext().get())
+ .items(List.of(patchAdd))
+ .build(),
+ false,
+ true);
+
+ assertEquals(results.size(), 4, "Expected default aspects + globalTags");
+
+ // List aspects urns
+ ListUrnsResult batch = _entityServiceImpl.listUrns(opContext, entityUrn.getEntityType(), 0, 1);
+
+ assertEquals(batch.getStart().intValue(), 0);
+ assertEquals(batch.getCount().intValue(), 1);
+ assertEquals(batch.getTotal().intValue(), 1);
+ assertEquals(batch.getEntities().size(), 1);
+ assertEquals(entityUrn.toString(), batch.getEntities().get(0).toString());
+
+ EnvelopedAspect envelopedAspect =
+ _entityServiceImpl.getLatestEnvelopedAspect(
+ opContext, DATASET_ENTITY_NAME, entityUrn, GLOBAL_TAGS_ASPECT_NAME);
+ assertEquals(envelopedAspect.getSystemMetadata().getVersion(), "1", "Expected version 4");
+ assertEquals(
+ new GlobalTags(envelopedAspect.getValue().data())
+ .getTags().stream().map(TagAssociation::getTag).collect(Collectors.toSet()),
+ Set.of(tag1),
+ "Expected all tags");
+ }
+
@Nonnull
protected com.linkedin.entity.Entity createCorpUserEntity(Urn entityUrn, String email)
throws Exception {
@@ -2210,4 +2695,14 @@ protected Pair getAspectRecor
RecordUtils.toRecordTemplate(clazz, objectMapper.writeValueAsString(aspect));
return new Pair<>(AspectGenerationUtils.getAspectName(aspect), recordTemplate);
}
+
+ private static GenericJsonPatch.PatchOp tagPatchOp(PatchOperationType op, Urn tagUrn) {
+ GenericJsonPatch.PatchOp patchOp = new GenericJsonPatch.PatchOp();
+ patchOp.setOp(op.getValue());
+ patchOp.setPath(String.format("/tags/%s", tagUrn));
+ if (PatchOperationType.ADD.equals(op)) {
+ patchOp.setValue(Map.of("tag", tagUrn.toString()));
+ }
+ return patchOp;
+ }
}
diff --git a/metadata-io/src/test/java/com/linkedin/metadata/entity/ebean/batch/ChangeItemImplTest.java b/metadata-io/src/test/java/com/linkedin/metadata/entity/ebean/batch/ChangeItemImplTest.java
new file mode 100644
index 00000000000000..3f6b301e72aa5a
--- /dev/null
+++ b/metadata-io/src/test/java/com/linkedin/metadata/entity/ebean/batch/ChangeItemImplTest.java
@@ -0,0 +1,41 @@
+package com.linkedin.metadata.entity.ebean.batch;
+
+import static com.linkedin.metadata.Constants.STATUS_ASPECT_NAME;
+import static org.testng.Assert.assertFalse;
+
+import com.linkedin.common.AuditStamp;
+import com.linkedin.common.Status;
+import com.linkedin.common.urn.Urn;
+import com.linkedin.common.urn.UrnUtils;
+import com.linkedin.metadata.AspectGenerationUtils;
+import com.linkedin.mxe.SystemMetadata;
+import io.datahubproject.test.metadata.context.TestOperationContexts;
+import org.testng.annotations.Test;
+
+public class ChangeItemImplTest {
+ private static final AuditStamp TEST_AUDIT_STAMP = AspectGenerationUtils.createAuditStamp();
+
+ @Test
+ public void testBatchDuplicate() throws Exception {
+ Urn entityUrn = UrnUtils.getUrn("urn:li:corpuser:batchDuplicateTest");
+ SystemMetadata systemMetadata = AspectGenerationUtils.createSystemMetadata();
+ ChangeItemImpl item1 =
+ ChangeItemImpl.builder()
+ .urn(entityUrn)
+ .aspectName(STATUS_ASPECT_NAME)
+ .recordTemplate(new Status().setRemoved(true))
+ .systemMetadata(systemMetadata.copy())
+ .auditStamp(TEST_AUDIT_STAMP)
+ .build(TestOperationContexts.emptyAspectRetriever(null));
+ ChangeItemImpl item2 =
+ ChangeItemImpl.builder()
+ .urn(entityUrn)
+ .aspectName(STATUS_ASPECT_NAME)
+ .recordTemplate(new Status().setRemoved(false))
+ .systemMetadata(systemMetadata.copy())
+ .auditStamp(TEST_AUDIT_STAMP)
+ .build(TestOperationContexts.emptyAspectRetriever(null));
+
+ assertFalse(item1.isDatabaseDuplicateOf(item2));
+ }
+}
diff --git a/metadata-io/src/test/java/com/linkedin/metadata/schemafields/sideeffects/SchemaFieldSideEffectTest.java b/metadata-io/src/test/java/com/linkedin/metadata/schemafields/sideeffects/SchemaFieldSideEffectTest.java
index 6139776702c715..1661f5f02ee593 100644
--- a/metadata-io/src/test/java/com/linkedin/metadata/schemafields/sideeffects/SchemaFieldSideEffectTest.java
+++ b/metadata-io/src/test/java/com/linkedin/metadata/schemafields/sideeffects/SchemaFieldSideEffectTest.java
@@ -151,7 +151,7 @@ public void schemaMetadataToSchemaFieldKeyTest() {
UrnUtils.getUrn(
"urn:li:schemaField:(urn:li:dataset:(urn:li:dataPlatform:hive,fct_users_created,PROD),user_id)"))
.aspectName(SCHEMA_FIELD_ALIASES_ASPECT)
- .changeType(changeType)
+ .changeType(ChangeType.UPSERT)
.entitySpec(TEST_REGISTRY.getEntitySpec(SCHEMA_FIELD_ENTITY_NAME))
.aspectSpec(
TEST_REGISTRY
@@ -172,7 +172,7 @@ public void schemaMetadataToSchemaFieldKeyTest() {
UrnUtils.getUrn(
"urn:li:schemaField:(urn:li:dataset:(urn:li:dataPlatform:hive,fct_users_created,PROD),user_name)"))
.aspectName(SCHEMA_FIELD_ALIASES_ASPECT)
- .changeType(changeType)
+ .changeType(ChangeType.UPSERT)
.entitySpec(TEST_REGISTRY.getEntitySpec(SCHEMA_FIELD_ENTITY_NAME))
.aspectSpec(
TEST_REGISTRY
@@ -248,7 +248,7 @@ public void statusToSchemaFieldStatusTest() {
UrnUtils.getUrn(
"urn:li:schemaField:(urn:li:dataset:(urn:li:dataPlatform:hive,fct_users_created,PROD),user_id)"))
.aspectName(STATUS_ASPECT_NAME)
- .changeType(changeType)
+ .changeType(ChangeType.UPSERT)
.entitySpec(TEST_REGISTRY.getEntitySpec(SCHEMA_FIELD_ENTITY_NAME))
.aspectSpec(
TEST_REGISTRY
@@ -263,7 +263,7 @@ public void statusToSchemaFieldStatusTest() {
UrnUtils.getUrn(
"urn:li:schemaField:(urn:li:dataset:(urn:li:dataPlatform:hive,fct_users_created,PROD),user_name)"))
.aspectName(STATUS_ASPECT_NAME)
- .changeType(changeType)
+ .changeType(ChangeType.UPSERT)
.entitySpec(TEST_REGISTRY.getEntitySpec(SCHEMA_FIELD_ENTITY_NAME))
.aspectSpec(
TEST_REGISTRY
@@ -324,7 +324,7 @@ public void statusToSchemaFieldStatusTest() {
UrnUtils.getUrn(
"urn:li:schemaField:(urn:li:dataset:(urn:li:dataPlatform:hive,fct_users_created,PROD),user_id)"))
.aspectName(STATUS_ASPECT_NAME)
- .changeType(changeType)
+ .changeType(ChangeType.UPSERT)
.entitySpec(TEST_REGISTRY.getEntitySpec(SCHEMA_FIELD_ENTITY_NAME))
.aspectSpec(
TEST_REGISTRY
@@ -339,7 +339,7 @@ public void statusToSchemaFieldStatusTest() {
UrnUtils.getUrn(
"urn:li:schemaField:(urn:li:dataset:(urn:li:dataPlatform:hive,fct_users_created,PROD),user_name)"))
.aspectName(STATUS_ASPECT_NAME)
- .changeType(changeType)
+ .changeType(ChangeType.UPSERT)
.entitySpec(TEST_REGISTRY.getEntitySpec(SCHEMA_FIELD_ENTITY_NAME))
.aspectSpec(
TEST_REGISTRY
@@ -354,7 +354,7 @@ public void statusToSchemaFieldStatusTest() {
UrnUtils.getUrn(
"urn:li:schemaField:(urn:li:dataset:(urn:li:dataPlatform:hive,fct_users_created,PROD),user_id)"))
.aspectName(SCHEMA_FIELD_ALIASES_ASPECT)
- .changeType(changeType)
+ .changeType(ChangeType.UPSERT)
.entitySpec(TEST_REGISTRY.getEntitySpec(SCHEMA_FIELD_ENTITY_NAME))
.aspectSpec(
TEST_REGISTRY
@@ -375,7 +375,7 @@ public void statusToSchemaFieldStatusTest() {
UrnUtils.getUrn(
"urn:li:schemaField:(urn:li:dataset:(urn:li:dataPlatform:hive,fct_users_created,PROD),user_name)"))
.aspectName(SCHEMA_FIELD_ALIASES_ASPECT)
- .changeType(changeType)
+ .changeType(ChangeType.UPSERT)
.entitySpec(TEST_REGISTRY.getEntitySpec(SCHEMA_FIELD_ENTITY_NAME))
.aspectSpec(
TEST_REGISTRY
diff --git a/metadata-service/configuration/src/main/resources/application.yaml b/metadata-service/configuration/src/main/resources/application.yaml
index 4945b36a251c26..15cd126408a7cc 100644
--- a/metadata-service/configuration/src/main/resources/application.yaml
+++ b/metadata-service/configuration/src/main/resources/application.yaml
@@ -159,7 +159,7 @@ ebean:
autoCreateDdl: ${EBEAN_AUTOCREATE:false}
postgresUseIamAuth: ${EBEAN_POSTGRES_USE_AWS_IAM_AUTH:false}
locking:
- enabled: ${EBEAN_LOCKING_ENABLED:true}
+ enabled: ${EBEAN_LOCKING_ENABLED:false}
durationSeconds: ${EBEAN_LOCKING_DURATION_SECONDS:60}
maximumLocks: ${EBEAN_LOCKING_MAXIMUM_LOCKS:20000}
diff --git a/metadata-service/openapi-servlet/src/main/java/io/datahubproject/openapi/controller/GenericEntitiesController.java b/metadata-service/openapi-servlet/src/main/java/io/datahubproject/openapi/controller/GenericEntitiesController.java
index c17a4a6294f015..32252e80330646 100644
--- a/metadata-service/openapi-servlet/src/main/java/io/datahubproject/openapi/controller/GenericEntitiesController.java
+++ b/metadata-service/openapi-servlet/src/main/java/io/datahubproject/openapi/controller/GenericEntitiesController.java
@@ -57,6 +57,7 @@
import java.util.stream.Collectors;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
+import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.http.MediaType;
@@ -190,7 +191,8 @@ public ResponseEntity getEntities(
@RequestParam(value = "count", defaultValue = "10") Integer count,
@RequestParam(value = "query", defaultValue = "*") String query,
@RequestParam(value = "scrollId", required = false) String scrollId,
- @RequestParam(value = "sort", required = false, defaultValue = "urn") String sortField,
+ @RequestParam(value = "sort", required = false, defaultValue = "urn") @Deprecated
+ String sortField,
@RequestParam(value = "sortCriteria", required = false) List sortFields,
@RequestParam(value = "sortOrder", required = false, defaultValue = "ASCENDING")
String sortOrder,
@@ -199,7 +201,9 @@ public ResponseEntity getEntities(
@RequestParam(value = "skipCache", required = false, defaultValue = "false")
Boolean skipCache,
@RequestParam(value = "includeSoftDelete", required = false, defaultValue = "false")
- Boolean includeSoftDelete)
+ Boolean includeSoftDelete,
+ @RequestParam(value = "pitKeepAlive", required = false, defaultValue = "5m")
+ String pitKeepALive)
throws URISyntaxException {
EntitySpec entitySpec = entityRegistry.getEntitySpec(entityName);
@@ -220,14 +224,20 @@ public ResponseEntity getEntities(
authentication.getActor().toUrnStr() + " is unauthorized to " + READ + " entities.");
}
+ SortOrder finalSortOrder =
+ SortOrder.valueOf(Optional.ofNullable(sortOrder).orElse("ASCENDING"));
+
List sortCriteria;
- if (!CollectionUtils.isEmpty(sortFields)) {
+ if (!CollectionUtils.isEmpty(sortFields)
+ && sortFields.stream().anyMatch(StringUtils::isNotBlank)) {
sortCriteria = new ArrayList<>();
- sortFields.forEach(
- field -> sortCriteria.add(SearchUtil.sortBy(field, SortOrder.valueOf(sortOrder))));
+ sortFields.stream()
+ .filter(StringUtils::isNotBlank)
+ .forEach(field -> sortCriteria.add(SearchUtil.sortBy(field, finalSortOrder)));
+ } else if (StringUtils.isNotBlank(sortField)) {
+ sortCriteria = Collections.singletonList(SearchUtil.sortBy(sortField, finalSortOrder));
} else {
- sortCriteria =
- Collections.singletonList(SearchUtil.sortBy(sortField, SortOrder.valueOf(sortOrder)));
+ sortCriteria = Collections.singletonList(SearchUtil.sortBy("urn", finalSortOrder));
}
ScrollResult result =
@@ -241,7 +251,7 @@ public ResponseEntity getEntities(
null,
sortCriteria,
scrollId,
- null,
+ pitKeepALive,
count);
if (!AuthUtil.isAPIAuthorizedResult(opContext, result)) {
diff --git a/metadata-service/openapi-servlet/src/main/java/io/datahubproject/openapi/v3/OpenAPIV3Generator.java b/metadata-service/openapi-servlet/src/main/java/io/datahubproject/openapi/v3/OpenAPIV3Generator.java
index d179ea8f3a0682..3c35a5c1984c1d 100644
--- a/metadata-service/openapi-servlet/src/main/java/io/datahubproject/openapi/v3/OpenAPIV3Generator.java
+++ b/metadata-service/openapi-servlet/src/main/java/io/datahubproject/openapi/v3/OpenAPIV3Generator.java
@@ -45,6 +45,7 @@ public class OpenAPIV3Generator {
private static final String NAME_VERSION = "version";
private static final String NAME_SCROLL_ID = "scrollId";
private static final String NAME_INCLUDE_SOFT_DELETE = "includeSoftDelete";
+ private static final String NAME_PIT_KEEP_ALIVE = "pitKeepAlive";
private static final String PROPERTY_VALUE = "value";
private static final String PROPERTY_URN = "urn";
private static final String PROPERTY_PATCH = "patch";
@@ -502,6 +503,12 @@ private static PathItem buildGenericListEntitiesPath() {
.name(NAME_SKIP_CACHE)
.description("Skip cache when listing entities.")
.schema(new Schema().type(TYPE_BOOLEAN)._default(false)),
+ new Parameter()
+ .in(NAME_QUERY)
+ .name(NAME_PIT_KEEP_ALIVE)
+ .description(
+ "Point In Time keep alive, accepts a time based string like \"5m\" for five minutes.")
+ .schema(new Schema().type(TYPE_STRING)._default("5m")),
new Parameter().$ref("#/components/parameters/PaginationCount" + MODEL_VERSION),
new Parameter().$ref("#/components/parameters/ScrollId" + MODEL_VERSION),
new Parameter().$ref("#/components/parameters/SortBy" + MODEL_VERSION),
@@ -563,7 +570,7 @@ private static void addExtraParameters(final Components components) {
"SortBy" + MODEL_VERSION,
new Parameter()
.in(NAME_QUERY)
- .name("sort")
+ .name("sortCriteria")
.explode(true)
.description("Sort fields for pagination.")
.example(PROPERTY_URN)
@@ -571,11 +578,7 @@ private static void addExtraParameters(final Components components) {
new Schema()
.type(TYPE_ARRAY)
._default(List.of(PROPERTY_URN))
- .items(
- new Schema<>()
- .type(TYPE_STRING)
- ._enum(List.of(PROPERTY_URN))
- ._default(PROPERTY_URN))));
+ .items(new Schema<>().type(TYPE_STRING)._default(PROPERTY_URN))));
components.addParameters(
"SortOrder" + MODEL_VERSION,
new Parameter()
diff --git a/metadata-service/openapi-servlet/src/main/java/io/datahubproject/openapi/v3/controller/EntityController.java b/metadata-service/openapi-servlet/src/main/java/io/datahubproject/openapi/v3/controller/EntityController.java
index aa659b196f1872..5544fb845b2687 100644
--- a/metadata-service/openapi-servlet/src/main/java/io/datahubproject/openapi/v3/controller/EntityController.java
+++ b/metadata-service/openapi-servlet/src/main/java/io/datahubproject/openapi/v3/controller/EntityController.java
@@ -146,6 +146,8 @@ public ResponseEntity scrollEntities(
Boolean skipCache,
@RequestParam(value = "includeSoftDelete", required = false, defaultValue = "false")
Boolean includeSoftDelete,
+ @RequestParam(value = "pitKeepAlive", required = false, defaultValue = "5m")
+ String pitKeepALive,
@RequestBody @Nonnull GenericEntityAspectsBodyV3 entityAspectsBody)
throws URISyntaxException {
@@ -202,7 +204,7 @@ public ResponseEntity scrollEntities(
null,
sortCriteria,
scrollId,
- null,
+ pitKeepALive,
count);
if (!AuthUtil.isAPIAuthorizedResult(opContext, result)) {
diff --git a/metadata-utils/src/main/java/com/linkedin/metadata/utils/GenericRecordUtils.java b/metadata-utils/src/main/java/com/linkedin/metadata/utils/GenericRecordUtils.java
index fafca9b1139731..993edc44daeff1 100644
--- a/metadata-utils/src/main/java/com/linkedin/metadata/utils/GenericRecordUtils.java
+++ b/metadata-utils/src/main/java/com/linkedin/metadata/utils/GenericRecordUtils.java
@@ -4,6 +4,7 @@
import com.fasterxml.jackson.databind.JsonNode;
import com.linkedin.common.urn.Urn;
import com.linkedin.data.ByteString;
+import com.linkedin.data.DataMap;
import com.linkedin.data.template.RecordTemplate;
import com.linkedin.entity.Aspect;
import com.linkedin.entity.EntityResponse;
@@ -13,6 +14,8 @@
import com.linkedin.metadata.models.registry.EntityRegistry;
import com.linkedin.mxe.GenericAspect;
import com.linkedin.mxe.GenericPayload;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import java.util.stream.Collectors;
@@ -23,6 +26,22 @@ public class GenericRecordUtils {
private GenericRecordUtils() {}
+ public static T copy(T input, Class clazz) {
+ try {
+ if (input == null) {
+ return null;
+ }
+ Constructor constructor = clazz.getConstructor(DataMap.class);
+ return constructor.newInstance(input.data().copy());
+ } catch (CloneNotSupportedException
+ | InvocationTargetException
+ | NoSuchMethodException
+ | InstantiationException
+ | IllegalAccessException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
/** Deserialize the given value into the aspect based on the input aspectSpec */
@Nonnull
public static RecordTemplate deserializeAspect(