@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.
- package/dist/{chunk-WLRBZXFB.js → chunk-2BMYCH3D.js} +2 -2
- package/dist/{chunk-AUGM74CO.js → chunk-5WZGZ6B2.js} +4 -4
- package/dist/{chunk-QBDHNUQX.js → chunk-6QJ2QRQE.js} +4 -4
- package/dist/{chunk-UU4DW7JX.cjs → chunk-BVHMDHQY.cjs} +31 -31
- package/dist/{chunk-UU4DW7JX.cjs.map → chunk-BVHMDHQY.cjs.map} +1 -1
- package/dist/{chunk-KTD2MGYF.cjs → chunk-C4QKMOQK.cjs} +7 -7
- package/dist/{chunk-KTD2MGYF.cjs.map → chunk-C4QKMOQK.cjs.map} +1 -1
- package/dist/{chunk-HWK5CH5N.cjs → chunk-F3BH5B7M.cjs} +11 -11
- package/dist/{chunk-HWK5CH5N.cjs.map → chunk-F3BH5B7M.cjs.map} +1 -1
- package/dist/{chunk-EO7MUGY4.cjs → chunk-HQITFULS.cjs} +9 -9
- package/dist/{chunk-EO7MUGY4.cjs.map → chunk-HQITFULS.cjs.map} +1 -1
- package/dist/{chunk-GXOUXAXU.js → chunk-J5KX5K5T.js} +2 -2
- package/dist/{chunk-SDNMOAT4.js → chunk-KA4ILVCX.js} +2 -2
- package/dist/{chunk-ITEPRN6L.js → chunk-KRKNTK5Y.js} +2 -2
- package/dist/{chunk-UBK5HIMH.js → chunk-MD5E5BKO.js} +4 -4
- package/dist/{chunk-RAYDQGJV.cjs → chunk-NTHTORJ3.cjs} +59 -59
- package/dist/{chunk-RAYDQGJV.cjs.map → chunk-NTHTORJ3.cjs.map} +1 -1
- package/dist/{chunk-TJSJPOPL.cjs → chunk-NZYUSFMI.cjs} +350 -573
- package/dist/chunk-NZYUSFMI.cjs.map +1 -0
- package/dist/{chunk-NETEA5E4.js → chunk-OW33LYO2.js} +4 -4
- package/dist/{chunk-4TFUB2CR.cjs → chunk-PHYEC3EG.cjs} +26 -26
- package/dist/{chunk-4TFUB2CR.cjs.map → chunk-PHYEC3EG.cjs.map} +1 -1
- package/dist/{chunk-QL7S3CH4.cjs → chunk-RB6HX4KC.cjs} +26 -26
- package/dist/{chunk-QL7S3CH4.cjs.map → chunk-RB6HX4KC.cjs.map} +1 -1
- package/dist/{chunk-BC74IKAX.js → chunk-REURWDBC.js} +4482 -4705
- package/dist/chunk-REURWDBC.js.map +1 -0
- package/dist/{chunk-K6MITU6M.cjs → chunk-RILRBUDL.cjs} +62 -62
- package/dist/{chunk-K6MITU6M.cjs.map → chunk-RILRBUDL.cjs.map} +1 -1
- package/dist/{chunk-W6GDTB4R.cjs → chunk-RR6XPEEG.cjs} +15 -15
- package/dist/{chunk-W6GDTB4R.cjs.map → chunk-RR6XPEEG.cjs.map} +1 -1
- package/dist/{chunk-QW6SPNPT.cjs → chunk-XJ6VL3AP.cjs} +37 -37
- package/dist/{chunk-QW6SPNPT.cjs.map → chunk-XJ6VL3AP.cjs.map} +1 -1
- package/dist/{chunk-SMD67GJP.js → chunk-XOIKQW2H.js} +2 -2
- package/dist/{chunk-BTMYUK4P.js → chunk-Z3V42W7P.js} +2 -2
- package/dist/{chunk-G4XU7BYU.js → chunk-ZTRRB2Y2.js} +2 -2
- package/dist/{chunk-6SBAF5VG.cjs → chunk-ZVFH4LTD.cjs} +5 -5
- package/dist/{chunk-6SBAF5VG.cjs.map → chunk-ZVFH4LTD.cjs.map} +1 -1
- package/dist/components/case-studies/index.cjs +8 -8
- package/dist/components/case-studies/index.js +2 -2
- package/dist/components/chat/index.cjs +2 -2
- package/dist/components/chat/index.js +1 -1
- package/dist/components/contact/index.cjs +3 -3
- package/dist/components/contact/index.js +2 -2
- package/dist/components/docs/index.cjs +5 -5
- package/dist/components/docs/index.js +4 -4
- package/dist/components/embeds/index.cjs +3 -3
- package/dist/components/embeds/index.js +2 -2
- package/dist/components/faq/index.cjs +3 -3
- package/dist/components/faq/index.js +2 -2
- package/dist/components/features/index.cjs +2 -2
- package/dist/components/features/index.js +1 -1
- package/dist/components/help-center-pages/index.cjs +20 -20
- package/dist/components/help-center-pages/index.js +11 -11
- package/dist/components/index.cjs +130 -144
- package/dist/components/index.cjs.map +1 -1
- package/dist/components/index.js +9 -23
- package/dist/components/index.js.map +1 -1
- package/dist/components/navigation/index.cjs +2 -2
- package/dist/components/navigation/index.js +1 -1
- package/dist/components/onboarding-guides/index.cjs +5 -5
- package/dist/components/onboarding-guides/index.js +4 -4
- package/dist/components/related-content/index.cjs +3 -3
- package/dist/components/related-content/index.js +2 -2
- package/dist/components/tickets/index.cjs +5 -5
- package/dist/components/tickets/index.js +4 -4
- package/dist/components/ui/assignee-dropdown.d.ts.map +1 -1
- package/dist/components/ui/index.cjs +2 -16
- package/dist/components/ui/index.cjs.map +1 -1
- package/dist/components/ui/index.d.ts +0 -2
- package/dist/components/ui/index.d.ts.map +1 -1
- package/dist/components/ui/index.js +1 -15
- package/dist/index.cjs +2 -16
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +1 -15
- package/package.json +1 -1
- package/src/components/ui/assignee-dropdown.tsx +91 -36
- package/src/components/ui/index.ts +0 -2
- package/dist/chunk-BC74IKAX.js.map +0 -1
- package/dist/chunk-TJSJPOPL.cjs.map +0 -1
- package/dist/components/ui/searchable-select.d.ts +0 -43
- package/dist/components/ui/searchable-select.d.ts.map +0 -1
- package/dist/components/ui/test-run.d.ts +0 -74
- package/dist/components/ui/test-run.d.ts.map +0 -1
- package/src/components/ui/searchable-select.tsx +0 -186
- package/src/components/ui/test-run.tsx +0 -282
- package/src/stories/SearchableSelect.stories.tsx +0 -139
- package/src/stories/TestRun.stories.tsx +0 -118
- /package/dist/{chunk-WLRBZXFB.js.map → chunk-2BMYCH3D.js.map} +0 -0
- /package/dist/{chunk-AUGM74CO.js.map → chunk-5WZGZ6B2.js.map} +0 -0
- /package/dist/{chunk-QBDHNUQX.js.map → chunk-6QJ2QRQE.js.map} +0 -0
- /package/dist/{chunk-GXOUXAXU.js.map → chunk-J5KX5K5T.js.map} +0 -0
- /package/dist/{chunk-SDNMOAT4.js.map → chunk-KA4ILVCX.js.map} +0 -0
- /package/dist/{chunk-ITEPRN6L.js.map → chunk-KRKNTK5Y.js.map} +0 -0
- /package/dist/{chunk-UBK5HIMH.js.map → chunk-MD5E5BKO.js.map} +0 -0
- /package/dist/{chunk-NETEA5E4.js.map → chunk-OW33LYO2.js.map} +0 -0
- /package/dist/{chunk-SMD67GJP.js.map → chunk-XOIKQW2H.js.map} +0 -0
- /package/dist/{chunk-BTMYUK4P.js.map → chunk-Z3V42W7P.js.map} +0 -0
- /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
|
-
}
|