@flamingo-stack/openframe-frontend-core 0.0.486-snapshot.20260727163255 → 0.0.486

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/dist/{chunk-WLRBZXFB.js → chunk-2BMYCH3D.js} +2 -2
  2. package/dist/{chunk-AUGM74CO.js → chunk-5WZGZ6B2.js} +4 -4
  3. package/dist/{chunk-QBDHNUQX.js → chunk-6QJ2QRQE.js} +4 -4
  4. package/dist/{chunk-UU4DW7JX.cjs → chunk-BVHMDHQY.cjs} +31 -31
  5. package/dist/{chunk-UU4DW7JX.cjs.map → chunk-BVHMDHQY.cjs.map} +1 -1
  6. package/dist/{chunk-KTD2MGYF.cjs → chunk-C4QKMOQK.cjs} +7 -7
  7. package/dist/{chunk-KTD2MGYF.cjs.map → chunk-C4QKMOQK.cjs.map} +1 -1
  8. package/dist/{chunk-HWK5CH5N.cjs → chunk-F3BH5B7M.cjs} +11 -11
  9. package/dist/{chunk-HWK5CH5N.cjs.map → chunk-F3BH5B7M.cjs.map} +1 -1
  10. package/dist/{chunk-EO7MUGY4.cjs → chunk-HQITFULS.cjs} +9 -9
  11. package/dist/{chunk-EO7MUGY4.cjs.map → chunk-HQITFULS.cjs.map} +1 -1
  12. package/dist/{chunk-GXOUXAXU.js → chunk-J5KX5K5T.js} +2 -2
  13. package/dist/{chunk-SDNMOAT4.js → chunk-KA4ILVCX.js} +2 -2
  14. package/dist/{chunk-ITEPRN6L.js → chunk-KRKNTK5Y.js} +2 -2
  15. package/dist/{chunk-UBK5HIMH.js → chunk-MD5E5BKO.js} +4 -4
  16. package/dist/{chunk-RAYDQGJV.cjs → chunk-NTHTORJ3.cjs} +59 -59
  17. package/dist/{chunk-RAYDQGJV.cjs.map → chunk-NTHTORJ3.cjs.map} +1 -1
  18. package/dist/{chunk-TJSJPOPL.cjs → chunk-NZYUSFMI.cjs} +350 -573
  19. package/dist/chunk-NZYUSFMI.cjs.map +1 -0
  20. package/dist/{chunk-NETEA5E4.js → chunk-OW33LYO2.js} +4 -4
  21. package/dist/{chunk-4TFUB2CR.cjs → chunk-PHYEC3EG.cjs} +26 -26
  22. package/dist/{chunk-4TFUB2CR.cjs.map → chunk-PHYEC3EG.cjs.map} +1 -1
  23. package/dist/{chunk-QL7S3CH4.cjs → chunk-RB6HX4KC.cjs} +26 -26
  24. package/dist/{chunk-QL7S3CH4.cjs.map → chunk-RB6HX4KC.cjs.map} +1 -1
  25. package/dist/{chunk-BC74IKAX.js → chunk-REURWDBC.js} +4482 -4705
  26. package/dist/chunk-REURWDBC.js.map +1 -0
  27. package/dist/{chunk-K6MITU6M.cjs → chunk-RILRBUDL.cjs} +62 -62
  28. package/dist/{chunk-K6MITU6M.cjs.map → chunk-RILRBUDL.cjs.map} +1 -1
  29. package/dist/{chunk-W6GDTB4R.cjs → chunk-RR6XPEEG.cjs} +15 -15
  30. package/dist/{chunk-W6GDTB4R.cjs.map → chunk-RR6XPEEG.cjs.map} +1 -1
  31. package/dist/{chunk-QW6SPNPT.cjs → chunk-XJ6VL3AP.cjs} +37 -37
  32. package/dist/{chunk-QW6SPNPT.cjs.map → chunk-XJ6VL3AP.cjs.map} +1 -1
  33. package/dist/{chunk-SMD67GJP.js → chunk-XOIKQW2H.js} +2 -2
  34. package/dist/{chunk-BTMYUK4P.js → chunk-Z3V42W7P.js} +2 -2
  35. package/dist/{chunk-G4XU7BYU.js → chunk-ZTRRB2Y2.js} +2 -2
  36. package/dist/{chunk-6SBAF5VG.cjs → chunk-ZVFH4LTD.cjs} +5 -5
  37. package/dist/{chunk-6SBAF5VG.cjs.map → chunk-ZVFH4LTD.cjs.map} +1 -1
  38. package/dist/components/case-studies/index.cjs +8 -8
  39. package/dist/components/case-studies/index.js +2 -2
  40. package/dist/components/chat/index.cjs +2 -2
  41. package/dist/components/chat/index.js +1 -1
  42. package/dist/components/contact/index.cjs +3 -3
  43. package/dist/components/contact/index.js +2 -2
  44. package/dist/components/docs/index.cjs +5 -5
  45. package/dist/components/docs/index.js +4 -4
  46. package/dist/components/embeds/index.cjs +3 -3
  47. package/dist/components/embeds/index.js +2 -2
  48. package/dist/components/faq/index.cjs +3 -3
  49. package/dist/components/faq/index.js +2 -2
  50. package/dist/components/features/index.cjs +2 -2
  51. package/dist/components/features/index.js +1 -1
  52. package/dist/components/help-center-pages/index.cjs +20 -20
  53. package/dist/components/help-center-pages/index.js +11 -11
  54. package/dist/components/index.cjs +130 -144
  55. package/dist/components/index.cjs.map +1 -1
  56. package/dist/components/index.js +9 -23
  57. package/dist/components/index.js.map +1 -1
  58. package/dist/components/navigation/index.cjs +2 -2
  59. package/dist/components/navigation/index.js +1 -1
  60. package/dist/components/onboarding-guides/index.cjs +5 -5
  61. package/dist/components/onboarding-guides/index.js +4 -4
  62. package/dist/components/related-content/index.cjs +3 -3
  63. package/dist/components/related-content/index.js +2 -2
  64. package/dist/components/tickets/index.cjs +5 -5
  65. package/dist/components/tickets/index.js +4 -4
  66. package/dist/components/ui/assignee-dropdown.d.ts.map +1 -1
  67. package/dist/components/ui/index.cjs +2 -16
  68. package/dist/components/ui/index.cjs.map +1 -1
  69. package/dist/components/ui/index.d.ts +0 -2
  70. package/dist/components/ui/index.d.ts.map +1 -1
  71. package/dist/components/ui/index.js +1 -15
  72. package/dist/index.cjs +2 -16
  73. package/dist/index.cjs.map +1 -1
  74. package/dist/index.js +1 -15
  75. package/package.json +1 -1
  76. package/src/components/ui/assignee-dropdown.tsx +91 -36
  77. package/src/components/ui/index.ts +0 -2
  78. package/dist/chunk-BC74IKAX.js.map +0 -1
  79. package/dist/chunk-TJSJPOPL.cjs.map +0 -1
  80. package/dist/components/ui/searchable-select.d.ts +0 -43
  81. package/dist/components/ui/searchable-select.d.ts.map +0 -1
  82. package/dist/components/ui/test-run.d.ts +0 -74
  83. package/dist/components/ui/test-run.d.ts.map +0 -1
  84. package/src/components/ui/searchable-select.tsx +0 -186
  85. package/src/components/ui/test-run.tsx +0 -282
  86. package/src/stories/SearchableSelect.stories.tsx +0 -139
  87. package/src/stories/TestRun.stories.tsx +0 -118
  88. /package/dist/{chunk-WLRBZXFB.js.map → chunk-2BMYCH3D.js.map} +0 -0
  89. /package/dist/{chunk-AUGM74CO.js.map → chunk-5WZGZ6B2.js.map} +0 -0
  90. /package/dist/{chunk-QBDHNUQX.js.map → chunk-6QJ2QRQE.js.map} +0 -0
  91. /package/dist/{chunk-GXOUXAXU.js.map → chunk-J5KX5K5T.js.map} +0 -0
  92. /package/dist/{chunk-SDNMOAT4.js.map → chunk-KA4ILVCX.js.map} +0 -0
  93. /package/dist/{chunk-ITEPRN6L.js.map → chunk-KRKNTK5Y.js.map} +0 -0
  94. /package/dist/{chunk-UBK5HIMH.js.map → chunk-MD5E5BKO.js.map} +0 -0
  95. /package/dist/{chunk-NETEA5E4.js.map → chunk-OW33LYO2.js.map} +0 -0
  96. /package/dist/{chunk-SMD67GJP.js.map → chunk-XOIKQW2H.js.map} +0 -0
  97. /package/dist/{chunk-BTMYUK4P.js.map → chunk-Z3V42W7P.js.map} +0 -0
  98. /package/dist/{chunk-G4XU7BYU.js.map → chunk-ZTRRB2Y2.js.map} +0 -0
