├── src ├── App.css ├── lib │ ├── yaml.worker.js │ └── monaco-workers.js ├── components │ ├── diagram │ │ ├── index.js │ │ ├── DiagramStyles.css │ │ └── layoutUtils.js │ ├── error │ │ ├── index.js │ │ ├── PreviewErrorBoundary.jsx │ │ └── FormPageErrorBoundary.jsx │ ├── ui │ │ ├── index.js │ │ ├── TypeSelector │ │ │ ├── index.js │ │ │ ├── TypeSelectorPopover.jsx │ │ │ ├── TypeSelector.jsx │ │ │ └── LogicalTypeSelect.jsx │ │ ├── icons │ │ │ ├── ChevronDownIcon.jsx │ │ │ ├── StringIcon.jsx │ │ │ ├── NumberIcon.jsx │ │ │ ├── TimeIcon.jsx │ │ │ ├── IntegerIcon.jsx │ │ │ ├── LockClosedIcon.jsx │ │ │ ├── BooleanIcon.jsx │ │ │ ├── CheckCircleIcon.jsx │ │ │ ├── QuestionMarkCircleIcon.jsx │ │ │ ├── TimestampIcon.jsx │ │ │ ├── DateIcon.jsx │ │ │ ├── DocumentTextIcon.jsx │ │ │ ├── ExclamationCircleIcon.jsx │ │ │ ├── ArrayIcon.jsx │ │ │ ├── LinkIcon.jsx │ │ │ ├── ObjectIcon.jsx │ │ │ ├── AsteriskIcon.jsx │ │ │ ├── CheckIcon.jsx │ │ │ ├── CustomPropertyIcon.jsx │ │ │ ├── ChevronRightIcon.jsx │ │ │ ├── QualityCheckIcons.jsx │ │ │ └── AuthoritativeDefinitionsIcon.jsx │ │ ├── Tag.jsx │ │ ├── Tooltip.jsx │ │ ├── AuthoritativeDefinitionsPreview.jsx │ │ ├── ResizeDivider.jsx │ │ ├── CustomPropertiesPreview.jsx │ │ ├── ArrayInput.jsx │ │ ├── ValidatedInput.jsx │ │ ├── PropertyValueRenderer.jsx │ │ └── Tags.jsx │ └── features │ │ ├── index.js │ │ ├── schema │ │ ├── propertyIcons.js │ │ └── PropertyIndicators.jsx │ │ ├── preview │ │ ├── PricingSection.jsx │ │ ├── CustomPropertiesSection.jsx │ │ ├── RolesSection.jsx │ │ └── SlaSection.jsx │ │ ├── DataContractPreview.jsx │ │ └── SettingsModal.jsx ├── routes │ ├── Schemas.jsx │ ├── Servers.jsx │ ├── Diagram.jsx │ ├── Schema.jsx │ ├── Server.jsx │ ├── index.js │ ├── Roles.jsx │ └── CustomProperties.jsx ├── layouts │ └── index.js ├── assets │ ├── server-icons │ │ ├── glue.svg │ │ ├── denodo.svg │ │ ├── informix.svg │ │ ├── vertica.svg │ │ ├── kinesis.svg │ │ ├── sftp.svg │ │ ├── cloudsql.svg │ │ ├── local.svg │ │ ├── pubsub.svg │ │ ├── databricks.svg │ │ ├── duckdb.svg │ │ ├── clickhouse.svg │ │ ├── bigquery.svg │ │ ├── azure.svg │ │ ├── synapse.svg │ │ ├── mssql.svg │ │ ├── db2.svg │ │ ├── athena.svg │ │ ├── oracle.svg │ │ ├── serverIcons.jsx │ │ ├── custom.svg │ │ └── database.svg │ ├── support-icons │ │ ├── email.svg │ │ ├── other.svg │ │ ├── ticket.svg │ │ ├── slack.svg │ │ ├── supportIcons.jsx │ │ ├── discord.svg │ │ ├── googlechat.svg │ │ └── teams.svg │ └── link-icons │ │ ├── databricks.jsx │ │ ├── sap.jsx │ │ ├── onetrust.jsx │ │ ├── bigquery.jsx │ │ ├── confluent.jsx │ │ ├── index.js │ │ ├── openmetadata.jsx │ │ ├── leanix.jsx │ │ ├── starburst.jsx │ │ ├── catalog.jsx │ │ ├── changelog.jsx │ │ ├── file-code.jsx │ │ ├── confluent-schema-registry.jsx │ │ ├── mssql.jsx │ │ ├── repository.jsx │ │ ├── gitlab.jsx │ │ ├── teams.jsx │ │ ├── snowflake.jsx │ │ ├── powerbi.jsx │ │ ├── collibra.jsx │ │ ├── datawarehouse.jsx │ │ └── kafka.jsx ├── hooks │ └── useActiveServerType.js ├── main.jsx ├── config │ └── storage.js ├── utils │ ├── yaml.js │ └── schemaPathBuilder.js └── services │ └── FileStorageBackend.js ├── docs └── screenshot.png ├── .editorconfig ├── Dockerfile ├── .gitignore ├── .dockerignore ├── tailwind.config.js ├── index.html ├── .claude └── settings.json ├── CONTRIBUTING.md ├── CLAUDE.md ├── eslint.config.js ├── public ├── index.html └── logo_fuchsia_v2.svg ├── LICENSE ├── package.json ├── .github └── workflows │ ├── azure-static-web-apps-orange-desert-03e149203.yml │ └── release.yml ├── vite.config.js └── README.md /src/App.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/lib/yaml.worker.js: -------------------------------------------------------------------------------- 1 | import 'monaco-yaml/yaml.worker.js'; 2 | -------------------------------------------------------------------------------- /docs/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datacontract/datacontract-editor/main/docs/screenshot.png -------------------------------------------------------------------------------- /src/components/diagram/index.js: -------------------------------------------------------------------------------- 1 | export { default as DiagramView } from './DiagramView.jsx'; 2 | export { default as SchemaNode } from './SchemaNode.jsx'; 3 | -------------------------------------------------------------------------------- /src/routes/Schemas.jsx: -------------------------------------------------------------------------------- 1 | import { SchemasEditor } from '../components/features/index.js'; 2 | 3 | const Schemas = () => { 4 | return ; 5 | }; 6 | 7 | export default Schemas; -------------------------------------------------------------------------------- /src/routes/Servers.jsx: -------------------------------------------------------------------------------- 1 | import { ServersEditor } from '../components/features/index.js'; 2 | 3 | const Servers = () => { 4 | return ; 5 | }; 6 | 7 | export default Servers; 8 | -------------------------------------------------------------------------------- /src/layouts/index.js: -------------------------------------------------------------------------------- 1 | export { default as Header } from './Header.jsx'; 2 | export { default as SidebarNavigation } from './SidebarNavigation.jsx'; 3 | export { default as MainContent } from './MainContent.jsx'; -------------------------------------------------------------------------------- /src/assets/server-icons/glue.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/assets/server-icons/denodo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/assets/server-icons/informix.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/assets/server-icons/vertica.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 2 7 | indent_style = tab 8 | insert_final_newline = true 9 | tab_width = 2 10 | trim_trailing_whitespace = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /src/routes/Diagram.jsx: -------------------------------------------------------------------------------- 1 | import DiagramView from '../components/diagram/DiagramView.jsx'; 2 | 3 | const Diagram = () => { 4 | return ( 5 |
6 | 7 |
8 | ); 9 | }; 10 | 11 | export default Diagram; 12 | -------------------------------------------------------------------------------- /src/assets/server-icons/kinesis.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/assets/server-icons/sftp.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/components/error/index.js: -------------------------------------------------------------------------------- 1 | export { default as ErrorBoundary } from './ErrorBoundary.jsx'; 2 | export { default as PreviewErrorBoundary } from './PreviewErrorBoundary.jsx'; 3 | export { default as DiagramErrorBoundary } from './DiagramErrorBoundary.jsx'; 4 | export { default as FormPageErrorBoundary } from './FormPageErrorBoundary.jsx'; 5 | -------------------------------------------------------------------------------- /src/assets/support-icons/email.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/components/ui/index.js: -------------------------------------------------------------------------------- 1 | export { default as Modal } from './Modal.jsx'; 2 | export { default as Combobox } from './Combobox.jsx'; 3 | export { default as Tooltip } from './Tooltip.jsx'; 4 | export { default as CustomPropertiesEditor } from './CustomPropertiesEditor.jsx'; 5 | export { default as ValidatedCombobox } from './ValidatedCombobox.jsx'; -------------------------------------------------------------------------------- /src/assets/server-icons/cloudsql.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/routes/Schema.jsx: -------------------------------------------------------------------------------- 1 | import { useParams } from 'react-router-dom'; 2 | import { SchemaEditor } from '../components/features/index.js'; 3 | 4 | const Schema = () => { 5 | const { schemaId } = useParams(); 6 | const schemaIndex = parseInt(schemaId, 10); 7 | return ; 8 | }; 9 | 10 | export default Schema; -------------------------------------------------------------------------------- /src/assets/server-icons/local.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/routes/Server.jsx: -------------------------------------------------------------------------------- 1 | import { useParams } from 'react-router-dom'; 2 | import { ServerEditor } from '../components/features/index.js'; 3 | 4 | const Server = () => { 5 | const { serverId } = useParams(); 6 | const serverIndex = parseInt(serverId, 10); 7 | return ; 8 | }; 9 | 10 | export default Server; 11 | -------------------------------------------------------------------------------- /src/components/ui/TypeSelector/index.js: -------------------------------------------------------------------------------- 1 | export { default as TypeSelector } from './TypeSelector'; 2 | export { default as LogicalTypeSelect } from './LogicalTypeSelect'; 3 | export { default as PhysicalTypeCombobox } from './PhysicalTypeCombobox'; 4 | export { default as TypeSelectorPopover } from './TypeSelectorPopover'; 5 | export * from './physicalTypeMappings'; 6 | -------------------------------------------------------------------------------- /src/components/ui/icons/ChevronDownIcon.jsx: -------------------------------------------------------------------------------- 1 | export default ({...rest}) => ( 2 | 5 | ) 6 | -------------------------------------------------------------------------------- /src/assets/server-icons/pubsub.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Build stage 2 | FROM node:22-slim AS builder 3 | 4 | WORKDIR /app 5 | 6 | COPY package*.json ./ 7 | RUN npm ci 8 | 9 | COPY . . 10 | RUN npm run build 11 | 12 | # Production stage 13 | FROM node:22-slim 14 | 15 | WORKDIR /app 16 | 17 | COPY package*.json ./ 18 | RUN npm ci 19 | 20 | COPY --from=builder /app/dist ./dist 21 | 22 | EXPOSE 4173 23 | 24 | CMD ["npm", "run", "preview", "--", "--host", "0.0.0.0"] -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | 26 | # local templates 27 | /templates 28 | /.playwright-mcp 29 | -------------------------------------------------------------------------------- /src/hooks/useActiveServerType.js: -------------------------------------------------------------------------------- 1 | import { useEditorStore } from '../store'; 2 | 3 | /** 4 | * Hook to get the active server type from the first server in the data contract 5 | * @returns {string|null} The server type (e.g., 'postgres', 'snowflake') or null if no servers 6 | */ 7 | export function useActiveServerType() { 8 | const servers = useEditorStore((state) => state.getValue('servers')); 9 | return servers?.[0]?.type || null; 10 | } 11 | -------------------------------------------------------------------------------- /src/components/diagram/DiagramStyles.css: -------------------------------------------------------------------------------- 1 | /* Make selected edges more visible */ 2 | .react-flow__edge.selected .react-flow__edge-path { 3 | stroke: #888 !important; 4 | stroke-width: 3px !important; 5 | } 6 | 7 | /* Increase hit area for edge selection */ 8 | .react-flow__edge-path { 9 | cursor: pointer; 10 | } 11 | 12 | .react-flow__edge:hover .react-flow__edge-path { 13 | stroke: #888 !important; 14 | stroke-width: 2.5px !important; 15 | } 16 | -------------------------------------------------------------------------------- /src/components/ui/icons/StringIcon.jsx: -------------------------------------------------------------------------------- 1 | const StringIcon = ({ className, ...props }) => ( 2 | 13 | 14 | 15 | ); 16 | 17 | export default StringIcon; 18 | -------------------------------------------------------------------------------- /src/components/ui/icons/NumberIcon.jsx: -------------------------------------------------------------------------------- 1 | const NumberIcon = ({ className, ...props }) => ( 2 | 13 | 14 | 15 | ); 16 | 17 | export default NumberIcon; 18 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules 3 | 4 | # Build output (will be rebuilt in container) 5 | dist 6 | 7 | # Development files 8 | .git 9 | .gitignore 10 | .env 11 | .env.* 12 | *.log 13 | 14 | # IDE 15 | .idea 16 | .vscode 17 | *.swp 18 | *.swo 19 | 20 | # OS files 21 | .DS_Store 22 | Thumbs.db 23 | 24 | # Test files 25 | coverage 26 | *.test.js 27 | 28 | # Documentation 29 | *.md 30 | !README.md 31 | 32 | # Docker 33 | Dockerfile 34 | docker-compose*.yml 35 | .dockerignore -------------------------------------------------------------------------------- /src/assets/server-icons/databricks.svg: -------------------------------------------------------------------------------- 1 | Databricks 2 | -------------------------------------------------------------------------------- /src/components/ui/icons/TimeIcon.jsx: -------------------------------------------------------------------------------- 1 | const TimeIcon = ({ className, ...props }) => ( 2 | 13 | 14 | 15 | 16 | ); 17 | 18 | export default TimeIcon; 19 | -------------------------------------------------------------------------------- /src/components/ui/icons/IntegerIcon.jsx: -------------------------------------------------------------------------------- 1 | const IntegerIcon = ({ className, ...props }) => ( 2 | 13 | 14 | 15 | 16 | ); 17 | 18 | export default IntegerIcon; 19 | -------------------------------------------------------------------------------- /src/components/ui/icons/LockClosedIcon.jsx: -------------------------------------------------------------------------------- 1 | const LockClosedIcon = ({ className, ...props }) => ( 2 | 9 | 10 | 11 | ); 12 | 13 | export default LockClosedIcon; -------------------------------------------------------------------------------- /src/assets/link-icons/databricks.jsx: -------------------------------------------------------------------------------- 1 | export default () => Databricks 2 | -------------------------------------------------------------------------------- /src/components/features/index.js: -------------------------------------------------------------------------------- 1 | import SchemasEditor from './SchemasEditor.jsx'; 2 | import SchemaEditor from './SchemaEditor.jsx'; 3 | import ServersEditor from './ServersEditor.jsx'; 4 | import ServerEditor from './ServerEditor.jsx'; 5 | import DataContractPreview from './DataContractPreview.jsx'; 6 | import YamlEditor from './code/YamlEditor.jsx'; 7 | import TestResultsPanel from './TestResultsPanel.jsx'; 8 | 9 | export { SchemasEditor, SchemaEditor, ServersEditor, ServerEditor, DataContractPreview, YamlEditor, TestResultsPanel }; 10 | -------------------------------------------------------------------------------- /src/components/ui/Tag.jsx: -------------------------------------------------------------------------------- 1 | import { memo } from 'react'; 2 | 3 | const Tag = memo(({children, className = ""}) => ( 4 | 6 | 9 | {children} 10 | 11 | )); 12 | 13 | Tag.displayName = 'Tag'; 14 | 15 | export default Tag; 16 | -------------------------------------------------------------------------------- /src/components/ui/icons/BooleanIcon.jsx: -------------------------------------------------------------------------------- 1 | const BooleanIcon = ({ className, ...props }) => ( 2 | 13 | 14 | 15 | 16 | ); 17 | 18 | export default BooleanIcon; 19 | -------------------------------------------------------------------------------- /src/components/ui/icons/CheckCircleIcon.jsx: -------------------------------------------------------------------------------- 1 | const CheckCircleIcon = ({ className, ...props }) => ( 2 | 9 | 10 | 11 | ); 12 | 13 | export default CheckCircleIcon; -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | content: [ 4 | "./index.html", 5 | "./src/**/*.{js,ts,jsx,tsx}", 6 | ], 7 | theme: { 8 | extend: { 9 | keyframes: { 10 | 'slide-in-right': { 11 | '0%': { transform: 'translateX(100%)' }, 12 | '100%': { transform: 'translateX(0)' }, 13 | }, 14 | }, 15 | animation: { 16 | 'slide-in-right': 'slide-in-right 0.2s ease-out', 17 | }, 18 | }, 19 | }, 20 | plugins: [], 21 | } -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | datacontract-editor 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/components/ui/icons/QuestionMarkCircleIcon.jsx: -------------------------------------------------------------------------------- 1 | const QuestionMarkCircleIcon = ({ className = "w-3 h-3 text-gray-400" }) => ( 2 | 8 | 9 | 10 | ); 11 | 12 | export default QuestionMarkCircleIcon; 13 | -------------------------------------------------------------------------------- /src/components/ui/icons/TimestampIcon.jsx: -------------------------------------------------------------------------------- 1 | const TimestampIcon = ({ className, ...props }) => ( 2 | 13 | 14 | 15 | 16 | 17 | 18 | ); 19 | 20 | export default TimestampIcon; 21 | -------------------------------------------------------------------------------- /src/components/ui/icons/DateIcon.jsx: -------------------------------------------------------------------------------- 1 | const DateIcon = ({ className, ...props }) => ( 2 | 13 | 14 | 15 | 16 | 17 | 18 | ); 19 | 20 | export default DateIcon; 21 | -------------------------------------------------------------------------------- /src/components/ui/icons/DocumentTextIcon.jsx: -------------------------------------------------------------------------------- 1 | const DocumentTextIcon = ({ className, ...props }) => ( 2 | 9 | 10 | 11 | ); 12 | 13 | export default DocumentTextIcon; -------------------------------------------------------------------------------- /src/assets/support-icons/other.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/ui/icons/ExclamationCircleIcon.jsx: -------------------------------------------------------------------------------- 1 | const ExclamationCircleIcon = ({ className, ...props }) => ( 2 | 9 | 10 | 11 | ); 12 | 13 | export default ExclamationCircleIcon; -------------------------------------------------------------------------------- /src/components/ui/icons/ArrayIcon.jsx: -------------------------------------------------------------------------------- 1 | const ArrayIcon = ({ className, ...props }) => ( 2 | 13 | 14 | 15 | 16 | 17 | 18 | ); 19 | 20 | export default ArrayIcon; 21 | -------------------------------------------------------------------------------- /src/lib/monaco-workers.js: -------------------------------------------------------------------------------- 1 | import { loader } from '@monaco-editor/react'; 2 | 3 | import * as monaco from 'monaco-editor'; 4 | import editorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker'; 5 | import YamlWorker from './yaml.worker.js?worker'; 6 | 7 | // Configure Monaco Editor web workers for Vite 8 | if (typeof window !== 'undefined') { 9 | window.MonacoEnvironment = { 10 | getWorker(_, label) { 11 | if (label === 'yaml') { 12 | return new YamlWorker(); 13 | } 14 | return new editorWorker(); 15 | }, 16 | }; 17 | } 18 | 19 | loader.config({ monaco }); 20 | 21 | loader.init(); 22 | 23 | export { monaco }; 24 | -------------------------------------------------------------------------------- /.claude/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "permissions": { 3 | "allow": [ 4 | "Bash(npm:*)", 5 | "Bash(lsof:*)", 6 | "Bash(cat:*)", 7 | "Bash(npx prettier:*)", 8 | "Bash(find:*)", 9 | "Bash(ls:*)", 10 | "WebFetch(domain:raw.githubusercontent.com)", 11 | "mcp__playwright__browser_click", 12 | "mcp__playwright__browser_type", 13 | "mcp__playwright__browser_evaluate", 14 | "mcp__playwright__browser_take_screenshot", 15 | "mcp__playwright__browser_navigate", 16 | "mcp__playwright__browser_close", 17 | "mcp__playwright__browser_snapshot" 18 | ], 19 | "deny": [], 20 | "ask": [] 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/components/ui/icons/LinkIcon.jsx: -------------------------------------------------------------------------------- 1 | const LinkIcon = ({ className, ...props }) => ( 2 | 9 | 10 | 11 | 12 | ); 13 | 14 | export default LinkIcon; -------------------------------------------------------------------------------- /src/components/ui/icons/ObjectIcon.jsx: -------------------------------------------------------------------------------- 1 | const ObjectIcon = ({ className, ...props }) => ( 2 | 13 | 14 | 15 | ); 16 | 17 | export default ObjectIcon; 18 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Development Setup 4 | 5 | ```bash 6 | # Install dependencies 7 | npm install 8 | 9 | # Start development server 10 | npm run dev 11 | ``` 12 | 13 | The app runs at http://localhost:5173 14 | 15 | ## Build 16 | 17 | Builds the ES module and the standalone app for production to the `dist` folder. 18 | ```bash 19 | npm run build 20 | ``` 21 | 22 | ## Standalone 23 | 24 | This will run the app in standalone mode. 25 | 26 | ```bash 27 | npm start 28 | ``` 29 | 30 | Opens the app at http://localhost:9090 31 | 32 | After publishing to npm: 33 | 34 | ``` 35 | npx datacontract-editor 36 | ``` 37 | 38 | 39 | 40 | ## Publish 41 | 42 | ```bash 43 | npm login 44 | npm publish 45 | ``` 46 | -------------------------------------------------------------------------------- /src/assets/link-icons/sap.jsx: -------------------------------------------------------------------------------- 1 | export default () => 2 | 3 | -------------------------------------------------------------------------------- /CLAUDE.md: -------------------------------------------------------------------------------- 1 | Automatically use context7 for code generation and library documentation. 2 | 3 | We use React with Javascript and Vite. 4 | 5 | As documentation and when looking up apis for the react libraries use the following context7 stuffs: 6 | - https://react.dev/ 7 | - /remix-run/react-router 8 | - /eemeli/yaml 9 | - /pmndrs/zustand 10 | - /websites/reactflow_dev 11 | 12 | Use Tailwind CSS with Tailwind Plus as a style guide. 13 | Use TailwindPlus react for TailwindCSS v4. 14 | 15 | Use Playwright mcp tool to test if you are unsure. 16 | If you need to start a server, use port 9090 or greater. 17 | 18 | When the term data contract is used, this refers to https://raw.githubusercontent.com/bitol-io/open-data-contract-standard/refs/heads/dev/docs/README.md 19 | -------------------------------------------------------------------------------- /src/routes/index.js: -------------------------------------------------------------------------------- 1 | export { default as Overview } from './Overview.jsx'; 2 | export { default as TermsOfUse } from './TermsOfUse.jsx'; 3 | export { default as Schemas } from './Schemas.jsx'; 4 | export { default as Schema } from './Schema.jsx'; 5 | export { default as Diagram } from './Diagram.jsx'; 6 | export { default as Pricing } from './Pricing.jsx'; 7 | export { default as Team } from './Team.jsx'; 8 | export { default as Support } from './Support.jsx'; 9 | export { default as Servers } from './Servers.jsx'; 10 | export { default as Server } from './Server.jsx'; 11 | export { default as Roles } from './Roles.jsx'; 12 | export { default as ServiceLevelAgreement } from './ServiceLevelAgreement.jsx'; 13 | export { default as CustomProperties } from './CustomProperties.jsx'; 14 | -------------------------------------------------------------------------------- /src/assets/support-icons/ticket.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/ui/icons/AsteriskIcon.jsx: -------------------------------------------------------------------------------- 1 | const AsteriskIcon = ({ className, ...props }) => ( 2 | 9 | 10 | 11 | ); 12 | 13 | export default AsteriskIcon; 14 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js' 2 | import globals from 'globals' 3 | import reactHooks from 'eslint-plugin-react-hooks' 4 | import reactRefresh from 'eslint-plugin-react-refresh' 5 | import { defineConfig, globalIgnores } from 'eslint/config' 6 | 7 | export default defineConfig([ 8 | globalIgnores(['dist']), 9 | { 10 | files: ['**/*.{js,jsx}'], 11 | extends: [ 12 | js.configs.recommended, 13 | reactHooks.configs['recommended-latest'], 14 | reactRefresh.configs.vite, 15 | ], 16 | languageOptions: { 17 | ecmaVersion: 2020, 18 | globals: globals.browser, 19 | parserOptions: { 20 | ecmaVersion: 'latest', 21 | ecmaFeatures: { jsx: true }, 22 | sourceType: 'module', 23 | }, 24 | }, 25 | rules: { 26 | 'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }], 27 | }, 28 | }, 29 | ]) 30 | -------------------------------------------------------------------------------- /src/assets/link-icons/onetrust.jsx: -------------------------------------------------------------------------------- 1 | export default () => ( 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | ) 10 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | datacontract-editor 8 | 9 | 13 | 14 | 15 |
16 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/main.jsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from 'react' 2 | import { createRoot } from 'react-dom/client' 3 | import App from './App.jsx' 4 | import { createStorageBackend } from './config/storage.js' 5 | 6 | // Create the configured storage backend 7 | // To change the storage backend, edit src/config/storage.js 8 | const storageBackendSlug = "standalone" 9 | const storageBackend = createStorageBackend(storageBackendSlug); 10 | 11 | // Editor configuration 12 | // Configure the Data Contract CLI API server URL for running tests 13 | // Set to null to use relative /test endpoint (default for server mode) 14 | // Set to a URL like 'https://api.datacontract.com' for remote API 15 | const editorConfig = { 16 | tests: { 17 | enabled: true, 18 | dataContractCliApiServerUrl: null, // e.g., 'https://api.datacontract.com' 19 | }, 20 | }; 21 | 22 | createRoot(document.getElementById('root')).render( 23 | 24 | 25 | 26 | ) 27 | -------------------------------------------------------------------------------- /src/components/ui/icons/CheckIcon.jsx: -------------------------------------------------------------------------------- 1 | export default ({...rest}) => () 2 | -------------------------------------------------------------------------------- /src/config/storage.js: -------------------------------------------------------------------------------- 1 | import { ServerFileStorageBackend } from '../services/ServerFileStorageBackend.js'; 2 | import { LocalFileStorageBackend } from '../services/LocalFileStorageBackend.js'; 3 | 4 | /** 5 | * Storage backend configuration 6 | * 7 | * To switch between storage backends, change the STORAGE_TYPE constant: 8 | * - 'standalone': Use browser's File System Access API (default) 9 | * - 'server': Use remote server API 10 | */ 11 | 12 | const SERVER_API_URL = 'http://localhost:4001'; 13 | 14 | /** 15 | * Create and return the configured storage backend 16 | * @returns {FileStorageBackend} 17 | */ 18 | export function createStorageBackend(storageBackendSlug = 'standalone') { 19 | switch (storageBackendSlug) { 20 | case 'server': 21 | console.log(`Initializing ServerFileStorageBackend with URL: ${SERVER_API_URL}`); 22 | return new ServerFileStorageBackend(SERVER_API_URL) 23 | 24 | case 'standalone': 25 | default: 26 | console.log('Initializing LocalFileStorageBackend'); 27 | return new LocalFileStorageBackend(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/utils/yaml.js: -------------------------------------------------------------------------------- 1 | import * as YAML from 'yaml'; 2 | 3 | /** 4 | * Default options for YAML stringification. 5 | * - QUOTE_DOUBLE for values (e.g., name: "value") 6 | * - PLAIN for keys (e.g., name: not "name":) 7 | */ 8 | const defaultOptions = { 9 | defaultStringType: 'QUOTE_DOUBLE', 10 | defaultKeyType: 'PLAIN', 11 | }; 12 | 13 | /** 14 | * Stringify a JavaScript object to YAML with consistent formatting. 15 | * @param {any} value - The value to stringify 16 | * @param {object} options - Optional YAML stringify options (merged with defaults) 17 | * @returns {string} The YAML string 18 | */ 19 | export function stringifyYaml(value, options = {}) { 20 | return YAML.stringify(value, { ...defaultOptions, ...options }); 21 | } 22 | 23 | /** 24 | * Parse a YAML string to a JavaScript object. 25 | * @param {string} yaml - The YAML string to parse 26 | * @returns {any} The parsed value 27 | */ 28 | export function parseYaml(yaml) { 29 | return YAML.parse(yaml); 30 | } 31 | 32 | // Re-export the original YAML module for cases where direct access is needed 33 | export { YAML }; 34 | -------------------------------------------------------------------------------- /src/components/ui/icons/CustomPropertyIcon.jsx: -------------------------------------------------------------------------------- 1 | const CustomPropertyIcon = ({ className, ...props }) => ( 2 | 10 | 16 | 22 | 23 | ); 24 | 25 | export default CustomPropertyIcon; 26 | -------------------------------------------------------------------------------- /src/components/ui/TypeSelector/TypeSelectorPopover.jsx: -------------------------------------------------------------------------------- 1 | import LogicalTypeSelect from './LogicalTypeSelect'; 2 | import PhysicalTypeCombobox from './PhysicalTypeCombobox'; 3 | 4 | /** 5 | * TypeSelectorPopover - Content for the type selector popover 6 | * Contains both logical and physical type selectors 7 | */ 8 | const TypeSelectorPopover = ({ 9 | logicalType, 10 | onLogicalTypeChange, 11 | physicalType, 12 | onPhysicalTypeChange, 13 | serverType, 14 | disabled = false, 15 | }) => { 16 | return ( 17 |
18 | 24 | 33 |
34 | ); 35 | }; 36 | 37 | export default TypeSelectorPopover; 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Entropy Data GmbH 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/assets/server-icons/duckdb.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/components/ui/icons/ChevronRightIcon.jsx: -------------------------------------------------------------------------------- 1 | export default ({...rest}) => ( 2 | 5 | ) 6 | -------------------------------------------------------------------------------- /src/assets/support-icons/slack.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/assets/server-icons/clickhouse.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/routes/Roles.jsx: -------------------------------------------------------------------------------- 1 | import { useEditorStore } from '../store.js'; 2 | import RolesList from '../components/features/RolesList.jsx'; 3 | import {useShallow} from "zustand/react/shallow"; 4 | 5 | const Roles = () => { 6 | const roles = useEditorStore(useShallow((state) => state.getValue('roles'))); 7 | const setValue = useEditorStore(useShallow((state) => state.setValue)); 8 | 9 | // Update YAML when form fields change 10 | const updateRoles = (value) => { 11 | try { 12 | if (value && value.length > 0) { 13 | setValue('roles', value); 14 | } 15 | } catch (error) { 16 | console.error('Error updating YAML:', error); 17 | } 18 | }; 19 | 20 | return ( 21 |
22 |
23 |
24 |
25 |

Roles

26 |

27 | A list of roles that will provide user access to the dataset. 28 |

29 | 30 | 31 |
32 |
33 |
34 |
35 | ); 36 | }; 37 | 38 | export default Roles; 39 | -------------------------------------------------------------------------------- /src/services/FileStorageBackend.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Abstract base class for file storage backends 3 | * Defines the interface that all storage implementations must follow 4 | */ 5 | export class FileStorageBackend { 6 | /** 7 | * Load a YAML file from the storage backend 8 | * @returns {Promise} The YAML content as a string 9 | * @throws {Error} If the operation is cancelled or fails 10 | */ 11 | async loadYamlFile() { 12 | throw new Error('loadYamlFile must be implemented by subclass'); 13 | } 14 | 15 | /** 16 | * Save YAML content to the storage backend 17 | * @param {string} yamlContent - The YAML content to save 18 | * @param {string} [suggestedName] - Optional suggested filename 19 | * @returns {Promise} 20 | * @throws {Error} If the operation is cancelled or fails 21 | */ 22 | async saveYamlFile(yamlContent, suggestedName = 'datacontract.yaml') { 23 | throw new Error('saveYamlFile must be implemented by subclass'); 24 | } 25 | 26 | /** 27 | * Check if the backend supports file selection dialog 28 | * @returns {boolean} 29 | */ 30 | supportsFileDialog() { 31 | return false; 32 | } 33 | 34 | /** 35 | * Get the name/type of this backend for display purposes 36 | * @returns {string} 37 | */ 38 | getBackendName() { 39 | return 'Unknown Backend'; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/assets/server-icons/bigquery.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/assets/link-icons/bigquery.jsx: -------------------------------------------------------------------------------- 1 | export default () => 2 | -------------------------------------------------------------------------------- /src/assets/server-icons/azure.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | path21 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/assets/support-icons/supportIcons.jsx: -------------------------------------------------------------------------------- 1 | // Support channel icons - imports SVG files for all support tool types with fallback to other.svg 2 | 3 | // Import SVG files as URLs 4 | import EmailIcon from './email.svg'; 5 | import SlackIcon from './slack.svg'; 6 | import TeamsIcon from './teams.svg'; 7 | import DiscordIcon from './discord.svg'; 8 | import TicketIcon from './ticket.svg'; 9 | import GoogleChatIcon from './googlechat.svg'; 10 | import OtherIcon from './other.svg'; 11 | 12 | // Wrapper component to render SVG as img tag with consistent styling 13 | const IconWrapper = ({ src }) => ; 14 | 15 | // Map support tool types to their icon components with other.svg as fallback 16 | const supportIcons = { 17 | email: () => , 18 | slack: () => , 19 | teams: () => , 20 | discord: () => , 21 | ticket: () => , 22 | googlechat: () => , 23 | other: () => , 24 | }; 25 | 26 | // Export function that returns icon component with fallback to other.svg 27 | export default new Proxy(supportIcons, { 28 | get(target, prop) { 29 | // If the requested icon exists, return it 30 | if (prop in target) { 31 | return target[prop]; 32 | } 33 | // Otherwise, return the other fallback icon 34 | return target.other; 35 | } 36 | }); 37 | -------------------------------------------------------------------------------- /src/components/features/schema/propertyIcons.js: -------------------------------------------------------------------------------- 1 | import StringIcon from "../../ui/icons/StringIcon.jsx"; 2 | import NumberIcon from "../../ui/icons/NumberIcon.jsx"; 3 | import IntegerIcon from "../../ui/icons/IntegerIcon.jsx"; 4 | import DateIcon from "../../ui/icons/DateIcon.jsx"; 5 | import TimeIcon from "../../ui/icons/TimeIcon.jsx"; 6 | import TimestampIcon from "../../ui/icons/TimestampIcon.jsx"; 7 | import ObjectIcon from "../../ui/icons/ObjectIcon.jsx"; 8 | import ArrayIcon from "../../ui/icons/ArrayIcon.jsx"; 9 | import BooleanIcon from "../../ui/icons/BooleanIcon.jsx"; 10 | 11 | /** 12 | * Get icon component for a logical type 13 | * @param {string} logicalType - The logical type (string, number, date, etc.) 14 | * @returns {Component|null} Icon component or null if not found 15 | */ 16 | export const getLogicalTypeIcon = (logicalType) => { 17 | const iconMap = { 18 | 'string': StringIcon, 19 | 'number': NumberIcon, 20 | 'integer': IntegerIcon, 21 | 'date': DateIcon, 22 | 'time': TimeIcon, 23 | 'timestamp': TimestampIcon, 24 | 'object': ObjectIcon, 25 | 'array': ArrayIcon, 26 | 'boolean': BooleanIcon 27 | }; 28 | return iconMap[logicalType] || null; 29 | }; 30 | 31 | /** 32 | * Fallback logical type options (used if schema not loaded) 33 | */ 34 | export const fallbackLogicalTypeOptions = [ 35 | 'string', 36 | 'date', 37 | 'timestamp', 38 | 'time', 39 | 'number', 40 | 'integer', 41 | 'object', 42 | 'array', 43 | 'boolean' 44 | ]; 45 | -------------------------------------------------------------------------------- /src/components/ui/icons/QualityCheckIcons.jsx: -------------------------------------------------------------------------------- 1 | // Quality Check Type Icons 2 | 3 | export const TextCheckIcon = ({ className = "w-3 h-3" }) => ( 4 | 5 | 6 | 7 | ); 8 | 9 | export const SqlCheckIcon = ({ className = "w-3 h-3" }) => ( 10 | 11 | 12 | 13 | 14 | ); 15 | 16 | export const LibraryCheckIcon = ({ className = "w-3 h-3" }) => ( 17 | 18 | 19 | 20 | ); 21 | 22 | export const CustomCheckIcon = ({ className = "w-3 h-3" }) => ( 23 | 24 | 25 | 26 | ); 27 | 28 | export const getQualityCheckIcon = (type) => { 29 | switch (type?.toLowerCase()) { 30 | case 'text': 31 | return TextCheckIcon; 32 | case 'sql': 33 | return SqlCheckIcon; 34 | case 'library': 35 | return LibraryCheckIcon; 36 | case 'custom': 37 | return CustomCheckIcon; 38 | default: 39 | return LibraryCheckIcon; 40 | } 41 | }; 42 | -------------------------------------------------------------------------------- /src/assets/server-icons/synapse.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/components/ui/Tooltip.jsx: -------------------------------------------------------------------------------- 1 | import { useState, useRef, useEffect } from 'react'; 2 | import { createPortal } from 'react-dom'; 3 | 4 | const Tooltip = ({ content, children }) => { 5 | const [isVisible, setIsVisible] = useState(false); 6 | const [position, setPosition] = useState({ top: 0, left: 0 }); 7 | const triggerRef = useRef(null); 8 | 9 | useEffect(() => { 10 | if (isVisible && triggerRef.current) { 11 | const rect = triggerRef.current.getBoundingClientRect(); 12 | setPosition({ 13 | top: rect.top - 8, // 8px above the trigger 14 | left: rect.left + rect.width / 2, // centered horizontally 15 | }); 16 | } 17 | }, [isVisible]); 18 | 19 | const handleMouseEnter = () => { 20 | setIsVisible(true); 21 | }; 22 | 23 | const handleMouseLeave = () => { 24 | setIsVisible(false); 25 | }; 26 | 27 | return ( 28 | <> 29 |
35 | {children} 36 |
37 | {isVisible && createPortal( 38 |
46 | {content} 47 |
48 |
, 49 | document.body 50 | )} 51 | 52 | ); 53 | }; 54 | 55 | export default Tooltip; 56 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "datacontract-editor", 3 | "version": "0.1.2", 4 | "description": "A visual editor for data contracts using the Open Data Contract Standard (ODCS)", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/datacontract/datacontract-editor" 8 | }, 9 | "type": "module", 10 | "bin": { 11 | "datacontract-editor": "./bin/datacontract-editor.js" 12 | }, 13 | "files": [ 14 | "dist", 15 | "bin" 16 | ], 17 | "scripts": { 18 | "dev": "vite", 19 | "build": "vite build", 20 | "build:debug": "vite build --mode debug", 21 | "lint": "eslint .", 22 | "preview": "vite preview", 23 | "prepublishOnly": "npm run build", 24 | "start": "npm run build && node bin/datacontract-editor.js", 25 | "start:debug": "npm run build:debug && node bin/datacontract-editor.js" 26 | }, 27 | "engines": { 28 | "node": ">=22" 29 | }, 30 | "dependencies": { 31 | "@headlessui/react": "^2.2.9", 32 | "@monaco-editor/react": "^4.7.0", 33 | "@xyflow/react": "^12.9.0", 34 | "dagre": "^0.8.5", 35 | "monaco-yaml": "^5.4.0", 36 | "open": "^10.1.0", 37 | "path-browserify": "^1.0.1", 38 | "react": "^19.2.1", 39 | "react-dom": "^19.2.1", 40 | "react-router-dom": "^7.9.5", 41 | "yaml": "^2.8.1", 42 | "zustand": "^5.0.8" 43 | }, 44 | "devDependencies": { 45 | "@eslint/js": "^9.39.1", 46 | "@tailwindcss/vite": "^4.1.17", 47 | "@types/react": "^19.2.2", 48 | "@types/react-dom": "^19.2.2", 49 | "@vitejs/plugin-react": "^5.1.0", 50 | "eslint": "^9.39.1", 51 | "eslint-plugin-react-hooks": "^5.2.0", 52 | "eslint-plugin-react-refresh": "^0.4.24", 53 | "globals": "^16.5.0", 54 | "tailwindcss": "^4.1.17", 55 | "vite": "^7.2.2" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/assets/link-icons/confluent.jsx: -------------------------------------------------------------------------------- 1 | export default () => 3 | 4 | 15 | 16 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/assets/link-icons/index.js: -------------------------------------------------------------------------------- 1 | export { default as ApiIcon } from './api.jsx'; 2 | export { default as BigqueryIcon } from './bigquery.jsx'; 3 | export { default as CatalogIcon } from './catalog.jsx'; 4 | export { default as ChangelogIcon } from './changelog.jsx'; 5 | export { default as CollibraIcon } from './collibra.jsx'; 6 | export { default as ConfluentSchemaRegistryIcon } from './confluent-schema-registry.jsx'; 7 | export { default as ConfluentIcon } from './confluent.jsx'; 8 | export { default as DatabricksIcon } from './databricks.jsx'; 9 | export { default as DatawarehouseIcon } from './datawarehouse.jsx'; 10 | export { default as DeletedIcon } from './deleted.jsx'; 11 | export { default as DocumentationIcon } from './documentation.jsx'; 12 | export { default as FileCodeIcon } from './file-code.jsx'; 13 | export { default as GithubIcon } from './github.jsx'; 14 | export { default as GitlabIcon } from './gitlab.jsx'; 15 | export { default as KafkaIcon } from './kafka.jsx'; 16 | export { default as LeanixIcon } from './leanix.jsx'; 17 | export { default as MssqlIcon } from './mssql.jsx'; 18 | export { default as OnetrustIcon } from './onetrust.jsx'; 19 | export { default as OpenmetadataIcon } from './openmetadata.jsx'; 20 | export { default as PostgresIcon } from './postgres.jsx'; 21 | export { default as PowerbiIcon } from './powerbi.jsx'; 22 | export { default as PurviewIcon } from './purview.jsx'; 23 | export { default as RepositoryIcon } from './repository.jsx'; 24 | export { default as S3Icon } from './s3.jsx'; 25 | export { default as SampleIcon } from './sample.jsx'; 26 | export { default as SapIcon } from './sap.jsx'; 27 | export { default as SnowflakeIcon } from './snowflake.jsx'; 28 | export { default as StarburstIcon } from './starburst.jsx'; 29 | export { default as TeamsIcon } from './teams.jsx'; 30 | -------------------------------------------------------------------------------- /src/components/features/preview/PricingSection.jsx: -------------------------------------------------------------------------------- 1 | import {useEditorStore} from "../../../store.js"; 2 | import {useShallow} from "zustand/react/shallow"; 3 | 4 | const PricingSection = () => { 5 | const price = useEditorStore(useShallow(state => state.getValue('price'))); 6 | if (!price) return null; 7 | 8 | return ( 9 |
10 |
11 |

