From d6d2ffa881e4496b9a0abf7a5e1973565fb0e2a0 Mon Sep 17 00:00:00 2001 From: Matt Jenkinson <75292329+mattdjenkinson@users.noreply.github.com> Date: Fri, 17 Apr 2026 15:56:44 +0100 Subject: [PATCH 1/2] add datumctl passthrough to could-portal --- .env.example | 255 +- .github/workflows/ci.yml | 740 +- .github/workflows/set-project-month.yaml | 20 +- .gitignore | 62 +- .tool-versions | 4 +- Dockerfile | 142 +- app/components/avatar-stack/avatar-stack.tsx | 148 +- app/components/back-button/back-button.tsx | 212 +- app/components/badge/badge-copy.tsx | 190 +- app/components/badge/badge-status.tsx | 364 +- .../chips-overflow/chips-overflow.tsx | 306 +- .../code-editor/code-editor-tabs.tsx | 386 +- app/components/code-editor/code-editor.tsx | 302 +- .../code-editor/code-editor.types.ts | 162 +- .../coming-soon/coming-soon-feature-card.tsx | 66 +- .../confirmation-dialog.tsx | 368 +- .../content-wrapper/content-wrapper.tsx | 44 +- app/components/danger-card/danger-card.tsx | 108 +- app/components/date-time/date-time.tsx | 436 +- app/components/demo/alert.tsx | 1034 +- app/components/demo/badge.tsx | 566 +- app/components/demo/button.tsx | 796 +- app/components/demo/form-fields.tsx | 1356 +- app/components/demo/grid.tsx | 382 +- app/components/demo/status-badge.tsx | 620 +- app/components/demo/toast.tsx | 372 +- app/components/demo/tooltip.tsx | 752 +- app/components/error/auth.tsx | 58 +- app/components/error/generic.tsx | 130 +- app/components/field/field-label.tsx | 118 +- app/components/field/field.tsx | 106 +- app/components/header/breadcrumb.tsx | 266 +- app/components/header/header.tsx | 226 +- app/components/header/index.ts | 14 +- .../header/mobile-switcher-sheet.tsx | 631 +- .../header/org-project-switcher.tsx | 172 +- app/components/header/org-switcher.tsx | 80 +- app/components/header/project-switcher.tsx | 293 +- app/components/header/search-bar.tsx | 170 +- app/components/header/user-dropdown.tsx | 286 +- app/components/input-name/input-name.tsx | 226 +- app/components/key-value-form-dialog.tsx | 152 +- app/components/list/list.tsx | 108 +- app/components/logo/logo-icon.tsx | 84 +- app/components/logo/logo-text.tsx | 100 +- app/components/metadata/metadata-form.tsx | 94 +- app/components/metadata/metadata-preview.tsx | 88 +- app/components/mobile-menu/mobile-menu.tsx | 202 +- app/components/multi-select/multi-select.tsx | 1170 +- app/components/note-card/note-card.tsx | 156 +- .../notification-checkbox-item.tsx | 42 +- .../notification-settings-card-skeleton.tsx | 118 +- .../notification-settings-card.tsx | 120 +- .../items/invitation-notification-item.tsx | 278 +- .../notification/notification-bell.tsx | 72 +- .../notification/notification-dropdown.tsx | 150 +- .../notification-item-wrapper.tsx | 58 +- .../personal-badge/personal-badge.tsx | 32 +- .../profile-identity/profile-identity.tsx | 194 +- .../select-annotations/select-annotations.tsx | 246 +- .../select-autocomplete.tsx | 246 +- .../select-autocomplete.types.ts | 56 +- .../select-autocomplete/virtualized-list.tsx | 344 +- app/components/select-box/select-box.tsx | 726 +- app/components/select-group/select-group.tsx | 110 +- .../select-labels/select-labels.tsx | 244 +- .../select-member/select-member.tsx | 120 +- .../select-organization/organization-item.tsx | 42 +- .../select-organization.tsx | 302 +- .../select-project/select-project.tsx | 114 +- app/components/select-role/select-role.tsx | 208 +- .../select-secret/select-secret.tsx | 114 +- .../select-timezone/select-timezone.tsx | 192 +- app/components/select-timezone/timezones.json | 4996 ++-- .../status-pulse-dot/status-pulse-dot.tsx | 64 +- .../sub-navigation/sub-navigation-tabs.tsx | 144 +- app/components/text-copy/text-copy-box.tsx | 152 +- app/components/text-copy/text-copy.tsx | 106 +- app/components/waiting-page/waiting-page.tsx | 58 +- app/entry.client.tsx | 208 +- app/entry.server.tsx | 148 +- app/features/account/cards/2fa-card.tsx | 146 +- app/features/account/cards/danger-card.tsx | 104 +- app/features/account/cards/identity-card.tsx | 236 +- .../account/cards/newsletter-card.tsx | 88 +- .../account/cards/notification-card.tsx | 400 +- app/features/account/cards/profile-card.tsx | 206 +- .../account/cards/sign-in-method-card.tsx | 174 +- app/features/account/cards/team-auth-card.tsx | 40 +- .../account/identity-item-skeleton.tsx | 128 +- app/features/account/identity-item.tsx | 134 +- .../activity-log/activity-log-columns.tsx | 228 +- .../activity-log/activity-log-filters.ts | 18 +- .../activity-log/activity-log-table.tsx | 586 +- app/features/activity-log/index.ts | 30 +- .../activity-log/use-activity-log-table.ts | 706 +- .../connectors/connector-download-card.tsx | 186 +- .../dns-records/dns-record-ai-edge-cell.tsx | 238 +- .../edge/dns-records/dns-record-card.tsx | 72 +- .../dns-records/dns-record-modal-form.tsx | 176 +- .../edge/dns-records/dns-record-table.tsx | 484 +- .../components/dropzone-state-content.tsx | 104 +- .../components/import-result-table.tsx | 354 +- .../dns-record-import-action.tsx | 624 +- .../hooks/use-dns-record-export.ts | 96 +- app/features/edge/dns-records/index.ts | 22 +- app/features/edge/dns-records/types.ts | 212 +- .../components/refresh-nameservers-button.tsx | 274 +- .../edge/dns-zone/discovery-preview.tsx | 554 +- .../edge/dns-zone/dns-zone-form-dialog.tsx | 206 +- .../edge/dns-zone/form/dns-record-form.tsx | 522 +- .../dns-zone/form/types/a-record-field.tsx | 14 +- .../dns-zone/form/types/aaaa-record-field.tsx | 14 +- .../form/types/alias-record-field.tsx | 14 +- .../dns-zone/form/types/caa-record-field.tsx | 80 +- .../form/types/cname-record-field.tsx | 14 +- .../form/types/https-record-field.tsx | 34 +- .../dns-zone/form/types/mx-record-field.tsx | 26 +- .../dns-zone/form/types/ns-record-field.tsx | 14 +- .../dns-zone/form/types/ptr-record-field.tsx | 14 +- .../dns-zone/form/types/soa-record-field.tsx | 66 +- .../dns-zone/form/types/srv-record-field.tsx | 42 +- .../dns-zone/form/types/svcb-record-field.tsx | 34 +- .../dns-zone/form/types/tlsa-record-field.tsx | 58 +- .../dns-zone/form/types/txt-record-field.tsx | 14 +- .../dns-zone/overview/coming-soon-card.tsx | 62 +- .../overview/description-form-card.tsx | 186 +- .../overview/task-nameserver-card.tsx | 164 +- .../dns-zone/overview/task-record-card.tsx | 130 +- .../bulk-add/bulk-add-domains-action.tsx | 448 +- .../edge/domain/domain-form-dialog.tsx | 166 +- app/features/edge/domain/expiration.tsx | 58 +- .../edge/domain/overview/general-card.tsx | 254 +- .../edge/domain/overview/quick-setup-card.tsx | 230 +- .../domain/overview/verification-card.tsx | 232 +- app/features/edge/domain/select-domain.tsx | 578 +- app/features/edge/domain/status.tsx | 204 +- .../edge/nameservers/nameserver-card.tsx | 88 +- .../edge/nameservers/nameserver-table.tsx | 194 +- .../edge/proxy/form/hostnames-field.tsx | 126 +- .../proxy/form/protocol-endpoint-input.tsx | 156 +- .../proxy/form/subdomain-hostname-field.tsx | 718 +- app/features/edge/proxy/form/tls-field.tsx | 42 +- .../proxy/metrics/connector-sparkline.tsx | 272 +- .../edge/proxy/metrics/edge-requests.tsx | 300 +- .../edge/proxy/metrics/proxy-sparkline.tsx | 256 +- .../edge/proxy/metrics/upstream-table.tsx | 148 +- .../edge/proxy/metrics/waf-events.tsx | 222 +- .../edge/proxy/overview/active-pops-card.tsx | 258 +- .../edge/proxy/overview/config-card.tsx | 560 +- .../edge/proxy/overview/general-card.tsx | 418 +- .../proxy/overview/grafana-setup-card.tsx | 72 +- .../edge/proxy/overview/hostnames-card.tsx | 400 +- .../edge/proxy/overview/origins-card.tsx | 168 +- .../edge/proxy/proxy-basic-auth-dialog.tsx | 502 +- .../edge/proxy/proxy-display-name-dialog.tsx | 180 +- app/features/edge/proxy/proxy-form-dialog.tsx | 304 +- .../edge/proxy/proxy-hostnames-dialog.tsx | 222 +- .../edge/proxy/proxy-origins-dialog.tsx | 228 +- app/features/edge/proxy/proxy-waf-dialog.tsx | 346 +- .../export-policies/card/activity-card.tsx | 496 +- .../export-policies/card/coming-soon-card.tsx | 50 +- .../export-policies/card/general-card.tsx | 184 +- .../export-policies/card/grafana-card.tsx | 96 +- .../form/sink/prometheus/auth-field.tsx | 178 +- .../form/sink/prometheus/batch-field.tsx | 74 +- .../form/sink/prometheus/prometheus-field.tsx | 68 +- .../form/sink/prometheus/retry-field.tsx | 74 +- .../export-policies/form/sink/sink-field.tsx | 208 +- .../export-policies/form/sink/sinks-form.tsx | 144 +- .../form/sink/sinks-preview.tsx | 156 +- .../form/source/source-field.tsx | 110 +- .../form/source/sources-form.tsx | 110 +- .../form/source/sources-preview.tsx | 84 +- .../export-policies/form/stepper-form.tsx | 442 +- .../export-policies/form/update-form.tsx | 562 +- .../providers/grafana/grafana-dialog.tsx | 140 +- .../providers/grafana/grafana-form.tsx | 706 +- .../metric/export-policies/sinks-table.tsx | 328 +- .../metric/export-policies/sources-table.tsx | 228 +- app/features/notes/note-card.tsx | 96 +- app/features/notes/note-form-dialog.tsx | 268 +- app/features/notes/note-meta.tsx | 44 +- app/features/notes/notes-section.tsx | 300 +- .../cards/create-organization-card.tsx | 144 +- .../organization/cards/organization-card.tsx | 552 +- app/features/organization/list-card.tsx | 94 +- .../organization/settings/danger-card.tsx | 106 +- .../organization/settings/general-card.tsx | 212 +- .../organization/team/group-header.tsx | 182 +- .../organization/team/groups/group-form.tsx | 106 +- .../organization/team/groups/index.ts | 2 +- .../organization/team/invitation-form.tsx | 200 +- .../team/manage-members-dialog.tsx | 446 +- .../organization/team/manage-role.tsx | 268 +- .../organization/team/roles/action-bar.tsx | 124 +- .../team/roles/add-role-screen.tsx | 710 +- .../team/roles/permissions-panel.tsx | 438 +- .../organization/team/roles/role-row.tsx | 148 +- .../organization/team/roles/roles-panel.tsx | 308 +- .../form/policy-binding-form-dialog.tsx | 736 +- .../policy-binding/form/select-resource.tsx | 76 +- .../policy-binding/policy-binding-table.tsx | 68 +- .../policy-binding/policy-binding.helpers.tsx | 334 +- .../chat/assistant-message.tsx | 300 +- .../project-bottom-bar/chat/chat-input.tsx | 214 +- .../project-bottom-bar/chat/chat-panel.tsx | 716 +- .../project-bottom-bar/chat/chat-sidebar.tsx | 254 +- .../chat/thinking-block.tsx | 130 +- .../project-bottom-bar/chat/use-chat-logic.ts | 606 +- .../project-bottom-bar/project-bottom-bar.tsx | 489 +- .../project-bottom-bar/terminal-panel.tsx | 1004 +- app/features/project/dashboard.tsx | 214 +- app/features/project/settings/danger-card.tsx | 118 +- .../project/settings/general-card.tsx | 192 +- app/features/quotas/quotas-table.tsx | 294 +- .../form/edit/edit-key-value-dialog.tsx | 202 +- app/features/secret/form/edit/edit-keys.tsx | 250 +- .../secret/form/key-value-field-array.tsx | 164 +- .../secret/form/keys/keys-form-dialog.tsx | 204 +- .../secret/form/overview/general-card.tsx | 94 +- .../secret/form/secret-form-dialog.tsx | 218 +- app/hooks/useCopyToClipboard.ts | 106 +- app/hooks/useOs.ts | 104 +- app/layouts/blank.layout.tsx | 58 +- app/layouts/dashboard.layout.tsx | 280 +- app/layouts/private.layout.tsx | 257 +- app/layouts/sub/sub.layout.tsx | 188 +- app/layouts/sub/sub.types.ts | 22 +- app/modules/auth/strategies/zitadel.server.ts | 94 +- app/modules/axios/axios.server.ts | 438 +- app/modules/axios/request-context.ts | 94 +- app/modules/control-plane/iam/index.ts | 1670 +- app/modules/control-plane/iam/schemas.gen.ts | 7336 ++--- app/modules/control-plane/iam/sdk.gen.ts | 6570 ++--- app/modules/control-plane/iam/types.gen.ts | 22992 ++++++++-------- app/modules/control-plane/identity/index.ts | 114 +- .../control-plane/identity/schemas.gen.ts | 2268 +- app/modules/control-plane/identity/sdk.gen.ts | 238 +- .../control-plane/identity/types.gen.ts | 1662 +- app/modules/datum-ui/README.md | 434 +- .../datum-ui/components/alert/README.md | 886 +- .../datum-ui/components/alert/alert.tsx | 282 +- .../datum-ui/components/alert/index.ts | 2 +- .../components/avatar-stack/avatar-stack.tsx | 174 +- .../datum-ui/components/avatar-stack/index.ts | 2 +- .../datum-ui/components/badge/README.md | 634 +- .../datum-ui/components/badge/badge.tsx | 456 +- .../datum-ui/components/badge/index.ts | 2 +- .../datum-ui/components/button/README.md | 698 +- .../datum-ui/components/button/button.tsx | 684 +- .../datum-ui/components/button/index.tsx | 8 +- .../components/button/link-button.tsx | 142 +- .../calendar-date-picker.tsx | 1746 +- .../components/calendar-date-picker/index.ts | 2 +- .../datum-ui/components/calendar/calendar.tsx | 350 +- .../datum-ui/components/calendar/index.ts | 2 +- app/modules/datum-ui/components/card/card.tsx | 76 +- app/modules/datum-ui/components/card/index.ts | 2 +- .../components/data-table/MIGRATION.md | 1330 +- .../datum-ui/components/data-table/README.md | 3004 +- .../components/data-table/TOOLBAR_GUIDE.md | 858 +- .../data-table/core/data-table-card-view.tsx | 174 +- .../data-table/core/data-table-loading.tsx | 292 +- .../data-table/core/data-table-view.tsx | 232 +- .../data-table/core/data-table.context.tsx | 1544 +- .../components/data-table/core/data-table.tsx | 1474 +- .../data-table/core/data-table.types.ts | 862 +- .../actions/data-table-inline-actions.tsx | 172 +- .../actions/data-table-row-actions.tsx | 216 +- .../columns/data-table-column-header.tsx | 244 +- .../columns/data-table-column-meta.d.ts | 80 +- .../columns/data-table-column.types.ts | 112 +- .../data-table/features/filter/README.md | 1552 +- .../filter/components/checkbox-popover.tsx | 278 +- .../features/filter/components/checkbox.tsx | 284 +- .../features/filter/components/datepicker.tsx | 396 +- .../filter/components/global-search.tsx | 314 +- .../filter/components/radio-popover.tsx | 244 +- .../features/filter/components/radio.tsx | 164 +- .../features/filter/components/search.tsx | 118 +- .../features/filter/components/select.tsx | 534 +- .../filter/components/shared/search-input.tsx | 156 +- .../components/shared/use-search-state.ts | 110 +- .../features/filter/components/tag.tsx | 184 +- .../features/filter/components/time-range.tsx | 168 +- .../features/filter/data-table-filter.tsx | 408 +- .../data-table/features/filter/index.ts | 84 +- .../data-table-inline-content.tsx | 210 +- .../pagination/data-table-pagination.tsx | 254 +- .../data-table-toolbar-filter-dropdown.tsx | 128 +- .../data-table-toolbar-multi-actions.tsx | 244 +- .../toolbar/data-table-toolbar-row-count.tsx | 110 +- .../toolbar/data-table-toolbar-search.tsx | 132 +- .../toolbar/data-table-toolbar-skeleton.tsx | 250 +- .../features/toolbar/data-table-toolbar.tsx | 794 +- .../data-table/hooks/useFilterQueryState.ts | 348 +- .../data-table/hooks/useInlineContent.ts | 134 +- .../datum-ui/components/data-table/index.ts | 368 +- .../data-table/utils/date-serialization.ts | 200 +- .../data-table/utils/global-search.helpers.ts | 566 +- .../data-table/utils/sort-labels.ts | 98 +- .../data-table/utils/sorting.helpers.ts | 444 +- .../utils/time-range-serialization.ts | 136 +- .../datum-ui/components/dialog/README.md | 360 +- .../datum-ui/components/dialog/dialog.tsx | 358 +- .../datum-ui/components/dialog/index.ts | 18 +- .../datum-ui/components/dropdown/dropdown.tsx | 444 +- .../datum-ui/components/dropdown/index.ts | 2 +- .../datum-ui/components/dropzone/dropzone.tsx | 418 +- .../empty-content/empty-content.tsx | 476 +- .../components/empty-content/index.ts | 4 +- .../file-input-button/file-input-button.tsx | 270 +- .../datum-ui/components/form/README.md | 1148 +- .../form/components/form-autocomplete.tsx | 148 +- .../form/components/form-button.tsx | 102 +- .../form/components/form-checkbox.tsx | 124 +- .../form/components/form-copy-box.tsx | 234 +- .../form/components/form-custom.tsx | 80 +- .../form/components/form-description.tsx | 60 +- .../form/components/form-dialog.tsx | 398 +- .../components/form/components/form-error.tsx | 122 +- .../form/components/form-field-array.tsx | 260 +- .../components/form/components/form-field.tsx | 602 +- .../form/components/form-input-group.tsx | 112 +- .../components/form/components/form-input.tsx | 86 +- .../form/components/form-radio-group.tsx | 176 +- .../components/form/components/form-root.tsx | 428 +- .../form/components/form-select.tsx | 152 +- .../form/components/form-submit.tsx | 54 +- .../form/components/form-switch.tsx | 124 +- .../form/components/form-textarea.tsx | 86 +- .../components/form/components/form-when.tsx | 132 +- .../components/form/components/index.ts | 56 +- .../form/components/stepper/form-step.tsx | 64 +- .../form/components/stepper/form-stepper.tsx | 994 +- .../form/components/stepper/index.ts | 8 +- .../components/stepper/stepper-controls.tsx | 218 +- .../components/stepper/stepper-navigation.tsx | 348 +- .../components/form/context/field-context.tsx | 72 +- .../components/form/context/form-context.tsx | 88 +- .../datum-ui/components/form/context/index.ts | 34 +- .../datum-ui/components/form/hooks/index.ts | 10 +- .../form/hooks/use-field-context.ts | 54 +- .../components/form/hooks/use-field.ts | 216 +- .../components/form/hooks/use-form-context.ts | 48 +- .../components/form/hooks/use-stepper.ts | 146 +- .../components/form/hooks/use-watch.ts | 238 +- app/modules/datum-ui/components/form/index.ts | 564 +- .../form/primitives/autocomplete.tsx | 886 +- .../form/primitives/autocomplete.types.ts | 204 +- .../components/form/primitives/checkbox.tsx | 28 +- .../components/form/primitives/index.ts | 50 +- .../components/form/primitives/input.tsx | 60 +- .../components/form/primitives/label.tsx | 28 +- .../form/primitives/radio-group.tsx | 52 +- .../components/form/primitives/select.tsx | 240 +- .../components/form/primitives/switch.tsx | 28 +- .../components/form/primitives/textarea.tsx | 60 +- .../datum-ui/components/form/types/index.ts | 1440 +- .../datum-ui/components/grid/README.md | 460 +- .../components/grid/components/col.tsx | 168 +- .../components/grid/components/index.tsx | 18 +- .../components/grid/components/row.tsx | 198 +- .../grid/constants/grid.constants.ts | 52 +- app/modules/datum-ui/components/grid/index.ts | 48 +- .../datum-ui/components/grid/style.css | 2176 +- .../components/grid/types/grid.types.ts | 96 +- .../components/grid/utils/responsive.ts | 144 +- .../datum-ui/components/icons/close-icon.tsx | 50 +- .../components/icons/icon-wrapper.tsx | 44 +- .../datum-ui/components/icons/index.ts | 6 +- .../components/icons/spinner-icon.tsx | 138 +- app/modules/datum-ui/components/index.ts | 74 +- .../datum-ui/components/input-number/index.ts | 2 +- .../components/input-number/input-number.tsx | 294 +- .../components/input-with-addons/index.ts | 2 +- .../input-with-addons/input-with-addons.tsx | 108 +- .../datum-ui/components/label/index.ts | 34 +- .../datum-ui/components/label/label.tsx | 80 +- .../components/loader-overlay/index.ts | 2 +- .../loader-overlay/loader-overlay.tsx | 50 +- .../datum-ui/components/mobile-sheet/index.ts | 2 +- .../components/mobile-sheet/mobile-sheet.tsx | 108 +- .../datum-ui/components/more-actions/index.ts | 4 +- .../components/more-actions/more-actions.tsx | 208 +- .../datum-ui/components/nprogress/index.ts | 62 +- .../components/nprogress/nprogress.css | 48 +- .../datum-ui/components/page-title/index.ts | 4 +- .../components/page-title/page-title.tsx | 112 +- .../components/responsive-dropdown/index.ts | 2 +- .../responsive-dropdown.tsx | 162 +- .../components/rich-text-editor/index.ts | 6 +- .../rich-text-editor/rich-text-content.tsx | 100 +- .../rich-text-editor/rich-text-editor.tsx | 276 +- .../rich-text-editor/toolbar/bold-toolbar.tsx | 34 +- .../toolbar/italic-toolbar.tsx | 34 +- .../rich-text-editor/toolbar/link-toolbar.tsx | 154 +- .../toolbar/strike-toolbar.tsx | 34 +- .../toolbar/toolbar-button.tsx | 92 +- .../rich-text-editor/toolbar/toolbar.tsx | 40 +- .../toolbar/underline-toolbar.tsx | 34 +- .../components/rich-text-editor/types.ts | 44 +- .../datum-ui/components/sheet/index.ts | 22 +- .../datum-ui/components/sheet/sheet.tsx | 216 +- .../components/sidebar/app-sidebar.tsx | 234 +- .../datum-ui/components/sidebar/index.ts | 6 +- .../datum-ui/components/sidebar/nav-main.tsx | 1212 +- .../datum-ui/components/sidebar/sidebar.tsx | 1844 +- .../datum-ui/components/skeleton/index.ts | 2 +- .../datum-ui/components/skeleton/skeleton.tsx | 62 +- .../datum-ui/components/stepper/index.ts | 2 +- .../datum-ui/components/stepper/stepper.tsx | 996 +- app/modules/datum-ui/components/tabs/index.ts | 2 +- app/modules/datum-ui/components/tabs/tabs.tsx | 152 +- .../datum-ui/components/tag-input/index.ts | 2 +- .../components/tag-input/tag-input.tsx | 1028 +- .../task-queue/FUTURE_ENHANCEMENTS.md | 468 +- .../datum-ui/components/task-queue/README.md | 1036 +- .../components/task-queue/constants.ts | 16 +- .../components/task-queue/core/index.ts | 18 +- .../task-queue/core/task-panel-actions.tsx | 168 +- .../task-queue/core/task-panel-counter.tsx | 96 +- .../task-queue/core/task-panel-header.tsx | 14 +- .../task-queue/core/task-panel-item.tsx | 334 +- .../components/task-queue/core/task-panel.tsx | 72 +- .../task-queue/core/task-queue-dropdown.tsx | 198 +- .../task-queue/core/task-queue-trigger.tsx | 174 +- .../task-queue/core/task-summary-dialog.tsx | 334 +- .../components/task-queue/engine/executor.ts | 302 +- .../components/task-queue/engine/index.ts | 8 +- .../components/task-queue/engine/queue.ts | 1074 +- .../engine/storage/detect-storage.ts | 82 +- .../task-queue/engine/storage/index.ts | 10 +- .../engine/storage/local-storage.ts | 144 +- .../engine/storage/memory-storage.ts | 66 +- .../engine/storage/redis-storage.ts | 200 +- .../task-queue/engine/storage/storage.ts | 2 +- .../components/task-queue/hooks/index.ts | 18 +- .../task-queue/hooks/use-task-queue.ts | 88 +- .../task-queue/hooks/use-task-scope.ts | 296 +- .../datum-ui/components/task-queue/index.ts | 106 +- .../components/task-queue/provider/index.ts | 2 +- .../provider/task-queue-provider.tsx | 126 +- .../datum-ui/components/task-queue/types.ts | 536 +- .../components/task-queue/utils/index.ts | 168 +- .../components/absolute-range-panel.tsx | 642 +- .../time-range-picker/components/index.ts | 10 +- .../components/quick-ranges-panel.tsx | 104 +- .../components/timezone-selector.tsx | 80 +- .../components/time-range-picker/index.ts | 98 +- .../components/time-range-picker/presets.ts | 320 +- .../time-range-picker/time-range-picker.tsx | 702 +- .../components/time-range-picker/types.ts | 134 +- .../time-range-picker/utils/format-display.ts | 174 +- .../time-range-picker/utils/index.ts | 46 +- .../time-range-picker/utils/timezone.ts | 330 +- .../time-range-picker/utils/to-api-format.ts | 102 +- .../components/toast/headless-toast.tsx | 104 +- .../datum-ui/components/toast/index.ts | 6 +- .../datum-ui/components/toast/toast.ts | 102 +- .../datum-ui/components/toast/toaster.tsx | 26 +- .../datum-ui/components/toast/types.ts | 12 +- .../datum-ui/components/toast/use-toast.ts | 40 +- .../datum-ui/components/tooltip/README.md | 740 +- .../datum-ui/components/tooltip/index.ts | 2 +- .../datum-ui/components/tooltip/tooltip.tsx | 334 +- app/modules/datum-ui/datum-ui.css | 4 +- app/modules/datum-ui/hooks/index.ts | 4 +- .../datum-ui/hooks/use-copy-to-clipboard.ts | 96 +- app/modules/datum-ui/hooks/use-debounce.ts | 46 +- app/modules/datum-ui/index.ts | 28 +- app/modules/datum-ui/utils/index.ts | 2 +- app/modules/datum-ui/utils/timezone.ts | 70 +- app/modules/graphql/generated/guards.esm.js | 622 +- app/modules/graphql/generated/schema.graphql | 2380 +- app/modules/graphql/generated/schema.ts | 3564 +-- app/modules/graphql/generated/types.esm.js | 714 +- .../metrics/components/base-metric.tsx | 222 +- .../components/controls/refresh-control.tsx | 270 +- .../components/controls/step-control.tsx | 142 +- .../controls/time-range-control.tsx | 262 +- .../filters/base/metrics-filter-radio.tsx | 176 +- .../filters/base/metrics-filter-search.tsx | 180 +- .../filters/base/metrics-filter-select.tsx | 336 +- app/modules/metrics/components/index.ts | 8 +- .../metrics/components/metric-card.tsx | 450 +- .../metrics/components/metric-chart.tsx | 618 +- .../metrics/components/metrics-toolbar.tsx | 168 +- app/modules/metrics/utils/date-parsers.ts | 350 +- app/modules/prometheus/service.ts | 466 +- app/modules/rbac/README.md | 2250 +- app/modules/rbac/client/index.ts | 10 +- app/modules/rbac/client/rbac-api.ts | 206 +- .../rbac/components/PermissionCheck.tsx | 164 +- .../rbac/components/PermissionGate.tsx | 114 +- app/modules/rbac/components/index.ts | 16 +- .../rbac/components/withPermission.tsx | 132 +- app/modules/rbac/context/index.ts | 12 +- app/modules/rbac/context/rbac.context.tsx | 26 +- app/modules/rbac/context/rbac.provider.tsx | 210 +- app/modules/rbac/hooks/index.ts | 30 +- app/modules/rbac/hooks/useHasPermission.ts | 260 +- app/modules/rbac/hooks/usePermissionCheck.ts | 298 +- app/modules/rbac/hooks/usePermissions.ts | 78 +- app/modules/rbac/index.ts | 190 +- app/modules/rbac/permission-checker.ts | 244 +- app/modules/rbac/rbac.middleware.ts | 538 +- app/modules/rbac/service/index.ts | 10 +- app/modules/rbac/service/rbac.service.ts | 462 +- app/modules/rbac/types.ts | 738 +- app/modules/redis/connection.ts | 258 +- app/modules/sentry/filters.ts | 64 +- app/modules/shadcn/README.md | 664 +- app/modules/shadcn/hooks/use-mobile.ts | 38 +- app/modules/shadcn/lib/utils.ts | 32 +- app/modules/shadcn/style.css | 4 +- app/modules/shadcn/styles/animations.css | 202 +- app/modules/shadcn/styles/shadcn.css | 456 +- app/modules/shadcn/ui/alert.tsx | 96 +- app/modules/shadcn/ui/avatar.tsx | 82 +- app/modules/shadcn/ui/badge.tsx | 74 +- app/modules/shadcn/ui/breadcrumb.tsx | 198 +- app/modules/shadcn/ui/button-group.tsx | 154 +- app/modules/shadcn/ui/button.tsx | 106 +- app/modules/shadcn/ui/card.tsx | 114 +- app/modules/shadcn/ui/chart.tsx | 666 +- app/modules/shadcn/ui/checkbox.tsx | 48 +- app/modules/shadcn/ui/collapsible.tsx | 18 +- app/modules/shadcn/ui/command.tsx | 328 +- app/modules/shadcn/ui/dialog.tsx | 240 +- app/modules/shadcn/ui/dropdown-menu.tsx | 374 +- app/modules/shadcn/ui/hover-card.tsx | 70 +- app/modules/shadcn/ui/input-group.tsx | 314 +- app/modules/shadcn/ui/input.tsx | 48 +- app/modules/shadcn/ui/label.tsx | 36 +- app/modules/shadcn/ui/map.tsx | 2824 +- app/modules/shadcn/ui/place-autocomplete.tsx | 702 +- app/modules/shadcn/ui/popover.tsx | 78 +- app/modules/shadcn/ui/radio-group.tsx | 80 +- app/modules/shadcn/ui/select.tsx | 324 +- app/modules/shadcn/ui/separator.tsx | 50 +- app/modules/shadcn/ui/sheet.tsx | 252 +- app/modules/shadcn/ui/skeleton.tsx | 14 +- app/modules/shadcn/ui/sonner.tsx | 48 +- app/modules/shadcn/ui/spinner.tsx | 30 +- app/modules/shadcn/ui/switch.tsx | 48 +- app/modules/shadcn/ui/table.tsx | 178 +- app/modules/shadcn/ui/tabs.tsx | 116 +- app/modules/shadcn/ui/textarea.tsx | 44 +- app/modules/shadcn/ui/tooltip.tsx | 108 +- app/modules/shadcn/ui/visuallyhidden.tsx | 10 +- app/modules/tanstack/query.ts | 6 +- app/providers/terminal-session.provider.tsx | 128 + .../access-review/access-review.service.ts | 152 +- .../activity-logs/activity-log.helpers.ts | 576 +- app/resources/base/types.ts | 6 +- app/resources/connectors/connector.queries.ts | 132 +- app/resources/connectors/index.ts | 38 +- .../dns-records/dns-record.adapter.ts | 380 +- .../dns-records/dns-record.queries.ts | 484 +- .../dns-records/dns-record.service.ts | 672 +- app/resources/dns-records/index.ts | 248 +- app/resources/dns-zones/dns-zone.queries.ts | 278 +- app/resources/dns-zones/dns-zone.service.ts | 484 +- app/resources/dns-zones/index.ts | 68 +- app/resources/domains/domain.queries.ts | 352 +- app/resources/domains/domain.service.ts | 616 +- app/resources/domains/index.ts | 104 +- .../export-policies/export-policy.service.ts | 518 +- .../export-policies/export-policy.watch.ts | 126 +- app/resources/groups/group.service.ts | 486 +- .../http-proxies/http-proxy.adapter.ts | 1026 +- .../http-proxies/http-proxy.queries.ts | 384 +- .../http-proxies/http-proxy.schema.ts | 644 +- .../http-proxies/http-proxy.service.ts | 1258 +- app/resources/http-proxies/index.ts | 140 +- .../invitations/invitation.queries.ts | 534 +- .../invitations/invitation.schema.ts | 246 +- app/resources/members/member.queries.ts | 256 +- app/resources/notes/note.service.ts | 282 +- ...ontact-group-membership-removal.service.ts | 358 +- .../contact-group-membership.service.ts | 344 +- .../contact-group/contact-group.service.ts | 448 +- .../notifications/contact/contact.service.ts | 442 +- .../organizations/organization.gql-service.ts | 578 +- .../organizations/organization.service.ts | 432 +- .../policy-bindings/policy-binding.adapter.ts | 282 +- .../policy-bindings/policy-binding.queries.ts | 208 +- .../policy-bindings/policy-binding.schema.ts | 284 +- .../policy-bindings/policy-binding.service.ts | 466 +- app/resources/projects/index.ts | 74 +- app/resources/projects/project.queries.ts | 260 +- app/resources/projects/project.service.ts | 414 +- app/resources/secrets/index.ts | 110 +- app/resources/secrets/secret.queries.ts | 252 +- app/resources/secrets/secret.service.ts | 442 +- app/resources/users/index.ts | 8 +- app/resources/users/user.adapter.ts | 360 +- app/resources/users/user.queries.ts | 302 +- app/resources/users/user.schema.ts | 202 +- app/root.tsx | 588 +- app/routes.ts | 406 +- app/routes/account/organizations/index.tsx | 548 +- app/routes/account/settings/access-tokens.tsx | 208 +- .../account/settings/active-sessions.tsx | 364 +- app/routes/account/settings/activity.tsx | 26 +- app/routes/account/settings/general.tsx | 84 +- app/routes/account/settings/layout.tsx | 94 +- app/routes/account/settings/security.tsx | 52 +- app/routes/auth/callback.tsx | 242 +- app/routes/fraud/account-suspended.tsx | 126 +- app/routes/fraud/account-under-review.tsx | 114 +- app/routes/fraud/verifying.tsx | 210 +- app/routes/invitation/index.tsx | 426 +- app/routes/not-found.tsx | 146 +- app/routes/onboarding/complete-profile.tsx | 264 +- app/routes/org/detail/layout.tsx | 284 +- app/routes/org/detail/projects/index.tsx | 732 +- app/routes/org/detail/settings/activity.tsx | 44 +- app/routes/org/detail/settings/general.tsx | 68 +- app/routes/org/detail/settings/layout.tsx | 112 +- .../org/detail/settings/notifications.tsx | 44 +- .../org/detail/settings/policy-bindings.tsx | 228 +- app/routes/org/detail/settings/quotas.tsx | 56 +- app/routes/org/detail/team/create-group.tsx | 116 +- app/routes/org/detail/team/group-detail.tsx | 726 +- app/routes/org/detail/team/groups.tsx | 412 +- app/routes/org/detail/team/index.tsx | 870 +- app/routes/org/detail/team/invite.tsx | 330 +- app/routes/org/detail/team/member-roles.tsx | 818 +- .../project/detail/connectors/index.tsx | 614 +- .../detail/dns-zones/detail/dns-records.tsx | 936 +- .../detail/dns-zones/detail/layout.tsx | 280 +- .../detail/dns-zones/detail/nameservers.tsx | 224 +- .../detail/dns-zones/detail/overview.tsx | 230 +- .../detail/dns-zones/detail/settings.tsx | 166 +- app/routes/project/detail/dns-zones/index.tsx | 788 +- .../project/detail/domains/detail/layout.tsx | 202 +- .../detail/domains/detail/overview.tsx | 496 +- .../detail/domains/detail/settings.tsx | 160 +- app/routes/project/detail/domains/index.tsx | 1148 +- .../project/detail/edge/detail/index.tsx | 214 +- .../project/detail/edge/detail/layout.tsx | 106 +- app/routes/project/detail/edge/index.tsx | 562 +- app/routes/project/detail/home.tsx | 608 +- app/routes/project/detail/layout.tsx | 630 +- .../project/detail/metrics/detail/edit.tsx | 54 +- .../project/detail/metrics/detail/layout.tsx | 72 +- .../detail/metrics/detail/overview.tsx | 140 +- app/routes/project/detail/metrics/index.tsx | 416 +- app/routes/project/detail/metrics/layout.tsx | 104 +- app/routes/project/detail/metrics/new.tsx | 106 +- .../project/detail/secrets/detail/layout.tsx | 74 +- .../detail/secrets/detail/overview.tsx | 82 +- app/routes/project/detail/secrets/index.tsx | 398 +- .../project/detail/settings/activity.tsx | 34 +- .../project/detail/settings/general.tsx | 74 +- app/routes/project/detail/settings/layout.tsx | 108 +- .../project/detail/settings/notifications.tsx | 44 +- app/routes/project/detail/settings/quotas.tsx | 64 +- app/routes/test/demo.tsx | 390 +- .../components/dns-record-test-card.tsx | 520 +- app/routes/test/dns-record/dns-record.tsx | 548 +- app/routes/test/metrics.tsx | 534 +- app/routes/test/permissions.tsx | 552 +- app/server/entry.ts | 415 +- app/server/middleware/error-handler.ts | 96 +- app/server/middleware/rate-limit.ts | 501 +- app/server/routes/api.ts | 140 +- app/server/routes/assistant.ts | 200 +- app/server/routes/permissions.ts | 106 +- app/server/routes/terminal.ts | 893 + app/styles/custom.css | 132 +- app/styles/fonts.css | 122 +- app/styles/root.css | 68 +- app/styles/themes/alpha.css | 1148 +- app/styles/tokens/brand-tokens.css | 114 +- app/styles/tokens/figma-tokens.css | 1252 +- app/types.d.ts | 102 +- app/utils/common.ts | 242 +- app/utils/config/paths.config.ts | 252 +- app/utils/env/env.server.ts | 439 +- app/utils/env/index.ts | 196 +- app/utils/env/types.ts | 178 +- app/utils/errors/app-error.ts | 362 +- app/utils/errors/index.ts | 74 +- app/utils/helpers/resource-labels.ts | 64 +- .../middlewares/fraud-status.middleware.ts | 182 +- bun.lock | 8408 +++--- config/base/deployment.yaml | 162 +- cypress.config.ts | 168 +- cypress/component/button.cy.tsx | 16 +- cypress/component/form-field.cy.tsx | 94 +- cypress/component/resource-labels.cy.ts | 54 +- cypress/e2e/regression/dns-zones.cy.ts | 194 +- cypress/e2e/regression/organisations.cy.ts | 188 +- cypress/e2e/regression/projects.cy.ts | 204 +- cypress/e2e/smoke/account.cy.ts | 64 +- cypress/support/e2e.ts | 872 +- docker-compose.yml | 122 +- docs/getting-started/02-environment-setup.md | 327 +- eslint.config.mjs | 162 +- graphql.config.json | 24 +- observability/providers/sentry.ts | 476 +- package.json | 400 +- renovate.json | 38 +- scripts/graphql.ts | 1090 +- scripts/openapi.ts | 678 +- tsconfig.json | 94 +- vite.config.ts | 232 +- 711 files changed, 125719 insertions(+), 123693 deletions(-) create mode 100644 app/providers/terminal-session.provider.tsx create mode 100644 app/server/routes/terminal.ts diff --git a/.env.example b/.env.example index 70a1bc83d..080e3e3fa 100644 --- a/.env.example +++ b/.env.example @@ -1,121 +1,134 @@ -# ═══════════════════════════════════════════════════════════ -# Environment Configuration -# Copy this file to .env and fill in your values -# ═══════════════════════════════════════════════════════════ - -# ─────────────────────────────────────────────────────────── -# Required: Core URLs -# ─────────────────────────────────────────────────────────── - -# Application URLs (must be valid URLs) -APP_URL=http://localhost:3000 -API_URL=http://localhost:8080 -GRAPHQL_URL=http://localhost:8080/graphql - -# ─────────────────────────────────────────────────────────── -# Required: Authentication -# ─────────────────────────────────────────────────────────── - -# Session secret (minimum 32 characters) -SESSION_SECRET=your-super-secret-session-key-min-32-chars - -# OIDC Authentication -AUTH_OIDC_ISSUER=https://your-oidc-provider.com -AUTH_OIDC_CLIENT_ID=your-client-id - -# ─────────────────────────────────────────────────────────── -# Required: Feature Services -# ─────────────────────────────────────────────────────────── - -# Prometheus metrics (for proxy metrics dashboard) -PROMETHEUS_URL=http://localhost:9090 - -# CloudValid DNS validation (for domain auto-verification) -CLOUDVALID_API_URL=https://api.cloudvalid.com/api/v2 -CLOUDVALID_API_KEY=your-cloudvalid-api-key -CLOUDVALID_TEMPLATE_ID=your-cloudvalid-template-id - -# ─────────────────────────────────────────────────────────── -# Optional: Runtime Configuration -# ─────────────────────────────────────────────────────────── - -# Environment mode: production | development | test -NODE_ENV=development - -# Application version (e.g., git commit SHA) -# VERSION= - -# Enable debug mode: true | false -# DEBUG=false - -# ─────────────────────────────────────────────────────────── -# Optional: Logging -# ─────────────────────────────────────────────────────────── - -# Log level: debug | info | warn | error -# LOG_LEVEL=debug - -# Log format: json | pretty | compact -# LOG_FORMAT=pretty - -# Generate curl commands for API calls: true | false -# LOG_CURL=true - -# Redact sensitive tokens in logs: true | false -# LOG_REDACT_TOKENS=true - -# Log request/response payloads: true | false -# LOG_PAYLOADS=false - -# ─────────────────────────────────────────────────────────── -# Optional: Observability (graceful degradation when missing) -# ─────────────────────────────────────────────────────────── - -# Sentry error tracking (must be valid URL if provided) -# SENTRY_DSN=https://xxx@sentry.io/xxx -# SENTRY_ENV=development - -# OpenTelemetry tracing (must be valid URL if provided) -# OTEL_ENABLED=false -# OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317 -# OTEL_EXPORTER_TIMEOUT=10000 -# OTEL_LOG_LEVEL=info - -# ─────────────────────────────────────────────────────────── -# Optional: External Integrations (graceful degradation) -# ─────────────────────────────────────────────────────────── - -# Grafana (must be valid URL if provided) -# GRAFANA_URL=http://localhost:3001 - -# ─────────────────────────────────────────────────────────── -# Optional: Analytics & Support (graceful degradation) -# ─────────────────────────────────────────────────────────── - -# Fathom Analytics -# FATHOM_ID= - -# HelpScout Beacon -# HELPSCOUT_BEACON_ID= -# HELPSCOUT_SECRET_KEY= - -# ─────────────────────────────────────────────────────────── -# Optional: Redis (falls back to in-memory rate limiting) -# ─────────────────────────────────────────────────────────── - -# Redis connection URL (must be valid URL if provided) -# REDIS_URL=redis://localhost:6379 - -# Redis connection settings (have defaults) -# REDIS_MAX_RETRIES=3 -# REDIS_CONNECT_TIMEOUT=5000 -# REDIS_COMMAND_TIMEOUT=3000 -# REDIS_KEY_PREFIX=cloud-portal: - -# ─────────────────────────────────────────────────────────── -# Optional: Chatbot -# ─────────────────────────────────────────────────────────── - -# Enable chatbot: true | false -# CHATBOT_ENABLED=false -# ANTHROPIC_API_KEY=your-anthropic-api-key \ No newline at end of file +# ═══════════════════════════════════════════════════════════ +# Environment Configuration +# Copy this file to .env and fill in your values +# ═══════════════════════════════════════════════════════════ + +# ─────────────────────────────────────────────────────────── +# Required: Core URLs +# ─────────────────────────────────────────────────────────── + +# Application URLs (must be valid URLs) +APP_URL=http://localhost:3000 +API_URL=http://localhost:8080 +GRAPHQL_URL=http://localhost:8080/graphql + +# ─────────────────────────────────────────────────────────── +# Required: Authentication +# ─────────────────────────────────────────────────────────── + +# Session secret (minimum 32 characters) +SESSION_SECRET=your-super-secret-session-key-min-32-chars + +# OIDC Authentication +AUTH_OIDC_ISSUER=https://your-oidc-provider.com +AUTH_OIDC_CLIENT_ID=your-client-id + +# ─────────────────────────────────────────────────────────── +# Required: Feature Services +# ─────────────────────────────────────────────────────────── + +# Prometheus metrics (for proxy metrics dashboard) +PROMETHEUS_URL=http://localhost:9090 + +# CloudValid DNS validation (for domain auto-verification) +CLOUDVALID_API_URL=https://api.cloudvalid.com/api/v2 +CLOUDVALID_API_KEY=your-cloudvalid-api-key +CLOUDVALID_TEMPLATE_ID=your-cloudvalid-template-id + +# ─────────────────────────────────────────────────────────── +# Optional: Runtime Configuration +# ─────────────────────────────────────────────────────────── + +# Environment mode: production | development | test +NODE_ENV=development + +# Application version (e.g., git commit SHA) +# VERSION= + +# Enable debug mode: true | false +# DEBUG=false + +# ─────────────────────────────────────────────────────────── +# Optional: Logging +# ─────────────────────────────────────────────────────────── + +# Log level: debug | info | warn | error +# LOG_LEVEL=debug + +# Log format: json | pretty | compact +# LOG_FORMAT=pretty + +# Generate curl commands for API calls: true | false +# LOG_CURL=true + +# Redact sensitive tokens in logs: true | false +# LOG_REDACT_TOKENS=true + +# Log request/response payloads: true | false +# LOG_PAYLOADS=false + +# ─────────────────────────────────────────────────────────── +# Optional: Observability (graceful degradation when missing) +# ─────────────────────────────────────────────────────────── + +# Sentry error tracking (must be valid URL if provided) +# SENTRY_DSN=https://xxx@sentry.io/xxx +# SENTRY_ENV=development + +# OpenTelemetry tracing (must be valid URL if provided) +# OTEL_ENABLED=false +# OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317 +# OTEL_EXPORTER_TIMEOUT=10000 +# OTEL_LOG_LEVEL=info + +# ─────────────────────────────────────────────────────────── +# Optional: External Integrations (graceful degradation) +# ─────────────────────────────────────────────────────────── + +# Grafana (must be valid URL if provided) +# GRAFANA_URL=http://localhost:3001 + +# ─────────────────────────────────────────────────────────── +# Optional: Analytics & Support (graceful degradation) +# ─────────────────────────────────────────────────────────── + +# Fathom Analytics +# FATHOM_ID= + +# HelpScout Beacon +# HELPSCOUT_BEACON_ID= +# HELPSCOUT_SECRET_KEY= + +# ─────────────────────────────────────────────────────────── +# Optional: Redis (falls back to in-memory rate limiting) +# ─────────────────────────────────────────────────────────── + +# Redis connection URL (must be valid URL if provided) +# REDIS_URL=redis://localhost:6379 + +# Redis connection settings (have defaults) +# REDIS_MAX_RETRIES=3 +# REDIS_CONNECT_TIMEOUT=5000 +# REDIS_COMMAND_TIMEOUT=3000 +# REDIS_KEY_PREFIX=cloud-portal: + +# ─────────────────────────────────────────────────────────── +# Optional: Chatbot +# ─────────────────────────────────────────────────────────── + +# Enable chatbot: true | false +# CHATBOT_ENABLED=false +# ANTHROPIC_API_KEY=your-anthropic-api-key + +# ─────────────────────────────────────────────────────────── +# Optional: Embedded datumctl terminal (Developer Tools) +# ─────────────────────────────────────────────────────────── + +# Absolute path to a locally-built datumctl binary that supports +# ambient-token mode (DATUMCTL_TOKEN / DATUM_API_HOSTNAME). +# For local development, clone datum-cloud/datumctl as a sibling +# of cloud-portal, check out the cloud-portal/ambient-token branch, +# and run `go build -o bin/datumctl .`, then set: +# DATUMCTL_BIN=/Users/matthewjenkinson/Git/datum/datumctl/bin/datumctl +# When unset, the Developer Tools terminal is disabled. +# DATUMCTL_BIN= \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0a0f9869b..c027a14ae 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,370 +1,370 @@ -name: CI - -on: - pull_request: - push: - branches: [main] - paths-ignore: - - 'docs/**' - - 'README.md' - release: - types: ['published'] - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -env: - CYPRESS_CACHE_FOLDER: ~/.cache/Cypress - BUN_CACHE: ~/.bun/install/cache - -jobs: - # ─── Quality ──────────────────────────────────────────────── - install: - name: Install Dependencies - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - - uses: oven-sh/setup-bun@v2 - with: { bun-version: latest } - - uses: actions/cache@v5 - id: cache-deps - with: - path: | - ${{ env.BUN_CACHE }} - **/node_modules - key: ${{ runner.os }}-deps-${{ hashFiles('**/bun.lock', '**/bun.lockb', '**/package.json') }} - - if: steps.cache-deps.outputs.cache-hit != 'true' - run: bun install --frozen-lockfile - - lint: - name: Lint & Format - needs: install - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - - uses: oven-sh/setup-bun@v2 - - uses: actions/cache@v5 - with: - path: | - ${{ env.BUN_CACHE }} - **/node_modules - key: ${{ runner.os }}-deps-${{ hashFiles('**/bun.lock', '**/bun.lockb', '**/package.json') }} - - run: bun run lint && bun run format - - typecheck: - name: Type Check - needs: install - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - - uses: oven-sh/setup-bun@v2 - with: { bun-version: latest } - - uses: actions/cache@v5 - with: - path: | - ${{ env.BUN_CACHE }} - **/node_modules - key: ${{ runner.os }}-deps-${{ hashFiles('**/bun.lock', '**/bun.lockb', '**/package.json') }} - - run: bun typecheck - - build: - name: Build - needs: [lint, typecheck] - runs-on: ubuntu-latest - if: ${{ !failure() && !cancelled() }} - steps: - - uses: actions/checkout@v6 - - uses: oven-sh/setup-bun@v2 - with: { bun-version: latest } - - uses: actions/cache@v5 - with: - path: | - ${{ env.BUN_CACHE }} - **/node_modules - key: ${{ runner.os }}-deps-${{ hashFiles('**/bun.lock', '**/bun.lockb', '**/package.json') }} - - run: bun run build - - uses: actions/cache@v5 - with: - path: build - key: build-${{ github.sha }} - - # ─── Tests ────────────────────────────────────────────────── - unit-tests: - name: Unit Tests - needs: build - runs-on: ubuntu-latest - if: ${{ !failure() && !cancelled() }} - steps: - - uses: actions/checkout@v6 - - uses: oven-sh/setup-bun@v2 - with: { bun-version: latest } - - uses: actions/cache@v5 - with: - path: | - ${{ env.BUN_CACHE }} - **/node_modules - key: ${{ runner.os }}-deps-${{ hashFiles('**/bun.lock', '**/bun.lockb', '**/package.json') }} - - run: bunx cypress install && bunx cypress verify - - run: bun run test:unit:prod - env: - NODE_ENV: test - CYPRESS: 'true' - - e2e-smoke: - name: E2E Smoke - needs: build - runs-on: ubuntu-latest - timeout-minutes: 10 - if: ${{ !failure() && !cancelled() }} - steps: - - uses: actions/checkout@v6 - - uses: oven-sh/setup-bun@v2 - with: { bun-version: latest } - - uses: actions/cache@v5 - with: - path: | - ${{ env.BUN_CACHE }} - **/node_modules - key: ${{ runner.os }}-deps-${{ hashFiles('**/bun.lock', '**/bun.lockb', '**/package.json') }} - - uses: actions/cache@v5 - with: - path: build - key: build-${{ github.sha }} - - name: Setup Environment - env: - SESSION_SECRET: ${{ secrets.SESSION_SECRET }} - ACCESS_TOKEN: ${{ secrets.ACCESS_TOKEN }} - SUB: ${{ secrets.SUB }} - API_URL: ${{ secrets.API_URL }} - GRAPHQL_URL: ${{ secrets.GRAPHQL_URL }} - AUTH_OIDC_ISSUER: ${{ secrets.AUTH_OIDC_ISSUER }} - AUTH_OIDC_CLIENT_ID: ${{ secrets.AUTH_OIDC_CLIENT_ID }} - run: | - cat > .env << EOF - NODE_ENV=test - CYPRESS=true - APP_URL=http://localhost:3000 - API_URL=$API_URL - GRAPHQL_URL=$GRAPHQL_URL - AUTH_OIDC_ISSUER=$AUTH_OIDC_ISSUER - AUTH_OIDC_CLIENT_ID=$AUTH_OIDC_CLIENT_ID - SESSION_SECRET=$SESSION_SECRET - LOG_LEVEL=warn - CYPRESS_BASE_URL=http://localhost:3000 - EOF - - run: bunx cypress install && bunx cypress verify - - name: Run Smoke Tests - env: - NODE_ENV: test - CYPRESS: 'true' - CYPRESS_BASE_URL: http://localhost:3000 - SESSION_SECRET: ${{ secrets.SESSION_SECRET }} - ACCESS_TOKEN: ${{ secrets.ACCESS_TOKEN }} - SUB: ${{ secrets.SUB }} - run: >- - bunx start-server-and-test - 'bun run start' http://localhost:3000/_healthz - 'bunx cypress run --spec "cypress/e2e/smoke/**" --config-file cypress.config.ts' - - if: failure() - uses: actions/upload-artifact@v7 - with: - name: e2e-smoke-artifacts-${{ github.run_id }} - path: | - cypress/videos - cypress/screenshots - retention-days: 7 - if-no-files-found: warn - - e2e-regression: - name: E2E Regression - needs: e2e-smoke - runs-on: ubuntu-latest - timeout-minutes: 20 - if: ${{ !failure() && !cancelled() && github.event_name == 'push' && github.ref == 'refs/heads/main' }} - # if: ${{ !failure() && !cancelled() }} - strategy: - fail-fast: false - matrix: - shard: [0, 1, 2, 3] - steps: - - uses: actions/checkout@v6 - - uses: oven-sh/setup-bun@v2 - with: { bun-version: latest } - - uses: actions/cache@v5 - with: - path: | - ${{ env.BUN_CACHE }} - **/node_modules - key: ${{ runner.os }}-deps-${{ hashFiles('**/bun.lock', '**/bun.lockb', '**/package.json') }} - - uses: actions/cache@v5 - with: - path: build - key: build-${{ github.sha }} - - name: Setup Environment - env: - SESSION_SECRET: ${{ secrets.SESSION_SECRET }} - ACCESS_TOKEN: ${{ secrets.ACCESS_TOKEN }} - SUB: ${{ secrets.SUB }} - API_URL: ${{ secrets.API_URL }} - GRAPHQL_URL: ${{ secrets.GRAPHQL_URL }} - AUTH_OIDC_ISSUER: ${{ secrets.AUTH_OIDC_ISSUER }} - AUTH_OIDC_CLIENT_ID: ${{ secrets.AUTH_OIDC_CLIENT_ID }} - run: | - cat > .env << EOF - NODE_ENV=test - CYPRESS=true - APP_URL=http://localhost:3000 - API_URL=$API_URL - GRAPHQL_URL=$GRAPHQL_URL - AUTH_OIDC_ISSUER=$AUTH_OIDC_ISSUER - AUTH_OIDC_CLIENT_ID=$AUTH_OIDC_CLIENT_ID - SESSION_SECRET=$SESSION_SECRET - LOG_LEVEL=warn - CYPRESS_BASE_URL=http://localhost:3000 - EOF - - run: bunx cypress install && bunx cypress verify - - name: Run Regression Tests - env: - NODE_ENV: test - CYPRESS: 'true' - CYPRESS_BASE_URL: http://localhost:3000 - SESSION_SECRET: ${{ secrets.SESSION_SECRET }} - ACCESS_TOKEN: ${{ secrets.ACCESS_TOKEN }} - SUB: ${{ secrets.SUB }} - SPLIT: ${{ strategy.job-total }} - SPLIT_INDEX: ${{ strategy.job-index }} - CYPRESS_specPattern: 'cypress/e2e/regression/**/*.{cy,spec}.{js,jsx,ts,tsx}' - run: >- - bunx start-server-and-test - 'bun run start' http://localhost:3000/_healthz - 'bunx cypress run --config-file cypress.config.ts' - - if: failure() - uses: actions/upload-artifact@v7 - with: - name: e2e-regression-artifacts-${{ github.run_id }}-shard-${{ matrix.shard }} - path: | - cypress/videos - cypress/screenshots - retention-days: 7 - if-no-files-found: warn - - # ─── Status Gate ──────────────────────────────────────────── - status-check: - name: Final Status Check - needs: [install, lint, typecheck, build, unit-tests, e2e-smoke, e2e-regression] - if: always() - runs-on: ubuntu-latest - steps: - - name: Check Workflow Status - run: | - if [[ "${{ contains(needs.*.result, 'failure') }}" == "true" ]]; then - echo "❌ One or more jobs failed" - exit 1 - elif [[ "${{ contains(needs.*.result, 'cancelled') }}" == "true" ]]; then - echo "⚠️ One or more jobs were cancelled" - exit 1 - else - echo "✅ All jobs passed" - fi - - # ─── Publish (only after all checks pass) ────────────────── - publish-container-image: - name: Publish Container Image - needs: [status-check] - if: ${{ !failure() && !cancelled() }} - permissions: - id-token: write - contents: read - packages: write - attestations: write - uses: datum-cloud/actions/.github/workflows/publish-docker.yaml@v1.13.3 - with: - image-name: cloud-portal - secrets: inherit - - publish-kustomize-bundles: - name: Publish Kustomize Bundles - needs: [publish-container-image] - if: ${{ !failure() && !cancelled() }} - permissions: - id-token: write - contents: read - packages: write - uses: datum-cloud/actions/.github/workflows/publish-kustomize-bundle.yaml@v1.13.3 - with: - bundle-name: ghcr.io/datum-cloud/cloud-portal-kustomize - bundle-path: config - image-overlays: config/base - image-name: ghcr.io/datum-cloud/cloud-portal - secrets: inherit - - # ─── PR Comment ───────────────────────────────────────────── - pr-comment: - name: PR Test Summary - needs: [unit-tests, e2e-smoke] - runs-on: ubuntu-latest - if: github.event_name == 'pull_request' - permissions: - contents: read - pull-requests: write - actions: read - steps: - - uses: actions/checkout@v6 - - name: Build test summary - uses: actions/github-script@v9 - with: - script: | - const fs = require('fs'); - const runId = context.runId; - const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${runId}`; - - const { data: jobs } = await github.rest.actions.listJobsForWorkflowRun({ - owner: context.repo.owner, - repo: context.repo.repo, - run_id: runId, - }); - - const testJobs = jobs.jobs.filter(j => - j.name === 'Unit Tests' || j.name.startsWith('E2E') - ); - - const statusEmoji = (conclusion) => { - if (!conclusion || conclusion === 'skipped') return '⏭️'; - if (conclusion === 'success') return '✅'; - if (conclusion === 'failure') return '❌'; - if (conclusion === 'cancelled') return '🚫'; - return '❓'; - }; - - let table = '| Job | Status |\n|-----|--------|\n'; - for (const job of testJobs.sort((a, b) => a.name.localeCompare(b.name))) { - table += `| ${job.name} | ${statusEmoji(job.conclusion)} ${job.conclusion || 'pending'} |\n`; - } - - const { data: artifacts } = await github.rest.actions.listWorkflowRunArtifacts({ - owner: context.repo.owner, - repo: context.repo.repo, - run_id: runId, - }); - - let body = `## 🧪 Test Summary\n\n${table}\n\n`; - body += `[View workflow run](${runUrl})`; - body += `\n\n### 📎 Artifacts\n\n`; - if (artifacts.artifacts.length > 0) { - body += `Videos and screenshots from failed E2E tests:\n\n`; - for (const a of artifacts.artifacts) { - body += `- **${a.name}** – [Download](${runUrl})\n`; - } - } else { - body += `No artifacts (all tests passed).\n`; - } - - fs.writeFileSync('pr-summary.md', body); - - - uses: peter-evans/create-or-update-comment@v5 - with: - issue-number: ${{ github.event.pull_request.number }} - body-path: pr-summary.md - comment-tag: ci-summary - edit-mode: replace +name: CI + +on: + pull_request: + push: + branches: [main] + paths-ignore: + - 'docs/**' + - 'README.md' + release: + types: ['published'] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + CYPRESS_CACHE_FOLDER: ~/.cache/Cypress + BUN_CACHE: ~/.bun/install/cache + +jobs: + # ─── Quality ──────────────────────────────────────────────── + install: + name: Install Dependencies + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: oven-sh/setup-bun@v2 + with: { bun-version: latest } + - uses: actions/cache@v5 + id: cache-deps + with: + path: | + ${{ env.BUN_CACHE }} + **/node_modules + key: ${{ runner.os }}-deps-${{ hashFiles('**/bun.lock', '**/bun.lockb', '**/package.json') }} + - if: steps.cache-deps.outputs.cache-hit != 'true' + run: bun install --frozen-lockfile + + lint: + name: Lint & Format + needs: install + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: oven-sh/setup-bun@v2 + - uses: actions/cache@v5 + with: + path: | + ${{ env.BUN_CACHE }} + **/node_modules + key: ${{ runner.os }}-deps-${{ hashFiles('**/bun.lock', '**/bun.lockb', '**/package.json') }} + - run: bun run lint && bun run format + + typecheck: + name: Type Check + needs: install + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: oven-sh/setup-bun@v2 + with: { bun-version: latest } + - uses: actions/cache@v5 + with: + path: | + ${{ env.BUN_CACHE }} + **/node_modules + key: ${{ runner.os }}-deps-${{ hashFiles('**/bun.lock', '**/bun.lockb', '**/package.json') }} + - run: bun typecheck + + build: + name: Build + needs: [lint, typecheck] + runs-on: ubuntu-latest + if: ${{ !failure() && !cancelled() }} + steps: + - uses: actions/checkout@v6 + - uses: oven-sh/setup-bun@v2 + with: { bun-version: latest } + - uses: actions/cache@v5 + with: + path: | + ${{ env.BUN_CACHE }} + **/node_modules + key: ${{ runner.os }}-deps-${{ hashFiles('**/bun.lock', '**/bun.lockb', '**/package.json') }} + - run: bun run build + - uses: actions/cache@v5 + with: + path: build + key: build-${{ github.sha }} + + # ─── Tests ────────────────────────────────────────────────── + unit-tests: + name: Unit Tests + needs: build + runs-on: ubuntu-latest + if: ${{ !failure() && !cancelled() }} + steps: + - uses: actions/checkout@v6 + - uses: oven-sh/setup-bun@v2 + with: { bun-version: latest } + - uses: actions/cache@v5 + with: + path: | + ${{ env.BUN_CACHE }} + **/node_modules + key: ${{ runner.os }}-deps-${{ hashFiles('**/bun.lock', '**/bun.lockb', '**/package.json') }} + - run: bunx cypress install && bunx cypress verify + - run: bun run test:unit:prod + env: + NODE_ENV: test + CYPRESS: 'true' + + e2e-smoke: + name: E2E Smoke + needs: build + runs-on: ubuntu-latest + timeout-minutes: 10 + if: ${{ !failure() && !cancelled() }} + steps: + - uses: actions/checkout@v6 + - uses: oven-sh/setup-bun@v2 + with: { bun-version: latest } + - uses: actions/cache@v5 + with: + path: | + ${{ env.BUN_CACHE }} + **/node_modules + key: ${{ runner.os }}-deps-${{ hashFiles('**/bun.lock', '**/bun.lockb', '**/package.json') }} + - uses: actions/cache@v5 + with: + path: build + key: build-${{ github.sha }} + - name: Setup Environment + env: + SESSION_SECRET: ${{ secrets.SESSION_SECRET }} + ACCESS_TOKEN: ${{ secrets.ACCESS_TOKEN }} + SUB: ${{ secrets.SUB }} + API_URL: ${{ secrets.API_URL }} + GRAPHQL_URL: ${{ secrets.GRAPHQL_URL }} + AUTH_OIDC_ISSUER: ${{ secrets.AUTH_OIDC_ISSUER }} + AUTH_OIDC_CLIENT_ID: ${{ secrets.AUTH_OIDC_CLIENT_ID }} + run: | + cat > .env << EOF + NODE_ENV=test + CYPRESS=true + APP_URL=http://localhost:3000 + API_URL=$API_URL + GRAPHQL_URL=$GRAPHQL_URL + AUTH_OIDC_ISSUER=$AUTH_OIDC_ISSUER + AUTH_OIDC_CLIENT_ID=$AUTH_OIDC_CLIENT_ID + SESSION_SECRET=$SESSION_SECRET + LOG_LEVEL=warn + CYPRESS_BASE_URL=http://localhost:3000 + EOF + - run: bunx cypress install && bunx cypress verify + - name: Run Smoke Tests + env: + NODE_ENV: test + CYPRESS: 'true' + CYPRESS_BASE_URL: http://localhost:3000 + SESSION_SECRET: ${{ secrets.SESSION_SECRET }} + ACCESS_TOKEN: ${{ secrets.ACCESS_TOKEN }} + SUB: ${{ secrets.SUB }} + run: >- + bunx start-server-and-test + 'bun run start' http://localhost:3000/_healthz + 'bunx cypress run --spec "cypress/e2e/smoke/**" --config-file cypress.config.ts' + - if: failure() + uses: actions/upload-artifact@v7 + with: + name: e2e-smoke-artifacts-${{ github.run_id }} + path: | + cypress/videos + cypress/screenshots + retention-days: 7 + if-no-files-found: warn + + e2e-regression: + name: E2E Regression + needs: e2e-smoke + runs-on: ubuntu-latest + timeout-minutes: 20 + if: ${{ !failure() && !cancelled() && github.event_name == 'push' && github.ref == 'refs/heads/main' }} + # if: ${{ !failure() && !cancelled() }} + strategy: + fail-fast: false + matrix: + shard: [0, 1, 2, 3] + steps: + - uses: actions/checkout@v6 + - uses: oven-sh/setup-bun@v2 + with: { bun-version: latest } + - uses: actions/cache@v5 + with: + path: | + ${{ env.BUN_CACHE }} + **/node_modules + key: ${{ runner.os }}-deps-${{ hashFiles('**/bun.lock', '**/bun.lockb', '**/package.json') }} + - uses: actions/cache@v5 + with: + path: build + key: build-${{ github.sha }} + - name: Setup Environment + env: + SESSION_SECRET: ${{ secrets.SESSION_SECRET }} + ACCESS_TOKEN: ${{ secrets.ACCESS_TOKEN }} + SUB: ${{ secrets.SUB }} + API_URL: ${{ secrets.API_URL }} + GRAPHQL_URL: ${{ secrets.GRAPHQL_URL }} + AUTH_OIDC_ISSUER: ${{ secrets.AUTH_OIDC_ISSUER }} + AUTH_OIDC_CLIENT_ID: ${{ secrets.AUTH_OIDC_CLIENT_ID }} + run: | + cat > .env << EOF + NODE_ENV=test + CYPRESS=true + APP_URL=http://localhost:3000 + API_URL=$API_URL + GRAPHQL_URL=$GRAPHQL_URL + AUTH_OIDC_ISSUER=$AUTH_OIDC_ISSUER + AUTH_OIDC_CLIENT_ID=$AUTH_OIDC_CLIENT_ID + SESSION_SECRET=$SESSION_SECRET + LOG_LEVEL=warn + CYPRESS_BASE_URL=http://localhost:3000 + EOF + - run: bunx cypress install && bunx cypress verify + - name: Run Regression Tests + env: + NODE_ENV: test + CYPRESS: 'true' + CYPRESS_BASE_URL: http://localhost:3000 + SESSION_SECRET: ${{ secrets.SESSION_SECRET }} + ACCESS_TOKEN: ${{ secrets.ACCESS_TOKEN }} + SUB: ${{ secrets.SUB }} + SPLIT: ${{ strategy.job-total }} + SPLIT_INDEX: ${{ strategy.job-index }} + CYPRESS_specPattern: 'cypress/e2e/regression/**/*.{cy,spec}.{js,jsx,ts,tsx}' + run: >- + bunx start-server-and-test + 'bun run start' http://localhost:3000/_healthz + 'bunx cypress run --config-file cypress.config.ts' + - if: failure() + uses: actions/upload-artifact@v7 + with: + name: e2e-regression-artifacts-${{ github.run_id }}-shard-${{ matrix.shard }} + path: | + cypress/videos + cypress/screenshots + retention-days: 7 + if-no-files-found: warn + + # ─── Status Gate ──────────────────────────────────────────── + status-check: + name: Final Status Check + needs: [install, lint, typecheck, build, unit-tests, e2e-smoke, e2e-regression] + if: always() + runs-on: ubuntu-latest + steps: + - name: Check Workflow Status + run: | + if [[ "${{ contains(needs.*.result, 'failure') }}" == "true" ]]; then + echo "❌ One or more jobs failed" + exit 1 + elif [[ "${{ contains(needs.*.result, 'cancelled') }}" == "true" ]]; then + echo "⚠️ One or more jobs were cancelled" + exit 1 + else + echo "✅ All jobs passed" + fi + + # ─── Publish (only after all checks pass) ────────────────── + publish-container-image: + name: Publish Container Image + needs: [status-check] + if: ${{ !failure() && !cancelled() }} + permissions: + id-token: write + contents: read + packages: write + attestations: write + uses: datum-cloud/actions/.github/workflows/publish-docker.yaml@v1.13.3 + with: + image-name: cloud-portal + secrets: inherit + + publish-kustomize-bundles: + name: Publish Kustomize Bundles + needs: [publish-container-image] + if: ${{ !failure() && !cancelled() }} + permissions: + id-token: write + contents: read + packages: write + uses: datum-cloud/actions/.github/workflows/publish-kustomize-bundle.yaml@v1.13.3 + with: + bundle-name: ghcr.io/datum-cloud/cloud-portal-kustomize + bundle-path: config + image-overlays: config/base + image-name: ghcr.io/datum-cloud/cloud-portal + secrets: inherit + + # ─── PR Comment ───────────────────────────────────────────── + pr-comment: + name: PR Test Summary + needs: [unit-tests, e2e-smoke] + runs-on: ubuntu-latest + if: github.event_name == 'pull_request' + permissions: + contents: read + pull-requests: write + actions: read + steps: + - uses: actions/checkout@v6 + - name: Build test summary + uses: actions/github-script@v9 + with: + script: | + const fs = require('fs'); + const runId = context.runId; + const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${runId}`; + + const { data: jobs } = await github.rest.actions.listJobsForWorkflowRun({ + owner: context.repo.owner, + repo: context.repo.repo, + run_id: runId, + }); + + const testJobs = jobs.jobs.filter(j => + j.name === 'Unit Tests' || j.name.startsWith('E2E') + ); + + const statusEmoji = (conclusion) => { + if (!conclusion || conclusion === 'skipped') return '⏭️'; + if (conclusion === 'success') return '✅'; + if (conclusion === 'failure') return '❌'; + if (conclusion === 'cancelled') return '🚫'; + return '❓'; + }; + + let table = '| Job | Status |\n|-----|--------|\n'; + for (const job of testJobs.sort((a, b) => a.name.localeCompare(b.name))) { + table += `| ${job.name} | ${statusEmoji(job.conclusion)} ${job.conclusion || 'pending'} |\n`; + } + + const { data: artifacts } = await github.rest.actions.listWorkflowRunArtifacts({ + owner: context.repo.owner, + repo: context.repo.repo, + run_id: runId, + }); + + let body = `## 🧪 Test Summary\n\n${table}\n\n`; + body += `[View workflow run](${runUrl})`; + body += `\n\n### 📎 Artifacts\n\n`; + if (artifacts.artifacts.length > 0) { + body += `Videos and screenshots from failed E2E tests:\n\n`; + for (const a of artifacts.artifacts) { + body += `- **${a.name}** – [Download](${runUrl})\n`; + } + } else { + body += `No artifacts (all tests passed).\n`; + } + + fs.writeFileSync('pr-summary.md', body); + + - uses: peter-evans/create-or-update-comment@v5 + with: + issue-number: ${{ github.event.pull_request.number }} + body-path: pr-summary.md + comment-tag: ci-summary + edit-mode: replace diff --git a/.github/workflows/set-project-month.yaml b/.github/workflows/set-project-month.yaml index e6d62d26b..7b99cd2d8 100644 --- a/.github/workflows/set-project-month.yaml +++ b/.github/workflows/set-project-month.yaml @@ -1,10 +1,10 @@ -name: Set Project Month - -on: - pull_request: - types: [opened, ready_for_review, reopened] - -jobs: - set-month: - uses: datum-cloud/actions/.github/workflows/set-project-month.yaml@v1.13.2 - secrets: inherit +name: Set Project Month + +on: + pull_request: + types: [opened, ready_for_review, reopened] + +jobs: + set-month: + uses: datum-cloud/actions/.github/workflows/set-project-month.yaml@v1.13.2 + secrets: inherit diff --git a/.gitignore b/.gitignore index e7a147304..086a041d3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,31 +1,31 @@ -node_modules - -/.cache -/build -.env -.DS_Store -.react-router/ - -# AI assistant files -.cursor/ -.windsurf/ -.claude/ -!.claude/settings.json -CLAUDE.MD -.superpowers/ -docs/plans/ -docs/superpowers/ - -# OpenAPI generation temp files -temp-openapi-spec.json - -# GraphQL generation temp files -temp-filtered-schema.graphql - -# Cypress files -cypress/screenshots -cypress/videos -cypress/results - -# Playwright MCP -.playwright-mcp/ +node_modules + +/.cache +/build +.DS_Store +.react-router/ + +# AI assistant files +.cursor/ +.windsurf/ +.claude/ +!.claude/settings.json +CLAUDE.MD +.superpowers/ +docs/plans/ +docs/superpowers/ + +# OpenAPI generation temp files +temp-openapi-spec.json + +# GraphQL generation temp files +temp-filtered-schema.graphql + +# Cypress files +cypress/screenshots +cypress/videos +cypress/results + +# Playwright MCP +.playwright-mcp/ +config.bat diff --git a/.tool-versions b/.tool-versions index ea5d138f0..69c0c1a9d 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,2 +1,2 @@ -nodejs 24.14.1 -bun 1.3.12 +nodejs 24.14.1 +bun 1.3.12 diff --git a/Dockerfile b/Dockerfile index 83a9b5f5b..2ba599cb4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,71 +1,71 @@ -# ========================================== -# BASE STAGE - Common dependencies and setup -# ========================================== - -# syntax = docker/dockerfile:1 - -# Use the latest Bun version -FROM oven/bun:1.3.12 AS base - -# Install system dependencies and clean up in the same layer -RUN apt-get update && \ - apt-get install -y --no-install-recommends unzip ca-certificates && \ - update-ca-certificates && \ - rm -rf /var/lib/apt/lists/* && \ - apt-get clean - -# Remix app lives here -WORKDIR /app - -# Set production environment -ENV NODE_ENV=production \ - PORT=3000 - -# ========================================== -# BUILD STAGE - Compile and prepare the app -# ========================================== -FROM base AS build - -ARG SENTRY_AUTH_TOKEN -ARG VERSION=dev - -# Set environment variables -ENV VERSION=${VERSION} -ENV SENTRY_AUTH_TOKEN=${SENTRY_AUTH_TOKEN} - -# Install dependencies -COPY --link package.json bun.lock ./ -RUN bun install --frozen-lockfile - -# Copy application code -COPY --link . . - -# Build application -RUN bun run build && \ - bun install --production --frozen-lockfile && \ - touch .env - -# ========================================== -# PRODUCTION STAGE - Final lightweight image -# ========================================== -FROM base - -# Accept VERSION as build argument and set as environment variable -ARG VERSION=dev -ENV VERSION=${VERSION} - -# Copy only necessary files from build stage -COPY --from=build /app /app - -# Start the server by default, this can be overwritten at runtime -EXPOSE ${PORT} - -# Use non-root user for better security -RUN groupadd --gid 1001 datum && \ - useradd --uid 1001 --gid 1001 --no-create-home datum && \ - chown -R datum:datum /app - -USER datum - -# Use the start script from package.json -CMD [ "bun", "run", "start" ] +# ========================================== +# BASE STAGE - Common dependencies and setup +# ========================================== + +# syntax = docker/dockerfile:1 + +# Use the latest Bun version +FROM oven/bun:1.3.12 AS base + +# Install system dependencies and clean up in the same layer +RUN apt-get update && \ + apt-get install -y --no-install-recommends unzip ca-certificates && \ + update-ca-certificates && \ + rm -rf /var/lib/apt/lists/* && \ + apt-get clean + +# Remix app lives here +WORKDIR /app + +# Set production environment +ENV NODE_ENV=production \ + PORT=3000 + +# ========================================== +# BUILD STAGE - Compile and prepare the app +# ========================================== +FROM base AS build + +ARG SENTRY_AUTH_TOKEN +ARG VERSION=dev + +# Set environment variables +ENV VERSION=${VERSION} +ENV SENTRY_AUTH_TOKEN=${SENTRY_AUTH_TOKEN} + +# Install dependencies +COPY --link package.json bun.lock ./ +RUN bun install --frozen-lockfile + +# Copy application code +COPY --link . . + +# Build application +RUN bun run build && \ + bun install --production --frozen-lockfile && \ + touch .env + +# ========================================== +# PRODUCTION STAGE - Final lightweight image +# ========================================== +FROM base + +# Accept VERSION as build argument and set as environment variable +ARG VERSION=dev +ENV VERSION=${VERSION} + +# Copy only necessary files from build stage +COPY --from=build /app /app + +# Start the server by default, this can be overwritten at runtime +EXPOSE ${PORT} + +# Use non-root user for better security +RUN groupadd --gid 1001 datum && \ + useradd --uid 1001 --gid 1001 --no-create-home datum && \ + chown -R datum:datum /app + +USER datum + +# Use the start script from package.json +CMD [ "bun", "run", "start" ] diff --git a/app/components/avatar-stack/avatar-stack.tsx b/app/components/avatar-stack/avatar-stack.tsx index 0e42f98f4..1b315f8ff 100644 --- a/app/components/avatar-stack/avatar-stack.tsx +++ b/app/components/avatar-stack/avatar-stack.tsx @@ -1,74 +1,74 @@ -import { getInitials } from '@/utils/helpers/text.helper'; -import { cn } from '@shadcn/lib/utils'; -import { Avatar, AvatarFallback, AvatarImage } from '@shadcn/ui/avatar'; - -export interface AvatarStackItem { - name: string; - avatarUrl?: string; -} - -export interface AvatarStackProps { - items: AvatarStackItem[]; - max?: number; - size?: 'xs' | 'sm'; -} - -const COLORS = [ - 'bg-blue-500', - 'bg-rose-500', - 'bg-amber-600', - 'bg-purple-500', - 'bg-emerald-500', - 'bg-cyan-500', - 'bg-pink-500', - 'bg-indigo-500', -]; - -function hashName(name: string): number { - let hash = 0; - for (let i = 0; i < name.length; i++) { - hash = (hash << 5) - hash + name.charCodeAt(i); - hash |= 0; - } - return Math.abs(hash); -} - -export function AvatarStack({ items, max = 4, size = 'xs' }: AvatarStackProps) { - if (items.length === 0) return null; - - const visible = items.slice(0, max); - const overflow = items.length - max; - - const sizeClasses = size === 'xs' ? 'size-6 text-[9px]' : 'size-8 text-[11px]'; - - return ( -
- {visible.map((item, i) => ( - 0 && '-ml-1.5')} - style={{ zIndex: visible.length - i }}> - {item.avatarUrl && } - - {getInitials(item.name)} - - - ))} - {overflow > 0 && ( -
- +{overflow} -
- )} -
- ); -} +import { getInitials } from '@/utils/helpers/text.helper'; +import { cn } from '@shadcn/lib/utils'; +import { Avatar, AvatarFallback, AvatarImage } from '@shadcn/ui/avatar'; + +export interface AvatarStackItem { + name: string; + avatarUrl?: string; +} + +export interface AvatarStackProps { + items: AvatarStackItem[]; + max?: number; + size?: 'xs' | 'sm'; +} + +const COLORS = [ + 'bg-blue-500', + 'bg-rose-500', + 'bg-amber-600', + 'bg-purple-500', + 'bg-emerald-500', + 'bg-cyan-500', + 'bg-pink-500', + 'bg-indigo-500', +]; + +function hashName(name: string): number { + let hash = 0; + for (let i = 0; i < name.length; i++) { + hash = (hash << 5) - hash + name.charCodeAt(i); + hash |= 0; + } + return Math.abs(hash); +} + +export function AvatarStack({ items, max = 4, size = 'xs' }: AvatarStackProps) { + if (items.length === 0) return null; + + const visible = items.slice(0, max); + const overflow = items.length - max; + + const sizeClasses = size === 'xs' ? 'size-6 text-[9px]' : 'size-8 text-[11px]'; + + return ( +
+ {visible.map((item, i) => ( + 0 && '-ml-1.5')} + style={{ zIndex: visible.length - i }}> + {item.avatarUrl && } + + {getInitials(item.name)} + + + ))} + {overflow > 0 && ( +
+ +{overflow} +
+ )} +
+ ); +} diff --git a/app/components/back-button/back-button.tsx b/app/components/back-button/back-button.tsx index 6864f3e44..3c11d5d5b 100644 --- a/app/components/back-button/back-button.tsx +++ b/app/components/back-button/back-button.tsx @@ -1,106 +1,106 @@ -import { paths } from '@/utils/config/paths.config'; -import { Button, ButtonProps } from '@datum-ui/components'; -import { Icon } from '@datum-ui/components/icons/icon-wrapper'; -import { ArrowLeft } from 'lucide-react'; -import { useNavigate } from 'react-router'; - -export interface BackButtonProps extends Omit { - /** - * Custom back link to navigate to - * If not provided, will use navigate(-1) or fallback to account root - */ - to?: string; - /** - * Callback function called before navigation - * Return false to prevent navigation - */ - onBeforeNavigate?: () => boolean | void; - /** - * Callback function called after navigation - */ - onAfterNavigate?: () => void; - /** - * Custom icon component - * @default ArrowLeft - */ - customIcon?: React.ReactNode; - /** - * Show icon on the button - * @default true - */ - showIcon?: boolean; - /** - * Fallback path when navigate(-1) is not available - * @default paths.account.root - */ - fallbackPath?: string; -} - -export const BackButton = ({ - to, - onBeforeNavigate, - onAfterNavigate, - customIcon, - showIcon = true, - fallbackPath = paths.account.root, - children = 'Back', - type = 'quaternary', - theme = 'outline', - size = 'xs', - ...buttonProps -}: BackButtonProps) => { - const navigate = useNavigate(); - - const handleClick = () => { - // Call onBeforeNavigate callback if provided - if (onBeforeNavigate) { - const shouldContinue = onBeforeNavigate(); - if (shouldContinue === false) { - return; - } - } - - // Navigate based on priority: - // 1. Custom 'to' prop - // 2. Browser history back (navigate(-1)) - // 3. Fallback path - if (to) { - navigate(to); - } else { - // Check if there's history to go back to - // Note: window.history.length > 1 indicates there's a previous page - const hasHistory = window.history.length > 1; - - if (hasHistory) { - navigate(-1); - } else { - navigate(fallbackPath); - } - } - - // Call onAfterNavigate callback if provided - if (onAfterNavigate) { - onAfterNavigate(); - } - }; - - const icon = showIcon - ? customIcon || - : undefined; - - return ( - - ); -}; - -BackButton.displayName = 'BackButton'; +import { paths } from '@/utils/config/paths.config'; +import { Button, ButtonProps } from '@datum-ui/components'; +import { Icon } from '@datum-ui/components/icons/icon-wrapper'; +import { ArrowLeft } from 'lucide-react'; +import { useNavigate } from 'react-router'; + +export interface BackButtonProps extends Omit { + /** + * Custom back link to navigate to + * If not provided, will use navigate(-1) or fallback to account root + */ + to?: string; + /** + * Callback function called before navigation + * Return false to prevent navigation + */ + onBeforeNavigate?: () => boolean | void; + /** + * Callback function called after navigation + */ + onAfterNavigate?: () => void; + /** + * Custom icon component + * @default ArrowLeft + */ + customIcon?: React.ReactNode; + /** + * Show icon on the button + * @default true + */ + showIcon?: boolean; + /** + * Fallback path when navigate(-1) is not available + * @default paths.account.root + */ + fallbackPath?: string; +} + +export const BackButton = ({ + to, + onBeforeNavigate, + onAfterNavigate, + customIcon, + showIcon = true, + fallbackPath = paths.account.root, + children = 'Back', + type = 'quaternary', + theme = 'outline', + size = 'xs', + ...buttonProps +}: BackButtonProps) => { + const navigate = useNavigate(); + + const handleClick = () => { + // Call onBeforeNavigate callback if provided + if (onBeforeNavigate) { + const shouldContinue = onBeforeNavigate(); + if (shouldContinue === false) { + return; + } + } + + // Navigate based on priority: + // 1. Custom 'to' prop + // 2. Browser history back (navigate(-1)) + // 3. Fallback path + if (to) { + navigate(to); + } else { + // Check if there's history to go back to + // Note: window.history.length > 1 indicates there's a previous page + const hasHistory = window.history.length > 1; + + if (hasHistory) { + navigate(-1); + } else { + navigate(fallbackPath); + } + } + + // Call onAfterNavigate callback if provided + if (onAfterNavigate) { + onAfterNavigate(); + } + }; + + const icon = showIcon + ? customIcon || + : undefined; + + return ( + + ); +}; + +BackButton.displayName = 'BackButton'; diff --git a/app/components/badge/badge-copy.tsx b/app/components/badge/badge-copy.tsx index 1ac900999..f82a4d988 100644 --- a/app/components/badge/badge-copy.tsx +++ b/app/components/badge/badge-copy.tsx @@ -1,95 +1,95 @@ -import { useCopyToClipboard } from '@/hooks/useCopyToClipboard'; -import { Badge, type BadgeProps, toast, Tooltip } from '@datum-ui/components'; -import { Icon } from '@datum-ui/components/icons/icon-wrapper'; -import { cn } from '@shadcn/lib/utils'; -import { CopyIcon } from 'lucide-react'; -import { useState } from 'react'; - -export interface BadgeCopyProps { - value: string; - text?: string; - className?: string; - textClassName?: string; - containerClassName?: string; - badgeType?: BadgeProps['type']; - badgeTheme?: BadgeProps['theme']; - showTooltip?: boolean; - copyButtonClassName?: string; - 'data-e2e'?: string; - wrapperTooltipMessage?: string | React.ReactNode; -} - -export const BadgeCopy = ({ - value, - text, - className, - textClassName, - containerClassName, - badgeType = 'secondary', - badgeTheme = 'light', - showTooltip = true, - copyButtonClassName, - 'data-e2e': dataE2e, - wrapperTooltipMessage, -}: BadgeCopyProps) => { - const [_, copy] = useCopyToClipboard(); - const [copied, setCopied] = useState(false); - - const copyToClipboard = () => { - if (!value) return; - - copy(value).then(() => { - toast.success('Copied to clipboard'); - setCopied(true); - setTimeout(() => { - setCopied(false); - }, 2000); - }); - }; - - const displayText = text ?? value; - - const copyButton = ( - - - - ); - - const badge = ( - { - event.preventDefault(); - event.stopPropagation(); - copyToClipboard(); - }}> - {displayText} - {copyButton} - - ); - - const badgeContent = showTooltip ? ( - {badge} - ) : ( - badge - ); - - const wrappedBadge = ( -
- {badgeContent} -
- ); - - if (wrapperTooltipMessage) { - return {wrappedBadge}; - } - - return wrappedBadge; -}; +import { useCopyToClipboard } from '@/hooks/useCopyToClipboard'; +import { Badge, type BadgeProps, toast, Tooltip } from '@datum-ui/components'; +import { Icon } from '@datum-ui/components/icons/icon-wrapper'; +import { cn } from '@shadcn/lib/utils'; +import { CopyIcon } from 'lucide-react'; +import { useState } from 'react'; + +export interface BadgeCopyProps { + value: string; + text?: string; + className?: string; + textClassName?: string; + containerClassName?: string; + badgeType?: BadgeProps['type']; + badgeTheme?: BadgeProps['theme']; + showTooltip?: boolean; + copyButtonClassName?: string; + 'data-e2e'?: string; + wrapperTooltipMessage?: string | React.ReactNode; +} + +export const BadgeCopy = ({ + value, + text, + className, + textClassName, + containerClassName, + badgeType = 'secondary', + badgeTheme = 'light', + showTooltip = true, + copyButtonClassName, + 'data-e2e': dataE2e, + wrapperTooltipMessage, +}: BadgeCopyProps) => { + const [_, copy] = useCopyToClipboard(); + const [copied, setCopied] = useState(false); + + const copyToClipboard = () => { + if (!value) return; + + copy(value).then(() => { + toast.success('Copied to clipboard'); + setCopied(true); + setTimeout(() => { + setCopied(false); + }, 2000); + }); + }; + + const displayText = text ?? value; + + const copyButton = ( + + + + ); + + const badge = ( + { + event.preventDefault(); + event.stopPropagation(); + copyToClipboard(); + }}> + {displayText} + {copyButton} + + ); + + const badgeContent = showTooltip ? ( + {badge} + ) : ( + badge + ); + + const wrappedBadge = ( +
+ {badgeContent} +
+ ); + + if (wrapperTooltipMessage) { + return {wrappedBadge}; + } + + return wrappedBadge; +}; diff --git a/app/components/badge/badge-status.tsx b/app/components/badge/badge-status.tsx index 28b429dc3..73926b84c 100644 --- a/app/components/badge/badge-status.tsx +++ b/app/components/badge/badge-status.tsx @@ -1,182 +1,182 @@ -import { ControlPlaneStatus, IControlPlaneStatus } from '@/resources/base'; -import { Badge, type BadgeProps, Tooltip, SpinnerIcon } from '@datum-ui/components'; -import { cn } from '@shadcn/lib/utils'; -import { ReactNode } from 'react'; - -interface StatusConfig { - badgeType?: BadgeProps['type']; - badgeTheme?: BadgeProps['theme']; - // Custom colors (overrides badgeType/badgeTheme if provided) - customColor?: { - border: string; - text: string; - bg: string; - }; - icon?: ReactNode; - defaultLabel: string; -} - -// Centralized status configuration -// Customize colors here - all usages will inherit these settings -const STATUS_CONFIG: Record = { - active: { - badgeType: 'success', - badgeTheme: 'light', - defaultLabel: 'Active', - }, - pending: { - // Option 1: Use existing Badge type/theme - badgeType: 'info', - badgeTheme: 'light', - // Option 2: Use custom colors - // customColor: { - // border: 'border-blue-500', - // text: 'text-blue-600 dark:text-blue-400', - // bg: 'bg-blue-500/20 dark:bg-blue-500/20', - // }, - icon: ( -