@@ -1,43 +0,0 @@
1
- import * as React from 'react';
2
- export interface SearchableSelectOption {
3
- value: string;
4
- label: string;
5
- /** Optional leading visual for the option row (avatar, icon, badge). */
6
- icon?: React.ReactNode;
7
- }
8
- export interface SearchableSelectProps {
9
- options: SearchableSelectOption[];
10
- /** Currently selected option value; null/undefined when nothing is selected. */
11
- value?: string | null;
12
- onValueChange: (value: string) => void;
13
- /** Default trigger text while nothing is selected. */
14
- placeholder?: string;
15
- /** Placeholder of the search input rendered as the first dropdown item. */
16
- searchPlaceholder?: string;
17
- emptyText?: string;
18
- loadingText?: string;
19
- isLoading?: boolean;
20
- disabled?: boolean;
21
- /**
22
- * Custom trigger node (rendered via Popover.Trigger asChild). When omitted,
23
- * a select-like button (Input/SelectTrigger styling, chevron, placeholder)
24
- * is rendered.
25
- */
26
- trigger?: React.ReactNode;
27
- /** Custom option-row renderer; default is icon + label + selected check. */
28
- renderOption?: (option: SearchableSelectOption, isSelected: boolean) => React.ReactNode;
29
- align?: 'start' | 'center' | 'end';
30
- /** Classes for the default trigger button. */
31
- className?: string;
32
- /** Classes for the popover content (e.g. a fixed width for icon triggers). */
33
- contentClassName?: string;
34
- }
35
- /**
36
- * Select-style dropdown whose first item is a search field filtering the
37
- * option list — the pattern used by the ticket assignee picker ("Search
38
- * users...") generalized for any entity (devices, users, ...). Provide a
39
- * custom `trigger` for icon-button use cases; the default trigger looks and
40
- * behaves like `SelectTrigger`.
41
- */
42
- export declare function SearchableSelect({ options, value, onValueChange, placeholder, searchPlaceholder, emptyText, loadingText, isLoading, disabled, trigger, renderOption, align, className, contentClassName, }: SearchableSelectProps): React.JSX.Element;
43
- //# sourceMappingURL=searchable-select.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"searchable-select.d.ts","sourceRoot":"","sources":["../../../src/components/ui/searchable-select.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAM9B,MAAM,WAAW,sBAAsB;IACrC,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,wEAAwE;IACxE,IAAI,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;CACvB;AAED,MAAM,WAAW,qBAAqB;IACpC,OAAO,EAAE,sBAAsB,EAAE,CAAA;IACjC,gFAAgF;IAChF,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACrB,aAAa,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IACtC,sDAAsD;IACtD,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,2EAA2E;IAC3E,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB;;;;OAIG;IACH,OAAO,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IACzB,4EAA4E;IAC5E,YAAY,CAAC,EAAE,CAAC,MAAM,EAAE,sBAAsB,EAAE,UAAU,EAAE,OAAO,KAAK,KAAK,CAAC,SAAS,CAAA;IACvF,KAAK,CAAC,EAAE,OAAO,GAAG,QAAQ,GAAG,KAAK,CAAA;IAClC,8CAA8C;IAC9C,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,8EAA8E;IAC9E,gBAAgB,CAAC,EAAE,MAAM,CAAA;CAC1B;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,EAC/B,OAAO,EACP,KAAY,EACZ,aAAa,EACb,WAAyB,EACzB,iBAA+B,EAC/B,SAA8B,EAC9B,WAAwB,EACxB,SAAiB,EACjB,QAAgB,EAChB,OAAO,EACP,YAAY,EACZ,KAAe,EACf,SAAS,EACT,gBAAgB,GACjB,EAAE,qBAAqB,qBAuHvB"}
@@ -1,74 +0,0 @@
1
- import * as React from 'react';
2
- import type { QueryResultRow } from './query-report-table';
3
- export declare function formatTestRunDuration(ms: number): string;
4
- /** Column-bar skeleton (header cells + one card row): fixed 160px columns on
5
- every breakpoint, scrolling horizontally when they do not fit. Bars use
6
- bg-ods-bg-surface like the table skeletons — the bg-ods-skeleton token is
7
- the same color as the card background and would be invisible on the row. */
8
- export declare function TestResultsSkeleton(): React.JSX.Element;
9
- /**
10
- * Live test results rendered with the core DataTable (identical header,
11
- * skeleton, and card-row styling to the standard tables). Columns are derived
12
- * from the keys of the returned rows; wide result sets scroll horizontally.
13
- * Unlike the default tables, headers stay visible on every breakpoint.
14
- */
15
- export declare function TestResultsTable({ rows, loading }: {
16
- rows: QueryResultRow[];
17
- loading: boolean;
18
- }): React.JSX.Element;
19
- export interface TestRunResultsProps {
20
- isActive: boolean;
21
- displayRows: QueryResultRow[];
22
- firstError: string | null;
23
- className?: string;
24
- }
25
- /** Error line + empty state + result table for a finished/running test run. */
26
- export declare function TestRunResults({ isActive, displayRows, firstError, className }: TestRunResultsProps): React.JSX.Element;
27
- /** Started / Duration stat cell shown in a test-run controls row. */
28
- export declare function TimingStat({ value, label, className }: {
29
- value: string;
30
- label: string;
31
- className?: string;
32
- }): React.JSX.Element;
33
- /**
34
- * The transport-side contract `useTestRunState` observes. Matches the shape
35
- * of a live-campaign hook (e.g. Fleet live queries in OpenFrame) without
36
- * depending on any concrete backend.
37
- */
38
- export interface TestRunCampaignState {
39
- isRunning: boolean;
40
- startedAt: Date | null;
41
- /** '' before the first run, then 'pending' → 'finished'. */
42
- campaignStatus: string;
43
- results: QueryResultRow[];
44
- errors: Array<{
45
- error: string;
46
- }>;
47
- stopCampaign: () => void;
48
- }
49
- /**
50
- * State machine for one live test run: wraps a campaign-shaped object with
51
- * the run/stop/reset flow, elapsed-duration ticking, and the display flags
52
- * test panels render from. The consumer starts the actual campaign inside the
53
- * `start` callback passed to `run`:
54
- *
55
- * ```tsx
56
- * const campaign = useLiveCampaign() // app-owned transport
57
- * const test = useTestRunState(campaign)
58
- * const handleRun = () => test.run(() => campaign.startCampaign(sql, [hostId]))
59
- * ```
60
- */
61
- export declare function useTestRunState(campaign: TestRunCampaignState): {
62
- hasRun: boolean;
63
- isActive: boolean;
64
- isFinished: boolean;
65
- showResults: boolean;
66
- firstError: string | null;
67
- startedLabel: string;
68
- durationLabel: string;
69
- displayRows: QueryResultRow[];
70
- run: (start: () => Promise<unknown>) => Promise<void>;
71
- stop: () => void;
72
- reset: () => void;
73
- };
74
- //# sourceMappingURL=test-run.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"test-run.d.ts","sourceRoot":"","sources":["../../../src/components/ui/test-run.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAO9B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAA;AAY1D,wBAAgB,qBAAqB,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAMxD;AAKD;;;+EAG+E;AAC/E,wBAAgB,mBAAmB,sBA2BlC;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE;IAAE,IAAI,EAAE,cAAc,EAAE,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,qBAkD/F;AAED,MAAM,WAAW,mBAAmB;IAClC,QAAQ,EAAE,OAAO,CAAA;IACjB,WAAW,EAAE,cAAc,EAAE,CAAA;IAC7B,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;IACzB,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,+EAA+E;AAC/E,wBAAgB,cAAc,CAAC,EAAE,QAAQ,EAAE,WAAW,EAAE,UAAU,EAAE,SAAS,EAAE,EAAE,mBAAmB,qBAsBnG;AAED,qEAAqE;AACrE,wBAAgB,UAAU,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,SAAS,EAAE,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,qBAO3G;AAED;;;;GAIG;AACH,MAAM,WAAW,oBAAoB;IACnC,SAAS,EAAE,OAAO,CAAA;IAClB,SAAS,EAAE,IAAI,GAAG,IAAI,CAAA;IACtB,4DAA4D;IAC5D,cAAc,EAAE,MAAM,CAAA;IACtB,OAAO,EAAE,cAAc,EAAE,CAAA;IACzB,MAAM,EAAE,KAAK,CAAC;QAAE,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;IAChC,YAAY,EAAE,MAAM,IAAI,CAAA;CACzB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,oBAAoB;;;;;;;;;iBAwBhB,MAAM,OAAO,CAAC,OAAO,CAAC;;;EAgEnE"}
@@ -1,186 +0,0 @@
1
- 'use client'
2
-
3
- import * as PopoverPrimitive from '@radix-ui/react-popover'
4
- import * as React from 'react'
5
- import { CheckIcon, SearchIcon } from '../icons-v2-generated'
6
- import { Chevron02DownIcon } from '../icons-v2-generated/arrows/chevron-02-down-icon'
7
- import { cn } from '../../utils/cn'
8
- import { Input } from './input'
9
-
10
- export interface SearchableSelectOption {
11
- value: string
12
- label: string
13
- /** Optional leading visual for the option row (avatar, icon, badge). */
14
- icon?: React.ReactNode
15
- }
16
-
17
- export interface SearchableSelectProps {
18
- options: SearchableSelectOption[]
19
- /** Currently selected option value; null/undefined when nothing is selected. */
20
- value?: string | null
21
- onValueChange: (value: string) => void
22
- /** Default trigger text while nothing is selected. */
23
- placeholder?: string
24
- /** Placeholder of the search input rendered as the first dropdown item. */
25
- searchPlaceholder?: string
26
- emptyText?: string
27
- loadingText?: string
28
- isLoading?: boolean
29
- disabled?: boolean
30
- /**
31
- * Custom trigger node (rendered via Popover.Trigger asChild). When omitted,
32
- * a select-like button (Input/SelectTrigger styling, chevron, placeholder)
33
- * is rendered.
34
- */
35
- trigger?: React.ReactNode
36
- /** Custom option-row renderer; default is icon + label + selected check. */
37
- renderOption?: (option: SearchableSelectOption, isSelected: boolean) => React.ReactNode
38
- align?: 'start' | 'center' | 'end'
39
- /** Classes for the default trigger button. */
40
- className?: string
41
- /** Classes for the popover content (e.g. a fixed width for icon triggers). */
42
- contentClassName?: string
43
- }
44
-
45
- /**
46
- * Select-style dropdown whose first item is a search field filtering the
47
- * option list — the pattern used by the ticket assignee picker ("Search
48
- * users...") generalized for any entity (devices, users, ...). Provide a
49
- * custom `trigger` for icon-button use cases; the default trigger looks and
50
- * behaves like `SelectTrigger`.
51
- */
52
- export function SearchableSelect({
53
- options,
54
- value = null,
55
- onValueChange,
56
- placeholder = 'Select...',
57
- searchPlaceholder = 'Search...',
58
- emptyText = 'No results found',
59
- loadingText = 'Loading…',
60
- isLoading = false,
61
- disabled = false,
62
- trigger,
63
- renderOption,
64
- align = 'start',
65
- className,
66
- contentClassName,
67
- }: SearchableSelectProps) {
68
- const [isOpen, setIsOpen] = React.useState(false)
69
- const [search, setSearch] = React.useState('')
70
-
71
- React.useEffect(() => {
72
- if (!isOpen) setSearch('')
73
- }, [isOpen])
74
-
75
- const filtered = React.useMemo(() => {
76
- const q = search.trim().toLowerCase()
77
- if (!q) return options
78
- return options.filter(o => o.label.toLowerCase().includes(q))
79
- }, [options, search])
80
-
81
- const selectedOption = React.useMemo(
82
- () => (value != null ? options.find(o => o.value === value) : undefined),
83
- [options, value],
84
- )
85
-
86
- const handleSelect = (optionValue: string) => {
87
- onValueChange(optionValue)
88
- setIsOpen(false)
89
- }
90
-
91
- const defaultTrigger = (
92
- <button
93
- type="button"
94
- disabled={disabled}
95
- className={cn(
96
- // Mirrors SelectTrigger so the closed state is indistinguishable from
97
- // the regular Select.
98
- 'flex w-full items-center justify-between gap-2 rounded-md border px-3 h-11 md:h-12 outline-none text-left',
99
- 'text-h4 bg-ods-card border-ods-border',
100
- selectedOption ? 'text-ods-text-primary' : 'text-ods-text-secondary',
101
- 'enabled:hover:bg-ods-bg-hover enabled:hover:border-ods-border-hover enabled:active:bg-ods-bg-active enabled:active:border-ods-border-active',
102
- 'data-[state=open]:border-ods-accent data-[state=open]:hover:border-ods-accent',
103
- 'group disabled:!cursor-not-allowed disabled:bg-ods-bg transition-colors duration-200 cursor-pointer',
104
- className,
105
- )}
106
- >
107
- <span className="line-clamp-1">{selectedOption ? selectedOption.label : placeholder}</span>
108
- <Chevron02DownIcon
109
- className="shrink-0 text-ods-text-secondary transition-all duration-200 group-data-[state=open]:rotate-180 group-data-[state=open]:text-ods-accent"
110
- size={24}
111
- />
112
- </button>
113
- )
114
-
115
- return (
116
- <PopoverPrimitive.Root open={isOpen} onOpenChange={disabled ? undefined : setIsOpen} modal={false}>
117
- <PopoverPrimitive.Trigger asChild>{trigger ?? defaultTrigger}</PopoverPrimitive.Trigger>
118
- <PopoverPrimitive.Portal>
119
- <PopoverPrimitive.Content
120
- align={align}
121
- sideOffset={6}
122
- className={cn(
123
- 'z-50 min-w-[var(--radix-popover-trigger-width)] bg-ods-card border border-ods-border rounded-md shadow-lg overflow-hidden',
124
- 'data-[state=open]:animate-in data-[state=closed]:animate-out',
125
- 'data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
126
- 'data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95',
127
- 'data-[side=bottom]:slide-in-from-top-2 data-[side=top]:slide-in-from-bottom-2',
128
- contentClassName,
129
- )}
130
- >
131
- <div className="border-b border-ods-border">
132
- <Input
133
- autoFocus
134
- value={search}
135
- onChange={e => setSearch(e.target.value)}
136
- placeholder={searchPlaceholder}
137
- startAdornment={<SearchIcon className="size-4 text-ods-text-secondary" />}
138
- className="rounded-none border-0"
139
- />
140
- </div>
141
- <div className="max-h-80 overflow-y-auto py-[var(--spacing-system-xs)]" role="listbox">
142
- {isLoading ? (
143
- <div className="px-[var(--spacing-system-sf)] py-[var(--spacing-system-s)] text-h5 text-ods-text-secondary">
144
- {loadingText}
145
- </div>
146
- ) : filtered.length === 0 ? (
147
- <div className="px-[var(--spacing-system-sf)] py-[var(--spacing-system-s)] text-h5 text-ods-text-secondary">
148
- {emptyText}
149
- </div>
150
- ) : (
151
- filtered.map(opt => {
152
- const isSelected = value === opt.value
153
- return (
154
- <button
155
- key={opt.value}
156
- type="button"
157
- role="option"
158
- aria-selected={isSelected}
159
- onClick={() => handleSelect(opt.value)}
160
- className={cn(
161
- 'flex items-center gap-[var(--spacing-system-xs)] w-full px-[var(--spacing-system-sf)] py-[var(--spacing-system-xs)] text-left',
162
- 'hover:bg-ods-bg-hover transition-colors',
163
- isSelected && 'bg-ods-bg-hover',
164
- )}
165
- >
166
- {renderOption ? (
167
- renderOption(opt, isSelected)
168
- ) : (
169
- <>
170
- {opt.icon}
171
- <span className="flex-1 truncate text-h4 text-ods-text-primary" title={opt.label}>
172
- {opt.label}
173
- </span>
174
- {isSelected && <CheckIcon className="size-4 shrink-0 text-ods-accent" />}
175
- </>
176
- )}
177
- </button>
178
- )
179
- })
180
- )}
181
- </div>
182
- </PopoverPrimitive.Content>
183
- </PopoverPrimitive.Portal>
184
- </PopoverPrimitive.Root>
185
- )
186
- }
@@ -1,282 +0,0 @@
1
- 'use client'
2
-
3
- import type { ColumnDef, Row } from '@tanstack/react-table'
4
- import * as React from 'react'
5
- import { SearchIcon } from '../icons-v2-generated'
6
- import { cn } from '../../utils/cn'
7
- import { formatTime } from '../../utils/format-date'
8
- import { DataTable } from './data-table'
9
- import { useDataTable } from './data-table/use-data-table'
10
- import { NoData } from './no-data'
11
- import type { QueryResultRow } from './query-report-table'
12
- import { ScrollShadow } from './scroll-fade'
13
-
14
- /*
15
- * Live test-run building blocks: the UI (timing stats, one-row skeleton,
16
- * results table) and the run state machine for "run a query/script somewhere
17
- * and stream tabular results back" flows. The transport is NOT part of this
18
- * module — consumers own campaign creation (Fleet live queries, script runs,
19
- * ...) and pass a `TestRunCampaignState`-shaped object into
20
- * `useTestRunState`.
21
- */
22
-
23
- export function formatTestRunDuration(ms: number): string {
24
- const totalSeconds = Math.floor(ms / 1000)
25
- const hours = Math.floor(totalSeconds / 3600)
26
- const minutes = Math.floor((totalSeconds % 3600) / 60)
27
- const seconds = totalSeconds % 60
28
- return `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`
29
- }
30
-
31
- const RESULT_COLUMN_MIN_WIDTH = 176
32
- const SKELETON_COLUMNS = 6
33
-
34
- /** Column-bar skeleton (header cells + one card row): fixed 160px columns on
35
- every breakpoint, scrolling horizontally when they do not fit. Bars use
36
- bg-ods-bg-surface like the table skeletons — the bg-ods-skeleton token is
37
- the same color as the card background and would be invisible on the row. */
38
- export function TestResultsSkeleton() {
39
- return (
40
- <ScrollShadow axis="horizontal">
41
- {/* No gap between the header bars and the row card — the real table's
42
- header and body sit flush. */}
43
- <div className="flex w-max min-w-full flex-col">
44
- <div className="flex items-center gap-[var(--spacing-system-mf)] px-[var(--spacing-system-mf)]">
45
- {Array.from({ length: SKELETON_COLUMNS }).map((_, i) => (
46
- <div key={`skeleton-header-${i}`} className="flex h-12 w-[160px] shrink-0 items-center">
47
- <div className="h-4 w-3/4 rounded-sm bg-ods-bg-surface animate-pulse" />
48
- </div>
49
- ))}
50
- </div>
51
- <div className="rounded-[6px] border border-ods-border bg-ods-card overflow-hidden animate-pulse">
52
- {/* Same INNER row heights as the DataTable (66/78px + the 1px card
53
- border on each side = 68/80px total, per design). */}
54
- <div className="flex h-[66px] md:h-[78px] items-center gap-[var(--spacing-system-mf)] px-[var(--spacing-system-mf)]">
55
- {Array.from({ length: SKELETON_COLUMNS }).map((_, i) => (
56
- <div key={`skeleton-cell-${i}`} className="w-[160px] shrink-0">
57
- <div className="h-5 w-3/4 rounded-sm bg-ods-bg-surface" />
58
- </div>
59
- ))}
60
- </div>
61
- </div>
62
- </div>
63
- </ScrollShadow>
64
- )
65
- }
66
-
67
- /**
68
- * Live test results rendered with the core DataTable (identical header,
69
- * skeleton, and card-row styling to the standard tables). Columns are derived
70
- * from the keys of the returned rows; wide result sets scroll horizontally.
71
- * Unlike the default tables, headers stay visible on every breakpoint.
72
- */
73
- export function TestResultsTable({ rows, loading }: { rows: QueryResultRow[]; loading: boolean }) {
74
- const columnKeys = React.useMemo(() => (rows.length > 0 ? Object.keys(rows[0]) : []), [rows])
75
-
76
- const columns = React.useMemo<ColumnDef<QueryResultRow>[]>(() => {
77
- return columnKeys.map(key => ({
78
- id: key,
79
- accessorFn: (row: QueryResultRow) => row[key],
80
- header: key,
81
- enableSorting: false,
82
- cell: ({ row }: { row: Row<QueryResultRow> }) => {
83
- const value = row.original[key]
84
- return (
85
- <span className="text-h4 text-ods-text-primary truncate">
86
- {value === null || value === undefined ? '-' : String(value)}
87
- </span>
88
- )
89
- },
90
- meta: {
91
- width: 'flex-1 min-w-[176px]',
92
- // Show headers on every breakpoint; headerClassName sizes the cells
93
- // below lg where meta.width is not applied.
94
- alwaysShowHeader: true,
95
- headerClassName: 'flex-1 min-w-[176px]',
96
- },
97
- }))
98
- }, [columnKeys])
99
-
100
- const table = useDataTable<QueryResultRow>({
101
- data: rows,
102
- columns,
103
- getRowId: (_row: QueryResultRow, index: number) => String(index),
104
- enableSorting: false,
105
- })
106
-
107
- if (loading) {
108
- return <TestResultsSkeleton />
109
- }
110
-
111
- return (
112
- <ScrollShadow axis="horizontal">
113
- <div style={{ minWidth: Math.max(1, columnKeys.length) * RESULT_COLUMN_MIN_WIDTH }}>
114
- <DataTable table={table}>
115
- {/* "flex" overrides the header's default "hidden md:flex" so the
116
- headers render on mobile too. */}
117
- <DataTable.Header className="flex flex-col" />
118
- <DataTable.Body loading={false} />
119
- </DataTable>
120
- </div>
121
- </ScrollShadow>
122
- )
123
- }
124
-
125
- export interface TestRunResultsProps {
126
- isActive: boolean
127
- displayRows: QueryResultRow[]
128
- firstError: string | null
129
- className?: string
130
- }
131
-
132
- /** Error line + empty state + result table for a finished/running test run. */
133
- export function TestRunResults({ isActive, displayRows, firstError, className }: TestRunResultsProps) {
134
- return (
135
- <div className={cn('flex flex-col gap-[var(--spacing-system-xsf)]', className)}>
136
- {firstError && (
137
- <p role="alert" className="text-h6 text-ods-error">
138
- {firstError}
139
- </p>
140
- )}
141
- {!isActive && displayRows.length === 0 ? (
142
- // With zero rows the error line alone explains the outcome; a
143
- // "No results returned" empty state next to it would mislead.
144
- // Otherwise: fixed-height empty state matching the 1-row
145
- // skeleton/result height (48px header + 80px row, no gap) so the
146
- // block does not jump between loading/result/empty.
147
- !firstError && (
148
- <NoData icon={<SearchIcon />} title="No results returned" className="h-[128px] justify-center !py-0" />
149
- )
150
- ) : (
151
- <TestResultsTable rows={displayRows} loading={isActive && displayRows.length === 0} />
152
- )}
153
- </div>
154
- )
155
- }
156
-
157
- /** Started / Duration stat cell shown in a test-run controls row. */
158
- export function TimingStat({ value, label, className }: { value: string; label: string; className?: string }) {
159
- return (
160
- <div className={cn('flex flex-col justify-center min-w-0', className)}>
161
- <span className="text-h4 text-ods-text-secondary truncate">{value}</span>
162
- <span className="text-h6 text-ods-text-secondary truncate">{label}</span>
163
- </div>
164
- )
165
- }
166
-
167
- /**
168
- * The transport-side contract `useTestRunState` observes. Matches the shape
169
- * of a live-campaign hook (e.g. Fleet live queries in OpenFrame) without
170
- * depending on any concrete backend.
171
- */
172
- export interface TestRunCampaignState {
173
- isRunning: boolean
174
- startedAt: Date | null
175
- /** '' before the first run, then 'pending' → 'finished'. */
176
- campaignStatus: string
177
- results: QueryResultRow[]
178
- errors: Array<{ error: string }>
179
- stopCampaign: () => void
180
- }
181
-
182
- /**
183
- * State machine for one live test run: wraps a campaign-shaped object with
184
- * the run/stop/reset flow, elapsed-duration ticking, and the display flags
185
- * test panels render from. The consumer starts the actual campaign inside the
186
- * `start` callback passed to `run`:
187
- *
188
- * ```tsx
189
- * const campaign = useLiveCampaign() // app-owned transport
190
- * const test = useTestRunState(campaign)
191
- * const handleRun = () => test.run(() => campaign.startCampaign(sql, [hostId]))
192
- * ```
193
- */
194
- export function useTestRunState(campaign: TestRunCampaignState) {
195
- // Sticky "a run has been started" flag: campaignStatus resets to '' for a
196
- // moment when a new run starts, which would flash the initial Run button
197
- // between "Test Again" clicks without it.
198
- const [hasRun, setHasRun] = React.useState(false)
199
- // True while the campaign is being created (before isRunning flips) —
200
- // without it the button would flash "Test Again" right after a Run click.
201
- const [isStarting, setIsStarting] = React.useState(false)
202
- const [durationMs, setDurationMs] = React.useState(0)
203
-
204
- React.useEffect(() => {
205
- if (!campaign.startedAt || !campaign.isRunning) {
206
- if (campaign.startedAt && !campaign.isRunning) {
207
- setDurationMs(Date.now() - campaign.startedAt.getTime())
208
- }
209
- return
210
- }
211
- setDurationMs(Date.now() - campaign.startedAt.getTime())
212
- const interval = setInterval(() => {
213
- setDurationMs(Date.now() - campaign.startedAt!.getTime())
214
- }, 1000)
215
- return () => clearInterval(interval)
216
- }, [campaign.startedAt, campaign.isRunning])
217
-
218
- const run = React.useCallback(async (start: () => Promise<unknown>) => {
219
- setHasRun(true)
220
- setDurationMs(0)
221
- setIsStarting(true)
222
- try {
223
- await start()
224
- } finally {
225
- setIsStarting(false)
226
- }
227
- }, [])
228
-
229
- // Stop/reset also drop isStarting so the Stop button doesn't stay "active"
230
- // for the moment between the user's click and the pending start promise
231
- // settling (its finally clears the flag again — harmless).
232
- const stop = React.useCallback(() => {
233
- setIsStarting(false)
234
- campaign.stopCampaign()
235
- }, [campaign.stopCampaign])
236
-
237
- /** Stop anything in flight and clear the run state (panel close/cancel). */
238
- const reset = React.useCallback(() => {
239
- setIsStarting(false)
240
- campaign.stopCampaign()
241
- setDurationMs(0)
242
- setHasRun(false)
243
- }, [campaign.stopCampaign])
244
-
245
- const isActive = campaign.isRunning || isStarting
246
- const isFinished = campaign.campaignStatus === 'finished'
247
- const showResults = isActive || (hasRun && isFinished)
248
-
249
- const firstError = isFinished && campaign.errors.length > 0 ? campaign.errors[0].error : null
250
-
251
- // Started/Duration show zeros until the current run's timing is real:
252
- // hasRun=false after a reset, and campaignStatus resets while the next
253
- // run is being created, so stale values never linger on screen.
254
- const showTiming = hasRun && campaign.startedAt !== null && (campaign.isRunning || isFinished)
255
-
256
- const startedLabel = showTiming && campaign.startedAt ? formatTime(campaign.startedAt) : '00:00 PM'
257
- const durationLabel = showTiming ? formatTestRunDuration(durationMs) : '00:00:00'
258
-
259
- // Humanize raw result keys for the table headers (snake_case -> spaced
260
- // words); the row data itself is shown exactly as returned.
261
- const displayRows = React.useMemo<QueryResultRow[]>(
262
- () =>
263
- campaign.results.map(row =>
264
- Object.fromEntries(Object.entries(row).map(([key, value]) => [key.replace(/_/g, ' '), value])),
265
- ),
266
- [campaign.results],
267
- )
268
-
269
- return {
270
- hasRun,
271
- isActive,
272
- isFinished,
273
- showResults,
274
- firstError,
275
- startedLabel,
276
- durationLabel,
277
- displayRows,
278
- run,
279
- stop,
280
- reset,
281
- }
282
- }