@lightdash/cli 2.265.0 → 2.267.0

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.
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Renders the small Markdown subset an investigation explanation uses:
3
+ * headings, paragraphs, bullet and numbered lists, bold, italics and inline
4
+ * code. Anything else is shown as plain text. Never renders HTML.
5
+ */
6
+ import type { ReactNode } from 'react';
7
+
8
+ const inline = (text: string): ReactNode[] => {
9
+ const parts: ReactNode[] = [];
10
+ const pattern = /(\*\*[^*]+\*\*|\*[^*]+\*|`[^`]+`)/g;
11
+ let last = 0;
12
+ let key = 0;
13
+ for (const match of text.matchAll(pattern)) {
14
+ const start = match.index ?? 0;
15
+ if (start > last) parts.push(text.slice(last, start));
16
+ const token = match[0];
17
+ if (token.startsWith('**')) {
18
+ parts.push(<strong key={key++}>{token.slice(2, -2)}</strong>);
19
+ } else if (token.startsWith('`')) {
20
+ parts.push(
21
+ <code key={key++} className="rounded bg-muted px-1 font-mono text-xs">
22
+ {token.slice(1, -1)}
23
+ </code>,
24
+ );
25
+ } else {
26
+ parts.push(<em key={key++}>{token.slice(1, -1)}</em>);
27
+ }
28
+ last = start + token.length;
29
+ }
30
+ if (last < text.length) parts.push(text.slice(last));
31
+ return parts;
32
+ };
33
+
34
+ type Block =
35
+ | { kind: 'heading'; text: string }
36
+ | { kind: 'paragraph'; text: string }
37
+ | { kind: 'bullets'; items: string[] }
38
+ | { kind: 'numbered'; items: string[] };
39
+
40
+ const parse = (source: string): Block[] => {
41
+ const blocks: Block[] = [];
42
+ const lines = source.replace(/\r\n/g, '\n').split('\n');
43
+ let paragraph: string[] = [];
44
+ const flush = () => {
45
+ if (paragraph.length) {
46
+ blocks.push({ kind: 'paragraph', text: paragraph.join(' ') });
47
+ paragraph = [];
48
+ }
49
+ };
50
+ for (const raw of lines) {
51
+ const line = raw.trim();
52
+ const heading = /^#{1,6}\s+(.*)$/.exec(line);
53
+ const bullet = /^[-*]\s+(.*)$/.exec(line);
54
+ const numbered = /^\d+[.)]\s+(.*)$/.exec(line);
55
+ if (line === '') {
56
+ flush();
57
+ } else if (heading) {
58
+ flush();
59
+ blocks.push({ kind: 'heading', text: heading[1] });
60
+ } else if (bullet) {
61
+ flush();
62
+ const prev = blocks[blocks.length - 1];
63
+ if (prev?.kind === 'bullets') prev.items.push(bullet[1]);
64
+ else blocks.push({ kind: 'bullets', items: [bullet[1]] });
65
+ } else if (numbered) {
66
+ flush();
67
+ const prev = blocks[blocks.length - 1];
68
+ if (prev?.kind === 'numbered') prev.items.push(numbered[1]);
69
+ else blocks.push({ kind: 'numbered', items: [numbered[1]] });
70
+ } else {
71
+ paragraph.push(line);
72
+ }
73
+ }
74
+ flush();
75
+ return blocks;
76
+ };
77
+
78
+ export function Markdown({ text }: { text: string }) {
79
+ return (
80
+ <div className="space-y-2 leading-relaxed">
81
+ {parse(text).map((block, index) => {
82
+ switch (block.kind) {
83
+ case 'heading':
84
+ return (
85
+ <p key={index} className="pt-1 font-semibold">
86
+ {inline(block.text)}
87
+ </p>
88
+ );
89
+ case 'bullets':
90
+ return (
91
+ <ul key={index} className="list-disc space-y-1 pl-5">
92
+ {block.items.map((item, i) => (
93
+ <li key={i}>{inline(item)}</li>
94
+ ))}
95
+ </ul>
96
+ );
97
+ case 'numbered':
98
+ return (
99
+ <ol key={index} className="list-decimal space-y-1 pl-5">
100
+ {block.items.map((item, i) => (
101
+ <li key={i}>{inline(item)}</li>
102
+ ))}
103
+ </ol>
104
+ );
105
+ default:
106
+ return <p key={index}>{inline(block.text)}</p>;
107
+ }
108
+ })}
109
+ </div>
110
+ );
111
+ }
@@ -0,0 +1,10 @@
1
+ export { InsightsSummary } from './InsightsSummary';
2
+ export { InvestigationCard } from './InvestigationCard';
3
+ export { InvestigateMenuItem } from './InvestigateMenuItem';
4
+ export {
5
+ InsightMarker,
6
+ insightCellProps,
7
+ insightTooltipText,
8
+ markerFor,
9
+ MARKER_COLOR,
10
+ } from './markers';
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Severity-aware markers for query-bound charts. `high`, `medium` and
3
+ * `positive` earn a marker; `info` is context for the summary and never
4
+ * paints a data point. `fieldId` is the series' `dataKey`, so an anomaly on
5
+ * one metric never marks the other series of the same chart.
6
+ */
7
+ import type { CSSProperties, MouseEvent } from 'react';
8
+ import type { Insight, QueryInsights } from '@lightdash/query-sdk';
9
+
10
+ const MARKED = new Set<Insight['severity']>(['high', 'medium', 'positive']);
11
+
12
+ export const MARKER_COLOR: Record<Insight['severity'], string> = {
13
+ high: 'var(--destructive)',
14
+ medium: 'var(--chart-4)',
15
+ positive: 'var(--chart-2)',
16
+ info: 'var(--muted-foreground)',
17
+ };
18
+
19
+ /** The anomaly that should mark this row of this series, or null. */
20
+ export function markerFor(
21
+ insights: QueryInsights,
22
+ row: Record<string, unknown> | null | undefined,
23
+ fieldId?: string,
24
+ ): Insight | null {
25
+ return (
26
+ insights
27
+ .matches(row as never, fieldId)
28
+ .find((a) => MARKED.has(a.severity)) ?? null
29
+ );
30
+ }
31
+
32
+ type MarkerProps = {
33
+ insights: QueryInsights;
34
+ /** The series' `dataKey`; passed through to `onOpenMenu`. */
35
+ fieldId: string;
36
+ /** Opens the point's action menu with the row, the event and the series. */
37
+ onOpenMenu: (
38
+ row: Record<string, unknown>,
39
+ event: MouseEvent,
40
+ fieldId: string,
41
+ ) => void;
42
+ /** Series colour for unflagged points; defaults to the first chart colour. */
43
+ color?: string;
44
+ // Recharts fills these in when the element is used as `dot` / `activeDot`.
45
+ cx?: number;
46
+ cy?: number;
47
+ payload?: Record<string, unknown>;
48
+ };
49
+
50
+ /**
51
+ * Line / area dot renderer. Pass the same element as both `dot` and
52
+ * `activeDot` so the hover dot never covers the marker and steals the click:
53
+ *
54
+ * const dot = <InsightMarker insights={insights} fieldId="revenue" onOpenMenu={openMenu} />;
55
+ * <Line dataKey="revenue" dot={dot} activeDot={dot} />
56
+ */
57
+ export function InsightMarker({
58
+ insights,
59
+ fieldId,
60
+ onOpenMenu,
61
+ color = 'var(--chart-1)',
62
+ cx,
63
+ cy,
64
+ payload,
65
+ }: MarkerProps) {
66
+ if (cx == null || cy == null || !payload) return null;
67
+ const anomaly = markerFor(insights, payload, fieldId);
68
+ const style: CSSProperties = { cursor: 'pointer' };
69
+ return (
70
+ <circle
71
+ cx={cx}
72
+ cy={cy}
73
+ r={anomaly ? 5 : 3}
74
+ fill={anomaly ? MARKER_COLOR[anomaly.severity] : color}
75
+ stroke={anomaly ? 'var(--background)' : 'none'}
76
+ strokeWidth={anomaly ? 1.5 : 0}
77
+ style={style}
78
+ onClick={(event) => onOpenMenu(payload, event, fieldId)}
79
+ />
80
+ );
81
+ }
82
+
83
+ /**
84
+ * Bar / cell props for the same treatment on categorical charts:
85
+ *
86
+ * <Bar dataKey="revenue" onClick={(d, _i, e) => openMenu(d.payload, e, 'revenue')}>
87
+ * {rows.map((row) => <Cell key={row.status} {...insightCellProps(insights, row, 'revenue')} />)}
88
+ * </Bar>
89
+ */
90
+ export function insightCellProps(
91
+ insights: QueryInsights,
92
+ row: Record<string, unknown>,
93
+ fieldId: string,
94
+ ): { stroke?: string; strokeWidth?: number; style: CSSProperties } {
95
+ const anomaly = markerFor(insights, row, fieldId);
96
+ return {
97
+ stroke: anomaly ? MARKER_COLOR[anomaly.severity] : undefined,
98
+ strokeWidth: anomaly ? 2 : 0,
99
+ style: { cursor: 'pointer' },
100
+ };
101
+ }
102
+
103
+ /** Text for a tooltip line under a value; null when that series' row is not
104
+ * flagged. Omit `fieldId` to take the first flagged series of the row. */
105
+ export function insightTooltipText(
106
+ insights: QueryInsights,
107
+ row: Record<string, unknown> | null | undefined,
108
+ fieldId?: string,
109
+ ): string | null {
110
+ return markerFor(insights, row, fieldId)?.text ?? null;
111
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lightdash/cli",
3
- "version": "2.265.0",
3
+ "version": "2.267.0",
4
4
  "description": "Lightdash CLI tool",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -22,8 +22,8 @@
22
22
  "@actions/core": "1.11.1",
23
23
  "@aws-sdk/credential-providers": "3.995.0",
24
24
  "@casl/ability": "6.8.0",
25
- "@lightdash/common": "2.265.0",
26
- "@lightdash/warehouses": "2.265.0",
25
+ "@lightdash/common": "2.267.0",
26
+ "@lightdash/warehouses": "2.267.0",
27
27
  "@sidvind/better-ajv-errors": "7.0.0",
28
28
  "@types/columnify": "1.5.1",
29
29
  "ajv": "8.18.0",