mirror of
https://github.com/SigNoz/signoz.git
synced 2026-03-18 02:32:13 +00:00
Compare commits
5 Commits
feat/gaps-
...
refactor/s
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3ec91fcc19 | ||
|
|
53e46a3a2c | ||
|
|
756c3274ff | ||
|
|
05cd026983 | ||
|
|
1770ded9df |
@@ -24,8 +24,7 @@ const config: Config.InitialOptions = {
|
||||
'<rootDir>/node_modules/@signozhq/icons/dist/index.esm.js',
|
||||
'^react-syntax-highlighter/dist/esm/(.*)$':
|
||||
'<rootDir>/node_modules/react-syntax-highlighter/dist/cjs/$1',
|
||||
'^@signozhq/(?!ui$)([^/]+)$':
|
||||
'<rootDir>/node_modules/@signozhq/$1/dist/$1.js',
|
||||
'^@signozhq/([^/]+)$': '<rootDir>/node_modules/@signozhq/$1/dist/$1.js',
|
||||
},
|
||||
extensionsToTreatAsEsm: ['.ts'],
|
||||
testMatch: ['<rootDir>/src/**/*?(*.)(test).(ts|js)?(x)'],
|
||||
|
||||
@@ -67,7 +67,6 @@
|
||||
"@signozhq/table": "0.3.7",
|
||||
"@signozhq/toggle-group": "0.0.1",
|
||||
"@signozhq/tooltip": "0.0.2",
|
||||
"@signozhq/ui": "0.0.4",
|
||||
"@tanstack/react-table": "8.20.6",
|
||||
"@tanstack/react-virtual": "3.11.2",
|
||||
"@uiw/codemirror-theme-copilot": "4.23.11",
|
||||
|
||||
1
frontend/src/auto-import-registry.d.ts
vendored
1
frontend/src/auto-import-registry.d.ts
vendored
@@ -30,4 +30,3 @@ import '@signozhq/switch';
|
||||
import '@signozhq/table';
|
||||
import '@signozhq/toggle-group';
|
||||
import '@signozhq/tooltip';
|
||||
import '@signozhq/ui';
|
||||
|
||||
@@ -123,7 +123,7 @@ export const prepareUPlotConfig = ({
|
||||
drawStyle: hasSingleValidPoint ? DrawStyle.Points : DrawStyle.Line,
|
||||
label: label,
|
||||
colorMapping: widget.customLegendColors ?? {},
|
||||
spanGaps: widget.spanGaps ?? true,
|
||||
spanGaps: true,
|
||||
lineStyle: widget.lineStyle || LineStyle.Solid,
|
||||
lineInterpolation: widget.lineInterpolation || LineInterpolation.Spline,
|
||||
showPoints:
|
||||
|
||||
@@ -30,17 +30,6 @@
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.section-heading {
|
||||
font-family: 'Space Mono';
|
||||
color: var(--bg-vanilla-400);
|
||||
font-size: 13px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 18px; /* 138.462% */
|
||||
letter-spacing: 0.52px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
padding: 14px 14px 14px 12px;
|
||||
|
||||
@@ -7,10 +7,9 @@ import {
|
||||
} from 'lib/uPlotV2/config/types';
|
||||
import { Paintbrush } from 'lucide-react';
|
||||
|
||||
import DisconnectValuesSelector from '../../components/DisconnectValuesSelector/DisconnectValuesSelector';
|
||||
import FillModeSelector from '../../components/FillModeSelector/FillModeSelector';
|
||||
import LineInterpolationSelector from '../../components/LineInterpolationSelector/LineInterpolationSelector';
|
||||
import LineStyleSelector from '../../components/LineStyleSelector/LineStyleSelector';
|
||||
import { FillModeSelector } from '../../components/FillModeSelector/FillModeSelector';
|
||||
import { LineInterpolationSelector } from '../../components/LineInterpolationSelector/LineInterpolationSelector';
|
||||
import { LineStyleSelector } from '../../components/LineStyleSelector/LineStyleSelector';
|
||||
import SettingsSection from '../../components/SettingsSection/SettingsSection';
|
||||
|
||||
interface ChartAppearanceSectionProps {
|
||||
@@ -22,13 +21,10 @@ interface ChartAppearanceSectionProps {
|
||||
setLineInterpolation: Dispatch<SetStateAction<LineInterpolation>>;
|
||||
showPoints: boolean;
|
||||
setShowPoints: Dispatch<SetStateAction<boolean>>;
|
||||
spanGaps: boolean | number;
|
||||
setSpanGaps: Dispatch<SetStateAction<boolean | number>>;
|
||||
allowFillMode: boolean;
|
||||
allowLineStyle: boolean;
|
||||
allowLineInterpolation: boolean;
|
||||
allowShowPoints: boolean;
|
||||
allowSpanGaps: boolean;
|
||||
}
|
||||
|
||||
export default function ChartAppearanceSection({
|
||||
@@ -40,13 +36,10 @@ export default function ChartAppearanceSection({
|
||||
setLineInterpolation,
|
||||
showPoints,
|
||||
setShowPoints,
|
||||
spanGaps,
|
||||
setSpanGaps,
|
||||
allowFillMode,
|
||||
allowLineStyle,
|
||||
allowLineInterpolation,
|
||||
allowShowPoints,
|
||||
allowSpanGaps,
|
||||
}: ChartAppearanceSectionProps): JSX.Element {
|
||||
return (
|
||||
<SettingsSection title="Chart Appearance" icon={<Paintbrush size={14} />}>
|
||||
@@ -73,9 +66,6 @@ export default function ChartAppearanceSection({
|
||||
<Switch size="small" checked={showPoints} onChange={setShowPoints} />
|
||||
</section>
|
||||
)}
|
||||
{allowSpanGaps && (
|
||||
<DisconnectValuesSelector value={spanGaps} onChange={setSpanGaps} />
|
||||
)}
|
||||
</SettingsSection>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -178,8 +178,6 @@ describe('RightContainer - Alerts Section', () => {
|
||||
setLineStyle: jest.fn(),
|
||||
showPoints: false,
|
||||
setShowPoints: jest.fn(),
|
||||
spanGaps: false,
|
||||
setSpanGaps: jest.fn(),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
import { ToggleGroup, ToggleGroupItem } from '@signozhq/toggle-group';
|
||||
import { Typography } from 'antd';
|
||||
import { DisconnectedValuesMode } from 'lib/uPlotV2/config/types';
|
||||
|
||||
interface DisconnectValuesModeToggleProps {
|
||||
value: DisconnectedValuesMode;
|
||||
onChange: (value: DisconnectedValuesMode) => void;
|
||||
}
|
||||
|
||||
export default function DisconnectValuesModeToggle({
|
||||
value,
|
||||
onChange,
|
||||
}: DisconnectValuesModeToggleProps): JSX.Element {
|
||||
return (
|
||||
<ToggleGroup
|
||||
type="single"
|
||||
value={value}
|
||||
variant="outline"
|
||||
size="lg"
|
||||
onValueChange={(newValue): void => {
|
||||
if (newValue) {
|
||||
onChange(newValue as DisconnectedValuesMode);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ToggleGroupItem
|
||||
value={DisconnectedValuesMode.Never}
|
||||
aria-label="Never"
|
||||
title="Never"
|
||||
>
|
||||
<Typography.Text className="section-heading-small">Never</Typography.Text>
|
||||
</ToggleGroupItem>
|
||||
<ToggleGroupItem
|
||||
value={DisconnectedValuesMode.Threshold}
|
||||
aria-label="Threshold"
|
||||
title="Threshold"
|
||||
>
|
||||
<Typography.Text className="section-heading-small">
|
||||
Threshold
|
||||
</Typography.Text>
|
||||
</ToggleGroupItem>
|
||||
</ToggleGroup>
|
||||
);
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
.disconnect-values-selector {
|
||||
.disconnect-values-input-wrapper {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
gap: 8px;
|
||||
|
||||
.disconnect-values-threshold-input {
|
||||
width: 100px;
|
||||
height: auto;
|
||||
.disconnect-values-threshold-prefix {
|
||||
padding: 0 8px;
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Typography } from 'antd';
|
||||
import { DisconnectedValuesMode } from 'lib/uPlotV2/config/types';
|
||||
|
||||
import DisconnectValuesModeToggle from './DisconnectValuesModeToggle';
|
||||
import DisconnectValuesThresholdInput from './DisconnectValuesThresholdInput';
|
||||
|
||||
import './DisconnectValuesSelector.styles.scss';
|
||||
|
||||
const DEFAULT_THRESHOLD_SECONDS = 10;
|
||||
|
||||
interface DisconnectValuesSelectorProps {
|
||||
value: boolean | number;
|
||||
onChange: (value: boolean | number) => void;
|
||||
}
|
||||
|
||||
export default function DisconnectValuesSelector({
|
||||
value,
|
||||
onChange,
|
||||
}: DisconnectValuesSelectorProps): JSX.Element {
|
||||
const [mode, setMode] = useState<DisconnectedValuesMode>(() => {
|
||||
if (typeof value === 'number') {
|
||||
return DisconnectedValuesMode.Threshold;
|
||||
}
|
||||
return DisconnectedValuesMode.Never;
|
||||
});
|
||||
const [thresholdSeconds, setThresholdSeconds] = useState<number>(
|
||||
typeof value === 'number' ? value : DEFAULT_THRESHOLD_SECONDS,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof value === 'boolean') {
|
||||
setMode(DisconnectedValuesMode.Never);
|
||||
} else if (typeof value === 'number') {
|
||||
setMode(DisconnectedValuesMode.Threshold);
|
||||
setThresholdSeconds(value);
|
||||
}
|
||||
}, [value]);
|
||||
|
||||
const handleModeChange = (newMode: DisconnectedValuesMode): void => {
|
||||
setMode(newMode);
|
||||
switch (newMode) {
|
||||
case DisconnectedValuesMode.Never:
|
||||
onChange(true);
|
||||
break;
|
||||
case DisconnectedValuesMode.Threshold:
|
||||
onChange(thresholdSeconds);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
const handleThresholdChange = (seconds: number): void => {
|
||||
setThresholdSeconds(seconds);
|
||||
onChange(seconds);
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="disconnect-values-selector control-container">
|
||||
<Typography.Text className="section-heading">
|
||||
Disconnect values
|
||||
</Typography.Text>
|
||||
<div className="disconnect-values-input-wrapper">
|
||||
<DisconnectValuesModeToggle value={mode} onChange={handleModeChange} />
|
||||
{mode === DisconnectedValuesMode.Threshold && (
|
||||
<DisconnectValuesThresholdInput
|
||||
value={thresholdSeconds}
|
||||
onChange={handleThresholdChange}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { rangeUtil } from '@grafana/data';
|
||||
import { Input } from '@signozhq/ui';
|
||||
interface DisconnectValuesThresholdInputProps {
|
||||
value: number;
|
||||
onChange: (seconds: number) => void;
|
||||
}
|
||||
|
||||
export default function DisconnectValuesThresholdInput({
|
||||
value,
|
||||
onChange,
|
||||
}: DisconnectValuesThresholdInputProps): JSX.Element {
|
||||
const [inputValue, setInputValue] = useState<string>(
|
||||
rangeUtil.secondsToHms(value),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setInputValue(rangeUtil.secondsToHms(value));
|
||||
}, [value]);
|
||||
|
||||
const commitValue = (txt: string): void => {
|
||||
if (!txt) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
let seconds: number;
|
||||
if (rangeUtil.isValidTimeSpan(txt)) {
|
||||
seconds = rangeUtil.intervalToSeconds(txt);
|
||||
} else {
|
||||
const parsed = Number(txt);
|
||||
if (Number.isNaN(parsed) || parsed <= 0) {
|
||||
return;
|
||||
}
|
||||
seconds = parsed;
|
||||
}
|
||||
setInputValue(txt);
|
||||
onChange(seconds);
|
||||
} catch (err) {
|
||||
console.warn('Invalid threshold value', err);
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>): void => {
|
||||
if (e.key === 'Enter') {
|
||||
commitValue(e.currentTarget.value);
|
||||
}
|
||||
};
|
||||
|
||||
const handleBlur = (e: React.FocusEvent<HTMLInputElement>): void => {
|
||||
commitValue(e.currentTarget.value);
|
||||
};
|
||||
|
||||
return (
|
||||
<Input
|
||||
name="disconnect-values-threshold"
|
||||
className="disconnect-values-threshold-input"
|
||||
prefix={<span className="disconnect-values-threshold-prefix">></span>}
|
||||
value={inputValue}
|
||||
onChange={(e): void => setInputValue(e.currentTarget.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
onBlur={handleBlur}
|
||||
autoFocus={false}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -9,7 +9,7 @@ interface FillModeSelectorProps {
|
||||
onChange: (value: FillMode) => void;
|
||||
}
|
||||
|
||||
export default function FillModeSelector({
|
||||
export function FillModeSelector({
|
||||
value,
|
||||
onChange,
|
||||
}: FillModeSelectorProps): JSX.Element {
|
||||
|
||||
@@ -9,7 +9,7 @@ interface LineInterpolationSelectorProps {
|
||||
onChange: (value: LineInterpolation) => void;
|
||||
}
|
||||
|
||||
export default function LineInterpolationSelector({
|
||||
export function LineInterpolationSelector({
|
||||
value,
|
||||
onChange,
|
||||
}: LineInterpolationSelectorProps): JSX.Element {
|
||||
|
||||
@@ -9,7 +9,7 @@ interface LineStyleSelectorProps {
|
||||
onChange: (value: LineStyle) => void;
|
||||
}
|
||||
|
||||
export default function LineStyleSelector({
|
||||
export function LineStyleSelector({
|
||||
value,
|
||||
onChange,
|
||||
}: LineStyleSelectorProps): JSX.Element {
|
||||
|
||||
@@ -262,17 +262,3 @@ export const panelTypeVsShowPoints: {
|
||||
[PANEL_TYPES.TRACE]: false,
|
||||
[PANEL_TYPES.EMPTY_WIDGET]: false,
|
||||
} as const;
|
||||
|
||||
export const panelTypeVsSpanGaps: {
|
||||
[key in PANEL_TYPES]: boolean;
|
||||
} = {
|
||||
[PANEL_TYPES.TIME_SERIES]: true,
|
||||
[PANEL_TYPES.VALUE]: false,
|
||||
[PANEL_TYPES.TABLE]: false,
|
||||
[PANEL_TYPES.LIST]: false,
|
||||
[PANEL_TYPES.PIE]: false,
|
||||
[PANEL_TYPES.BAR]: false,
|
||||
[PANEL_TYPES.HISTOGRAM]: false,
|
||||
[PANEL_TYPES.TRACE]: false,
|
||||
[PANEL_TYPES.EMPTY_WIDGET]: false,
|
||||
} as const;
|
||||
|
||||
@@ -36,7 +36,6 @@ import {
|
||||
panelTypeVsPanelTimePreferences,
|
||||
panelTypeVsShowPoints,
|
||||
panelTypeVsSoftMinMax,
|
||||
panelTypeVsSpanGaps,
|
||||
panelTypeVsStackingChartPreferences,
|
||||
panelTypeVsThreshold,
|
||||
panelTypeVsYAxisUnit,
|
||||
@@ -69,8 +68,6 @@ function RightContainer({
|
||||
setLineStyle,
|
||||
showPoints,
|
||||
setShowPoints,
|
||||
spanGaps,
|
||||
setSpanGaps,
|
||||
bucketCount,
|
||||
bucketWidth,
|
||||
stackedBarChart,
|
||||
@@ -141,7 +138,6 @@ function RightContainer({
|
||||
const allowLineStyle = panelTypeVsLineStyle[selectedGraph];
|
||||
const allowFillMode = panelTypeVsFillMode[selectedGraph];
|
||||
const allowShowPoints = panelTypeVsShowPoints[selectedGraph];
|
||||
const allowSpanGaps = panelTypeVsSpanGaps[selectedGraph];
|
||||
|
||||
const decimapPrecisionOptions = useMemo(
|
||||
() => [
|
||||
@@ -180,15 +176,8 @@ function RightContainer({
|
||||
(allowFillMode ||
|
||||
allowLineStyle ||
|
||||
allowLineInterpolation ||
|
||||
allowShowPoints ||
|
||||
allowSpanGaps),
|
||||
[
|
||||
allowFillMode,
|
||||
allowLineStyle,
|
||||
allowLineInterpolation,
|
||||
allowShowPoints,
|
||||
allowSpanGaps,
|
||||
],
|
||||
allowShowPoints),
|
||||
[allowFillMode, allowLineStyle, allowLineInterpolation, allowShowPoints],
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -248,13 +237,10 @@ function RightContainer({
|
||||
setLineInterpolation={setLineInterpolation}
|
||||
showPoints={showPoints}
|
||||
setShowPoints={setShowPoints}
|
||||
spanGaps={spanGaps}
|
||||
setSpanGaps={setSpanGaps}
|
||||
allowFillMode={allowFillMode}
|
||||
allowLineStyle={allowLineStyle}
|
||||
allowLineInterpolation={allowLineInterpolation}
|
||||
allowShowPoints={allowShowPoints}
|
||||
allowSpanGaps={allowSpanGaps}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -378,8 +364,6 @@ export interface RightContainerProps {
|
||||
setLineStyle: Dispatch<SetStateAction<LineStyle>>;
|
||||
showPoints: boolean;
|
||||
setShowPoints: Dispatch<SetStateAction<boolean>>;
|
||||
spanGaps: boolean | number;
|
||||
setSpanGaps: Dispatch<SetStateAction<boolean | number>>;
|
||||
}
|
||||
|
||||
RightContainer.defaultProps = {
|
||||
|
||||
@@ -220,9 +220,6 @@ function NewWidget({
|
||||
const [showPoints, setShowPoints] = useState<boolean>(
|
||||
selectedWidget?.showPoints ?? false,
|
||||
);
|
||||
const [spanGaps, setSpanGaps] = useState<boolean | number>(
|
||||
selectedWidget?.spanGaps ?? false,
|
||||
);
|
||||
const [customLegendColors, setCustomLegendColors] = useState<
|
||||
Record<string, string>
|
||||
>(selectedWidget?.customLegendColors || {});
|
||||
@@ -292,7 +289,6 @@ function NewWidget({
|
||||
fillMode,
|
||||
lineStyle,
|
||||
showPoints,
|
||||
spanGaps,
|
||||
columnUnits,
|
||||
bucketCount,
|
||||
stackedBarChart,
|
||||
@@ -332,7 +328,6 @@ function NewWidget({
|
||||
fillMode,
|
||||
lineStyle,
|
||||
showPoints,
|
||||
spanGaps,
|
||||
customLegendColors,
|
||||
contextLinks,
|
||||
selectedWidget.columnWidths,
|
||||
@@ -546,7 +541,6 @@ function NewWidget({
|
||||
softMin: selectedWidget?.softMin || 0,
|
||||
softMax: selectedWidget?.softMax || 0,
|
||||
fillSpans: selectedWidget?.fillSpans,
|
||||
spanGaps: selectedWidget?.spanGaps ?? true,
|
||||
isLogScale: selectedWidget?.isLogScale || false,
|
||||
bucketWidth: selectedWidget?.bucketWidth || 0,
|
||||
bucketCount: selectedWidget?.bucketCount || 0,
|
||||
@@ -578,7 +572,6 @@ function NewWidget({
|
||||
softMin: selectedWidget?.softMin || 0,
|
||||
softMax: selectedWidget?.softMax || 0,
|
||||
fillSpans: selectedWidget?.fillSpans,
|
||||
spanGaps: selectedWidget?.spanGaps ?? true,
|
||||
isLogScale: selectedWidget?.isLogScale || false,
|
||||
bucketWidth: selectedWidget?.bucketWidth || 0,
|
||||
bucketCount: selectedWidget?.bucketCount || 0,
|
||||
@@ -896,8 +889,6 @@ function NewWidget({
|
||||
setLineStyle={setLineStyle}
|
||||
showPoints={showPoints}
|
||||
setShowPoints={setShowPoints}
|
||||
spanGaps={spanGaps}
|
||||
setSpanGaps={setSpanGaps}
|
||||
opacity={opacity}
|
||||
yAxisUnit={yAxisUnit}
|
||||
columnUnits={columnUnits}
|
||||
|
||||
@@ -7,7 +7,6 @@ import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFall
|
||||
import uPlot, { AlignedData, Options } from 'uplot';
|
||||
|
||||
import { usePlotContext } from '../context/PlotContext';
|
||||
import { applySpanGapsToAlignedData } from '../utils/dataUtils';
|
||||
import { UPlotChartProps } from './types';
|
||||
|
||||
/**
|
||||
@@ -85,13 +84,7 @@ export default function UPlotChart({
|
||||
} as Options;
|
||||
|
||||
// Create new plot instance
|
||||
const seriesSpanGaps = config.getSeriesSpanGapsOptions();
|
||||
const preparedData =
|
||||
seriesSpanGaps.length > 0
|
||||
? applySpanGapsToAlignedData(data, seriesSpanGaps)
|
||||
: (data as AlignedData);
|
||||
|
||||
const plot = new uPlot(plotConfig, preparedData, containerRef.current);
|
||||
const plot = new uPlot(plotConfig, data as AlignedData, containerRef.current);
|
||||
|
||||
if (plotRef) {
|
||||
plotRef(plot);
|
||||
@@ -169,13 +162,7 @@ export default function UPlotChart({
|
||||
}
|
||||
// Update data if only data changed
|
||||
else if (!sameData(prevProps, currentProps) && plotInstanceRef.current) {
|
||||
const seriesSpanGaps = config.getSeriesSpanGapsOptions?.() ?? [];
|
||||
const preparedData =
|
||||
seriesSpanGaps.length > 0
|
||||
? applySpanGapsToAlignedData(data as AlignedData, seriesSpanGaps)
|
||||
: (data as AlignedData);
|
||||
|
||||
plotInstanceRef.current.setData(preparedData as AlignedData);
|
||||
plotInstanceRef.current.setData(data as AlignedData);
|
||||
}
|
||||
|
||||
prevPropsRef.current = currentProps;
|
||||
|
||||
@@ -86,7 +86,6 @@ const createMockConfig = (): UPlotConfigBuilder => {
|
||||
}),
|
||||
getId: jest.fn().mockReturnValue(undefined),
|
||||
getShouldSaveSelectionPreference: jest.fn().mockReturnValue(false),
|
||||
getSeriesSpanGapsOptions: jest.fn().mockReturnValue([]),
|
||||
} as unknown) as UPlotConfigBuilder;
|
||||
};
|
||||
|
||||
@@ -329,78 +328,6 @@ describe('UPlotChart', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('spanGaps data transformation', () => {
|
||||
it('inserts null break points before passing data to uPlot when a gap exceeds the numeric threshold', () => {
|
||||
const config = createMockConfig();
|
||||
// gap 0→100 = 100 > threshold 50 → null inserted at midpoint x=50
|
||||
(config.getSeriesSpanGapsOptions as jest.Mock).mockReturnValue([
|
||||
{ spanGaps: 50 },
|
||||
]);
|
||||
const data: AlignedData = [
|
||||
[0, 100],
|
||||
[1, 2],
|
||||
];
|
||||
|
||||
render(<UPlotChart config={config} data={data} width={600} height={400} />, {
|
||||
wrapper: Wrapper,
|
||||
});
|
||||
|
||||
const [, receivedData] = mockUPlotConstructor.mock.calls[0];
|
||||
expect(receivedData[0]).toEqual([0, 50, 100]);
|
||||
expect(receivedData[1]).toEqual([1, null, 2]);
|
||||
});
|
||||
|
||||
it('passes data through unchanged when no gap exceeds the numeric threshold', () => {
|
||||
const config = createMockConfig();
|
||||
// all gaps = 10, threshold = 50 → no insertions, same reference returned
|
||||
(config.getSeriesSpanGapsOptions as jest.Mock).mockReturnValue([
|
||||
{ spanGaps: 50 },
|
||||
]);
|
||||
const data: AlignedData = [
|
||||
[0, 10, 20],
|
||||
[1, 2, 3],
|
||||
];
|
||||
|
||||
render(<UPlotChart config={config} data={data} width={600} height={400} />, {
|
||||
wrapper: Wrapper,
|
||||
});
|
||||
|
||||
const [, receivedData] = mockUPlotConstructor.mock.calls[0];
|
||||
expect(receivedData).toBe(data);
|
||||
});
|
||||
|
||||
it('transforms data passed to setData when data updates and a new gap exceeds the threshold', () => {
|
||||
const config = createMockConfig();
|
||||
(config.getSeriesSpanGapsOptions as jest.Mock).mockReturnValue([
|
||||
{ spanGaps: 50 },
|
||||
]);
|
||||
|
||||
// initial render: gap 10 < 50, no transformation
|
||||
const initialData: AlignedData = [
|
||||
[0, 10],
|
||||
[1, 2],
|
||||
];
|
||||
// updated data: gap 100 > 50 → null inserted at midpoint x=50
|
||||
const newData: AlignedData = [
|
||||
[0, 100],
|
||||
[3, 4],
|
||||
];
|
||||
|
||||
const { rerender } = render(
|
||||
<UPlotChart config={config} data={initialData} width={600} height={400} />,
|
||||
{ wrapper: Wrapper },
|
||||
);
|
||||
|
||||
rerender(
|
||||
<UPlotChart config={config} data={newData} width={600} height={400} />,
|
||||
);
|
||||
|
||||
const receivedData = instances[0].setData.mock.calls[0][0];
|
||||
expect(receivedData[0]).toEqual([0, 50, 100]);
|
||||
expect(receivedData[1]).toEqual([3, null, 4]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('prop updates', () => {
|
||||
it('calls setData without recreating the plot when only data changes', () => {
|
||||
const config = createMockConfig();
|
||||
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
STEP_INTERVAL_MULTIPLIER,
|
||||
} from '../constants';
|
||||
import { calculateWidthBasedOnStepInterval } from '../utils';
|
||||
import { SeriesSpanGapsOption } from '../utils/dataUtils';
|
||||
import {
|
||||
ConfigBuilder,
|
||||
ConfigBuilderProps,
|
||||
@@ -162,13 +161,6 @@ export class UPlotConfigBuilder extends ConfigBuilder<
|
||||
this.series.push(new UPlotSeriesBuilder(props));
|
||||
}
|
||||
|
||||
getSeriesSpanGapsOptions(): SeriesSpanGapsOption[] {
|
||||
return this.series.map((s) => {
|
||||
const { spanGaps } = s.props;
|
||||
return { spanGaps };
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a hook for extensibility
|
||||
*/
|
||||
|
||||
@@ -212,12 +212,7 @@ export class UPlotSeriesBuilder extends ConfigBuilder<SeriesProps, Series> {
|
||||
return {
|
||||
scale: scaleKey,
|
||||
label,
|
||||
// When spanGaps is numeric, we always disable uPlot's internal
|
||||
// spanGaps behavior and rely on data-prep to implement the
|
||||
// threshold-based null handling. When spanGaps is boolean we
|
||||
// map it directly. When spanGaps is undefined we fall back to
|
||||
// the default of false.
|
||||
spanGaps: typeof spanGaps === 'number' ? false : !!spanGaps,
|
||||
spanGaps: typeof spanGaps === 'boolean' ? spanGaps : false,
|
||||
value: (): string => '',
|
||||
pxAlign: true,
|
||||
show,
|
||||
|
||||
@@ -40,37 +40,6 @@ describe('UPlotSeriesBuilder', () => {
|
||||
expect(typeof config.value).toBe('function');
|
||||
});
|
||||
|
||||
it('maps boolean spanGaps directly to uPlot spanGaps', () => {
|
||||
const trueBuilder = new UPlotSeriesBuilder(
|
||||
createBaseProps({
|
||||
spanGaps: true,
|
||||
}),
|
||||
);
|
||||
const falseBuilder = new UPlotSeriesBuilder(
|
||||
createBaseProps({
|
||||
spanGaps: false,
|
||||
}),
|
||||
);
|
||||
|
||||
const trueConfig = trueBuilder.getConfig();
|
||||
const falseConfig = falseBuilder.getConfig();
|
||||
|
||||
expect(trueConfig.spanGaps).toBe(true);
|
||||
expect(falseConfig.spanGaps).toBe(false);
|
||||
});
|
||||
|
||||
it('disables uPlot spanGaps when spanGaps is a number', () => {
|
||||
const builder = new UPlotSeriesBuilder(
|
||||
createBaseProps({
|
||||
spanGaps: 10000,
|
||||
}),
|
||||
);
|
||||
|
||||
const config = builder.getConfig();
|
||||
|
||||
expect(config.spanGaps).toBe(false);
|
||||
});
|
||||
|
||||
it('uses explicit lineColor when provided, regardless of mapping', () => {
|
||||
const builder = new UPlotSeriesBuilder(
|
||||
createBaseProps({
|
||||
|
||||
@@ -99,11 +99,6 @@ export interface ScaleProps {
|
||||
distribution?: DistributionType;
|
||||
}
|
||||
|
||||
export enum DisconnectedValuesMode {
|
||||
Never = 'never',
|
||||
Threshold = 'threshold',
|
||||
}
|
||||
|
||||
/**
|
||||
* Props for configuring a series
|
||||
*/
|
||||
@@ -180,16 +175,7 @@ export interface SeriesProps extends LineConfig, PointsConfig, BarConfig {
|
||||
pointsFilter?: Series.Points.Filter;
|
||||
pointsBuilder?: Series.Points.Show;
|
||||
show?: boolean;
|
||||
/**
|
||||
* Controls how nulls are treated for this series.
|
||||
*
|
||||
* - boolean: mapped directly to uPlot's spanGaps behavior
|
||||
* - number: interpreted as an X-axis threshold (same unit as ref values),
|
||||
* where gaps smaller than this threshold are spanned by
|
||||
* converting short null runs to undefined during data prep
|
||||
* while uPlot's internal spanGaps is kept disabled.
|
||||
*/
|
||||
spanGaps?: boolean | number;
|
||||
spanGaps?: boolean;
|
||||
fillColor?: string;
|
||||
fillMode?: FillMode;
|
||||
isDarkMode?: boolean;
|
||||
|
||||
@@ -1,12 +1,4 @@
|
||||
import uPlot from 'uplot';
|
||||
|
||||
import {
|
||||
applySpanGapsToAlignedData,
|
||||
insertLargeGapNullsIntoAlignedData,
|
||||
isInvalidPlotValue,
|
||||
normalizePlotValue,
|
||||
SeriesSpanGapsOption,
|
||||
} from '../dataUtils';
|
||||
import { isInvalidPlotValue, normalizePlotValue } from '../dataUtils';
|
||||
|
||||
describe('dataUtils', () => {
|
||||
describe('isInvalidPlotValue', () => {
|
||||
@@ -67,217 +59,4 @@ describe('dataUtils', () => {
|
||||
expect(normalizePlotValue(42.5)).toBe(42.5);
|
||||
});
|
||||
});
|
||||
|
||||
describe('insertLargeGapNullsIntoAlignedData', () => {
|
||||
it('returns original data unchanged when no gap exceeds the threshold', () => {
|
||||
// all gaps = 10, threshold = 25 → no insertions
|
||||
const data: uPlot.AlignedData = [
|
||||
[0, 10, 20, 30],
|
||||
[1, 2, 3, 4],
|
||||
];
|
||||
const options: SeriesSpanGapsOption[] = [{ spanGaps: 25 }];
|
||||
|
||||
const result = insertLargeGapNullsIntoAlignedData(data, options);
|
||||
|
||||
expect(result).toBe(data);
|
||||
});
|
||||
|
||||
it('does not insert when the gap equals the threshold exactly', () => {
|
||||
// gap = 50, threshold = 50 → condition is gap > threshold, not >=
|
||||
const data: uPlot.AlignedData = [
|
||||
[0, 50],
|
||||
[1, 2],
|
||||
];
|
||||
const options: SeriesSpanGapsOption[] = [{ spanGaps: 50 }];
|
||||
|
||||
const result = insertLargeGapNullsIntoAlignedData(data, options);
|
||||
|
||||
expect(result).toBe(data);
|
||||
});
|
||||
|
||||
it('inserts a null at the midpoint when a single gap exceeds the threshold', () => {
|
||||
// gap 0→100 = 100 > 50 → insert null at x=50
|
||||
const data: uPlot.AlignedData = [
|
||||
[0, 100],
|
||||
[1, 2],
|
||||
];
|
||||
const options: SeriesSpanGapsOption[] = [{ spanGaps: 50 }];
|
||||
|
||||
const result = insertLargeGapNullsIntoAlignedData(data, options);
|
||||
|
||||
expect(result[0]).toEqual([0, 50, 100]);
|
||||
expect(result[1]).toEqual([1, null, 2]);
|
||||
});
|
||||
|
||||
it('inserts nulls at every gap that exceeds the threshold', () => {
|
||||
// gaps: 0→100=100, 100→110=10, 110→210=100; threshold=50
|
||||
// → insert at 0→100 and 110→210
|
||||
const data: uPlot.AlignedData = [
|
||||
[0, 100, 110, 210],
|
||||
[1, 2, 3, 4],
|
||||
];
|
||||
const options: SeriesSpanGapsOption[] = [{ spanGaps: 50 }];
|
||||
|
||||
const result = insertLargeGapNullsIntoAlignedData(data, options);
|
||||
|
||||
expect(result[0]).toEqual([0, 50, 100, 110, 160, 210]);
|
||||
expect(result[1]).toEqual([1, null, 2, 3, null, 4]);
|
||||
});
|
||||
|
||||
it('inserts null for all series at a gap triggered by any one series', () => {
|
||||
// series 0: threshold=50, gap=100 → triggers insertion
|
||||
// series 1: threshold=200, gap=100 → would not trigger alone
|
||||
// result: both series get null at the inserted x because the x-axis is shared
|
||||
const data: uPlot.AlignedData = [
|
||||
[0, 100],
|
||||
[1, 2],
|
||||
[3, 4],
|
||||
];
|
||||
const options: SeriesSpanGapsOption[] = [
|
||||
{ spanGaps: 50 },
|
||||
{ spanGaps: 200 },
|
||||
];
|
||||
|
||||
const result = insertLargeGapNullsIntoAlignedData(data, options);
|
||||
|
||||
expect(result[0]).toEqual([0, 50, 100]);
|
||||
expect(result[1]).toEqual([1, null, 2]);
|
||||
expect(result[2]).toEqual([3, null, 4]);
|
||||
});
|
||||
|
||||
it('ignores boolean spanGaps options (only numeric values trigger insertion)', () => {
|
||||
const data: uPlot.AlignedData = [
|
||||
[0, 100],
|
||||
[1, 2],
|
||||
];
|
||||
const options: SeriesSpanGapsOption[] = [{ spanGaps: true }];
|
||||
|
||||
const result = insertLargeGapNullsIntoAlignedData(data, options);
|
||||
|
||||
expect(result).toBe(data);
|
||||
});
|
||||
|
||||
it('returns original data when series options array is empty', () => {
|
||||
const data: uPlot.AlignedData = [
|
||||
[0, 100],
|
||||
[1, 2],
|
||||
];
|
||||
|
||||
const result = insertLargeGapNullsIntoAlignedData(data, []);
|
||||
|
||||
expect(result).toBe(data);
|
||||
});
|
||||
|
||||
it('returns original data when there is only one x point', () => {
|
||||
const data: uPlot.AlignedData = [[0], [1]];
|
||||
const options: SeriesSpanGapsOption[] = [{ spanGaps: 10 }];
|
||||
|
||||
const result = insertLargeGapNullsIntoAlignedData(data, options);
|
||||
|
||||
expect(result).toBe(data);
|
||||
});
|
||||
|
||||
it('preserves existing null values in the series alongside inserted ones', () => {
|
||||
// original series already has a null; gap 0→100 also triggers insertion
|
||||
const data: uPlot.AlignedData = [
|
||||
[0, 100, 110],
|
||||
[1, null, 2],
|
||||
];
|
||||
const options: SeriesSpanGapsOption[] = [{ spanGaps: 50 }];
|
||||
|
||||
const result = insertLargeGapNullsIntoAlignedData(data, options);
|
||||
|
||||
expect(result[0]).toEqual([0, 50, 100, 110]);
|
||||
expect(result[1]).toEqual([1, null, null, 2]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('applySpanGapsToAlignedData', () => {
|
||||
const xs: uPlot.AlignedData[0] = [0, 10, 20, 30];
|
||||
|
||||
it('returns original data when there are no series', () => {
|
||||
const data: uPlot.AlignedData = [xs];
|
||||
const result = applySpanGapsToAlignedData(data, []);
|
||||
|
||||
expect(result).toBe(data);
|
||||
});
|
||||
|
||||
it('leaves data unchanged when spanGaps is undefined', () => {
|
||||
const ys = [1, null, 2, null];
|
||||
const data: uPlot.AlignedData = [xs, ys];
|
||||
const options: SeriesSpanGapsOption[] = [{}];
|
||||
|
||||
const result = applySpanGapsToAlignedData(data, options);
|
||||
|
||||
expect(result[1]).toEqual(ys);
|
||||
});
|
||||
|
||||
it('converts nulls to undefined when spanGaps is true', () => {
|
||||
const ys = [1, null, 2, null];
|
||||
const data: uPlot.AlignedData = [xs, ys];
|
||||
const options: SeriesSpanGapsOption[] = [{ spanGaps: true }];
|
||||
|
||||
const result = applySpanGapsToAlignedData(data, options);
|
||||
|
||||
expect(result[1]).toEqual([1, undefined, 2, undefined]);
|
||||
});
|
||||
|
||||
it('leaves data unchanged when spanGaps is false', () => {
|
||||
const ys = [1, null, 2, null];
|
||||
const data: uPlot.AlignedData = [xs, ys];
|
||||
const options: SeriesSpanGapsOption[] = [{ spanGaps: false }];
|
||||
|
||||
const result = applySpanGapsToAlignedData(data, options);
|
||||
|
||||
expect(result[1]).toEqual(ys);
|
||||
});
|
||||
|
||||
it('inserts a null break point when a gap exceeds the numeric threshold', () => {
|
||||
// gap 0→100 = 100 > 50 → null inserted at midpoint x=50
|
||||
const data: uPlot.AlignedData = [
|
||||
[0, 100, 110],
|
||||
[1, 2, 3],
|
||||
];
|
||||
const options: SeriesSpanGapsOption[] = [{ spanGaps: 50 }];
|
||||
|
||||
const result = applySpanGapsToAlignedData(data, options);
|
||||
|
||||
expect(result[0]).toEqual([0, 50, 100, 110]);
|
||||
expect(result[1]).toEqual([1, null, 2, 3]);
|
||||
});
|
||||
|
||||
it('returns original data when no gap exceeds the numeric threshold', () => {
|
||||
// all gaps = 10, threshold = 25 → no insertions
|
||||
const data: uPlot.AlignedData = [xs, [1, 2, 3, 4]];
|
||||
const options: SeriesSpanGapsOption[] = [{ spanGaps: 25 }];
|
||||
|
||||
const result = applySpanGapsToAlignedData(data, options);
|
||||
|
||||
expect(result).toBe(data);
|
||||
});
|
||||
|
||||
it('applies both numeric gap insertion and boolean null-to-undefined in one pass', () => {
|
||||
// series 0: spanGaps: 50 → gap 0→100 triggers a null break at midpoint x=50
|
||||
// series 1: spanGaps: true → the inserted null at x=50 becomes undefined,
|
||||
// so the line spans over it rather than breaking
|
||||
const data: uPlot.AlignedData = [
|
||||
[0, 100],
|
||||
[1, 2],
|
||||
[3, 4],
|
||||
];
|
||||
const options: SeriesSpanGapsOption[] = [
|
||||
{ spanGaps: 50 },
|
||||
{ spanGaps: true },
|
||||
];
|
||||
|
||||
const result = applySpanGapsToAlignedData(data, options);
|
||||
|
||||
// x-axis extended with the inserted midpoint
|
||||
expect(result[0]).toEqual([0, 50, 100]);
|
||||
// series 0: null at midpoint breaks the line
|
||||
expect(result[1]).toEqual([1, null, 2]);
|
||||
// series 1: null at midpoint converted to undefined → line spans over it
|
||||
expect(result[2]).toEqual([3, undefined, 4]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -51,163 +51,3 @@ export function normalizePlotValue(
|
||||
// Already a valid number
|
||||
return value as number;
|
||||
}
|
||||
|
||||
export interface SeriesSpanGapsOption {
|
||||
spanGaps?: boolean | number;
|
||||
}
|
||||
|
||||
// Internal type alias: a series value array that may contain nulls/undefineds.
|
||||
// uPlot uses null to draw a visible gap and undefined to represent "no sample"
|
||||
// (the line continues across undefined points but breaks at null ones).
|
||||
type SeriesArray = Array<number | null | undefined>;
|
||||
|
||||
/**
|
||||
* Returns true if the given gap size exceeds the numeric spanGaps threshold
|
||||
* of at least one series. Used to decide whether to insert a null break point.
|
||||
*/
|
||||
function gapExceedsThreshold(
|
||||
gapSize: number,
|
||||
seriesOptions: SeriesSpanGapsOption[],
|
||||
): boolean {
|
||||
return seriesOptions.some(
|
||||
({ spanGaps }) =>
|
||||
typeof spanGaps === 'number' && spanGaps > 0 && gapSize > spanGaps,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* For each series with a numeric spanGaps threshold, insert a null data point
|
||||
* between consecutive x timestamps whose gap exceeds the threshold.
|
||||
*
|
||||
* Why: uPlot draws a continuous line between all non-null points. When the
|
||||
* time gap between two consecutive samples is larger than the configured
|
||||
* spanGaps value, we inject a synthetic null at the midpoint so uPlot renders
|
||||
* a visible break instead of a misleading straight line across the gap.
|
||||
*
|
||||
* Because uPlot's AlignedData shares a single x-axis across all series, a null
|
||||
* is inserted for every series at each position where any series needs a break.
|
||||
*
|
||||
* Two-pass approach for performance:
|
||||
* Pass 1 — count how many nulls will be inserted (no allocations).
|
||||
* Pass 2 — fill pre-allocated output arrays by index (no push/reallocation).
|
||||
*/
|
||||
export function insertLargeGapNullsIntoAlignedData(
|
||||
data: uPlot.AlignedData,
|
||||
seriesOptions: SeriesSpanGapsOption[],
|
||||
): uPlot.AlignedData {
|
||||
const [xValues, ...seriesValues] = data;
|
||||
|
||||
if (
|
||||
!Array.isArray(xValues) ||
|
||||
xValues.length < 2 ||
|
||||
seriesValues.length === 0
|
||||
) {
|
||||
return data;
|
||||
}
|
||||
|
||||
const xs = xValues as number[];
|
||||
const n = xs.length;
|
||||
|
||||
// Pass 1: count insertions needed so we know the exact output length.
|
||||
// This lets us pre-allocate arrays rather than growing them dynamically.
|
||||
let insertionCount = 0;
|
||||
for (let i = 0; i < n - 1; i += 1) {
|
||||
if (gapExceedsThreshold(xs[i + 1] - xs[i], seriesOptions)) {
|
||||
insertionCount += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// No gaps exceed any threshold — return the original data unchanged.
|
||||
if (insertionCount === 0) {
|
||||
return data;
|
||||
}
|
||||
|
||||
// Pass 2: build output arrays of exact size and fill them.
|
||||
// `out` is the write cursor into the output arrays.
|
||||
const outputLen = n + insertionCount;
|
||||
const newX = new Array<number>(outputLen);
|
||||
const newSeries: SeriesArray[] = seriesValues.map(
|
||||
() => new Array<number | null | undefined>(outputLen),
|
||||
);
|
||||
|
||||
let out = 0;
|
||||
for (let i = 0; i < n; i += 1) {
|
||||
// Copy the real data point at position i
|
||||
newX[out] = xs[i];
|
||||
for (let s = 0; s < seriesValues.length; s += 1) {
|
||||
newSeries[s][out] = (seriesValues[s] as SeriesArray)[i];
|
||||
}
|
||||
out += 1;
|
||||
|
||||
// If the gap to the next x timestamp exceeds the threshold, insert a
|
||||
// synthetic null at the midpoint. The midpoint x is placed halfway
|
||||
// between xs[i] and xs[i+1] (minimum 1 unit past xs[i] to stay unique).
|
||||
if (i < n - 1 && gapExceedsThreshold(xs[i + 1] - xs[i], seriesOptions)) {
|
||||
newX[out] = xs[i] + Math.max(1, Math.floor((xs[i + 1] - xs[i]) / 2));
|
||||
for (let s = 0; s < seriesValues.length; s += 1) {
|
||||
newSeries[s][out] = null; // null tells uPlot to break the line here
|
||||
}
|
||||
out += 1;
|
||||
}
|
||||
}
|
||||
|
||||
return [newX, ...newSeries] as uPlot.AlignedData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply per-series spanGaps (boolean | number) handling to an aligned dataset.
|
||||
*
|
||||
* spanGaps controls how uPlot handles gaps in a series:
|
||||
* - boolean true → convert null → undefined so uPlot spans over every gap
|
||||
* (draws a continuous line, skipping missing samples)
|
||||
* - boolean false → no change; nulls render as visible breaks (default)
|
||||
* - number → insert a null break point between any two consecutive
|
||||
* timestamps whose difference exceeds the threshold;
|
||||
* gaps smaller than the threshold are left as-is
|
||||
*
|
||||
* The input data is expected to be of the form:
|
||||
* [xValues, series1Values, series2Values, ...]
|
||||
*/
|
||||
export function applySpanGapsToAlignedData(
|
||||
data: uPlot.AlignedData,
|
||||
seriesOptions: SeriesSpanGapsOption[],
|
||||
): uPlot.AlignedData {
|
||||
const [xValues, ...seriesValues] = data;
|
||||
|
||||
if (!Array.isArray(xValues) || seriesValues.length === 0) {
|
||||
return data;
|
||||
}
|
||||
|
||||
// Numeric spanGaps: operates on the whole dataset at once because inserting
|
||||
// null break points requires modifying the shared x-axis.
|
||||
const hasNumericSpanGaps = seriesOptions.some(
|
||||
({ spanGaps }) => typeof spanGaps === 'number',
|
||||
);
|
||||
const gapProcessed = hasNumericSpanGaps
|
||||
? insertLargeGapNullsIntoAlignedData(data, seriesOptions)
|
||||
: data;
|
||||
|
||||
// Boolean spanGaps === true: convert null → undefined per series so uPlot
|
||||
// draws a continuous line across missing samples instead of breaking it.
|
||||
// Skip this pass entirely if no series uses spanGaps: true.
|
||||
const hasBooleanTrue = seriesOptions.some(({ spanGaps }) => spanGaps === true);
|
||||
if (!hasBooleanTrue) {
|
||||
return gapProcessed;
|
||||
}
|
||||
|
||||
const [newX, ...newSeries] = gapProcessed;
|
||||
const transformedSeries = newSeries.map((ys, idx) => {
|
||||
const { spanGaps } = seriesOptions[idx] ?? {};
|
||||
if (spanGaps !== true) {
|
||||
// This series doesn't use spanGaps: true — leave it unchanged.
|
||||
return ys;
|
||||
}
|
||||
// Replace null with undefined: uPlot skips undefined points without
|
||||
// breaking the line, effectively spanning over the gap.
|
||||
return (ys as SeriesArray).map((v) =>
|
||||
v === null ? undefined : v,
|
||||
) as uPlot.AlignedData[0];
|
||||
});
|
||||
|
||||
return [newX, ...transformedSeries] as uPlot.AlignedData;
|
||||
}
|
||||
|
||||
@@ -141,7 +141,6 @@ export interface IBaseWidget {
|
||||
showPoints?: boolean;
|
||||
lineStyle?: LineStyle;
|
||||
fillMode?: FillMode;
|
||||
spanGaps?: boolean | number;
|
||||
}
|
||||
export interface Widgets extends IBaseWidget {
|
||||
query: Query;
|
||||
|
||||
@@ -4506,19 +4506,6 @@
|
||||
"@radix-ui/react-use-callback-ref" "1.1.1"
|
||||
"@radix-ui/react-use-escape-keydown" "1.1.1"
|
||||
|
||||
"@radix-ui/react-dropdown-menu@^2.1.16":
|
||||
version "2.1.16"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.16.tgz#5ee045c62bad8122347981c479d92b1ff24c7254"
|
||||
integrity sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==
|
||||
dependencies:
|
||||
"@radix-ui/primitive" "1.1.3"
|
||||
"@radix-ui/react-compose-refs" "1.1.2"
|
||||
"@radix-ui/react-context" "1.1.2"
|
||||
"@radix-ui/react-id" "1.1.1"
|
||||
"@radix-ui/react-menu" "2.1.16"
|
||||
"@radix-ui/react-primitive" "2.1.3"
|
||||
"@radix-ui/react-use-controllable-state" "1.2.2"
|
||||
|
||||
"@radix-ui/react-focus-guards@1.0.0":
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-guards/-/react-focus-guards-1.0.0.tgz#339c1c69c41628c1a5e655f15f7020bf11aa01fa"
|
||||
@@ -4578,30 +4565,6 @@
|
||||
dependencies:
|
||||
"@radix-ui/react-use-layout-effect" "1.1.1"
|
||||
|
||||
"@radix-ui/react-menu@2.1.16":
|
||||
version "2.1.16"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-menu/-/react-menu-2.1.16.tgz#528a5a973c3a7413d3d49eb9ccd229aa52402911"
|
||||
integrity sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==
|
||||
dependencies:
|
||||
"@radix-ui/primitive" "1.1.3"
|
||||
"@radix-ui/react-collection" "1.1.7"
|
||||
"@radix-ui/react-compose-refs" "1.1.2"
|
||||
"@radix-ui/react-context" "1.1.2"
|
||||
"@radix-ui/react-direction" "1.1.1"
|
||||
"@radix-ui/react-dismissable-layer" "1.1.11"
|
||||
"@radix-ui/react-focus-guards" "1.1.3"
|
||||
"@radix-ui/react-focus-scope" "1.1.7"
|
||||
"@radix-ui/react-id" "1.1.1"
|
||||
"@radix-ui/react-popper" "1.2.8"
|
||||
"@radix-ui/react-portal" "1.1.9"
|
||||
"@radix-ui/react-presence" "1.1.5"
|
||||
"@radix-ui/react-primitive" "2.1.3"
|
||||
"@radix-ui/react-roving-focus" "1.1.11"
|
||||
"@radix-ui/react-slot" "1.2.3"
|
||||
"@radix-ui/react-use-callback-ref" "1.1.1"
|
||||
aria-hidden "^1.2.4"
|
||||
react-remove-scroll "^2.6.3"
|
||||
|
||||
"@radix-ui/react-popover@^1.1.15", "@radix-ui/react-popover@^1.1.2":
|
||||
version "1.1.15"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-popover/-/react-popover-1.1.15.tgz#9c852f93990a687ebdc949b2c3de1f37cdc4c5d5"
|
||||
@@ -4841,20 +4804,6 @@
|
||||
"@radix-ui/react-roving-focus" "1.0.4"
|
||||
"@radix-ui/react-use-controllable-state" "1.0.1"
|
||||
|
||||
"@radix-ui/react-tabs@^1.1.3":
|
||||
version "1.1.13"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz#3537ce379d7e7ff4eeb6b67a0973e139c2ac1f15"
|
||||
integrity sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==
|
||||
dependencies:
|
||||
"@radix-ui/primitive" "1.1.3"
|
||||
"@radix-ui/react-context" "1.1.2"
|
||||
"@radix-ui/react-direction" "1.1.1"
|
||||
"@radix-ui/react-id" "1.1.1"
|
||||
"@radix-ui/react-presence" "1.1.5"
|
||||
"@radix-ui/react-primitive" "2.1.3"
|
||||
"@radix-ui/react-roving-focus" "1.1.11"
|
||||
"@radix-ui/react-use-controllable-state" "1.2.2"
|
||||
|
||||
"@radix-ui/react-toggle-group@^1.1.7":
|
||||
version "1.1.11"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-toggle-group/-/react-toggle-group-1.1.11.tgz#e513d6ffdb07509b400ab5b26f2523747c0d51c1"
|
||||
@@ -5726,42 +5675,6 @@
|
||||
tailwind-merge "^2.5.2"
|
||||
tailwindcss-animate "^1.0.7"
|
||||
|
||||
"@signozhq/ui@0.0.4":
|
||||
version "0.0.4"
|
||||
resolved "https://registry.yarnpkg.com/@signozhq/ui/-/ui-0.0.4.tgz#2a9c403900311298d881ca9feb6245d94ca0aa0e"
|
||||
integrity sha512-ViiLsAciCzUgHCu3uDCOEMmjE6OkLpA2g8xvjjbbzi4XFosYBhEASx1Pf3a4f5wlh/JID7z12DOc5YnjzEcn4Q==
|
||||
dependencies:
|
||||
"@radix-ui/react-checkbox" "^1.2.3"
|
||||
"@radix-ui/react-dialog" "^1.1.11"
|
||||
"@radix-ui/react-dropdown-menu" "^2.1.16"
|
||||
"@radix-ui/react-icons" "^1.3.0"
|
||||
"@radix-ui/react-popover" "^1.1.15"
|
||||
"@radix-ui/react-radio-group" "^1.3.4"
|
||||
"@radix-ui/react-slot" "^1.2.3"
|
||||
"@radix-ui/react-switch" "^1.1.4"
|
||||
"@radix-ui/react-tabs" "^1.1.3"
|
||||
"@radix-ui/react-toggle" "^1.1.6"
|
||||
"@radix-ui/react-toggle-group" "^1.1.7"
|
||||
"@radix-ui/react-tooltip" "^1.2.6"
|
||||
"@tanstack/react-table" "^8.21.3"
|
||||
"@tanstack/react-virtual" "^3.13.9"
|
||||
"@types/lodash-es" "^4.17.12"
|
||||
class-variance-authority "^0.7.0"
|
||||
clsx "^2.1.1"
|
||||
cmdk "^1.1.1"
|
||||
date-fns "^4.1.0"
|
||||
dayjs "^1.11.10"
|
||||
lodash-es "^4.17.21"
|
||||
lucide-react "^0.445.0"
|
||||
lucide-solid "^0.510.0"
|
||||
motion "^11.11.17"
|
||||
next-themes "^0.4.6"
|
||||
nuqs "^2.8.9"
|
||||
react-day-picker "^9.8.1"
|
||||
react-resizable-panels "^4.7.1"
|
||||
sonner "^2.0.7"
|
||||
tailwind-merge "^3.5.0"
|
||||
|
||||
"@sinclair/typebox@^0.25.16":
|
||||
version "0.25.24"
|
||||
resolved "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.25.24.tgz"
|
||||
@@ -9660,11 +9573,6 @@ dayjs@^1.10.7, dayjs@^1.11.1:
|
||||
resolved "https://registry.npmjs.org/dayjs/-/dayjs-1.11.7.tgz"
|
||||
integrity sha512-+Yw9U6YO5TQohxLcIkrXBeY73WP3ejHWVvx8XCk3gxvQDCTEmS48ZrSZCKciI7Bhl/uCMyxYtE9UqRILmFphkQ==
|
||||
|
||||
dayjs@^1.11.10:
|
||||
version "1.11.20"
|
||||
resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.20.tgz#88d919fd639dc991415da5f4cb6f1b6650811938"
|
||||
integrity sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==
|
||||
|
||||
debounce@^1.2.1:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/debounce/-/debounce-1.2.1.tgz#38881d8f4166a5c5848020c11827b834bcb3e0a5"
|
||||
@@ -11184,15 +11092,6 @@ fraction.js@^4.3.7:
|
||||
resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.3.7.tgz#06ca0085157e42fda7f9e726e79fefc4068840f7"
|
||||
integrity sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==
|
||||
|
||||
framer-motion@^11.18.2:
|
||||
version "11.18.2"
|
||||
resolved "https://registry.yarnpkg.com/framer-motion/-/framer-motion-11.18.2.tgz#0c6bd05677f4cfd3b3bdead4eb5ecdd5ed245718"
|
||||
integrity sha512-5F5Och7wrvtLVElIpclDT0CBzMVg3dL22B64aZwHtsIY8RB4mXICLrkajK4G9R+ieSAGcgrLeae2SeUTg2pr6w==
|
||||
dependencies:
|
||||
motion-dom "^11.18.1"
|
||||
motion-utils "^11.18.1"
|
||||
tslib "^2.4.0"
|
||||
|
||||
framer-motion@^12.4.13:
|
||||
version "12.4.13"
|
||||
resolved "https://registry.yarnpkg.com/framer-motion/-/framer-motion-12.4.13.tgz#1efd954f95e6a54685b660929c00f5a61e35256a"
|
||||
@@ -15103,13 +15002,6 @@ moment@^2.29.4:
|
||||
resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.4.tgz#3dbe052889fe7c1b2ed966fcb3a77328964ef108"
|
||||
integrity sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==
|
||||
|
||||
motion-dom@^11.18.1:
|
||||
version "11.18.1"
|
||||
resolved "https://registry.yarnpkg.com/motion-dom/-/motion-dom-11.18.1.tgz#e7fed7b7dc6ae1223ef1cce29ee54bec826dc3f2"
|
||||
integrity sha512-g76KvA001z+atjfxczdRtw/RXOM3OMSdd1f4DL77qCTF/+avrRJiawSG4yDibEQ215sr9kpinSlX2pCTJ9zbhw==
|
||||
dependencies:
|
||||
motion-utils "^11.18.1"
|
||||
|
||||
motion-dom@^12.4.11:
|
||||
version "12.4.11"
|
||||
resolved "https://registry.yarnpkg.com/motion-dom/-/motion-dom-12.4.11.tgz#0419c8686cda4d523f08249deeb8fa6683a9b9d3"
|
||||
@@ -15117,11 +15009,6 @@ motion-dom@^12.4.11:
|
||||
dependencies:
|
||||
motion-utils "^12.4.10"
|
||||
|
||||
motion-utils@^11.18.1:
|
||||
version "11.18.1"
|
||||
resolved "https://registry.yarnpkg.com/motion-utils/-/motion-utils-11.18.1.tgz#671227669833e991c55813cf337899f41327db5b"
|
||||
integrity sha512-49Kt+HKjtbJKLtgO/LKj9Ld+6vw9BjH5d9sc40R/kVyH8GLAXgT42M2NnuPcJNuA3s9ZfZBUcwIgpmZWGEE+hA==
|
||||
|
||||
motion-utils@^12.4.10:
|
||||
version "12.4.10"
|
||||
resolved "https://registry.yarnpkg.com/motion-utils/-/motion-utils-12.4.10.tgz#3d93acea5454419eaaad8d5e5425cb71cbfa1e7f"
|
||||
@@ -15135,14 +15022,6 @@ motion@12.4.13:
|
||||
framer-motion "^12.4.13"
|
||||
tslib "^2.4.0"
|
||||
|
||||
motion@^11.11.17:
|
||||
version "11.18.2"
|
||||
resolved "https://registry.yarnpkg.com/motion/-/motion-11.18.2.tgz#17fb372f3ed94fc9ee1384a25a9068e9da1951e7"
|
||||
integrity sha512-JLjvFDuFr42NFtcVoMAyC2sEjnpA8xpy6qWPyzQvCloznAyQ8FIXioxWfHiLtgYhoVpfUqSWpn1h9++skj9+Wg==
|
||||
dependencies:
|
||||
framer-motion "^11.18.2"
|
||||
tslib "^2.4.0"
|
||||
|
||||
mri@^1.1.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/mri/-/mri-1.2.0.tgz#6721480fec2a11a4889861115a48b6cbe7cc8f0b"
|
||||
@@ -15413,13 +15292,6 @@ nuqs@2.8.8:
|
||||
dependencies:
|
||||
"@standard-schema/spec" "1.0.0"
|
||||
|
||||
nuqs@^2.8.9:
|
||||
version "2.8.9"
|
||||
resolved "https://registry.yarnpkg.com/nuqs/-/nuqs-2.8.9.tgz#e2c27d87c0dd0e3b4412fe867bcd0947cc4c998f"
|
||||
integrity sha512-8ou6AEwsxMWSYo2qkfZtYFVzngwbKmg4c00HVxC1fF6CEJv3Fwm6eoZmfVPALB+vw8Udo7KL5uy96PFcYe1BIQ==
|
||||
dependencies:
|
||||
"@standard-schema/spec" "1.0.0"
|
||||
|
||||
nwsapi@^2.2.2:
|
||||
version "2.2.23"
|
||||
resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.23.tgz#59712c3a88e6de2bb0b6ccc1070397267019cf6c"
|
||||
@@ -17085,11 +16957,6 @@ react-resizable-panels@^3.0.5:
|
||||
resolved "https://registry.yarnpkg.com/react-resizable-panels/-/react-resizable-panels-3.0.5.tgz#50a20645263eed02344de4a70d1319bbc0014bbd"
|
||||
integrity sha512-3z1yN25DMTXLg2wfyFrW32r5k4WEcUa3F7cJ2EgtNK07lnOs4mpM8yWLGunCpkhcQRwJX4fqoLcIh/pHPxzlmQ==
|
||||
|
||||
react-resizable-panels@^4.7.1:
|
||||
version "4.7.3"
|
||||
resolved "https://registry.yarnpkg.com/react-resizable-panels/-/react-resizable-panels-4.7.3.tgz#4040aa0f5c5c4cc4bb685cb69973601ccda3b014"
|
||||
integrity sha512-PYcYMLtvJD+Pr0TQNeMvddcnLOwUa/Yb4iNwU7ThNLlHaQYEEC9MIBWHaBGODzYuXIkPRZ/OWe5sbzG1Rzq5ew==
|
||||
|
||||
react-resizable@3.0.4:
|
||||
version "3.0.4"
|
||||
resolved "https://registry.npmjs.org/react-resizable/-/react-resizable-3.0.4.tgz"
|
||||
@@ -18930,11 +18797,6 @@ tailwind-merge@^2.5.2:
|
||||
resolved "https://registry.yarnpkg.com/tailwind-merge/-/tailwind-merge-2.6.0.tgz#ac5fb7e227910c038d458f396b7400d93a3142d5"
|
||||
integrity sha512-P+Vu1qXfzediirmHOC3xKGAYeZtPcV9g76X+xg2FD4tYgR71ewMA35Y3sCz3zhiN/dwefRpJX0yBcgwi1fXNQA==
|
||||
|
||||
tailwind-merge@^3.5.0:
|
||||
version "3.5.0"
|
||||
resolved "https://registry.yarnpkg.com/tailwind-merge/-/tailwind-merge-3.5.0.tgz#06502f4496ba15151445d97d916a26564d50d1ca"
|
||||
integrity sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==
|
||||
|
||||
tailwindcss-animate@^1.0.7:
|
||||
version "1.0.7"
|
||||
resolved "https://registry.yarnpkg.com/tailwindcss-animate/-/tailwindcss-animate-1.0.7.tgz#318b692c4c42676cc9e67b19b78775742388bef4"
|
||||
|
||||
@@ -186,7 +186,7 @@ func (provider *provider) addUserRoutes(router *mux.Router) error {
|
||||
Description: "This endpoint lists all users",
|
||||
Request: nil,
|
||||
RequestContentType: "",
|
||||
Response: make([]*types.GettableUser, 0),
|
||||
Response: make([]*types.User, 0),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{},
|
||||
@@ -203,7 +203,7 @@ func (provider *provider) addUserRoutes(router *mux.Router) error {
|
||||
Description: "This endpoint returns the user I belong to",
|
||||
Request: nil,
|
||||
RequestContentType: "",
|
||||
Response: new(types.GettableUser),
|
||||
Response: new(types.User),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{},
|
||||
@@ -220,7 +220,7 @@ func (provider *provider) addUserRoutes(router *mux.Router) error {
|
||||
Description: "This endpoint returns the user by id",
|
||||
Request: nil,
|
||||
RequestContentType: "",
|
||||
Response: new(types.GettableUser),
|
||||
Response: new(types.User),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{http.StatusNotFound},
|
||||
@@ -237,7 +237,7 @@ func (provider *provider) addUserRoutes(router *mux.Router) error {
|
||||
Description: "This endpoint updates the user by id",
|
||||
Request: new(types.User),
|
||||
RequestContentType: "application/json",
|
||||
Response: new(types.GettableUser),
|
||||
Response: new(types.User),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
|
||||
|
||||
@@ -17,8 +17,8 @@ func NewStore(sqlstore sqlstore.SQLStore) authtypes.AuthNStore {
|
||||
return &store{sqlstore: sqlstore}
|
||||
}
|
||||
|
||||
func (store *store) GetActiveUserAndFactorPasswordByEmailAndOrgID(ctx context.Context, email string, orgID valuer.UUID) (*types.User, *types.FactorPassword, error) {
|
||||
user := new(types.User)
|
||||
func (store *store) GetActiveUserAndFactorPasswordByEmailAndOrgID(ctx context.Context, email string, orgID valuer.UUID) (*types.StorableUser, *types.FactorPassword, error) {
|
||||
user := new(types.StorableUser)
|
||||
factorPassword := new(types.FactorPassword)
|
||||
|
||||
err := store.
|
||||
|
||||
@@ -30,7 +30,7 @@ func (module *module) Create(ctx context.Context, timestamp int64, name string,
|
||||
funnel.CreatedBy = userID.String()
|
||||
|
||||
// Set up the user relationship
|
||||
funnel.CreatedByUser = &types.User{
|
||||
funnel.CreatedByUser = &types.StorableUser{
|
||||
Identifiable: types.Identifiable{
|
||||
ID: userID,
|
||||
},
|
||||
|
||||
@@ -21,11 +21,15 @@ func NewGetter(store types.UserStore, flagger flagger.Flagger) user.Getter {
|
||||
}
|
||||
|
||||
func (module *getter) GetRootUserByOrgID(ctx context.Context, orgID valuer.UUID) (*types.User, error) {
|
||||
return module.store.GetRootUserByOrgID(ctx, orgID)
|
||||
storableUser, err := module.store.GetRootUserByOrgID(ctx, orgID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return types.NewUserFromStorable(storableUser), nil
|
||||
}
|
||||
|
||||
func (module *getter) ListByOrgID(ctx context.Context, orgID valuer.UUID) ([]*types.User, error) {
|
||||
users, err := module.store.ListUsersByOrgID(ctx, orgID)
|
||||
storableUsers, err := module.store.ListUsersByOrgID(ctx, orgID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -35,46 +39,46 @@ func (module *getter) ListByOrgID(ctx context.Context, orgID valuer.UUID) ([]*ty
|
||||
hideRootUsers := module.flagger.BooleanOrEmpty(ctx, flagger.FeatureHideRootUser, evalCtx)
|
||||
|
||||
if hideRootUsers {
|
||||
users = slices.DeleteFunc(users, func(user *types.User) bool { return user.IsRoot })
|
||||
storableUsers = slices.DeleteFunc(storableUsers, func(user *types.StorableUser) bool { return user.IsRoot })
|
||||
}
|
||||
|
||||
return users, nil
|
||||
return types.NewUsersFromStorables(storableUsers), nil
|
||||
}
|
||||
|
||||
func (module *getter) GetUsersByEmail(ctx context.Context, email valuer.Email) ([]*types.User, error) {
|
||||
users, err := module.store.GetUsersByEmail(ctx, email)
|
||||
storableUsers, err := module.store.GetUsersByEmail(ctx, email)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return users, nil
|
||||
return types.NewUsersFromStorables(storableUsers), nil
|
||||
}
|
||||
|
||||
func (module *getter) GetByOrgIDAndID(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*types.User, error) {
|
||||
user, err := module.store.GetByOrgIDAndID(ctx, orgID, id)
|
||||
storableUser, err := module.store.GetByOrgIDAndID(ctx, orgID, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return user, nil
|
||||
return types.NewUserFromStorable(storableUser), nil
|
||||
}
|
||||
|
||||
func (module *getter) Get(ctx context.Context, id valuer.UUID) (*types.User, error) {
|
||||
user, err := module.store.GetUser(ctx, id)
|
||||
storableUser, err := module.store.GetUser(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return user, nil
|
||||
return types.NewUserFromStorable(storableUser), nil
|
||||
}
|
||||
|
||||
func (module *getter) ListUsersByEmailAndOrgIDs(ctx context.Context, email valuer.Email, orgIDs []valuer.UUID) ([]*types.User, error) {
|
||||
users, err := module.store.ListUsersByEmailAndOrgIDs(ctx, email, orgIDs)
|
||||
storableUsers, err := module.store.ListUsersByEmailAndOrgIDs(ctx, email, orgIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return users, nil
|
||||
return types.NewUsersFromStorables(storableUsers), nil
|
||||
}
|
||||
|
||||
func (module *getter) CountByOrgID(ctx context.Context, orgID valuer.UUID) (int64, error) {
|
||||
|
||||
@@ -51,7 +51,7 @@ func NewModule(store types.UserStore, tokenizer tokenizer.Tokenizer, emailing em
|
||||
|
||||
func (m *Module) AcceptInvite(ctx context.Context, token string, password string) (*types.User, error) {
|
||||
// get the user by reset password token
|
||||
user, err := m.store.GetUserByResetPasswordToken(ctx, token)
|
||||
storableUser, err := m.store.GetUserByResetPasswordToken(ctx, token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -63,21 +63,23 @@ func (m *Module) AcceptInvite(ctx context.Context, token string, password string
|
||||
}
|
||||
|
||||
// query the user again
|
||||
user, err = m.store.GetByOrgIDAndID(ctx, user.OrgID, user.ID)
|
||||
storableUser, err = m.store.GetByOrgIDAndID(ctx, storableUser.OrgID, storableUser.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return user, nil
|
||||
return types.NewUserFromStorable(storableUser), nil
|
||||
}
|
||||
|
||||
func (m *Module) GetInviteByToken(ctx context.Context, token string) (*types.Invite, error) {
|
||||
// get the user
|
||||
user, err := m.store.GetUserByResetPasswordToken(ctx, token)
|
||||
storableUser, err := m.store.GetUserByResetPasswordToken(ctx, token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user := types.NewUserFromStorable(storableUser)
|
||||
|
||||
// create a dummy invite obj for backward compatibility
|
||||
invite := &types.Invite{
|
||||
Identifiable: types.Identifiable{
|
||||
@@ -109,10 +111,11 @@ func (m *Module) CreateBulkInvite(ctx context.Context, orgID valuer.UUID, userID
|
||||
for idx, invite := range bulkInvites.Invites {
|
||||
emails[idx] = invite.Email.StringValue()
|
||||
}
|
||||
users, err := m.store.GetUsersByEmailsOrgIDAndStatuses(ctx, orgID, emails, []string{types.UserStatusActive.StringValue(), types.UserStatusPendingInvite.StringValue()})
|
||||
storableUsers, err := m.store.GetUsersByEmailsOrgIDAndStatuses(ctx, orgID, emails, []string{types.UserStatusActive.StringValue(), types.UserStatusPendingInvite.StringValue()})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
users := types.NewUsersFromStorables(storableUsers)
|
||||
|
||||
if len(users) > 0 {
|
||||
if err := users[0].ErrIfRoot(); err != nil {
|
||||
@@ -220,12 +223,13 @@ func (m *Module) CreateBulkInvite(ctx context.Context, orgID valuer.UUID, userID
|
||||
|
||||
func (m *Module) ListInvite(ctx context.Context, orgID string) ([]*types.Invite, error) {
|
||||
// find all the users with pending_invite status
|
||||
users, err := m.store.ListUsersByOrgID(ctx, valuer.MustNewUUID(orgID))
|
||||
storableUsers, err := m.store.ListUsersByOrgID(ctx, valuer.MustNewUUID(orgID))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pendingUsers := slices.DeleteFunc(users, func(user *types.User) bool { return user.Status != types.UserStatusPendingInvite })
|
||||
pendingStorableUsers := slices.DeleteFunc(storableUsers, func(user *types.StorableUser) bool { return user.Status != types.UserStatusPendingInvite })
|
||||
pendingUsers := types.NewUsersFromStorables(pendingStorableUsers)
|
||||
|
||||
var invites []*types.Invite
|
||||
|
||||
@@ -268,7 +272,7 @@ func (module *Module) CreateUser(ctx context.Context, input *types.User, opts ..
|
||||
}
|
||||
|
||||
if err := module.store.RunInTx(ctx, func(ctx context.Context) error {
|
||||
if err := module.store.CreateUser(ctx, input); err != nil {
|
||||
if err := module.store.CreateUser(ctx, types.NewStorableUser(input)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -291,11 +295,13 @@ func (module *Module) CreateUser(ctx context.Context, input *types.User, opts ..
|
||||
}
|
||||
|
||||
func (m *Module) UpdateUser(ctx context.Context, orgID valuer.UUID, id string, user *types.User, updatedBy string) (*types.User, error) {
|
||||
existingUser, err := m.store.GetUser(ctx, valuer.MustNewUUID(id))
|
||||
existingStorableUser, err := m.store.GetUser(ctx, valuer.MustNewUUID(id))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
existingUser := types.NewUserFromStorable(existingStorableUser)
|
||||
|
||||
if err := existingUser.ErrIfRoot(); err != nil {
|
||||
return nil, errors.WithAdditionalf(err, "cannot update root user")
|
||||
}
|
||||
@@ -350,7 +356,8 @@ func (m *Module) UpdateUser(ctx context.Context, orgID valuer.UUID, id string, u
|
||||
}
|
||||
|
||||
func (module *Module) UpdateAnyUser(ctx context.Context, orgID valuer.UUID, user *types.User) error {
|
||||
if err := module.store.UpdateUser(ctx, orgID, user); err != nil {
|
||||
storableUser := types.NewStorableUser(user)
|
||||
if err := module.store.UpdateUser(ctx, orgID, storableUser); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -366,11 +373,13 @@ func (module *Module) UpdateAnyUser(ctx context.Context, orgID valuer.UUID, user
|
||||
}
|
||||
|
||||
func (module *Module) DeleteUser(ctx context.Context, orgID valuer.UUID, id string, deletedBy string) error {
|
||||
user, err := module.store.GetUser(ctx, valuer.MustNewUUID(id))
|
||||
storableUser, err := module.store.GetUser(ctx, valuer.MustNewUUID(id))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
user := types.NewUserFromStorable(storableUser)
|
||||
|
||||
if err := user.ErrIfRoot(); err != nil {
|
||||
return errors.WithAdditionalf(err, "cannot delete root user")
|
||||
}
|
||||
@@ -412,11 +421,13 @@ func (module *Module) DeleteUser(ctx context.Context, orgID valuer.UUID, id stri
|
||||
}
|
||||
|
||||
func (module *Module) GetOrCreateResetPasswordToken(ctx context.Context, userID valuer.UUID) (*types.ResetPasswordToken, error) {
|
||||
user, err := module.store.GetUser(ctx, userID)
|
||||
storableUser, err := module.store.GetUser(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user := types.NewUserFromStorable(storableUser)
|
||||
|
||||
if err := user.ErrIfRoot(); err != nil {
|
||||
return nil, errors.WithAdditionalf(err, "cannot reset password for root user")
|
||||
}
|
||||
@@ -534,11 +545,13 @@ func (module *Module) UpdatePasswordByResetPasswordToken(ctx context.Context, to
|
||||
return err
|
||||
}
|
||||
|
||||
user, err := module.store.GetUser(ctx, valuer.MustNewUUID(password.UserID))
|
||||
storableUser, err := module.store.GetUser(ctx, valuer.MustNewUUID(password.UserID))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
user := types.NewUserFromStorable(storableUser)
|
||||
|
||||
// handle deleted user
|
||||
if err := user.ErrIfDeleted(); err != nil {
|
||||
return errors.WithAdditionalf(err, "deleted users cannot reset their password")
|
||||
@@ -569,7 +582,7 @@ func (module *Module) UpdatePasswordByResetPasswordToken(ctx context.Context, to
|
||||
if err := user.UpdateStatus(types.UserStatusActive); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := module.store.UpdateUser(ctx, user.OrgID, user); err != nil {
|
||||
if err := module.store.UpdateUser(ctx, user.OrgID, types.NewStorableUser(user)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -587,11 +600,13 @@ func (module *Module) UpdatePasswordByResetPasswordToken(ctx context.Context, to
|
||||
}
|
||||
|
||||
func (module *Module) UpdatePassword(ctx context.Context, userID valuer.UUID, oldpasswd string, passwd string) error {
|
||||
user, err := module.store.GetUser(ctx, userID)
|
||||
storableUser, err := module.store.GetUser(ctx, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
user := types.NewUserFromStorable(storableUser)
|
||||
|
||||
if err := user.ErrIfDeleted(); err != nil {
|
||||
return errors.WithAdditionalf(err, "cannot change password for deleted user")
|
||||
}
|
||||
@@ -743,11 +758,13 @@ func (module *Module) Collect(ctx context.Context, orgID valuer.UUID) (map[strin
|
||||
|
||||
// this function restricts that only one non-deleted user email can exist for an org ID, if found more, it throws an error
|
||||
func (module *Module) GetNonDeletedUserByEmailAndOrgID(ctx context.Context, email valuer.Email, orgID valuer.UUID) (*types.User, error) {
|
||||
existingUsers, err := module.store.GetUsersByEmailAndOrgID(ctx, email, orgID)
|
||||
existingStorableUsers, err := module.store.GetUsersByEmailAndOrgID(ctx, email, orgID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
existingUsers := types.NewUsersFromStorables(existingStorableUsers)
|
||||
|
||||
// filter out the deleted users
|
||||
existingUsers = slices.DeleteFunc(existingUsers, func(user *types.User) bool { return user.ErrIfDeleted() != nil })
|
||||
|
||||
@@ -766,7 +783,7 @@ func (module *Module) GetNonDeletedUserByEmailAndOrgID(ctx context.Context, emai
|
||||
func (module *Module) createUserWithoutGrant(ctx context.Context, input *types.User, opts ...root.CreateUserOption) error {
|
||||
createUserOpts := root.NewCreateUserOptions(opts...)
|
||||
if err := module.store.RunInTx(ctx, func(ctx context.Context) error {
|
||||
if err := module.store.CreateUser(ctx, input); err != nil {
|
||||
if err := module.store.CreateUser(ctx, types.NewStorableUser(input)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -802,7 +819,7 @@ func (module *Module) activatePendingUser(ctx context.Context, user *types.User)
|
||||
if err := user.UpdateStatus(types.UserStatusActive); err != nil {
|
||||
return err
|
||||
}
|
||||
err = module.store.UpdateUser(ctx, user.OrgID, user)
|
||||
err = module.store.UpdateUser(ctx, user.OrgID, types.NewStorableUser(user))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -129,10 +129,11 @@ func (s *service) reconcileByName(ctx context.Context) error {
|
||||
}
|
||||
|
||||
func (s *service) reconcileRootUser(ctx context.Context, orgID valuer.UUID) error {
|
||||
existingRoot, err := s.store.GetRootUserByOrgID(ctx, orgID)
|
||||
existingStorableRoot, err := s.store.GetRootUserByOrgID(ctx, orgID)
|
||||
if err != nil && !errors.Ast(err, errors.TypeNotFound) {
|
||||
return err
|
||||
}
|
||||
existingRoot := types.NewUserFromStorable(existingStorableRoot)
|
||||
|
||||
if existingRoot == nil {
|
||||
return s.createOrPromoteRootUser(ctx, orgID)
|
||||
|
||||
@@ -39,7 +39,7 @@ func (store *store) CreatePassword(ctx context.Context, password *types.FactorPa
|
||||
return nil
|
||||
}
|
||||
|
||||
func (store *store) CreateUser(ctx context.Context, user *types.User) error {
|
||||
func (store *store) CreateUser(ctx context.Context, user *types.StorableUser) error {
|
||||
_, err := store.
|
||||
sqlstore.
|
||||
BunDBCtx(ctx).
|
||||
@@ -52,8 +52,8 @@ func (store *store) CreateUser(ctx context.Context, user *types.User) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (store *store) GetUsersByEmail(ctx context.Context, email valuer.Email) ([]*types.User, error) {
|
||||
var users []*types.User
|
||||
func (store *store) GetUsersByEmail(ctx context.Context, email valuer.Email) ([]*types.StorableUser, error) {
|
||||
var users []*types.StorableUser
|
||||
|
||||
err := store.
|
||||
sqlstore.
|
||||
@@ -69,8 +69,8 @@ func (store *store) GetUsersByEmail(ctx context.Context, email valuer.Email) ([]
|
||||
return users, nil
|
||||
}
|
||||
|
||||
func (store *store) GetUser(ctx context.Context, id valuer.UUID) (*types.User, error) {
|
||||
user := new(types.User)
|
||||
func (store *store) GetUser(ctx context.Context, id valuer.UUID) (*types.StorableUser, error) {
|
||||
user := new(types.StorableUser)
|
||||
|
||||
err := store.
|
||||
sqlstore.
|
||||
@@ -86,8 +86,8 @@ func (store *store) GetUser(ctx context.Context, id valuer.UUID) (*types.User, e
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (store *store) GetByOrgIDAndID(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*types.User, error) {
|
||||
user := new(types.User)
|
||||
func (store *store) GetByOrgIDAndID(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*types.StorableUser, error) {
|
||||
user := new(types.StorableUser)
|
||||
|
||||
err := store.
|
||||
sqlstore.
|
||||
@@ -104,8 +104,8 @@ func (store *store) GetByOrgIDAndID(ctx context.Context, orgID valuer.UUID, id v
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (store *store) GetUsersByEmailAndOrgID(ctx context.Context, email valuer.Email, orgID valuer.UUID) ([]*types.User, error) {
|
||||
var users []*types.User
|
||||
func (store *store) GetUsersByEmailAndOrgID(ctx context.Context, email valuer.Email, orgID valuer.UUID) ([]*types.StorableUser, error) {
|
||||
var users []*types.StorableUser
|
||||
|
||||
err := store.
|
||||
sqlstore.
|
||||
@@ -122,8 +122,8 @@ func (store *store) GetUsersByEmailAndOrgID(ctx context.Context, email valuer.Em
|
||||
return users, nil
|
||||
}
|
||||
|
||||
func (store *store) GetActiveUsersByRoleAndOrgID(ctx context.Context, role types.Role, orgID valuer.UUID) ([]*types.User, error) {
|
||||
var users []*types.User
|
||||
func (store *store) GetActiveUsersByRoleAndOrgID(ctx context.Context, role types.Role, orgID valuer.UUID) ([]*types.StorableUser, error) {
|
||||
var users []*types.StorableUser
|
||||
|
||||
err := store.
|
||||
sqlstore.
|
||||
@@ -141,7 +141,7 @@ func (store *store) GetActiveUsersByRoleAndOrgID(ctx context.Context, role types
|
||||
return users, nil
|
||||
}
|
||||
|
||||
func (store *store) UpdateUser(ctx context.Context, orgID valuer.UUID, user *types.User) error {
|
||||
func (store *store) UpdateUser(ctx context.Context, orgID valuer.UUID, user *types.StorableUser) error {
|
||||
_, err := store.
|
||||
sqlstore.
|
||||
BunDBCtx(ctx).
|
||||
@@ -162,8 +162,8 @@ func (store *store) UpdateUser(ctx context.Context, orgID valuer.UUID, user *typ
|
||||
return nil
|
||||
}
|
||||
|
||||
func (store *store) ListUsersByOrgID(ctx context.Context, orgID valuer.UUID) ([]*types.GettableUser, error) {
|
||||
users := []*types.User{}
|
||||
func (store *store) ListUsersByOrgID(ctx context.Context, orgID valuer.UUID) ([]*types.StorableUser, error) {
|
||||
users := []*types.StorableUser{}
|
||||
|
||||
err := store.
|
||||
sqlstore.
|
||||
@@ -247,7 +247,7 @@ func (store *store) DeleteUser(ctx context.Context, orgID string, id string) err
|
||||
|
||||
// delete user
|
||||
_, err = tx.NewDelete().
|
||||
Model(new(types.User)).
|
||||
Model(new(types.StorableUser)).
|
||||
Where("org_id = ?", orgID).
|
||||
Where("id = ?", id).
|
||||
Exec(ctx)
|
||||
@@ -332,7 +332,7 @@ func (store *store) SoftDeleteUser(ctx context.Context, orgID string, id string)
|
||||
// soft delete user
|
||||
now := time.Now()
|
||||
_, err = tx.NewUpdate().
|
||||
Model(new(types.User)).
|
||||
Model(new(types.StorableUser)).
|
||||
Set("status = ?", types.UserStatusDeleted).
|
||||
Set("deleted_at = ?", now).
|
||||
Set("updated_at = ?", now).
|
||||
@@ -580,7 +580,7 @@ func (store *store) CountByOrgID(ctx context.Context, orgID valuer.UUID) (int64,
|
||||
}
|
||||
|
||||
func (store *store) CountByOrgIDAndStatuses(ctx context.Context, orgID valuer.UUID, statuses []string) (map[valuer.String]int64, error) {
|
||||
user := new(types.User)
|
||||
user := new(types.StorableUser)
|
||||
var results []struct {
|
||||
Status valuer.String `bun:"status"`
|
||||
Count int64 `bun:"count"`
|
||||
@@ -633,8 +633,8 @@ func (store *store) RunInTx(ctx context.Context, cb func(ctx context.Context) er
|
||||
})
|
||||
}
|
||||
|
||||
func (store *store) GetRootUserByOrgID(ctx context.Context, orgID valuer.UUID) (*types.User, error) {
|
||||
user := new(types.User)
|
||||
func (store *store) GetRootUserByOrgID(ctx context.Context, orgID valuer.UUID) (*types.StorableUser, error) {
|
||||
user := new(types.StorableUser)
|
||||
err := store.
|
||||
sqlstore.
|
||||
BunDBCtx(ctx).
|
||||
@@ -649,8 +649,8 @@ func (store *store) GetRootUserByOrgID(ctx context.Context, orgID valuer.UUID) (
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (store *store) ListUsersByEmailAndOrgIDs(ctx context.Context, email valuer.Email, orgIDs []valuer.UUID) ([]*types.User, error) {
|
||||
users := []*types.User{}
|
||||
func (store *store) ListUsersByEmailAndOrgIDs(ctx context.Context, email valuer.Email, orgIDs []valuer.UUID) ([]*types.StorableUser, error) {
|
||||
users := []*types.StorableUser{}
|
||||
err := store.
|
||||
sqlstore.
|
||||
BunDB().
|
||||
@@ -666,8 +666,8 @@ func (store *store) ListUsersByEmailAndOrgIDs(ctx context.Context, email valuer.
|
||||
return users, nil
|
||||
}
|
||||
|
||||
func (store *store) GetUserByResetPasswordToken(ctx context.Context, token string) (*types.User, error) {
|
||||
user := new(types.User)
|
||||
func (store *store) GetUserByResetPasswordToken(ctx context.Context, token string) (*types.StorableUser, error) {
|
||||
user := new(types.StorableUser)
|
||||
|
||||
err := store.
|
||||
sqlstore.
|
||||
@@ -685,8 +685,8 @@ func (store *store) GetUserByResetPasswordToken(ctx context.Context, token strin
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (store *store) GetUsersByEmailsOrgIDAndStatuses(ctx context.Context, orgID valuer.UUID, emails []string, statuses []string) ([]*types.User, error) {
|
||||
users := []*types.User{}
|
||||
func (store *store) GetUsersByEmailsOrgIDAndStatuses(ctx context.Context, orgID valuer.UUID, emails []string, statuses []string) ([]*types.StorableUser, error) {
|
||||
users := []*types.StorableUser{}
|
||||
|
||||
err := store.
|
||||
sqlstore.
|
||||
|
||||
@@ -2,6 +2,7 @@ package sqlmigration
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
@@ -16,12 +17,12 @@ type funnel struct {
|
||||
types.Identifiable // funnel id
|
||||
types.TimeAuditable
|
||||
types.UserAuditable
|
||||
Name string `json:"funnel_name" bun:"name,type:text,notnull"` // funnel name
|
||||
Description string `json:"description" bun:"description,type:text"` // funnel description
|
||||
OrgID valuer.UUID `json:"org_id" bun:"org_id,type:varchar,notnull"`
|
||||
Steps []funnelStep `json:"steps" bun:"steps,type:text,notnull"`
|
||||
Tags string `json:"tags" bun:"tags,type:text"`
|
||||
CreatedByUser *types.User `json:"user" bun:"rel:belongs-to,join:created_by=id"`
|
||||
Name string `json:"funnel_name" bun:"name,type:text,notnull"` // funnel name
|
||||
Description string `json:"description" bun:"description,type:text"` // funnel description
|
||||
OrgID valuer.UUID `json:"org_id" bun:"org_id,type:varchar,notnull"`
|
||||
Steps []funnelStep `json:"steps" bun:"steps,type:text,notnull"`
|
||||
Tags string `json:"tags" bun:"tags,type:text"`
|
||||
CreatedByUser *types.StorableUser `json:"user" bun:"rel:belongs-to,join:created_by=id"`
|
||||
}
|
||||
|
||||
type funnelStep struct {
|
||||
|
||||
@@ -34,7 +34,7 @@ func (store *store) Create(ctx context.Context, token *authtypes.StorableToken)
|
||||
}
|
||||
|
||||
func (store *store) GetIdentityByUserID(ctx context.Context, userID valuer.UUID) (*authtypes.Identity, error) {
|
||||
user := new(types.User)
|
||||
user := new(types.StorableUser)
|
||||
|
||||
err := store.
|
||||
sqlstore.
|
||||
|
||||
@@ -128,7 +128,7 @@ func (typ *Identity) ToClaims() Claims {
|
||||
|
||||
type AuthNStore interface {
|
||||
// Get user and factor password by email and orgID.
|
||||
GetActiveUserAndFactorPasswordByEmailAndOrgID(ctx context.Context, email string, orgID valuer.UUID) (*types.User, *types.FactorPassword, error)
|
||||
GetActiveUserAndFactorPasswordByEmailAndOrgID(ctx context.Context, email string, orgID valuer.UUID) (*types.StorableUser, *types.FactorPassword, error)
|
||||
|
||||
// Get org domain from id.
|
||||
GetAuthDomainFromID(ctx context.Context, domainID valuer.UUID) (*AuthDomain, error)
|
||||
|
||||
@@ -39,15 +39,15 @@ type OrgUserAPIKey struct {
|
||||
}
|
||||
|
||||
type UserWithAPIKey struct {
|
||||
*User `bun:",extend"`
|
||||
APIKeys []*StorableAPIKeyUser `bun:"rel:has-many,join:id=user_id"`
|
||||
*StorableUser `bun:",extend"`
|
||||
APIKeys []*StorableAPIKeyUser `bun:"rel:has-many,join:id=user_id"`
|
||||
}
|
||||
|
||||
type StorableAPIKeyUser struct {
|
||||
StorableAPIKey `bun:",extend"`
|
||||
|
||||
CreatedByUser *User `json:"createdByUser" bun:"created_by_user,rel:belongs-to,join:created_by=id"`
|
||||
UpdatedByUser *User `json:"updatedByUser" bun:"updated_by_user,rel:belongs-to,join:updated_by=id"`
|
||||
CreatedByUser *StorableUser `json:"createdByUser" bun:"created_by_user,rel:belongs-to,join:created_by=id"`
|
||||
UpdatedByUser *StorableUser `json:"updatedByUser" bun:"updated_by_user,rel:belongs-to,join:updated_by=id"`
|
||||
}
|
||||
|
||||
type StorableAPIKey struct {
|
||||
@@ -138,7 +138,7 @@ func NewGettableAPIKeyFromStorableAPIKey(storableAPIKey *StorableAPIKeyUser) *Ge
|
||||
LastUsed: lastUsed,
|
||||
Revoked: storableAPIKey.Revoked,
|
||||
UserID: storableAPIKey.UserID.String(),
|
||||
CreatedByUser: storableAPIKey.CreatedByUser,
|
||||
UpdatedByUser: storableAPIKey.UpdatedByUser,
|
||||
CreatedByUser: NewUserFromStorable(storableAPIKey.CreatedByUser),
|
||||
UpdatedByUser: NewUserFromStorable(storableAPIKey.UpdatedByUser),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,12 +18,12 @@ type StorableFunnel struct {
|
||||
types.TimeAuditable
|
||||
types.UserAuditable
|
||||
bun.BaseModel `bun:"table:trace_funnel"`
|
||||
Name string `json:"funnel_name" bun:"name,type:text,notnull"`
|
||||
Description string `json:"description" bun:"description,type:text"`
|
||||
OrgID valuer.UUID `json:"org_id" bun:"org_id,type:varchar,notnull"`
|
||||
Steps []*FunnelStep `json:"steps" bun:"steps,type:text,notnull"`
|
||||
Tags string `json:"tags" bun:"tags,type:text"`
|
||||
CreatedByUser *types.User `json:"user" bun:"rel:belongs-to,join:created_by=id"`
|
||||
Name string `json:"funnel_name" bun:"name,type:text,notnull"`
|
||||
Description string `json:"description" bun:"description,type:text"`
|
||||
OrgID valuer.UUID `json:"org_id" bun:"org_id,type:varchar,notnull"`
|
||||
Steps []*FunnelStep `json:"steps" bun:"steps,type:text,notnull"`
|
||||
Tags string `json:"tags" bun:"tags,type:text"`
|
||||
CreatedByUser *types.StorableUser `json:"user" bun:"rel:belongs-to,join:created_by=id"`
|
||||
}
|
||||
|
||||
type FunnelStep struct {
|
||||
|
||||
@@ -443,7 +443,7 @@ func TestConstructFunnelResponse(t *testing.T) {
|
||||
},
|
||||
Name: "test-funnel",
|
||||
OrgID: orgID,
|
||||
CreatedByUser: &types.User{
|
||||
CreatedByUser: &types.StorableUser{
|
||||
Identifiable: types.Identifiable{
|
||||
ID: userID,
|
||||
},
|
||||
|
||||
@@ -33,15 +33,25 @@ var (
|
||||
ValidUserStatus = []valuer.String{UserStatusPendingInvite, UserStatusActive, UserStatusDeleted}
|
||||
)
|
||||
|
||||
type GettableUser = User
|
||||
|
||||
type User struct {
|
||||
Identifiable
|
||||
DisplayName string `json:"displayName"`
|
||||
Email valuer.Email `json:"email"`
|
||||
Role Role `json:"role"` // this will be moved to roles
|
||||
OrgID valuer.UUID `json:"orgId"`
|
||||
IsRoot bool `json:"isRoot"`
|
||||
Status valuer.String `json:"status"`
|
||||
DeletedAt time.Time `json:"-"`
|
||||
TimeAuditable
|
||||
}
|
||||
|
||||
type StorableUser struct {
|
||||
bun.BaseModel `bun:"table:users"`
|
||||
|
||||
Identifiable
|
||||
DisplayName string `bun:"display_name" json:"displayName"`
|
||||
Email valuer.Email `bun:"email" json:"email"`
|
||||
Role Role `bun:"role" json:"role"`
|
||||
Role Role `bun:"role" json:"role"` // this will be removed as column from here
|
||||
OrgID valuer.UUID `bun:"org_id" json:"orgId"`
|
||||
IsRoot bool `bun:"is_root" json:"isRoot"`
|
||||
Status valuer.String `bun:"status" json:"status"`
|
||||
@@ -57,6 +67,57 @@ type PostableRegisterOrgAndAdmin struct {
|
||||
OrgName string `json:"orgName"`
|
||||
}
|
||||
|
||||
func NewStorableUser(user *User) *StorableUser {
|
||||
if user == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &StorableUser{
|
||||
Identifiable: user.Identifiable,
|
||||
DisplayName: user.DisplayName,
|
||||
Email: user.Email,
|
||||
Role: user.Role,
|
||||
OrgID: user.OrgID,
|
||||
IsRoot: user.IsRoot,
|
||||
Status: user.Status,
|
||||
DeletedAt: user.DeletedAt,
|
||||
TimeAuditable: user.TimeAuditable,
|
||||
}
|
||||
}
|
||||
|
||||
func NewUserFromStorable(storableUser *StorableUser) *User {
|
||||
if storableUser == nil {
|
||||
return nil
|
||||
}
|
||||
return &User{
|
||||
Identifiable: storableUser.Identifiable,
|
||||
DisplayName: storableUser.DisplayName,
|
||||
Email: storableUser.Email,
|
||||
Role: storableUser.Role,
|
||||
OrgID: storableUser.OrgID,
|
||||
IsRoot: storableUser.IsRoot,
|
||||
Status: storableUser.Status,
|
||||
DeletedAt: storableUser.DeletedAt,
|
||||
TimeAuditable: storableUser.TimeAuditable,
|
||||
}
|
||||
}
|
||||
|
||||
func NewUsersFromStorables(storableUsers []*StorableUser) []*User {
|
||||
users := make([]*User, len(storableUsers))
|
||||
for i, s := range storableUsers {
|
||||
users[i] = NewUserFromStorable(s)
|
||||
}
|
||||
return users
|
||||
}
|
||||
|
||||
func NewStorableUsers(users []*User) []*StorableUser {
|
||||
storableUsers := make([]*StorableUser, len(users))
|
||||
for i, u := range users {
|
||||
storableUsers[i] = NewStorableUser(u)
|
||||
}
|
||||
return storableUsers
|
||||
}
|
||||
|
||||
func NewUser(displayName string, email valuer.Email, role Role, orgID valuer.UUID, status valuer.String) (*User, error) {
|
||||
if email.IsZero() {
|
||||
return nil, errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "email is required")
|
||||
@@ -215,33 +276,33 @@ func (request *PostableRegisterOrgAndAdmin) UnmarshalJSON(data []byte) error {
|
||||
|
||||
type UserStore interface {
|
||||
// Creates a user.
|
||||
CreateUser(ctx context.Context, user *User) error
|
||||
CreateUser(ctx context.Context, user *StorableUser) error
|
||||
|
||||
// Get user by id.
|
||||
GetUser(context.Context, valuer.UUID) (*User, error)
|
||||
GetUser(context.Context, valuer.UUID) (*StorableUser, error)
|
||||
|
||||
// Get user by orgID and id.
|
||||
GetByOrgIDAndID(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*User, error)
|
||||
GetByOrgIDAndID(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*StorableUser, error)
|
||||
|
||||
// Get user by email and orgID.
|
||||
GetUsersByEmailAndOrgID(ctx context.Context, email valuer.Email, orgID valuer.UUID) ([]*User, error)
|
||||
GetUsersByEmailAndOrgID(ctx context.Context, email valuer.Email, orgID valuer.UUID) ([]*StorableUser, error)
|
||||
|
||||
// Get users by email.
|
||||
GetUsersByEmail(ctx context.Context, email valuer.Email) ([]*User, error)
|
||||
GetUsersByEmail(ctx context.Context, email valuer.Email) ([]*StorableUser, error)
|
||||
|
||||
// Get users by role and org.
|
||||
GetActiveUsersByRoleAndOrgID(ctx context.Context, role Role, orgID valuer.UUID) ([]*User, error)
|
||||
GetActiveUsersByRoleAndOrgID(ctx context.Context, role Role, orgID valuer.UUID) ([]*StorableUser, error)
|
||||
|
||||
// List users by org.
|
||||
ListUsersByOrgID(ctx context.Context, orgID valuer.UUID) ([]*User, error)
|
||||
ListUsersByOrgID(ctx context.Context, orgID valuer.UUID) ([]*StorableUser, error)
|
||||
|
||||
// List users by email and org ids.
|
||||
ListUsersByEmailAndOrgIDs(ctx context.Context, email valuer.Email, orgIDs []valuer.UUID) ([]*User, error)
|
||||
ListUsersByEmailAndOrgIDs(ctx context.Context, email valuer.Email, orgIDs []valuer.UUID) ([]*StorableUser, error)
|
||||
|
||||
// Get users for an org id using emails and statuses
|
||||
GetUsersByEmailsOrgIDAndStatuses(context.Context, valuer.UUID, []string, []string) ([]*User, error)
|
||||
GetUsersByEmailsOrgIDAndStatuses(context.Context, valuer.UUID, []string, []string) ([]*StorableUser, error)
|
||||
|
||||
UpdateUser(ctx context.Context, orgID valuer.UUID, user *User) error
|
||||
UpdateUser(ctx context.Context, orgID valuer.UUID, user *StorableUser) error
|
||||
DeleteUser(ctx context.Context, orgID string, id string) error
|
||||
SoftDeleteUser(ctx context.Context, orgID string, id string) error
|
||||
|
||||
@@ -267,10 +328,10 @@ type UserStore interface {
|
||||
CountByOrgIDAndStatuses(ctx context.Context, orgID valuer.UUID, statuses []string) (map[valuer.String]int64, error)
|
||||
|
||||
// Get root user by org.
|
||||
GetRootUserByOrgID(ctx context.Context, orgID valuer.UUID) (*User, error)
|
||||
GetRootUserByOrgID(ctx context.Context, orgID valuer.UUID) (*StorableUser, error)
|
||||
|
||||
// Get user by reset password token
|
||||
GetUserByResetPasswordToken(ctx context.Context, token string) (*User, error)
|
||||
GetUserByResetPasswordToken(ctx context.Context, token string) (*StorableUser, error)
|
||||
|
||||
// Transaction
|
||||
RunInTx(ctx context.Context, cb func(ctx context.Context) error) error
|
||||
|
||||
Reference in New Issue
Block a user