Price

12 |

This section covers pricing when you bill your customer for using 13 | this data product.

14 |
15 |
16 |
17 |
18 | {price.priceAmount && ( 19 |
20 |
Price Amount
21 |
{price.priceAmount}
22 |
23 | )} 24 | 25 | {price.priceCurrency && ( 26 |
27 |
Price Currency
28 |
{price.priceCurrency}
29 |
30 | )} 31 | 32 | {price.priceUnit && ( 33 |
34 |
Price Unit
35 |
{price.priceUnit}
36 |
37 | )} 38 |
39 |
40 |
41 |
42 | ); 43 | }; 44 | 45 | PricingSection.displayName = 'PricingSection'; 46 | 47 | export default PricingSection; 48 | -------------------------------------------------------------------------------- /src/assets/link-icons/openmetadata.jsx: -------------------------------------------------------------------------------- 1 | export default () => 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/assets/link-icons/leanix.jsx: -------------------------------------------------------------------------------- 1 | export default () => 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/components/features/DataContractPreview.jsx: -------------------------------------------------------------------------------- 1 | import DescriptionPreview from '../ui/DescriptionPreview.jsx'; 2 | import ContractHeader from './preview/ContractHeader.jsx'; 3 | import FundamentalsSection from './preview/FundamentalsSection.jsx'; 4 | import SchemaSection from './preview/SchemaSection.jsx'; 5 | import ServersSection from './preview/ServersSection.jsx'; 6 | import TeamSection from './preview/TeamSection.jsx'; 7 | import SupportSection from './preview/SupportSection.jsx'; 8 | import RolesSection from './preview/RolesSection.jsx'; 9 | import PricingSection from './preview/PricingSection.jsx'; 10 | import SlaSection from './preview/SlaSection.jsx'; 11 | import CustomPropertiesSection from './preview/CustomPropertiesSection.jsx'; 12 | 13 | const DataContractPreview = () => { 14 | // Extract data for all sections 15 | 16 | return ( 17 |
18 | 19 | 20 | {/* Main Column */} 21 |
22 | {/* 1. Fundamentals Section */} 23 | 25 | 26 | {/* 2. Description Section */} 27 | 28 | 29 | {/* 3. Data Model Section */} 30 | 31 | 32 | {/* 4. Servers Section */} 33 | 34 | 35 | {/* 5. Team Section */} 36 | 37 | 38 | {/* 6. Support & Communication Section */} 39 | 40 | 41 | {/* 7. Roles Section */} 42 | 43 | 44 | {/* 8. Pricing Section */} 45 | 46 | 47 | {/* 9. SLA Section */} 48 | 49 | 50 | {/* 10. Custom Properties Section */} 51 | 52 | 53 |
54 |
55 | ); 56 | }; 57 | 58 | export default DataContractPreview; 59 | -------------------------------------------------------------------------------- /src/assets/link-icons/starburst.jsx: -------------------------------------------------------------------------------- 1 | export default ({ className }) => ( 2 | 10 | 11 | 12 | 13 | ); 14 | -------------------------------------------------------------------------------- /src/assets/support-icons/discord.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/components/ui/AuthoritativeDefinitionsPreview.jsx: -------------------------------------------------------------------------------- 1 | import {IconResolver} from './IconResolver.jsx'; 2 | import Tooltip from './Tooltip.jsx'; 3 | import LinkIcon from './icons/LinkIcon.jsx'; 4 | 5 | /** 6 | * AuthoritativeDefinitionsPreview component for displaying authoritative definitions 7 | * A compact, icon-only preview component with tooltips 8 | * 9 | * @param {Array} definitions - Array of authoritative definition objects with {type, url, description} 10 | * @param {string} size - Icon size class (default: 'w-4 h-4') 11 | */ 12 | const AuthoritativeDefinitionsPreview = ({definitions = [], size = 'w-4 h-4'}) => { 13 | if (!definitions || definitions.length === 0) { 14 | return null; 15 | } 16 | 17 | return ( 18 |
19 | {definitions.map((def, index) => { 20 | const tooltipContent = ( 21 |
22 |
{def.type}
23 | {def.description &&
{def.description}
} 24 | {def.url && ( 25 |
26 | 27 | {def.url} 28 |
29 | )} 30 |
31 | ); 32 | 33 | return ( 34 | 35 | 41 |
42 | 43 |
44 |
45 |
46 | ); 47 | })} 48 |
49 | ); 50 | }; 51 | 52 | export default AuthoritativeDefinitionsPreview; 53 | -------------------------------------------------------------------------------- /src/assets/link-icons/catalog.jsx: -------------------------------------------------------------------------------- 1 | export default () => 2 | -------------------------------------------------------------------------------- /.github/workflows/azure-static-web-apps-orange-desert-03e149203.yml: -------------------------------------------------------------------------------- 1 | name: Azure Static Web Apps CI/CD 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | types: [opened, synchronize, reopened, closed] 9 | branches: 10 | - main 11 | 12 | jobs: 13 | build_and_deploy_job: 14 | if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.action != 'closed') 15 | runs-on: ubuntu-latest 16 | name: Build and Deploy Job 17 | steps: 18 | - uses: actions/checkout@v3 19 | with: 20 | submodules: true 21 | lfs: false 22 | - name: Build And Deploy 23 | id: builddeploy 24 | uses: Azure/static-web-apps-deploy@v1 25 | with: 26 | azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_ORANGE_DESERT_03E149203 }} 27 | repo_token: ${{ secrets.GITHUB_TOKEN }} # Used for Github integrations (i.e. PR comments) 28 | action: "upload" 29 | ###### Repository/Build Configurations - These values can be configured to match your app requirements. ###### 30 | # For more information regarding Static Web App workflow configurations, please visit: https://aka.ms/swaworkflowconfig 31 | app_location: "/" # App source code path 32 | api_location: "" # Api source code path - optional 33 | output_location: "dist" # Built app content directory - optional 34 | ###### End of Repository/Build Configurations ###### 35 | 36 | close_pull_request_job: 37 | if: github.event_name == 'pull_request' && github.event.action == 'closed' 38 | runs-on: ubuntu-latest 39 | name: Close Pull Request Job 40 | steps: 41 | - name: Close Pull Request 42 | id: closepullrequest 43 | uses: Azure/static-web-apps-deploy@v1 44 | with: 45 | azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_ORANGE_DESERT_03E149203 }} 46 | action: "close" 47 | -------------------------------------------------------------------------------- /src/assets/link-icons/changelog.jsx: -------------------------------------------------------------------------------- 1 | export default () => 2 | -------------------------------------------------------------------------------- /src/components/ui/ResizeDivider.jsx: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useRef } from 'react'; 2 | 3 | const ResizeDivider = ({ onResize, minLeftPercent = 20, maxLeftPercent = 80 }) => { 4 | const isDragging = useRef(false); 5 | const containerRef = useRef(null); 6 | 7 | const handleMouseDown = useCallback((e) => { 8 | e.preventDefault(); 9 | isDragging.current = true; 10 | document.body.style.cursor = 'col-resize'; 11 | document.body.style.userSelect = 'none'; 12 | }, []); 13 | 14 | const handleMouseMove = useCallback((e) => { 15 | if (!isDragging.current) return; 16 | 17 | const container = containerRef.current?.parentElement; 18 | if (!container) return; 19 | 20 | const containerRect = container.getBoundingClientRect(); 21 | const newLeftPercent = ((e.clientX - containerRect.left) / containerRect.width) * 100; 22 | 23 | // Clamp between min and max 24 | const clampedPercent = Math.min(Math.max(newLeftPercent, minLeftPercent), maxLeftPercent); 25 | onResize(clampedPercent); 26 | }, [onResize, minLeftPercent, maxLeftPercent]); 27 | 28 | const handleMouseUp = useCallback(() => { 29 | isDragging.current = false; 30 | document.body.style.cursor = ''; 31 | document.body.style.userSelect = ''; 32 | }, []); 33 | 34 | useEffect(() => { 35 | document.addEventListener('mousemove', handleMouseMove); 36 | document.addEventListener('mouseup', handleMouseUp); 37 | 38 | return () => { 39 | document.removeEventListener('mousemove', handleMouseMove); 40 | document.removeEventListener('mouseup', handleMouseUp); 41 | }; 42 | }, [handleMouseMove, handleMouseUp]); 43 | 44 | return ( 45 |
51 | {/* Invisible wider hit area */} 52 |
53 |
54 | ); 55 | }; 56 | 57 | export default ResizeDivider; 58 | -------------------------------------------------------------------------------- /src/assets/link-icons/file-code.jsx: -------------------------------------------------------------------------------- 1 | export default () => 2 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | import tailwindcss from '@tailwindcss/vite' 4 | import { resolve } from 'path' 5 | 6 | export default defineConfig(({ mode }) => ({ 7 | plugins: [ 8 | tailwindcss(), 9 | react() 10 | ], 11 | // Use relative base for assets to support context paths 12 | base: './', 13 | define: { 14 | 'global': 'globalThis', 15 | 'process.env.NODE_ENV': JSON.stringify('production'), 16 | }, 17 | resolve: { 18 | alias: { 19 | 'path': 'path-browserify' 20 | } 21 | }, 22 | worker: { 23 | format: 'es', 24 | // Output workers to assets directory 25 | rollupOptions: { 26 | output: { 27 | entryFileNames: 'assets/[name]-[hash].js' 28 | } 29 | } 30 | }, 31 | build: { 32 | outDir: 'dist', 33 | copyPublicDir: true, 34 | // Enable sourcemaps in debug mode 35 | sourcemap: mode === 'debug' ? true : false, 36 | // Disable minification in debug mode for easier debugging 37 | minify: mode === 'debug' ? false : 'esbuild', 38 | lib: { 39 | entry: resolve(__dirname, 'src/embed.jsx'), 40 | name: 'DataContractEditor', 41 | formats: ['es'], 42 | fileName: () => `datacontract-editor.es.js` 43 | }, 44 | rollupOptions: { 45 | output: { 46 | exports: 'named', 47 | assetFileNames: (assetInfo) => { 48 | if (assetInfo.name === 'style.css') return 'datacontract-editor.css'; 49 | return assetInfo.name; 50 | }, 51 | // Ensure workers are placed in assets/ 52 | chunkFileNames: (chunkInfo) => { 53 | if (chunkInfo.name.includes('worker')) { 54 | return 'assets/[name]-[hash].js'; 55 | } 56 | return '[name]-[hash].js'; 57 | } 58 | } 59 | }, 60 | // Increase chunk size warnings threshold for Monaco 61 | chunkSizeWarningLimit: 5000, 62 | // Ensure all assets are properly bundled 63 | cssCodeSplit: false, 64 | }, 65 | optimizeDeps: { 66 | include: [ 67 | 'monaco-editor', 68 | 'monaco-yaml' 69 | ] 70 | } 71 | })) 72 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Data Contract Editor 2 | 3 | A web-based editor for creating and managing data contracts using the [Open Data Contract Standard](https://bitol-io.github.io/open-data-contract-standard/latest/) (ODCS). 4 | 5 | ![Screenshot](https://raw.githubusercontent.com/datacontract/datacontract-editor/main/docs/screenshot.png) 6 | 7 | ## Features 8 | 9 | - **Open Data Contract Standard**: ODCS is the industry-standard for data contracts. Now with support for v3.1.0. 10 | - **Editing Modes**: 11 | - **Visual Editor**: Define data models and relationships using a visual interface 12 | - **Form Editor**: Get guided input with a simple form interface 13 | - **YAML Editor**: Edit data contracts directly in YAML format with code completion 14 | - **Preview**: Live preview of data contracts as HTML 15 | - **Validation**: Get instant feedback on your data contracts 16 | - **Test**: Test your data contract directly against your data using the Data Contract CLI API Server. 17 | 18 | 19 | ## Usage 20 | 21 | ### Web Editor 22 | 23 | Open the editor as web application: 24 | 25 | https://editor.datacontract.com 26 | 27 | 28 | ### Standalone Application 29 | 30 | You can start the editor locally using the following command: 31 | 32 | ``` 33 | npx datacontract-editor 34 | ``` 35 | 36 | Or edit a data contract file directly: 37 | 38 | ``` 39 | npx datacontract-editor mydatacontract.odcs.yaml 40 | ``` 41 | 42 | 43 | 44 | ### Docker 45 | 46 | Run the editor locally in a Docker container: 47 | 48 | ``` 49 | docker run -d -p 4173:4173 datacontract/editor 50 | ``` 51 | 52 | Then open http://localhost:4173 53 | 54 | 55 | ### Data Contract CLI 56 | 57 | Coming soon! 58 | 59 | You can start the editor from the Data Contract CLI: 60 | 61 | ``` 62 | datacontract editor datacontract.yaml 63 | ``` 64 | 65 | 66 | 67 | ### Entropy Data 68 | 69 | The Data Contract Editor is fully integrated in our commercial product [Entropy Data](https://entropy-data.com) to manage multiple data contracts in a single application. 70 | 71 | 72 | 73 | ## License 74 | 75 | This project is maintained by [Entropy Data](https://entropy-data.com) and licensed under the [MIT LICENSE](LICENSE). 76 | -------------------------------------------------------------------------------- /src/assets/link-icons/confluent-schema-registry.jsx: -------------------------------------------------------------------------------- 1 | export default () => 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/components/features/schema/PropertyIndicators.jsx: -------------------------------------------------------------------------------- 1 | import {Tooltip} from '../../ui/index.js'; 2 | import KeyIcon from "../../ui/icons/KeyIcon.jsx"; 3 | import AsteriskIcon from "../../ui/icons/AsteriskIcon.jsx"; 4 | import LockClosedIcon from "../../ui/icons/LockClosedIcon.jsx"; 5 | import CheckCircleIcon from "../../ui/icons/CheckCircleIcon.jsx"; 6 | import LinkIcon from "../../ui/icons/LinkIcon.jsx"; 7 | 8 | /** 9 | * Visual indicator badges component showing property metadata 10 | * Displays icons for primary key, required, classification, quality rules, and relationships 11 | */ 12 | const PropertyIndicators = ({property}) => { 13 | const indicators = []; 14 | 15 | if (property.primaryKey) { 16 | indicators.push( 17 | 18 | 19 | 20 | ); 21 | } 22 | 23 | if (property.required) { 24 | indicators.push( 25 | 26 | 27 | 28 | ); 29 | } 30 | 31 | if (property.classification) { 32 | indicators.push( 33 | 34 | 35 | 36 | ); 37 | } 38 | 39 | if (property.quality && property.quality.length > 0) { 40 | indicators.push( 41 | 42 | 43 | 44 | ); 45 | } 46 | 47 | if (property.relationships && property.relationships.length > 0) { 48 | indicators.push( 49 | 50 | 51 | 52 | ); 53 | } 54 | 55 | return ( 56 |
57 | {indicators} 58 |
59 | ); 60 | }; 61 | 62 | export default PropertyIndicators; 63 | -------------------------------------------------------------------------------- /src/routes/CustomProperties.jsx: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react'; 2 | import { useEditorStore } from '../store.js'; 3 | import CustomPropertiesEditor from '../components/ui/CustomPropertiesEditor.jsx'; 4 | import { stringifyYaml, parseYaml } from '../utils/yaml.js'; 5 | 6 | const CustomProperties = () => { 7 | const yaml = useEditorStore((state) => state.yaml); 8 | const setYaml = useEditorStore((state) => state.setYaml); 9 | 10 | // Parse current YAML to extract form values 11 | const formData = useMemo(() => { 12 | if (!yaml?.trim()) { 13 | return { customProperties: [] }; 14 | } 15 | 16 | try { 17 | const parsed = parseYaml(yaml); 18 | return { 19 | customProperties: parsed.customProperties || [] 20 | }; 21 | } catch { 22 | return { customProperties: [] }; 23 | } 24 | }, [yaml]); 25 | 26 | // Update YAML when custom properties change 27 | const handleChange = (value) => { 28 | try { 29 | let parsed = {}; 30 | if (yaml?.trim()) { 31 | try { 32 | parsed = parseYaml(yaml) || {}; 33 | } catch { 34 | parsed = {}; 35 | } 36 | } 37 | 38 | if (value && value.length > 0) { 39 | parsed.customProperties = value; 40 | } else { 41 | delete parsed.customProperties; 42 | } 43 | 44 | const newYaml = stringifyYaml(parsed); 45 | setYaml(newYaml); 46 | } catch (error) { 47 | console.error('Error updating YAML:', error); 48 | } 49 | }; 50 | 51 | return ( 52 |
53 |
54 |
55 |
56 |

Custom Properties

57 |

58 | A list of key/value pairs for custom properties. Names should be in camel case. 59 |

60 | 61 | 66 |
67 |
68 |
69 |
70 | ); 71 | }; 72 | 73 | export default CustomProperties; 74 | -------------------------------------------------------------------------------- /src/components/ui/CustomPropertiesPreview.jsx: -------------------------------------------------------------------------------- 1 | import Tooltip from './Tooltip.jsx'; 2 | 3 | /** 4 | * CustomPropertiesPreview component for displaying custom properties as pills 5 | * A compact preview component showing property-value pairs 6 | * 7 | * @param {Array|Object} properties - Array of {property, value, description} objects OR an object with key-value pairs 8 | * @param {string} pillClassName - Additional CSS classes to apply to individual pills (e.g., "mr-1 mt-1") 9 | */ 10 | const CustomPropertiesPreview = ({properties = [], pillClassName = ""}) => { 11 | if (!properties) { 12 | return null; 13 | } 14 | 15 | // Normalize properties to array format 16 | // Handle both array format [{property, value, description}] and object format {key: value} 17 | let normalizedProperties = []; 18 | if (Array.isArray(properties)) { 19 | normalizedProperties = properties; 20 | } else if (typeof properties === 'object') { 21 | normalizedProperties = Object.entries(properties).map(([key, value]) => ({ 22 | property: key, 23 | value: value, 24 | })); 25 | } 26 | 27 | if (normalizedProperties.length === 0) { 28 | return null; 29 | } 30 | 31 | // Helper to format value for display 32 | const formatValue = (value) => { 33 | if (value === null || value === undefined) return ''; 34 | if (typeof value === 'object') { 35 | try { 36 | return JSON.stringify(value); 37 | } catch { 38 | return String(value); 39 | } 40 | } 41 | return String(value); 42 | }; 43 | 44 | const roundedClass = "rounded-md"; 45 | const paddingClass = "px-2 py-1"; 46 | 47 | return ( 48 | <> 49 | {normalizedProperties.map((prop, index) => { 50 | const pill = ( 51 | 54 | {prop.property}:{formatValue(prop.value)} 55 | 56 | ); 57 | 58 | if (prop.description) { 59 | return ( 60 | {prop.description}
}> 61 | {pill} 62 | 63 | ); 64 | } 65 | 66 | return {pill}; 67 | })} 68 | 69 | ); 70 | }; 71 | 72 | export default CustomPropertiesPreview; 73 | -------------------------------------------------------------------------------- /src/assets/server-icons/mssql.svg: -------------------------------------------------------------------------------- 1 | Icon-databases-130 2 | -------------------------------------------------------------------------------- /src/assets/link-icons/mssql.jsx: -------------------------------------------------------------------------------- 1 | export default () => Icon-databases-130 2 | -------------------------------------------------------------------------------- /src/assets/link-icons/repository.jsx: -------------------------------------------------------------------------------- 1 | export default () => 2 | -------------------------------------------------------------------------------- /src/assets/server-icons/db2.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/features/preview/CustomPropertiesSection.jsx: -------------------------------------------------------------------------------- 1 | import Tooltip from '../../ui/Tooltip.jsx'; 2 | import PropertyValueRenderer from '../../ui/PropertyValueRenderer.jsx'; 3 | import QuestionMarkCircleIcon from '../../ui/icons/QuestionMarkCircleIcon.jsx'; 4 | import {useEditorStore} from "../../../store.js"; 5 | import {useShallow} from "zustand/react/shallow"; 6 | 7 | const CustomPropertiesSection = () => { 8 | const customProperties = useEditorStore(useShallow(state => state.getValue('customProperties'))); 9 | 10 | // Normalize properties to array format 11 | // Handle both array format [{property, value, description}] and object format {key: value} 12 | let normalizedProperties = []; 13 | if (Array.isArray(customProperties)) { 14 | normalizedProperties = customProperties; 15 | } else if (customProperties && typeof customProperties === 'object') { 16 | normalizedProperties = Object.entries(customProperties).map(([key, value]) => ({ 17 | property: key, 18 | value: value, 19 | })); 20 | } 21 | 22 | if (normalizedProperties.length === 0) return null; 23 | 24 | return ( 25 |
26 |
27 |

Custom 28 | Properties

29 |

This section covers other properties you may find in a data 30 | contract.

31 |
32 |
33 |
34 |
35 | {normalizedProperties.map((property, index) => ( 36 |
37 |
38 | {property.property} 39 | {property.description && ( 40 | 41 | 42 | 43 | )} 44 |
45 |
46 | 47 |
48 |
49 | ))} 50 |
51 |
52 |
53 |
54 | ); 55 | }; 56 | 57 | CustomPropertiesSection.displayName = 'CustomPropertiesSection'; 58 | 59 | export default CustomPropertiesSection; 60 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | publish-npm: 9 | runs-on: ubuntu-latest 10 | permissions: 11 | contents: write 12 | id-token: write 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v4 16 | 17 | - name: Setup Node.js 18 | uses: actions/setup-node@v4 19 | with: 20 | node-version: '22' 21 | registry-url: 'https://registry.npmjs.org' 22 | 23 | # Ensure npm 11.5.1 or later is installed 24 | - name: Update npm 25 | run: npm install -g npm@latest 26 | 27 | - name: Set version from release tag 28 | run: | 29 | VERSION=${GITHUB_REF_NAME#v} 30 | npm version $VERSION --no-git-tag-version --allow-same-version 31 | 32 | - name: Install dependencies 33 | run: npm ci 34 | 35 | - name: Build 36 | run: npm run build 37 | 38 | - name: Publish to npm 39 | run: npm publish --provenance --access public 40 | 41 | - name: Commit version to repo 42 | run: | 43 | git config user.name "github-actions[bot]" 44 | git config user.email "github-actions[bot]@users.noreply.github.com" 45 | git add package.json 46 | git commit -m "chore: bump version to ${GITHUB_REF_NAME#v}" || echo "No changes to commit" 47 | git push origin HEAD:main 48 | 49 | publish-docker: 50 | runs-on: ubuntu-latest 51 | permissions: 52 | contents: read 53 | packages: write 54 | steps: 55 | - name: Checkout 56 | uses: actions/checkout@v4 57 | 58 | - name: Set up QEMU 59 | uses: docker/setup-qemu-action@v3 60 | 61 | - name: Set up Docker Buildx 62 | uses: docker/setup-buildx-action@v3 63 | 64 | - name: Log in to Docker Hub 65 | uses: docker/login-action@v3 66 | with: 67 | username: ${{ secrets.DOCKERHUB_USERNAME }} 68 | password: ${{ secrets.DOCKERHUB_TOKEN }} 69 | 70 | - name: Extract version from tag 71 | id: version 72 | run: echo "VERSION=${GITHUB_REF_NAME#v}" >> $GITHUB_OUTPUT 73 | 74 | - name: Build and push 75 | uses: docker/build-push-action@v6 76 | with: 77 | context: . 78 | platforms: linux/amd64,linux/arm64 79 | push: true 80 | tags: | 81 | datacontract/editor:latest 82 | datacontract/editor:${{ steps.version.outputs.VERSION }} 83 | cache-from: type=gha 84 | cache-to: type=gha,mode=max 85 | -------------------------------------------------------------------------------- /src/components/ui/ArrayInput.jsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | 3 | /** 4 | * ArrayInput component for editing array of strings 5 | * Allows adding/removing items dynamically 6 | */ 7 | const ArrayInput = ({ label, value = [], onChange, placeholder = "Add item...", helpText }) => { 8 | const [newItem, setNewItem] = useState(''); 9 | 10 | const handleAdd = () => { 11 | if (newItem.trim()) { 12 | const updatedArray = [...value, newItem.trim()]; 13 | onChange(updatedArray); 14 | setNewItem(''); 15 | } 16 | }; 17 | 18 | const handleRemove = (index) => { 19 | const updatedArray = value.filter((_, i) => i !== index); 20 | onChange(updatedArray.length > 0 ? updatedArray : undefined); 21 | }; 22 | 23 | const handleKeyPress = (e) => { 24 | if (e.key === 'Enter') { 25 | e.preventDefault(); 26 | handleAdd(); 27 | } 28 | }; 29 | 30 | return ( 31 |
32 | 33 | 34 | {/* Display existing items */} 35 | {value.length > 0 && ( 36 |
37 | {value.map((item, index) => ( 38 |
39 | {item} 40 | 47 |
48 | ))} 49 |
50 | )} 51 | 52 | {/* Add new item */} 53 |
54 | setNewItem(e.target.value)} 58 | onKeyPress={handleKeyPress} 59 | className="flex-1 rounded border border-gray-300 bg-white px-2 py-1 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 text-xs" 60 | placeholder={placeholder} 61 | /> 62 | 69 |
70 | 71 | {helpText &&

{helpText}

} 72 |
73 | ); 74 | }; 75 | 76 | export default ArrayInput; 77 | -------------------------------------------------------------------------------- /src/assets/support-icons/googlechat.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /public/logo_fuchsia_v2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/components/ui/ValidatedInput.jsx: -------------------------------------------------------------------------------- 1 | import { forwardRef } from 'react'; 2 | import Tooltip from './Tooltip.jsx'; 3 | import QuestionMarkCircleIcon from "./icons/QuestionMarkCircleIcon.jsx"; 4 | 5 | /** 6 | * A self-validating input component that shows errors when required field is empty 7 | * Follows Tailwind UI patterns for form validation 8 | * Supports ref forwarding for auto-focus functionality 9 | */ 10 | const ValidatedInput = forwardRef(({ 11 | name, 12 | label, 13 | value, 14 | onChange, 15 | required = false, 16 | tooltip, 17 | placeholder, 18 | className = '', 19 | externalErrors = [], 20 | ...props 21 | }, ref) => { 22 | 23 | // Internal validation - check if required field is empty 24 | const hasInternalError = required && (!value || value.toString().trim() === ''); 25 | 26 | // Combine internal and external errors 27 | const hasError = hasInternalError || externalErrors.length > 0; 28 | 29 | // Prepare error messages 30 | const errorMessages = []; 31 | if (hasInternalError) { 32 | errorMessages.push('This field is required'); 33 | } 34 | errorMessages.push(...externalErrors); 35 | 36 | // Determine ring color based on error state 37 | const ringClass = hasError 38 | ? 'ring-red-300 focus:ring-red-500' 39 | : 'ring-gray-300 focus:ring-indigo-600'; 40 | 41 | return ( 42 |
43 |
44 | 47 | {tooltip && ( 48 | 49 | 50 | 51 | )} 52 | {required && ( 53 | Required 54 | )} 55 |
56 | 69 | {hasError && errorMessages.map((message, idx) => ( 70 |

71 | {message} 72 |

73 | ))} 74 |
75 | ); 76 | }); 77 | 78 | ValidatedInput.displayName = 'ValidatedInput'; 79 | 80 | export default ValidatedInput; 81 | -------------------------------------------------------------------------------- /src/assets/server-icons/athena.svg: -------------------------------------------------------------------------------- 1 | Aws Athena Streamline Icon: https://streamlinehq.com 2 | -------------------------------------------------------------------------------- /src/components/ui/PropertyValueRenderer.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * PropertyValueRenderer - Recursively renders complex property values 3 | * Handles primitives, arrays, objects, and nested structures 4 | */ 5 | export default function PropertyValueRenderer({ value, depth = 0 }) { 6 | // Handle null or undefined 7 | if (value === null || value === undefined) { 8 | return null; 9 | } 10 | 11 | // Handle primitives 12 | if (typeof value === 'string') { 13 | return {value}; 14 | } 15 | 16 | if (typeof value === 'number') { 17 | return {value}; 18 | } 19 | 20 | if (typeof value === 'boolean') { 21 | return {value.toString()}; 22 | } 23 | 24 | // Handle arrays 25 | if (Array.isArray(value)) { 26 | if (value.length === 0) { 27 | return []; 28 | } 29 | 30 | // Check if array contains only primitives for compact display 31 | const allPrimitives = value.every(item => 32 | typeof item === 'string' || typeof item === 'number' || typeof item === 'boolean' 33 | ); 34 | 35 | if (allPrimitives && value.length <= 3) { 36 | return ( 37 | 38 | [{value.map((item, idx) => ( 39 | 40 | 41 | {idx < value.length - 1 && ', '} 42 | 43 | ))}] 44 | 45 | ); 46 | } 47 | 48 | // Display array items as a list 49 | return ( 50 |
51 | {value.map((item, index) => ( 52 |
53 | 54 |
55 | 56 |
57 |
58 | ))} 59 |
60 | ); 61 | } 62 | 63 | // Handle objects 64 | if (typeof value === 'object') { 65 | const entries = Object.entries(value); 66 | 67 | if (entries.length === 0) { 68 | return {'{}'}; 69 | } 70 | 71 | return ( 72 |
0 ? 'pl-4 border-l-2 border-gray-200' : ''}`}> 73 | {entries.map(([key, val]) => ( 74 |
75 |
76 | {key}: 77 |
78 |
79 | 80 |
81 |
82 | ))} 83 |
84 | ); 85 | } 86 | 87 | // Fallback for unknown types 88 | return {String(value)}; 89 | } 90 | -------------------------------------------------------------------------------- /src/assets/link-icons/gitlab.jsx: -------------------------------------------------------------------------------- 1 | export default () => 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/support-icons/teams.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /src/assets/link-icons/teams.jsx: -------------------------------------------------------------------------------- 1 | export default () => 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /src/components/ui/icons/AuthoritativeDefinitionsIcon.jsx: -------------------------------------------------------------------------------- 1 | const AuthoritativeDefinitionsIcon = ({ className = "w-4 h-4" }) => ( 2 | 7 | 8 | 13 | 18 | 23 | 28 | 29 | 30 | ); 31 | 32 | export default AuthoritativeDefinitionsIcon; 33 | -------------------------------------------------------------------------------- /src/utils/schemaPathBuilder.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Utilities for building schema property paths used with zustand store 3 | * Paths follow the format: schema[0].properties[1].items.properties[2].fieldName 4 | */ 5 | 6 | /** 7 | * Build a path string to a property 8 | * @param {number} schemaIdx - Schema index 9 | * @param {Array} propPath - Array of path segments (numbers for indices, 'items' for items nodes) 10 | * @param {string} field - Optional field name to append 11 | * @returns {string} Path string like "schema[0].properties[1].items.logicalType" 12 | */ 13 | export function buildPropertyPath(schemaIdx, propPath, field = null) { 14 | let pathStr = `schema[${schemaIdx}].properties`; 15 | 16 | for (let i = 0; i < propPath.length; i++) { 17 | if (propPath[i] === 'items') { 18 | pathStr += '.items'; 19 | // If there are more segments after 'items', continue to properties 20 | if (i < propPath.length - 1) { 21 | pathStr += '.properties'; 22 | } 23 | } else { 24 | pathStr += `[${propPath[i]}]`; 25 | // If next segment is not 'items', navigate to properties 26 | if (i < propPath.length - 1 && propPath[i + 1] !== 'items') { 27 | pathStr += '.properties'; 28 | } 29 | } 30 | } 31 | 32 | if (field) { 33 | pathStr += `.${field}`; 34 | } 35 | 36 | return pathStr; 37 | } 38 | 39 | /** 40 | * Build a path string to an items object 41 | * @param {number} schemaIdx - Schema index 42 | * @param {Array} propPath - Array of path segments 43 | * @param {string} field - Optional field name to append 44 | * @returns {string} Path string to items object 45 | */ 46 | export function buildItemsPath(schemaIdx, propPath, field = null) { 47 | let pathStr = buildPropertyPath(schemaIdx, propPath); 48 | pathStr += '.items'; 49 | 50 | if (field) { 51 | pathStr += `.${field}`; 52 | } 53 | 54 | return pathStr; 55 | } 56 | 57 | /** 58 | * Build a path string to a properties array 59 | * @param {number} schemaIdx - Schema index 60 | * @param {Array} propPath - Array of path segments 61 | * @param {boolean} isItems - Whether this is for items.properties 62 | * @returns {string} Path string to properties array 63 | */ 64 | export function buildPropertiesArrayPath(schemaIdx, propPath, isItems = false) { 65 | let pathStr = buildPropertyPath(schemaIdx, propPath); 66 | 67 | if (isItems) { 68 | pathStr += '.items.properties'; 69 | } else { 70 | pathStr += '.properties'; 71 | } 72 | 73 | return pathStr; 74 | } 75 | 76 | /** 77 | * Extract parent path and index from a bracket-notation path 78 | * @param {string} path - Path like "schema[0].properties[2]" 79 | * @returns {{parentPath: string, index: number}|null} Parent path and index, or null if invalid 80 | */ 81 | export function extractParentPathAndIndex(path) { 82 | const lastBracketIndex = path.lastIndexOf('['); 83 | if (lastBracketIndex === -1) { 84 | return null; 85 | } 86 | 87 | const parentPath = path.substring(0, lastBracketIndex); 88 | const indexStr = path.substring(lastBracketIndex + 1, path.length - 1); 89 | const index = parseInt(indexStr, 10); 90 | 91 | if (isNaN(index)) { 92 | return null; 93 | } 94 | 95 | return { parentPath, index }; 96 | } 97 | -------------------------------------------------------------------------------- /src/components/features/preview/RolesSection.jsx: -------------------------------------------------------------------------------- 1 | import { memo } from 'react'; 2 | import CustomPropertiesPreview from '../../ui/CustomPropertiesPreview.jsx'; 3 | import {useEditorStore} from "../../../store.js"; 4 | import {useShallow} from "zustand/react/shallow"; 5 | 6 | // Memoized Role Item component 7 | const RoleItem = memo(({ role }) => { 8 | return ( 9 |
  • 10 |
    11 | {role.role && ( 12 |
    13 |
    14 |
    Role
    15 |
    {role.role}
    16 |
    17 |
    18 | )} 19 | {role.access && ( 20 |
    21 |
    22 |
    Access
    23 |
    {role.access}
    24 |
    25 |
    26 | )} 27 | {role.firstLevelApprovers && ( 28 |
    29 |
    30 |
    First Level Approvers
    31 |
    {role.firstLevelApprovers}
    32 |
    33 |
    34 | )} 35 | {role.secondLevelApprovers && ( 36 |
    37 |
    38 |
    Second Level Approvers
    39 |
    {role.secondLevelApprovers}
    40 |
    41 |
    42 | )} 43 |
    44 |
    45 |
    Custom Properties
    46 |
    47 | 48 |
    49 |
    50 |
    51 |
    52 |
  • 53 | ); 54 | }, (prevProps, nextProps) => { 55 | try { 56 | return JSON.stringify(prevProps.role) === JSON.stringify(nextProps.role); 57 | } catch { 58 | return false; 59 | } 60 | }); 61 | 62 | RoleItem.displayName = 'RoleItem'; 63 | 64 | // Main RolesSection component 65 | const RolesSection = () => { 66 | const roles = useEditorStore(useShallow(state => state.getValue('roles'))); 67 | if (!roles || roles.length === 0) return null; 68 | 69 | return ( 70 |
    71 |
    72 |

    Roles

    73 |

    Support and communication channels help consumers find help 74 | regarding 75 | their use of the data contract

    76 |
    77 |
      79 | {roles.map((role, index) => ( 80 | 81 | ))} 82 |
    83 |
    84 | ); 85 | } 86 | 87 | RolesSection.displayName = 'RolesSection'; 88 | 89 | export default RolesSection; 90 | -------------------------------------------------------------------------------- /src/components/features/SettingsModal.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import Modal from '../ui/Modal'; 3 | import { useEditorStore, setEditorConfig } from '../../store'; 4 | 5 | const SettingsModal = ({ isOpen, onClose }) => { 6 | const editorConfig = useEditorStore((state) => state.editorConfig); 7 | 8 | // Local state for form values 9 | const [apiServerUrl, setApiServerUrl] = useState(''); 10 | const [apiKey, setApiKey] = useState(''); 11 | 12 | // Initialize form values when modal opens 13 | useEffect(() => { 14 | if (isOpen) { 15 | setApiServerUrl(editorConfig?.tests?.dataContractCliApiServerUrl || ''); 16 | setApiKey(editorConfig?.tests?.apiKey || ''); 17 | } 18 | }, [isOpen, editorConfig]); 19 | 20 | const handleSave = () => { 21 | // Update the editor config 22 | setEditorConfig({ 23 | tests: { 24 | enabled: true, 25 | dataContractCliApiServerUrl: apiServerUrl.trim() || null, 26 | apiKey: apiKey.trim() || null, 27 | }, 28 | }); 29 | onClose(); 30 | }; 31 | 32 | const handleCancel = () => { 33 | // Reset to original values 34 | setApiServerUrl(editorConfig?.tests?.dataContractCliApiServerUrl || ''); 35 | setApiKey(editorConfig?.tests?.apiKey || ''); 36 | onClose(); 37 | }; 38 | 39 | return ( 40 | 49 |
    50 |
    51 | 54 | setApiServerUrl(e.target.value)} 59 | placeholder="https://api.datacontract.com" 60 | className="w-full px-3 py-2 text-sm border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500" 61 | /> 62 |

    63 | Current: {apiServerUrl || 'https://api.datacontract.com'} 64 |

    65 |

    66 | The base URL for the Data Contract CLI API server. Leave empty to use the default. 67 |

    68 |
    69 |
    70 | 73 | setApiKey(e.target.value)} 78 | placeholder="Enter your API key" 79 | className="w-full px-3 py-2 text-sm border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500" 80 | /> 81 |

    82 | Optional API key for authentication with the Data Contract CLI API server. 83 |

    84 |
    85 |
    86 |
    87 | ); 88 | }; 89 | 90 | export default SettingsModal; 91 | -------------------------------------------------------------------------------- /src/components/ui/TypeSelector/TypeSelector.jsx: -------------------------------------------------------------------------------- 1 | import { Popover, PopoverButton, PopoverPanel } from '@headlessui/react'; 2 | import { useActiveServerType } from '../../../hooks/useActiveServerType'; 3 | import TypeSelectorPopover from './TypeSelectorPopover'; 4 | 5 | const ChevronDownIcon = ({ className }) => ( 6 | 9 | ); 10 | 11 | /** 12 | * TypeSelector - Compound component for logical and physical type selection 13 | * 14 | * Display logic: Shows physical type if set, otherwise falls back to logical type 15 | * Clicking opens a popover with both type selectors 16 | */ 17 | const TypeSelector = ({ 18 | logicalType, 19 | onLogicalTypeChange, 20 | physicalType, 21 | onPhysicalTypeChange, 22 | serverType: serverTypeProp, 23 | disabled = false, 24 | className = '', 25 | }) => { 26 | // Use provided server type or get from store 27 | const storeServerType = useActiveServerType(); 28 | const serverType = serverTypeProp || storeServerType; 29 | 30 | // Display: physical type if set, otherwise logical type 31 | const displayType = physicalType || logicalType || 'Select type...'; 32 | 33 | return ( 34 | 35 | {({ open, close }) => ( 36 | <> 37 | 50 | {/* Display type value */} 51 | 52 | {displayType} 53 | 54 | 55 | {/* Dropdown indicator - only visible on hover or when open */} 56 | 57 | 58 | 59 | e.stopPropagation()} 64 | > 65 | { 68 | onLogicalTypeChange(value); 69 | }} 70 | physicalType={physicalType} 71 | onPhysicalTypeChange={(value) => { 72 | onPhysicalTypeChange(value); 73 | }} 74 | serverType={serverType} 75 | disabled={disabled} 76 | /> 77 | 78 | 79 | )} 80 | 81 | ); 82 | }; 83 | 84 | export default TypeSelector; 85 | -------------------------------------------------------------------------------- /src/components/error/PreviewErrorBoundary.jsx: -------------------------------------------------------------------------------- 1 | import ErrorBoundary from './ErrorBoundary.jsx'; 2 | 3 | /** 4 | * Specialized error boundary for the DataContractPreview component 5 | * Provides context-specific error messaging and recovery options 6 | */ 7 | const PreviewErrorBoundary = ({ children, yamlContent }) => { 8 | const fallback = ({ error, resetError }) => ( 9 |
    10 |
    11 |
    12 |
    13 | 14 | 15 | 16 |
    17 |
    18 |

    19 | Preview Error 20 |

    21 |

    22 | The preview could not be rendered. This usually happens when the YAML contains unexpected data structures. 23 |

    24 | 25 |
    26 | Error: 27 | {error?.message || 'Unknown error'} 28 |
    29 | 30 |
    31 |

    Suggestions:

    32 |
      33 |
    • Check your YAML syntax for errors
    • 34 |
    • Verify data contract structure matches the ODCS schema
    • 35 |
    • Try editing the YAML directly to fix the issue
    • 36 |
    37 |
    38 | 39 |
    40 | 46 |
    47 | 48 | {process.env.NODE_ENV === 'development' && ( 49 |
    50 | 51 | Developer Details 52 | 53 |
    54 |                   {error?.stack}
    55 |                 
    56 |
    57 | )} 58 |
    59 |
    60 |
    61 |
    62 | ); 63 | 64 | return ( 65 | { 69 | console.error('Preview rendering error:', { 70 | error, 71 | errorInfo, 72 | yamlLength: yamlContent?.length, 73 | }); 74 | }} 75 | > 76 | {children} 77 | 78 | ); 79 | }; 80 | 81 | export default PreviewErrorBoundary; 82 | -------------------------------------------------------------------------------- /src/assets/link-icons/snowflake.jsx: -------------------------------------------------------------------------------- 1 | export default ({ className }) => ( 2 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 22 | 23 | 24 | ); 25 | -------------------------------------------------------------------------------- /src/assets/link-icons/powerbi.jsx: -------------------------------------------------------------------------------- 1 | export default () => Microsoft Power Bi Streamline Icon: https://streamlinehq.com 2 | -------------------------------------------------------------------------------- /src/assets/server-icons/oracle.svg: -------------------------------------------------------------------------------- 1 | Oracle Streamline Icon: https://streamlinehq.com 2 | -------------------------------------------------------------------------------- /src/assets/link-icons/collibra.jsx: -------------------------------------------------------------------------------- 1 | export default ({ className }) => ( 2 | 13 | 14 | 18 | 19 | 20 | ); 21 | -------------------------------------------------------------------------------- /src/assets/link-icons/datawarehouse.jsx: -------------------------------------------------------------------------------- 1 | export default () => 2 | -------------------------------------------------------------------------------- /src/components/ui/Tags.jsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import Tooltip from './Tooltip.jsx'; 3 | import QuestionMarkCircleIcon from './icons/QuestionMarkCircleIcon.jsx'; 4 | 5 | /** 6 | * Tags component for managing a list of tags 7 | * Allows adding/removing tags with a clean UI 8 | */ 9 | const Tags = ({ 10 | label = "Tags", 11 | value = [], 12 | onChange, 13 | placeholder = "Add a tag...", 14 | tooltip, 15 | className = '' 16 | }) => { 17 | const [newTag, setNewTag] = useState(''); 18 | 19 | const handleAdd = () => { 20 | if (newTag.trim()) { 21 | const updatedTags = [...value, newTag.trim()]; 22 | onChange(updatedTags); 23 | setNewTag(''); 24 | } 25 | }; 26 | 27 | const handleRemove = (index) => { 28 | const updatedTags = value.filter((_, i) => i !== index); 29 | onChange(updatedTags.length > 0 ? updatedTags : undefined); 30 | }; 31 | 32 | const handleKeyPress = (e) => { 33 | if (e.key === 'Enter') { 34 | e.preventDefault(); 35 | handleAdd(); 36 | } 37 | }; 38 | 39 | return ( 40 |
    41 | {label && ( 42 |
    43 | 46 | {tooltip && ( 47 | 48 | 49 | 50 | )} 51 |
    52 | )} 53 | 54 |
    55 | {/* Display existing tags */} 56 | {value && value.length > 0 && ( 57 |
    58 | {value.map((tag, index) => ( 59 | 63 | 66 | {tag} 67 | 77 | 78 | ))} 79 |
    80 | )} 81 | 82 | {/* Add new tag input */} 83 |
    84 | setNewTag(e.target.value)} 88 | onKeyPress={handleKeyPress} 89 | className="flex-1 rounded-md bg-white border-0 py-1.5 pl-2 pr-3 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 text-xs leading-4" 90 | placeholder={placeholder} 91 | /> 92 | 99 |
    100 |
    101 |
    102 | ); 103 | }; 104 | 105 | export default Tags; 106 | -------------------------------------------------------------------------------- /src/components/diagram/layoutUtils.js: -------------------------------------------------------------------------------- 1 | import dagre from 'dagre'; 2 | 3 | /** 4 | * Layout nodes using the Dagre graph layout algorithm 5 | * @param {Array} schemas - Array of schema objects to layout 6 | * @returns {Array} - Schemas with calculated positions 7 | */ 8 | export const getLayoutedElements = (schemas) => { 9 | const dagreGraph = new dagre.graphlib.Graph(); 10 | dagreGraph.setDefaultEdgeLabel(() => ({})); 11 | // Configure graph for better edge handling 12 | dagreGraph.setGraph({ 13 | rankdir: 'LR', // Left to right to match our left-to-right edges 14 | nodesep: 80, // Horizontal spacing between nodes 15 | ranksep: 150, // Vertical spacing between ranks 16 | edgesep: 50, // Spacing between edges 17 | marginx: 50, 18 | marginy: 50 19 | }); 20 | 21 | // Add nodes to the graph 22 | schemas.forEach((schema, index) => { 23 | // Estimate height based on number of properties 24 | const propertyCount = schema.properties?.length || 0; 25 | const nodeHeight = Math.max(150, 80 + propertyCount * 40); 26 | dagreGraph.setNode(`schema-${index}`, { width: 250, height: nodeHeight }); 27 | }); 28 | 29 | // Add edges to the graph based on property relationships 30 | schemas.forEach((schema, sourceIndex) => { 31 | if (!schema.properties) return; 32 | 33 | schema.properties.forEach((prop) => { 34 | // Check ODCS relationships first 35 | const relationships = prop?.relationships || []; 36 | 37 | relationships.forEach(relationship => { 38 | const reference = relationship.to; 39 | if (reference && typeof reference === 'string') { 40 | const [targetSchemaName] = reference.split('.'); 41 | 42 | // Find the target schema index 43 | const targetIndex = schemas.findIndex(s => s?.name === targetSchemaName); 44 | 45 | if (targetIndex !== -1) { 46 | // Add edge to Dagre graph (reversed direction: target -> source) 47 | // The referenced schema should point to the schema with the reference 48 | dagreGraph.setEdge(`schema-${targetIndex}`, `schema-${sourceIndex}`); 49 | } 50 | } 51 | }); 52 | 53 | // Backward compatibility: also check customProperties.references 54 | if (prop?.customProperties?.references) { 55 | const [targetSchemaName] = prop.customProperties.references.split('.'); 56 | 57 | // Find the target schema index 58 | const targetIndex = schemas.findIndex(s => s?.name === targetSchemaName); 59 | 60 | if (targetIndex !== -1) { 61 | // Add edge to Dagre graph (reversed direction: target -> source) 62 | // The referenced schema should point to the schema with the reference 63 | dagreGraph.setEdge(`schema-${targetIndex}`, `schema-${sourceIndex}`); 64 | } 65 | } 66 | }); 67 | }); 68 | 69 | // Run the layout algorithm 70 | dagre.layout(dagreGraph); 71 | 72 | // Apply calculated positions to schemas 73 | return schemas.map((schema, index) => { 74 | const nodeWithPosition = dagreGraph.node(`schema-${index}`); 75 | return { 76 | ...schema, 77 | position: { 78 | x: nodeWithPosition.x - 125, // Center the node 79 | y: nodeWithPosition.y - (nodeWithPosition.height / 2), 80 | }, 81 | }; 82 | }); 83 | }; 84 | 85 | /** 86 | * Calculate grid-based position for a schema node 87 | * @param {number} index - Index of the schema in the array 88 | * @returns {Object} - Position object with x and y coordinates 89 | */ 90 | export const getGridPosition = (index) => { 91 | const cols = 3; 92 | const nodeWidth = 300; 93 | const nodeHeight = 400; 94 | const startX = 50; 95 | const startY = 50; 96 | 97 | return { 98 | x: startX + (index % cols) * nodeWidth, 99 | y: startY + Math.floor(index / cols) * nodeHeight 100 | }; 101 | }; 102 | -------------------------------------------------------------------------------- /src/assets/link-icons/kafka.jsx: -------------------------------------------------------------------------------- 1 | export default () => 7 | 8 | 9 | 10 | image/svg+xml 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/assets/server-icons/serverIcons.jsx: -------------------------------------------------------------------------------- 1 | // Server icons - imports SVG files for all server types with fallback to database.svg 2 | 3 | // Import SVG files as URLs 4 | import ApiIcon from './api.svg'; 5 | import AthenaIcon from './athena.svg'; 6 | import AzureIcon from './azure.svg'; 7 | import BigQueryIcon from './bigquery.svg'; 8 | import ClickHouseIcon from './clickhouse.svg'; 9 | import CloudSQLIcon from './cloudsql.svg'; 10 | import CustomIcon from './custom.svg'; 11 | import DatabaseIcon from './database.svg'; 12 | import DatabricksIcon from './databricks.svg'; 13 | import DB2Icon from './db2.svg'; 14 | import DenodoIcon from './denodo.svg'; 15 | import DremioIcon from './dremio.svg'; 16 | import DuckDBIcon from './duckdb.svg'; 17 | import GlueIcon from './glue.svg'; 18 | import HiveIcon from './hive.svg'; 19 | import InformixIcon from './informix.svg'; 20 | import KafkaIcon from './kafka.svg'; 21 | import KinesisIcon from './kinesis.svg'; 22 | import LocalIcon from './local.svg'; 23 | import MySQLIcon from './mysql.svg'; 24 | import OracleIcon from './oracle.svg'; 25 | import PostgreSQLIcon from './postgresql.svg'; 26 | import PostgresIcon from './postgres.svg'; 27 | import PrestoIcon from './presto.svg'; 28 | import PubSubIcon from './pubsub.svg'; 29 | import RedshiftIcon from './redshift.svg'; 30 | import S3Icon from './s3.svg'; 31 | import SFTPIcon from './sftp.svg'; 32 | import SnowflakeIcon from './snowflake.svg'; 33 | import SQLServerIcon from './sqlserver.svg'; 34 | import SynapseIcon from './synapse.svg'; 35 | import TrinoIcon from './trino.svg'; 36 | import VerticaIcon from './vertica.svg'; 37 | 38 | // Wrapper component to render SVG as img tag with consistent styling 39 | const IconWrapper = ({ src }) => ; 40 | 41 | // Map server types to their icon components with database.svg as fallback 42 | const serverIcons = { 43 | api: () => , 44 | athena: () => , 45 | azure: () => , 46 | bigquery: () => , 47 | clickhouse: () => , 48 | cloudsql: () => , 49 | custom: () => , 50 | databricks: () => , 51 | db2: () => , 52 | denodo: () => , 53 | dremio: () => , 54 | duckdb: () => , 55 | glue: () => , 56 | hive: () => , 57 | informix: () => , 58 | kafka: () => , 59 | kinesis: () => , 60 | local: () => , 61 | mysql: () => , 62 | oracle: () => , 63 | postgresql: () => , 64 | postgres: () => , 65 | presto: () => , 66 | pubsub: () => , 67 | redshift: () => , 68 | s3: () => , 69 | sftp: () => , 70 | snowflake: () => , 71 | sqlserver: () => , 72 | synapse: () => , 73 | trino: () => , 74 | vertica: () => , 75 | 76 | // Fallback for unknown server types 77 | database: () => , 78 | }; 79 | 80 | // Export function that returns icon component with fallback to database.svg 81 | export default new Proxy(serverIcons, { 82 | get(target, prop) { 83 | // If the requested icon exists, return it 84 | if (prop in target) { 85 | return target[prop]; 86 | } 87 | // Otherwise, return the database fallback icon 88 | return target.database; 89 | } 90 | }); 91 | -------------------------------------------------------------------------------- /src/assets/server-icons/custom.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/server-icons/database.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/error/FormPageErrorBoundary.jsx: -------------------------------------------------------------------------------- 1 | import ErrorBoundary from './ErrorBoundary.jsx'; 2 | import { useEditorStore } from '../../store.js'; 3 | 4 | /** 5 | * Specialized error boundary for form pages (Overview, Schema, Servers, etc.) 6 | * Provides context-specific error messaging and recovery options 7 | */ 8 | const FormPageErrorBoundary = ({ children, pageName = 'form' }) => { 9 | const setView = useEditorStore((state) => state.setView); 10 | const yaml = useEditorStore((state) => state.yaml); 11 | 12 | const fallback = ({ error, resetError }) => ( 13 |
    14 |
    15 |
    16 |
    17 | 18 | 19 | 20 |
    21 |
    22 |

    23 | Form Error 24 |

    25 |

    26 | The {pageName} form encountered an error and could not be displayed. 27 |

    28 | 29 |
    30 | Error: 31 | {error?.message || 'Unknown form error'} 32 |
    33 | 34 |
    35 |

    What you can do:

    36 |
      37 |
    • Switch to YAML view to edit your data contract directly
    • 38 |
    • Check if your YAML structure is valid
    • 39 |
    • Try navigating to a different form page
    • 40 |
    • Your data is safe - the YAML has been preserved
    • 41 |
    42 |
    43 | 44 |
    45 | 51 | 57 |
    58 | 59 | {process.env.NODE_ENV === 'development' && ( 60 |
    61 | 62 | Developer Details 63 | 64 |
    65 |                   {error?.stack}
    66 |                 
    67 |
    68 | )} 69 |
    70 |
    71 |
    72 |
    73 | ); 74 | 75 | return ( 76 | { 80 | console.error(`Form page (${pageName}) error:`, { 81 | error, 82 | errorInfo, 83 | pageName, 84 | }); 85 | }} 86 | > 87 | {children} 88 | 89 | ); 90 | }; 91 | 92 | export default FormPageErrorBoundary; 93 | -------------------------------------------------------------------------------- /src/components/features/preview/SlaSection.jsx: -------------------------------------------------------------------------------- 1 | import { memo } from 'react'; 2 | import {useEditorStore} from "../../../store.js"; 3 | import {useShallow} from "zustand/react/shallow"; 4 | 5 | // Memoized SLA Item component 6 | const SlaItem = memo(({ sla }) => { 7 | return ( 8 |
  • 9 |
    10 | {sla.property && ( 11 |
    12 |
    13 |
    Property
    14 |
    {sla.property}
    15 |
    16 |
    17 | )} 18 | {sla.value !== undefined && sla.value !== null && ( 19 |
    20 |
    21 |
    Value
    22 |
    {sla.value}
    23 |
    24 |
    25 | )} 26 | {sla.unit && ( 27 |
    28 |
    29 |
    Unit
    30 |
    {sla.unit}
    31 |
    32 |
    33 | )} 34 | {sla.element && ( 35 |
    36 |
    37 |
    Element(s)
    38 |
    {sla.element}
    39 |
    40 |
    41 | )} 42 | {sla.driver && ( 43 |
    44 |
    45 |
    Driver
    46 |
    {sla.driver}
    47 |
    48 |
    49 | )} 50 | {sla.scheduler && ( 51 |
    52 |
    53 |
    Scheduler
    54 |
    {sla.scheduler}
    55 |
    56 |
    57 | )} 58 | {sla.schedule && ( 59 |
    60 |
    61 |
    Schedule
    62 |
    {sla.schedule}
    63 |
    64 |
    65 | )} 66 | {sla.description && ( 67 |
    68 |
    69 |
    Description
    70 |
    {sla.description}
    71 |
    72 |
    73 | )} 74 |
    75 |
  • 76 | ); 77 | }, (prevProps, nextProps) => { 78 | try { 79 | return JSON.stringify(prevProps.sla) === JSON.stringify(nextProps.sla); 80 | } catch { 81 | return false; 82 | } 83 | }); 84 | 85 | SlaItem.displayName = 'SlaItem'; 86 | 87 | // Main SlaSection component 88 | const SlaSection = () => { 89 | const slaProperties = useEditorStore(useShallow(state => state.getValue('slaProperties'))); 90 | if (!slaProperties || slaProperties.length === 0) return null; 91 | 92 | return ( 93 |
    94 |
    95 |

    Service-Level Agreement 96 | (SLA)

    97 |

    This section describes the service-level agreements (SLA).

    98 |
    99 |
      101 | {slaProperties.map((sla, index) => ( 102 | 103 | ))} 104 |
    105 |
    106 | ); 107 | } 108 | 109 | SlaSection.displayName = 'SlaSection'; 110 | 111 | export default SlaSection; 112 | -------------------------------------------------------------------------------- /src/components/ui/TypeSelector/LogicalTypeSelect.jsx: -------------------------------------------------------------------------------- 1 | import { Listbox, ListboxButton, ListboxOption, ListboxOptions } from '@headlessui/react'; 2 | import { getLogicalTypeIcon, fallbackLogicalTypeOptions } from '../../features/schema/propertyIcons'; 3 | 4 | const ChevronDownIcon = ({ className }) => ( 5 | 8 | ); 9 | 10 | const CheckIcon = ({ className }) => ( 11 | 14 | ); 15 | 16 | /** 17 | * LogicalTypeSelect - Dropdown for selecting logical types with icons 18 | */ 19 | const LogicalTypeSelect = ({ 20 | value, 21 | onChange, 22 | disabled = false, 23 | className = '', 24 | label = 'Logical Type', 25 | }) => { 26 | const IconComponent = getLogicalTypeIcon(value); 27 | 28 | return ( 29 | 30 |
    31 | {label && ( 32 | 33 | {label} 34 | 35 | )} 36 | 37 | 38 | {IconComponent && ( 39 | 40 | 41 | 42 | )} 43 | {value || 'Select type...'} 44 | 45 | 46 | 47 | 48 | 49 | 50 | e.stopPropagation()} 55 | > 56 | {fallbackLogicalTypeOptions.map((type) => { 57 | const TypeIcon = getLogicalTypeIcon(type); 58 | return ( 59 | 64 | 65 | {TypeIcon && ( 66 | 67 | 68 | 69 | )} 70 | 71 | {type} 72 | 73 | 74 | 75 | 76 | 77 | 78 | ); 79 | })} 80 | 81 |
    82 |
    83 | ); 84 | }; 85 | 86 | export default LogicalTypeSelect; 87 | --------------------------------------------------------------------------------