mirror of
https://github.com/SigNoz/signoz.git
synced 2026-03-15 17:32:45 +00:00
Compare commits
25 Commits
debug_time
...
refactor/c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
11ed15f4c5 | ||
|
|
f47877cca9 | ||
|
|
bb2b9215ba | ||
|
|
3111904223 | ||
|
|
003e2c30d8 | ||
|
|
00fe516d10 | ||
|
|
c5ef455283 | ||
|
|
2316b5be83 | ||
|
|
937ebc1582 | ||
|
|
dcc8173c79 | ||
|
|
0305f4f7db | ||
|
|
4b4ef5ce58 | ||
|
|
5b8d5fbfd3 | ||
|
|
c60019a6dc | ||
|
|
acde2a37fa | ||
|
|
945241a52a | ||
|
|
e967f80c86 | ||
|
|
a09dc325de | ||
|
|
379b4f7fc4 | ||
|
|
5e536ae077 | ||
|
|
234585e642 | ||
|
|
2cc14f1ad4 | ||
|
|
dc4ed4d239 | ||
|
|
7281c36873 | ||
|
|
40288776e8 |
5
.github/CODEOWNERS
vendored
5
.github/CODEOWNERS
vendored
@@ -127,12 +127,15 @@
|
|||||||
/frontend/src/pages/DashboardsListPage/ @SigNoz/pulse-frontend
|
/frontend/src/pages/DashboardsListPage/ @SigNoz/pulse-frontend
|
||||||
/frontend/src/container/ListOfDashboard/ @SigNoz/pulse-frontend
|
/frontend/src/container/ListOfDashboard/ @SigNoz/pulse-frontend
|
||||||
|
|
||||||
|
# Dashboard Widget Page
|
||||||
|
/frontend/src/pages/DashboardWidget/ @SigNoz/pulse-frontend
|
||||||
|
/frontend/src/container/NewWidget/ @SigNoz/pulse-frontend
|
||||||
|
|
||||||
## Dashboard Page
|
## Dashboard Page
|
||||||
|
|
||||||
/frontend/src/pages/DashboardPage/ @SigNoz/pulse-frontend
|
/frontend/src/pages/DashboardPage/ @SigNoz/pulse-frontend
|
||||||
/frontend/src/container/DashboardContainer/ @SigNoz/pulse-frontend
|
/frontend/src/container/DashboardContainer/ @SigNoz/pulse-frontend
|
||||||
/frontend/src/container/GridCardLayout/ @SigNoz/pulse-frontend
|
/frontend/src/container/GridCardLayout/ @SigNoz/pulse-frontend
|
||||||
/frontend/src/container/NewWidget/ @SigNoz/pulse-frontend
|
|
||||||
|
|
||||||
## Public Dashboard Page
|
## Public Dashboard Page
|
||||||
|
|
||||||
|
|||||||
14
ee/modules/cloudintegration/cloudintegration.go
Normal file
14
ee/modules/cloudintegration/cloudintegration.go
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
package cloudintegration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/SigNoz/signoz/pkg/types/cloudintegrationtypes"
|
||||||
|
"github.com/SigNoz/signoz/pkg/valuer"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Module interface {
|
||||||
|
GetConnectionArtifact(ctx context.Context, orgID valuer.UUID, provider cloudintegrationtypes.CloudProviderType)
|
||||||
|
}
|
||||||
|
|
||||||
|
type CloudProvider interface{}
|
||||||
@@ -297,7 +297,11 @@ function CustomTimePicker({
|
|||||||
resetErrorStatus();
|
resetErrorStatus();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleInputPressEnter = (): void => {
|
const handleInputPressEnter = (
|
||||||
|
event?: React.KeyboardEvent<HTMLInputElement>,
|
||||||
|
): void => {
|
||||||
|
event?.preventDefault();
|
||||||
|
event?.stopPropagation();
|
||||||
// check if the entered time is in the format of 1m, 2h, 3d, 4w
|
// check if the entered time is in the format of 1m, 2h, 3d, 4w
|
||||||
const isTimeDurationShortHandFormat = /^(\d+)([mhdw])$/.test(inputValue);
|
const isTimeDurationShortHandFormat = /^(\d+)([mhdw])$/.test(inputValue);
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import {
|
|||||||
} from 'hooks/dashboard/useDashboardVariables';
|
} from 'hooks/dashboard/useDashboardVariables';
|
||||||
import useVariablesFromUrl from 'hooks/dashboard/useVariablesFromUrl';
|
import useVariablesFromUrl from 'hooks/dashboard/useVariablesFromUrl';
|
||||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||||
import { initializeDefaultVariables } from 'providers/Dashboard/initializeDefaultVariables';
|
|
||||||
import { updateDashboardVariablesStore } from 'providers/Dashboard/store/dashboardVariables/dashboardVariablesStore';
|
import { updateDashboardVariablesStore } from 'providers/Dashboard/store/dashboardVariables/dashboardVariablesStore';
|
||||||
import {
|
import {
|
||||||
enqueueDescendantsOfVariable,
|
enqueueDescendantsOfVariable,
|
||||||
@@ -30,7 +29,7 @@ function DashboardVariableSelection(): JSX.Element | null {
|
|||||||
updateLocalStorageDashboardVariables,
|
updateLocalStorageDashboardVariables,
|
||||||
} = useDashboard();
|
} = useDashboard();
|
||||||
|
|
||||||
const { updateUrlVariable, getUrlVariables } = useVariablesFromUrl();
|
const { updateUrlVariable } = useVariablesFromUrl();
|
||||||
|
|
||||||
const { dashboardVariables } = useDashboardVariables();
|
const { dashboardVariables } = useDashboardVariables();
|
||||||
const dashboardId = useDashboardVariablesSelector(
|
const dashboardId = useDashboardVariablesSelector(
|
||||||
@@ -50,15 +49,6 @@ function DashboardVariableSelection(): JSX.Element | null {
|
|||||||
(state) => state.globalTime,
|
(state) => state.globalTime,
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// Initialize variables with default values if not in URL
|
|
||||||
initializeDefaultVariables(
|
|
||||||
dashboardVariables,
|
|
||||||
getUrlVariables,
|
|
||||||
updateUrlVariable,
|
|
||||||
);
|
|
||||||
}, [getUrlVariables, updateUrlVariable, dashboardVariables]);
|
|
||||||
|
|
||||||
// Memoize the order key to avoid unnecessary triggers
|
// Memoize the order key to avoid unnecessary triggers
|
||||||
const variableOrderKey = useMemo(() => {
|
const variableOrderKey = useMemo(() => {
|
||||||
const queryVariableOrderKey = dependencyData?.order?.join(',') ?? '';
|
const queryVariableOrderKey = dependencyData?.order?.join(',') ?? '';
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
.column-unit-selector {
|
.column-unit-selector {
|
||||||
margin-top: 16px;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
.heading {
|
.heading {
|
||||||
color: var(--bg-vanilla-400);
|
color: var(--bg-vanilla-400);
|
||||||
@@ -30,6 +32,11 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
&-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.lightMode {
|
.lightMode {
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ export function ColumnUnitSelector(
|
|||||||
return (
|
return (
|
||||||
<section className="column-unit-selector">
|
<section className="column-unit-selector">
|
||||||
<Typography.Text className="heading">Column Units</Typography.Text>
|
<Typography.Text className="heading">Column Units</Typography.Text>
|
||||||
|
<div className="column-unit-selector-content">
|
||||||
{aggregationQueries.map(({ value, label }) => {
|
{aggregationQueries.map(({ value, label }) => {
|
||||||
const baseQueryName = value.split('.')[0];
|
const baseQueryName = value.split('.')[0];
|
||||||
return (
|
return (
|
||||||
@@ -88,6 +89,7 @@ export function ColumnUnitSelector(
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,9 +56,6 @@ describe('ContextLinks Component', () => {
|
|||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Check that the component renders
|
|
||||||
expect(screen.getByText('Context Links')).toBeInTheDocument();
|
|
||||||
|
|
||||||
// Check that the add button is present
|
// Check that the add button is present
|
||||||
expect(
|
expect(
|
||||||
screen.getByRole('button', { name: /context link/i }),
|
screen.getByRole('button', { name: /context link/i }),
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import {
|
|||||||
verticalListSortingStrategy,
|
verticalListSortingStrategy,
|
||||||
} from '@dnd-kit/sortable';
|
} from '@dnd-kit/sortable';
|
||||||
import { CSS } from '@dnd-kit/utilities';
|
import { CSS } from '@dnd-kit/utilities';
|
||||||
import { Button, Modal, Typography } from 'antd';
|
import { Button, Modal } from 'antd';
|
||||||
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
|
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
|
||||||
import { GripVertical, Pencil, Plus, Trash2 } from 'lucide-react';
|
import { GripVertical, Pencil, Plus, Trash2 } from 'lucide-react';
|
||||||
import {
|
import {
|
||||||
@@ -134,11 +134,16 @@ function ContextLinks({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="context-links-container">
|
<div className="context-links-container">
|
||||||
<Typography.Text className="context-links-text">
|
|
||||||
Context Links
|
|
||||||
</Typography.Text>
|
|
||||||
|
|
||||||
<div className="context-links-list">
|
<div className="context-links-list">
|
||||||
|
<Button
|
||||||
|
type="default"
|
||||||
|
className="add-context-link-button"
|
||||||
|
icon={<Plus size={12} />}
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
onClick={handleAddContextLink}
|
||||||
|
>
|
||||||
|
Add Context Link
|
||||||
|
</Button>
|
||||||
<OverlayScrollbar>
|
<OverlayScrollbar>
|
||||||
<DndContext
|
<DndContext
|
||||||
sensors={sensors}
|
sensors={sensors}
|
||||||
@@ -160,16 +165,6 @@ function ContextLinks({
|
|||||||
</SortableContext>
|
</SortableContext>
|
||||||
</DndContext>
|
</DndContext>
|
||||||
</OverlayScrollbar>
|
</OverlayScrollbar>
|
||||||
|
|
||||||
{/* button to add context link */}
|
|
||||||
<Button
|
|
||||||
type="primary"
|
|
||||||
className="add-context-link-button"
|
|
||||||
icon={<Plus size={12} />}
|
|
||||||
onClick={handleAddContextLink}
|
|
||||||
>
|
|
||||||
Context Link
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
margin: 12px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.context-links-text {
|
.context-links-text {
|
||||||
@@ -110,10 +109,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.add-context-link-button {
|
.add-context-link-button {
|
||||||
display: flex;
|
width: 100%;
|
||||||
align-items: center;
|
|
||||||
margin: auto;
|
|
||||||
width: fit-content;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.lightMode {
|
.lightMode {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
.right-container {
|
.right-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
padding-bottom: 48px;
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -24,14 +25,14 @@
|
|||||||
letter-spacing: -0.07px;
|
letter-spacing: -0.07px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.control-container {
|
||||||
.name-description {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
padding: 12px 12px 16px 12px;
|
|
||||||
border-top: 1px solid var(--bg-slate-500);
|
|
||||||
border-bottom: 1px solid var(--bg-slate-500);
|
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name-description {
|
||||||
|
padding: 0 0 4px 0;
|
||||||
|
|
||||||
.typography {
|
.typography {
|
||||||
color: var(--bg-vanilla-400);
|
color: var(--bg-vanilla-400);
|
||||||
@@ -88,9 +89,6 @@
|
|||||||
.panel-config {
|
.panel-config {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
padding: 12px 12px 16px 12px;
|
|
||||||
gap: 8px;
|
|
||||||
border-bottom: 1px solid var(--bg-slate-500);
|
|
||||||
|
|
||||||
.typography {
|
.typography {
|
||||||
color: var(--bg-vanilla-400);
|
color: var(--bg-vanilla-400);
|
||||||
@@ -104,6 +102,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.panel-type-select {
|
.panel-type-select {
|
||||||
|
width: 100%;
|
||||||
.ant-select-selector {
|
.ant-select-selector {
|
||||||
display: flex;
|
display: flex;
|
||||||
height: 32px;
|
height: 32px;
|
||||||
@@ -137,7 +136,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.fill-gaps {
|
.fill-gaps {
|
||||||
margin-top: 16px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
@@ -156,31 +154,24 @@
|
|||||||
letter-spacing: 0.52px;
|
letter-spacing: 0.52px;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
|
.fill-gaps-text-description {
|
||||||
|
color: var(--bg-vanilla-400);
|
||||||
|
font-family: Inter;
|
||||||
|
font-size: 12px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
opacity: 0.6;
|
||||||
|
line-height: 16px; /* 133.333% */
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.log-scale,
|
.log-scale,
|
||||||
.decimal-precision-selector {
|
.decimal-precision-selector,
|
||||||
margin-top: 16px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.legend-position {
|
.legend-position {
|
||||||
margin-top: 16px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
flex-direction: column;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.legend-colors {
|
|
||||||
margin-top: 16px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.panel-time-text {
|
.panel-time-text {
|
||||||
margin-top: 16px;
|
|
||||||
color: var(--bg-vanilla-400);
|
color: var(--bg-vanilla-400);
|
||||||
font-family: 'Space Mono';
|
font-family: 'Space Mono';
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
@@ -193,7 +184,6 @@
|
|||||||
|
|
||||||
.y-axis-unit-selector,
|
.y-axis-unit-selector,
|
||||||
.y-axis-unit-selector-v2 {
|
.y-axis-unit-selector-v2 {
|
||||||
margin-top: 16px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
@@ -278,11 +268,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.stack-chart {
|
.stack-chart {
|
||||||
margin-top: 16px;
|
flex-direction: row;
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 8px;
|
|
||||||
|
|
||||||
.label {
|
.label {
|
||||||
color: var(--bg-vanilla-400);
|
color: var(--bg-vanilla-400);
|
||||||
font-family: 'Space Mono';
|
font-family: 'Space Mono';
|
||||||
@@ -296,11 +283,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.bucket-config {
|
.bucket-config {
|
||||||
margin-top: 16px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 8px;
|
|
||||||
|
|
||||||
.label {
|
.label {
|
||||||
color: var(--bg-vanilla-400);
|
color: var(--bg-vanilla-400);
|
||||||
font-family: 'Space Mono';
|
font-family: 'Space Mono';
|
||||||
@@ -352,16 +334,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.context-links {
|
|
||||||
border-bottom: 1px solid var(--bg-slate-500);
|
|
||||||
}
|
|
||||||
|
|
||||||
.alerts {
|
.alerts {
|
||||||
display: flex;
|
display: flex;
|
||||||
padding: 12px;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
border-bottom: 1px solid var(--bg-slate-500);
|
padding: 12px;
|
||||||
|
min-height: 44px;
|
||||||
|
border-top: 1px solid var(--bg-slate-500);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
.left-section {
|
.left-section {
|
||||||
@@ -387,6 +366,16 @@
|
|||||||
color: var(--bg-vanilla-400);
|
color: var(--bg-vanilla-400);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.context-links {
|
||||||
|
padding: 12px 12px 16px 12px;
|
||||||
|
border-bottom: 1px solid var(--bg-slate-500);
|
||||||
|
}
|
||||||
|
|
||||||
|
.thresholds-section {
|
||||||
|
padding: 12px 12px 16px 12px;
|
||||||
|
border-top: 1px solid var(--bg-slate-500);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.select-option {
|
.select-option {
|
||||||
@@ -418,9 +407,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.name-description {
|
.name-description {
|
||||||
border-top: 1px solid var(--bg-vanilla-300);
|
|
||||||
border-bottom: 1px solid var(--bg-vanilla-300);
|
|
||||||
|
|
||||||
.typography {
|
.typography {
|
||||||
color: var(--bg-ink-400);
|
color: var(--bg-ink-400);
|
||||||
}
|
}
|
||||||
@@ -441,8 +427,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.panel-config {
|
.panel-config {
|
||||||
border-bottom: 1px solid var(--bg-vanilla-300);
|
|
||||||
|
|
||||||
.typography {
|
.typography {
|
||||||
color: var(--bg-ink-400);
|
color: var(--bg-ink-400);
|
||||||
}
|
}
|
||||||
@@ -478,6 +462,9 @@
|
|||||||
.fill-gaps-text {
|
.fill-gaps-text {
|
||||||
color: var(--bg-ink-400);
|
color: var(--bg-ink-400);
|
||||||
}
|
}
|
||||||
|
.fill-gaps-text-description {
|
||||||
|
color: var(--bg-ink-400);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.bucket-config {
|
.bucket-config {
|
||||||
@@ -530,7 +517,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.alerts {
|
.alerts {
|
||||||
border-bottom: 1px solid var(--bg-vanilla-300);
|
border-top: 1px solid var(--bg-vanilla-300);
|
||||||
|
|
||||||
.left-section {
|
.left-section {
|
||||||
.bell-icon {
|
.bell-icon {
|
||||||
@@ -549,6 +536,10 @@
|
|||||||
.context-links {
|
.context-links {
|
||||||
border-bottom: 1px solid var(--bg-vanilla-300);
|
border-bottom: 1px solid var(--bg-vanilla-300);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.thresholds-section {
|
||||||
|
border-top: 1px solid var(--bg-vanilla-300);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.select-option {
|
.select-option {
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
.threshold-selector-container {
|
.threshold-selector-container {
|
||||||
padding: 12px;
|
|
||||||
padding-bottom: 80px;
|
padding-bottom: 80px;
|
||||||
|
|
||||||
.threshold-select {
|
.threshold-select {
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { DndProvider } from 'react-dnd';
|
import { DndProvider } from 'react-dnd';
|
||||||
import { HTML5Backend } from 'react-dnd-html5-backend';
|
import { HTML5Backend } from 'react-dnd-html5-backend';
|
||||||
import { Typography } from 'antd';
|
import { Button } from 'antd';
|
||||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||||
import { useGetQueryLabels } from 'hooks/useGetQueryLabels';
|
import { useGetQueryLabels } from 'hooks/useGetQueryLabels';
|
||||||
import { Antenna, Plus } from 'lucide-react';
|
import { Plus } from 'lucide-react';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
|
|
||||||
import Threshold from './Threshold';
|
import Threshold from './Threshold';
|
||||||
@@ -68,11 +68,14 @@ function ThresholdSelector({
|
|||||||
<DndProvider backend={HTML5Backend}>
|
<DndProvider backend={HTML5Backend}>
|
||||||
<div className="threshold-selector-container">
|
<div className="threshold-selector-container">
|
||||||
<div className="threshold-select" onClick={addThresholdHandler}>
|
<div className="threshold-select" onClick={addThresholdHandler}>
|
||||||
<div className="left-section">
|
<Button
|
||||||
<Antenna size={14} className="icon" />
|
type="default"
|
||||||
<Typography.Text className="text">Thresholds</Typography.Text>
|
icon={<Plus size={14} />}
|
||||||
</div>
|
style={{ width: '100%' }}
|
||||||
<Plus size={14} onClick={addThresholdHandler} className="icon" />
|
onClick={addThresholdHandler}
|
||||||
|
>
|
||||||
|
Add Threshold
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{thresholds.map((threshold, idx) => (
|
{thresholds.map((threshold, idx) => (
|
||||||
<Threshold
|
<Threshold
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
.settings-section {
|
||||||
|
border-top: 1px solid var(--bg-slate-500);
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-section-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px 12px;
|
||||||
|
min-height: 44px;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
.settings-section-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
color: var(--bg-vanilla-400);
|
||||||
|
font-family: 'Space Mono';
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 400;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chevron-icon {
|
||||||
|
color: var(--bg-vanilla-400);
|
||||||
|
transition: transform 0.2s ease-in-out;
|
||||||
|
|
||||||
|
&.open {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-section-content {
|
||||||
|
padding: 0 12px 0 12px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
max-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
opacity: 0;
|
||||||
|
transition: max-height 0.25s ease, opacity 0.25s ease, padding 0.25s ease;
|
||||||
|
|
||||||
|
&.open {
|
||||||
|
padding-bottom: 24px;
|
||||||
|
max-height: 1000px;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightMode {
|
||||||
|
.settings-section-header {
|
||||||
|
.chevron-icon {
|
||||||
|
color: var(--bg-ink-400);
|
||||||
|
}
|
||||||
|
.settings-section-title {
|
||||||
|
color: var(--bg-ink-400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-section {
|
||||||
|
border-top: 1px solid var(--bg-vanilla-300);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import { ReactNode, useState } from 'react';
|
||||||
|
import { ChevronDown } from 'lucide-react';
|
||||||
|
|
||||||
|
import './SettingsSection.styles.scss';
|
||||||
|
|
||||||
|
export interface SettingsSectionProps {
|
||||||
|
title: string;
|
||||||
|
defaultOpen?: boolean;
|
||||||
|
children: ReactNode;
|
||||||
|
icon?: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
function SettingsSection({
|
||||||
|
title,
|
||||||
|
defaultOpen = false,
|
||||||
|
children,
|
||||||
|
icon,
|
||||||
|
}: SettingsSectionProps): JSX.Element {
|
||||||
|
const [isOpen, setIsOpen] = useState(defaultOpen);
|
||||||
|
|
||||||
|
const toggleOpen = (): void => {
|
||||||
|
setIsOpen((prev) => !prev);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="settings-section">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="settings-section-header"
|
||||||
|
onClick={toggleOpen}
|
||||||
|
>
|
||||||
|
<span className="settings-section-title">
|
||||||
|
{icon ? icon : null} {title}
|
||||||
|
</span>
|
||||||
|
<ChevronDown
|
||||||
|
size={16}
|
||||||
|
className={isOpen ? 'chevron-icon open' : 'chevron-icon'}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<div
|
||||||
|
className={
|
||||||
|
isOpen ? 'settings-section-content open' : 'settings-section-content'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SettingsSection;
|
||||||
@@ -14,7 +14,6 @@ import {
|
|||||||
Input,
|
Input,
|
||||||
InputNumber,
|
InputNumber,
|
||||||
Select,
|
Select,
|
||||||
Space,
|
|
||||||
Switch,
|
Switch,
|
||||||
Typography,
|
Typography,
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
@@ -28,9 +27,16 @@ import { useDashboardVariables } from 'hooks/dashboard/useDashboardVariables';
|
|||||||
import useCreateAlerts from 'hooks/queryBuilder/useCreateAlerts';
|
import useCreateAlerts from 'hooks/queryBuilder/useCreateAlerts';
|
||||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||||
import {
|
import {
|
||||||
|
Antenna,
|
||||||
|
Axis3D,
|
||||||
ConciergeBell,
|
ConciergeBell,
|
||||||
|
Layers,
|
||||||
|
LayoutDashboard,
|
||||||
LineChart,
|
LineChart,
|
||||||
|
Link,
|
||||||
|
Pencil,
|
||||||
Plus,
|
Plus,
|
||||||
|
SlidersHorizontal,
|
||||||
Spline,
|
Spline,
|
||||||
SquareArrowOutUpRight,
|
SquareArrowOutUpRight,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
@@ -46,6 +52,7 @@ import { DataSource } from 'types/common/queryBuilder';
|
|||||||
import { popupContainer } from 'utils/selectPopupContainer';
|
import { popupContainer } from 'utils/selectPopupContainer';
|
||||||
|
|
||||||
import { ColumnUnitSelector } from './ColumnUnitSelector/ColumnUnitSelector';
|
import { ColumnUnitSelector } from './ColumnUnitSelector/ColumnUnitSelector';
|
||||||
|
import SettingsSection from './components/SettingsSection/SettingsSection';
|
||||||
import {
|
import {
|
||||||
panelTypeVsBucketConfig,
|
panelTypeVsBucketConfig,
|
||||||
panelTypeVsColumnUnitPreferences,
|
panelTypeVsColumnUnitPreferences,
|
||||||
@@ -178,6 +185,21 @@ function RightContainer({
|
|||||||
}));
|
}));
|
||||||
}, [dashboardVariables]);
|
}, [dashboardVariables]);
|
||||||
|
|
||||||
|
const isAxisSectionVisible = useMemo(() => allowSoftMinMax || allowLogScale, [
|
||||||
|
allowSoftMinMax,
|
||||||
|
allowLogScale,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const isFormattingSectionVisible = useMemo(
|
||||||
|
() => allowYAxisUnit || allowDecimalPrecision || allowPanelColumnPreference,
|
||||||
|
[allowYAxisUnit, allowDecimalPrecision, allowPanelColumnPreference],
|
||||||
|
);
|
||||||
|
|
||||||
|
const isLegendSectionVisible = useMemo(
|
||||||
|
() => allowLegendPosition || allowLegendColors,
|
||||||
|
[allowLegendPosition, allowLegendColors],
|
||||||
|
);
|
||||||
|
|
||||||
const updateCursorAndDropdown = (value: string, pos: number): void => {
|
const updateCursorAndDropdown = (value: string, pos: number): void => {
|
||||||
setCursorPos(pos);
|
setCursorPos(pos);
|
||||||
const lastDollar = value.lastIndexOf('$', pos - 1);
|
const lastDollar = value.lastIndexOf('$', pos - 1);
|
||||||
@@ -193,6 +215,15 @@ function RightContainer({
|
|||||||
}, 0);
|
}, 0);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const decimapPrecisionOptions = useMemo(() => {
|
||||||
|
return [
|
||||||
|
{ label: '0 decimals', value: PrecisionOptionsEnum.ZERO },
|
||||||
|
{ label: '1 decimal', value: PrecisionOptionsEnum.ONE },
|
||||||
|
{ label: '2 decimals', value: PrecisionOptionsEnum.TWO },
|
||||||
|
{ label: '3 decimals', value: PrecisionOptionsEnum.THREE },
|
||||||
|
];
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleInputCursor = (): void => {
|
const handleInputCursor = (): void => {
|
||||||
const pos = inputRef.current?.input?.selectionStart ?? 0;
|
const pos = inputRef.current?.input?.selectionStart ?? 0;
|
||||||
updateCursorAndDropdown(inputValue, pos);
|
updateCursorAndDropdown(inputValue, pos);
|
||||||
@@ -263,9 +294,11 @@ function RightContainer({
|
|||||||
<div className="right-container">
|
<div className="right-container">
|
||||||
<section className="header">
|
<section className="header">
|
||||||
<div className="purple-dot" />
|
<div className="purple-dot" />
|
||||||
<Typography.Text className="header-text">Panel details</Typography.Text>
|
<Typography.Text className="header-text">Panel Settings</Typography.Text>
|
||||||
</section>
|
</section>
|
||||||
<section className="name-description">
|
|
||||||
|
<SettingsSection title="General" defaultOpen icon={<Pencil size={14} />}>
|
||||||
|
<section className="name-description control-container">
|
||||||
<Typography.Text className="typography">Name</Typography.Text>
|
<Typography.Text className="typography">Name</Typography.Text>
|
||||||
<AutoComplete
|
<AutoComplete
|
||||||
options={dashboardVariableOptions}
|
options={dashboardVariableOptions}
|
||||||
@@ -298,12 +331,19 @@ function RightContainer({
|
|||||||
rootClassName="description-input"
|
rootClassName="description-input"
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
|
</SettingsSection>
|
||||||
|
|
||||||
<section className="panel-config">
|
<section className="panel-config">
|
||||||
|
<SettingsSection
|
||||||
|
title="Visualization"
|
||||||
|
defaultOpen
|
||||||
|
icon={<LayoutDashboard size={14} />}
|
||||||
|
>
|
||||||
|
<section className="panel-type control-container">
|
||||||
<Typography.Text className="typography">Panel Type</Typography.Text>
|
<Typography.Text className="typography">Panel Type</Typography.Text>
|
||||||
<Select
|
<Select
|
||||||
onChange={setGraphHandler}
|
onChange={setGraphHandler}
|
||||||
value={selectedGraph}
|
value={selectedGraph}
|
||||||
style={{ width: '100%' }}
|
|
||||||
className="panel-type-select"
|
className="panel-type-select"
|
||||||
data-testid="panel-change-select"
|
data-testid="panel-change-select"
|
||||||
data-stacking-state={stackedBarChart ? 'true' : 'false'}
|
data-stacking-state={stackedBarChart ? 'true' : 'false'}
|
||||||
@@ -317,20 +357,10 @@ function RightContainer({
|
|||||||
</Option>
|
</Option>
|
||||||
))}
|
))}
|
||||||
</Select>
|
</Select>
|
||||||
|
</section>
|
||||||
{allowFillSpans && (
|
|
||||||
<Space className="fill-gaps">
|
|
||||||
<Typography className="fill-gaps-text">Fill gaps</Typography>
|
|
||||||
<Switch
|
|
||||||
checked={isFillSpans}
|
|
||||||
size="small"
|
|
||||||
onChange={(checked): void => setIsFillSpans(checked)}
|
|
||||||
/>
|
|
||||||
</Space>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{allowPanelTimePreference && (
|
{allowPanelTimePreference && (
|
||||||
<>
|
<section className="panel-time-preference control-container">
|
||||||
<Typography.Text className="panel-time-text">
|
<Typography.Text className="panel-time-text">
|
||||||
Panel Time Preference
|
Panel Time Preference
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
@@ -340,17 +370,42 @@ function RightContainer({
|
|||||||
setSelectedTime,
|
setSelectedTime,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</>
|
</section>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{allowPanelColumnPreference && (
|
{allowStackingBarChart && (
|
||||||
<ColumnUnitSelector
|
<section className="stack-chart control-container">
|
||||||
columnUnits={columnUnits}
|
<Typography.Text className="label">Stack series</Typography.Text>
|
||||||
setColumnUnits={setColumnUnits}
|
<Switch
|
||||||
isNewDashboard={isNewDashboard}
|
checked={stackedBarChart}
|
||||||
|
size="small"
|
||||||
|
onChange={(checked): void => setStackedBarChart(checked)}
|
||||||
/>
|
/>
|
||||||
|
</section>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{allowFillSpans && (
|
||||||
|
<section className="fill-gaps">
|
||||||
|
<div className="fill-gaps-text-container">
|
||||||
|
<Typography className="fill-gaps-text">Fill gaps</Typography>
|
||||||
|
<Typography.Text className="fill-gaps-text-description">
|
||||||
|
Fill gaps in data with 0 for continuity
|
||||||
|
</Typography.Text>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
checked={isFillSpans}
|
||||||
|
size="small"
|
||||||
|
onChange={(checked): void => setIsFillSpans(checked)}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
</SettingsSection>
|
||||||
|
|
||||||
|
{isFormattingSectionVisible && (
|
||||||
|
<SettingsSection
|
||||||
|
title="Formatting & Units"
|
||||||
|
icon={<SlidersHorizontal size={14} />}
|
||||||
|
>
|
||||||
{allowYAxisUnit && (
|
{allowYAxisUnit && (
|
||||||
<DashboardYAxisUnitSelectorWrapper
|
<DashboardYAxisUnitSelectorWrapper
|
||||||
onSelect={setYAxisUnit}
|
onSelect={setYAxisUnit}
|
||||||
@@ -367,19 +422,12 @@ function RightContainer({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{allowDecimalPrecision && (
|
{allowDecimalPrecision && (
|
||||||
<section className="decimal-precision-selector">
|
<section className="decimal-precision-selector control-container">
|
||||||
<Typography.Text className="typography">
|
<Typography.Text className="typography">
|
||||||
Decimal Precision
|
Decimal Precision
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
<Select
|
<Select
|
||||||
options={[
|
options={decimapPrecisionOptions}
|
||||||
{ label: '0 decimals', value: PrecisionOptionsEnum.ZERO },
|
|
||||||
{ label: '1 decimal', value: PrecisionOptionsEnum.ONE },
|
|
||||||
{ label: '2 decimals', value: PrecisionOptionsEnum.TWO },
|
|
||||||
{ label: '3 decimals', value: PrecisionOptionsEnum.THREE },
|
|
||||||
{ label: '4 decimals', value: PrecisionOptionsEnum.FOUR },
|
|
||||||
{ label: 'Full Precision', value: PrecisionOptionsEnum.FULL },
|
|
||||||
]}
|
|
||||||
value={decimalPrecision}
|
value={decimalPrecision}
|
||||||
style={{ width: '100%' }}
|
style={{ width: '100%' }}
|
||||||
className="panel-type-select"
|
className="panel-type-select"
|
||||||
@@ -389,6 +437,18 @@ function RightContainer({
|
|||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{allowPanelColumnPreference && (
|
||||||
|
<ColumnUnitSelector
|
||||||
|
columnUnits={columnUnits}
|
||||||
|
setColumnUnits={setColumnUnits}
|
||||||
|
isNewDashboard={isNewDashboard}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</SettingsSection>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isAxisSectionVisible && (
|
||||||
|
<SettingsSection title="Axes" icon={<Axis3D size={14} />}>
|
||||||
{allowSoftMinMax && (
|
{allowSoftMinMax && (
|
||||||
<section className="soft-min-max">
|
<section className="soft-min-max">
|
||||||
<section className="container">
|
<section className="container">
|
||||||
@@ -412,19 +472,81 @@ function RightContainer({
|
|||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{allowStackingBarChart && (
|
{allowLogScale && (
|
||||||
<section className="stack-chart">
|
<section className="log-scale control-container">
|
||||||
<Typography.Text className="label">Stack series</Typography.Text>
|
<Typography.Text className="typography">Y Axis Scale</Typography.Text>
|
||||||
<Switch
|
<Select
|
||||||
checked={stackedBarChart}
|
onChange={(value): void =>
|
||||||
size="small"
|
setIsLogScale(value === LogScale.LOGARITHMIC)
|
||||||
onChange={(checked): void => setStackedBarChart(checked)}
|
}
|
||||||
/>
|
value={isLogScale ? LogScale.LOGARITHMIC : LogScale.LINEAR}
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
className="panel-type-select"
|
||||||
|
defaultValue={LogScale.LINEAR}
|
||||||
|
>
|
||||||
|
<Option value={LogScale.LINEAR}>
|
||||||
|
<div className="select-option">
|
||||||
|
<div className="icon">
|
||||||
|
<LineChart size={16} />
|
||||||
|
</div>
|
||||||
|
<Typography.Text className="display">Linear</Typography.Text>
|
||||||
|
</div>
|
||||||
|
</Option>
|
||||||
|
<Option value={LogScale.LOGARITHMIC}>
|
||||||
|
<div className="select-option">
|
||||||
|
<div className="icon">
|
||||||
|
<Spline size={16} />
|
||||||
|
</div>
|
||||||
|
<Typography.Text className="display">Logarithmic</Typography.Text>
|
||||||
|
</div>
|
||||||
|
</Option>
|
||||||
|
</Select>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
</SettingsSection>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isLegendSectionVisible && (
|
||||||
|
<SettingsSection title="Legend" icon={<Layers size={14} />}>
|
||||||
|
{allowLegendPosition && (
|
||||||
|
<section className="legend-position control-container">
|
||||||
|
<Typography.Text className="typography">Position</Typography.Text>
|
||||||
|
<Select
|
||||||
|
onChange={(value: LegendPosition): void => setLegendPosition(value)}
|
||||||
|
value={legendPosition}
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
className="panel-type-select"
|
||||||
|
defaultValue={LegendPosition.BOTTOM}
|
||||||
|
>
|
||||||
|
<Option value={LegendPosition.BOTTOM}>
|
||||||
|
<div className="select-option">
|
||||||
|
<Typography.Text className="display">Bottom</Typography.Text>
|
||||||
|
</div>
|
||||||
|
</Option>
|
||||||
|
<Option value={LegendPosition.RIGHT}>
|
||||||
|
<div className="select-option">
|
||||||
|
<Typography.Text className="display">Right</Typography.Text>
|
||||||
|
</div>
|
||||||
|
</Option>
|
||||||
|
</Select>
|
||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{allowLegendColors && (
|
||||||
|
<section className="legend-colors">
|
||||||
|
<LegendColors
|
||||||
|
customLegendColors={customLegendColors}
|
||||||
|
setCustomLegendColors={setCustomLegendColors}
|
||||||
|
queryResponse={queryResponse}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
</SettingsSection>
|
||||||
|
)}
|
||||||
|
|
||||||
{allowBucketConfig && (
|
{allowBucketConfig && (
|
||||||
<section className="bucket-config">
|
<SettingsSection title="Histogram / Buckets">
|
||||||
|
<section className="bucket-config control-container">
|
||||||
<Typography.Text className="label">Number of buckets</Typography.Text>
|
<Typography.Text className="label">Number of buckets</Typography.Text>
|
||||||
<InputNumber
|
<InputNumber
|
||||||
value={bucketCount || null}
|
value={bucketCount || null}
|
||||||
@@ -462,70 +584,7 @@ function RightContainer({
|
|||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
</section>
|
</section>
|
||||||
)}
|
</SettingsSection>
|
||||||
|
|
||||||
{allowLogScale && (
|
|
||||||
<section className="log-scale">
|
|
||||||
<Typography.Text className="typography">Y Axis Scale</Typography.Text>
|
|
||||||
<Select
|
|
||||||
onChange={(value): void => setIsLogScale(value === LogScale.LOGARITHMIC)}
|
|
||||||
value={isLogScale ? LogScale.LOGARITHMIC : LogScale.LINEAR}
|
|
||||||
style={{ width: '100%' }}
|
|
||||||
className="panel-type-select"
|
|
||||||
defaultValue={LogScale.LINEAR}
|
|
||||||
>
|
|
||||||
<Option value={LogScale.LINEAR}>
|
|
||||||
<div className="select-option">
|
|
||||||
<div className="icon">
|
|
||||||
<LineChart size={16} />
|
|
||||||
</div>
|
|
||||||
<Typography.Text className="display">Linear</Typography.Text>
|
|
||||||
</div>
|
|
||||||
</Option>
|
|
||||||
<Option value={LogScale.LOGARITHMIC}>
|
|
||||||
<div className="select-option">
|
|
||||||
<div className="icon">
|
|
||||||
<Spline size={16} />
|
|
||||||
</div>
|
|
||||||
<Typography.Text className="display">Logarithmic</Typography.Text>
|
|
||||||
</div>
|
|
||||||
</Option>
|
|
||||||
</Select>
|
|
||||||
</section>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{allowLegendPosition && (
|
|
||||||
<section className="legend-position">
|
|
||||||
<Typography.Text className="typography">Legend Position</Typography.Text>
|
|
||||||
<Select
|
|
||||||
onChange={(value: LegendPosition): void => setLegendPosition(value)}
|
|
||||||
value={legendPosition}
|
|
||||||
style={{ width: '100%' }}
|
|
||||||
className="panel-type-select"
|
|
||||||
defaultValue={LegendPosition.BOTTOM}
|
|
||||||
>
|
|
||||||
<Option value={LegendPosition.BOTTOM}>
|
|
||||||
<div className="select-option">
|
|
||||||
<Typography.Text className="display">Bottom</Typography.Text>
|
|
||||||
</div>
|
|
||||||
</Option>
|
|
||||||
<Option value={LegendPosition.RIGHT}>
|
|
||||||
<div className="select-option">
|
|
||||||
<Typography.Text className="display">Right</Typography.Text>
|
|
||||||
</div>
|
|
||||||
</Option>
|
|
||||||
</Select>
|
|
||||||
</section>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{allowLegendColors && (
|
|
||||||
<section className="legend-colors">
|
|
||||||
<LegendColors
|
|
||||||
customLegendColors={customLegendColors}
|
|
||||||
setCustomLegendColors={setCustomLegendColors}
|
|
||||||
queryResponse={queryResponse}
|
|
||||||
/>
|
|
||||||
</section>
|
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -541,17 +600,25 @@ function RightContainer({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{allowContextLinks && (
|
{allowContextLinks && (
|
||||||
<section className="context-links">
|
<SettingsSection
|
||||||
|
title="Context Links"
|
||||||
|
icon={<Link size={14} />}
|
||||||
|
defaultOpen={!!contextLinks.linksData.length}
|
||||||
|
>
|
||||||
<ContextLinks
|
<ContextLinks
|
||||||
contextLinks={contextLinks}
|
contextLinks={contextLinks}
|
||||||
setContextLinks={setContextLinks}
|
setContextLinks={setContextLinks}
|
||||||
selectedWidget={selectedWidget}
|
selectedWidget={selectedWidget}
|
||||||
/>
|
/>
|
||||||
</section>
|
</SettingsSection>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{allowThreshold && (
|
{allowThreshold && (
|
||||||
<section>
|
<SettingsSection
|
||||||
|
title="Thresholds"
|
||||||
|
icon={<Antenna size={14} />}
|
||||||
|
defaultOpen={!!thresholds.length}
|
||||||
|
>
|
||||||
<ThresholdSelector
|
<ThresholdSelector
|
||||||
thresholds={thresholds}
|
thresholds={thresholds}
|
||||||
setThresholds={setThresholds}
|
setThresholds={setThresholds}
|
||||||
@@ -559,7 +626,7 @@ function RightContainer({
|
|||||||
selectedGraph={selectedGraph}
|
selectedGraph={selectedGraph}
|
||||||
columnUnits={columnUnits}
|
columnUnits={columnUnits}
|
||||||
/>
|
/>
|
||||||
</section>
|
</SettingsSection>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ const checkStackSeriesState = (
|
|||||||
expect(getByTextUtil(container, 'Stack series')).toBeInTheDocument();
|
expect(getByTextUtil(container, 'Stack series')).toBeInTheDocument();
|
||||||
|
|
||||||
const stackSeriesSection = container.querySelector(
|
const stackSeriesSection = container.querySelector(
|
||||||
'section > .stack-chart',
|
'.stack-chart',
|
||||||
) as HTMLElement;
|
) as HTMLElement;
|
||||||
expect(stackSeriesSection).toBeInTheDocument();
|
expect(stackSeriesSection).toBeInTheDocument();
|
||||||
|
|
||||||
@@ -326,7 +326,7 @@ describe('Stacking bar in new panel', () => {
|
|||||||
expect(getByText('Stack series')).toBeInTheDocument();
|
expect(getByText('Stack series')).toBeInTheDocument();
|
||||||
|
|
||||||
// Verify section exists
|
// Verify section exists
|
||||||
const section = container.querySelector('section > .stack-chart');
|
const section = container.querySelector('.stack-chart');
|
||||||
expect(section).toBeInTheDocument();
|
expect(section).toBeInTheDocument();
|
||||||
|
|
||||||
// Verify switch is present and enabled (ant-switch-checked)
|
// Verify switch is present and enabled (ant-switch-checked)
|
||||||
|
|||||||
@@ -439,6 +439,19 @@ function NewWidget({
|
|||||||
globalSelectedInterval,
|
globalSelectedInterval,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const navigateToDashboardPage = useCallback(() => {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
|
||||||
|
const urlVariablesQueryString = query.get(QueryParams.variables);
|
||||||
|
if (urlVariablesQueryString) {
|
||||||
|
params.set(QueryParams.variables, urlVariablesQueryString);
|
||||||
|
}
|
||||||
|
|
||||||
|
const search = params.toString() ? `?${params.toString()}` : '';
|
||||||
|
|
||||||
|
safeNavigate(generatePath(ROUTES.DASHBOARD, { dashboardId }) + search);
|
||||||
|
}, [dashboardId, query, safeNavigate]);
|
||||||
|
|
||||||
const onClickSaveHandler = useCallback(() => {
|
const onClickSaveHandler = useCallback(() => {
|
||||||
if (!selectedDashboard) {
|
if (!selectedDashboard) {
|
||||||
return;
|
return;
|
||||||
@@ -554,9 +567,7 @@ function NewWidget({
|
|||||||
updateDashboardMutation.mutateAsync(dashboard, {
|
updateDashboardMutation.mutateAsync(dashboard, {
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
setToScrollWidgetId(selectedWidget?.id || '');
|
setToScrollWidgetId(selectedWidget?.id || '');
|
||||||
safeNavigate({
|
navigateToDashboardPage();
|
||||||
pathname: generatePath(ROUTES.DASHBOARD, { dashboardId }),
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}, [
|
}, [
|
||||||
@@ -572,7 +583,7 @@ function NewWidget({
|
|||||||
updateDashboardMutation,
|
updateDashboardMutation,
|
||||||
widgets,
|
widgets,
|
||||||
setToScrollWidgetId,
|
setToScrollWidgetId,
|
||||||
safeNavigate,
|
navigateToDashboardPage,
|
||||||
dashboardId,
|
dashboardId,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -581,12 +592,12 @@ function NewWidget({
|
|||||||
setDiscardModal(true);
|
setDiscardModal(true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
safeNavigate(generatePath(ROUTES.DASHBOARD, { dashboardId }));
|
navigateToDashboardPage();
|
||||||
}, [dashboardId, isQueryModified, safeNavigate]);
|
}, [isQueryModified, navigateToDashboardPage]);
|
||||||
|
|
||||||
const discardChanges = useCallback(() => {
|
const discardChanges = useCallback(() => {
|
||||||
safeNavigate(generatePath(ROUTES.DASHBOARD, { dashboardId }));
|
navigateToDashboardPage();
|
||||||
}, [dashboardId, safeNavigate]);
|
}, [navigateToDashboardPage]);
|
||||||
|
|
||||||
const setGraphHandler = (type: PANEL_TYPES): void => {
|
const setGraphHandler = (type: PANEL_TYPES): void => {
|
||||||
setIsLoadingPanelData(true);
|
setIsLoadingPanelData(true);
|
||||||
@@ -728,12 +739,14 @@ function NewWidget({
|
|||||||
}
|
}
|
||||||
const widgetId = query.get('widgetId') || '';
|
const widgetId = query.get('widgetId') || '';
|
||||||
const graphType = query.get('graphType') || '';
|
const graphType = query.get('graphType') || '';
|
||||||
|
const variables = query.get(QueryParams.variables) || '';
|
||||||
const queryParams = {
|
const queryParams = {
|
||||||
[QueryParams.expandedWidgetId]: widgetId,
|
[QueryParams.expandedWidgetId]: widgetId,
|
||||||
[QueryParams.graphType]: graphType,
|
[QueryParams.graphType]: graphType,
|
||||||
[QueryParams.compositeQuery]: encodeURIComponent(
|
[QueryParams.compositeQuery]: encodeURIComponent(
|
||||||
JSON.stringify(currentQuery),
|
JSON.stringify(currentQuery),
|
||||||
),
|
),
|
||||||
|
[QueryParams.variables]: variables,
|
||||||
};
|
};
|
||||||
|
|
||||||
const updatedSearch = createQueryParams(queryParams);
|
const updatedSearch = createQueryParams(queryParams);
|
||||||
@@ -822,7 +835,6 @@ function NewWidget({
|
|||||||
</LeftContainerWrapper>
|
</LeftContainerWrapper>
|
||||||
|
|
||||||
<RightContainerWrapper>
|
<RightContainerWrapper>
|
||||||
<OverlayScrollbar>
|
|
||||||
<RightContainer
|
<RightContainer
|
||||||
setGraphHandler={setGraphHandler}
|
setGraphHandler={setGraphHandler}
|
||||||
title={title}
|
title={title}
|
||||||
@@ -871,7 +883,6 @@ function NewWidget({
|
|||||||
enableDrillDown={enableDrillDown}
|
enableDrillDown={enableDrillDown}
|
||||||
isNewDashboard={isNewDashboard}
|
isNewDashboard={isNewDashboard}
|
||||||
/>
|
/>
|
||||||
</OverlayScrollbar>
|
|
||||||
</RightContainerWrapper>
|
</RightContainerWrapper>
|
||||||
</PanelContainer>
|
</PanelContainer>
|
||||||
<Modal
|
<Modal
|
||||||
|
|||||||
@@ -15,7 +15,14 @@ export const RightContainerWrapper = styled(Col)`
|
|||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
&::-webkit-scrollbar {
|
&::-webkit-scrollbar {
|
||||||
width: 0rem;
|
width: 0.3rem;
|
||||||
|
}
|
||||||
|
&::-webkit-scrollbar-thumb {
|
||||||
|
background: rgb(136, 136, 136);
|
||||||
|
border-radius: 0.625rem;
|
||||||
|
}
|
||||||
|
&::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,339 @@
|
|||||||
|
import { renderHook } from '@testing-library/react';
|
||||||
|
import { useDashboardVariablesFromLocalStorage } from 'hooks/dashboard/useDashboardFromLocalStorage';
|
||||||
|
import { useTransformDashboardVariables } from 'hooks/dashboard/useTransformDashboardVariables';
|
||||||
|
import useVariablesFromUrl from 'hooks/dashboard/useVariablesFromUrl';
|
||||||
|
import { Dashboard, IDashboardVariable } from 'types/api/dashboard/getAll';
|
||||||
|
|
||||||
|
jest.mock('hooks/dashboard/useDashboardFromLocalStorage');
|
||||||
|
jest.mock('hooks/dashboard/useVariablesFromUrl');
|
||||||
|
|
||||||
|
const mockUseDashboardVariablesFromLocalStorage = useDashboardVariablesFromLocalStorage as jest.MockedFunction<
|
||||||
|
typeof useDashboardVariablesFromLocalStorage
|
||||||
|
>;
|
||||||
|
const mockUseVariablesFromUrl = useVariablesFromUrl as jest.MockedFunction<
|
||||||
|
typeof useVariablesFromUrl
|
||||||
|
>;
|
||||||
|
|
||||||
|
const makeVariable = (
|
||||||
|
overrides: Partial<IDashboardVariable> = {},
|
||||||
|
): IDashboardVariable => ({
|
||||||
|
id: 'existing-id',
|
||||||
|
name: 'env',
|
||||||
|
description: '',
|
||||||
|
type: 'QUERY',
|
||||||
|
sort: 'DISABLED',
|
||||||
|
multiSelect: false,
|
||||||
|
showALLOption: false,
|
||||||
|
selectedValue: 'prod',
|
||||||
|
...overrides,
|
||||||
|
});
|
||||||
|
|
||||||
|
const makeDashboard = (
|
||||||
|
variables: Record<string, IDashboardVariable>,
|
||||||
|
): Dashboard => ({
|
||||||
|
id: 'dash-1',
|
||||||
|
createdAt: '',
|
||||||
|
updatedAt: '',
|
||||||
|
createdBy: '',
|
||||||
|
updatedBy: '',
|
||||||
|
data: {
|
||||||
|
title: 'Test',
|
||||||
|
variables,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const setupHook = (
|
||||||
|
currentDashboard: Record<string, any> = {},
|
||||||
|
urlVariables: Record<string, any> = {},
|
||||||
|
): ReturnType<typeof useTransformDashboardVariables> => {
|
||||||
|
mockUseDashboardVariablesFromLocalStorage.mockReturnValue({
|
||||||
|
currentDashboard,
|
||||||
|
updateLocalStorageDashboardVariables: jest.fn(),
|
||||||
|
});
|
||||||
|
mockUseVariablesFromUrl.mockReturnValue({
|
||||||
|
getUrlVariables: () => urlVariables,
|
||||||
|
setUrlVariables: jest.fn(),
|
||||||
|
updateUrlVariable: jest.fn(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useTransformDashboardVariables('dash-1'));
|
||||||
|
return result.current;
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('useTransformDashboardVariables', () => {
|
||||||
|
beforeEach(() => jest.clearAllMocks());
|
||||||
|
|
||||||
|
describe('order assignment', () => {
|
||||||
|
it('assigns order starting from 0 to variables that have none', () => {
|
||||||
|
const { transformDashboardVariables } = setupHook();
|
||||||
|
const dashboard = makeDashboard({
|
||||||
|
v1: makeVariable({ id: 'id1', name: 'v1', order: undefined }),
|
||||||
|
v2: makeVariable({ id: 'id2', name: 'v2', order: undefined }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = transformDashboardVariables(dashboard);
|
||||||
|
|
||||||
|
const orders = Object.values(result.data.variables).map((v) => v.order);
|
||||||
|
expect(orders).toContain(0);
|
||||||
|
expect(orders).toContain(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('preserves existing order values', () => {
|
||||||
|
const { transformDashboardVariables } = setupHook();
|
||||||
|
const dashboard = makeDashboard({
|
||||||
|
v1: makeVariable({ id: 'id1', name: 'v1', order: 5 }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = transformDashboardVariables(dashboard);
|
||||||
|
|
||||||
|
expect(result.data.variables.v1.order).toBe(5);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('assigns unique orders across multiple variables that all lack an order', () => {
|
||||||
|
const { transformDashboardVariables } = setupHook();
|
||||||
|
const dashboard = makeDashboard({
|
||||||
|
v1: makeVariable({ id: 'id1', name: 'v1', order: undefined }),
|
||||||
|
v2: makeVariable({ id: 'id2', name: 'v2', order: undefined }),
|
||||||
|
v3: makeVariable({ id: 'id3', name: 'v3', order: undefined }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = transformDashboardVariables(dashboard);
|
||||||
|
|
||||||
|
const orders = Object.values(result.data.variables).map((v) => v.order);
|
||||||
|
// All three newly assigned orders must be distinct
|
||||||
|
expect(new Set(orders).size).toBe(3);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('ID assignment', () => {
|
||||||
|
it('assigns a UUID to variables that have no id', () => {
|
||||||
|
const { transformDashboardVariables } = setupHook();
|
||||||
|
const variable = makeVariable({ name: 'v1' });
|
||||||
|
(variable as any).id = undefined;
|
||||||
|
const dashboard = makeDashboard({ v1: variable });
|
||||||
|
|
||||||
|
const result = transformDashboardVariables(dashboard);
|
||||||
|
|
||||||
|
expect(result.data.variables.v1.id).toMatch(
|
||||||
|
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('preserves existing IDs', () => {
|
||||||
|
const { transformDashboardVariables } = setupHook();
|
||||||
|
const dashboard = makeDashboard({
|
||||||
|
v1: makeVariable({ id: 'keep-me', name: 'v1' }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = transformDashboardVariables(dashboard);
|
||||||
|
|
||||||
|
expect(result.data.variables.v1.id).toBe('keep-me');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('TEXTBOX backward compatibility', () => {
|
||||||
|
it('copies textboxValue to defaultValue when defaultValue is missing', () => {
|
||||||
|
const { transformDashboardVariables } = setupHook();
|
||||||
|
const dashboard = makeDashboard({
|
||||||
|
v1: makeVariable({
|
||||||
|
id: 'id1',
|
||||||
|
name: 'v1',
|
||||||
|
type: 'TEXTBOX',
|
||||||
|
textboxValue: 'hello',
|
||||||
|
defaultValue: undefined,
|
||||||
|
order: undefined,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = transformDashboardVariables(dashboard);
|
||||||
|
|
||||||
|
expect(result.data.variables.v1.defaultValue).toBe('hello');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not overwrite an existing defaultValue', () => {
|
||||||
|
const { transformDashboardVariables } = setupHook();
|
||||||
|
const dashboard = makeDashboard({
|
||||||
|
v1: makeVariable({
|
||||||
|
id: 'id1',
|
||||||
|
name: 'v1',
|
||||||
|
type: 'TEXTBOX',
|
||||||
|
textboxValue: 'old',
|
||||||
|
defaultValue: 'keep',
|
||||||
|
order: undefined,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = transformDashboardVariables(dashboard);
|
||||||
|
|
||||||
|
expect(result.data.variables.v1.defaultValue).toBe('keep');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('localStorage merge', () => {
|
||||||
|
it('applies localStorage selectedValue over DB value', () => {
|
||||||
|
const { transformDashboardVariables } = setupHook({
|
||||||
|
env: { selectedValue: 'staging', allSelected: false },
|
||||||
|
});
|
||||||
|
const dashboard = makeDashboard({
|
||||||
|
v1: makeVariable({ id: 'id1', name: 'env', selectedValue: 'prod' }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = transformDashboardVariables(dashboard);
|
||||||
|
|
||||||
|
expect(result.data.variables.v1.selectedValue).toBe('staging');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('applies localStorage allSelected over DB value', () => {
|
||||||
|
const { transformDashboardVariables } = setupHook({
|
||||||
|
env: { selectedValue: undefined, allSelected: true },
|
||||||
|
});
|
||||||
|
const dashboard = makeDashboard({
|
||||||
|
v1: makeVariable({
|
||||||
|
id: 'id1',
|
||||||
|
name: 'env',
|
||||||
|
allSelected: false,
|
||||||
|
showALLOption: true,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = transformDashboardVariables(dashboard);
|
||||||
|
|
||||||
|
expect(result.data.variables.v1.allSelected).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('URL variable override', () => {
|
||||||
|
it('sets allSelected=true when URL value is __ALL__', () => {
|
||||||
|
const { transformDashboardVariables } = setupHook(
|
||||||
|
{ env: { selectedValue: 'prod', allSelected: false } },
|
||||||
|
{ env: '__ALL__' },
|
||||||
|
);
|
||||||
|
const dashboard = makeDashboard({
|
||||||
|
v1: makeVariable({
|
||||||
|
id: 'id1',
|
||||||
|
name: 'env',
|
||||||
|
showALLOption: true,
|
||||||
|
allSelected: false,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = transformDashboardVariables(dashboard);
|
||||||
|
|
||||||
|
expect(result.data.variables.v1.allSelected).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets selectedValue from URL and clears allSelected when showALLOption is true', () => {
|
||||||
|
const { transformDashboardVariables } = setupHook(
|
||||||
|
{ env: { selectedValue: undefined, allSelected: true } },
|
||||||
|
{ env: 'dev' },
|
||||||
|
);
|
||||||
|
const dashboard = makeDashboard({
|
||||||
|
v1: makeVariable({
|
||||||
|
id: 'id1',
|
||||||
|
name: 'env',
|
||||||
|
showALLOption: true,
|
||||||
|
allSelected: true,
|
||||||
|
multiSelect: false,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = transformDashboardVariables(dashboard);
|
||||||
|
|
||||||
|
expect(result.data.variables.v1.selectedValue).toBe('dev');
|
||||||
|
expect(result.data.variables.v1.allSelected).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not set allSelected=false when showALLOption is false', () => {
|
||||||
|
const { transformDashboardVariables } = setupHook(
|
||||||
|
{ env: { selectedValue: undefined, allSelected: true } },
|
||||||
|
{ env: 'dev' },
|
||||||
|
);
|
||||||
|
const dashboard = makeDashboard({
|
||||||
|
v1: makeVariable({
|
||||||
|
id: 'id1',
|
||||||
|
name: 'env',
|
||||||
|
showALLOption: false,
|
||||||
|
allSelected: true,
|
||||||
|
multiSelect: false,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = transformDashboardVariables(dashboard);
|
||||||
|
|
||||||
|
expect(result.data.variables.v1.selectedValue).toBe('dev');
|
||||||
|
expect(result.data.variables.v1.allSelected).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('normalizes array URL value to single value for single-select variable', () => {
|
||||||
|
const { transformDashboardVariables } = setupHook(
|
||||||
|
{},
|
||||||
|
{ env: ['prod', 'dev'] },
|
||||||
|
);
|
||||||
|
const dashboard = makeDashboard({
|
||||||
|
v1: makeVariable({
|
||||||
|
id: 'id1',
|
||||||
|
name: 'env',
|
||||||
|
multiSelect: false,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = transformDashboardVariables(dashboard);
|
||||||
|
|
||||||
|
expect(result.data.variables.v1.selectedValue).toBe('prod');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('wraps single URL value in array for multi-select variable', () => {
|
||||||
|
const { transformDashboardVariables } = setupHook({}, { env: 'prod' });
|
||||||
|
const dashboard = makeDashboard({
|
||||||
|
v1: makeVariable({
|
||||||
|
id: 'id1',
|
||||||
|
name: 'env',
|
||||||
|
multiSelect: true,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = transformDashboardVariables(dashboard);
|
||||||
|
|
||||||
|
expect(result.data.variables.v1.selectedValue).toEqual(['prod']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('looks up URL variable by variable id when name is absent', () => {
|
||||||
|
const { transformDashboardVariables } = setupHook(
|
||||||
|
{},
|
||||||
|
{ 'var-uuid': 'fallback' },
|
||||||
|
);
|
||||||
|
const variable = makeVariable({ id: 'var-uuid', multiSelect: false });
|
||||||
|
delete variable.name;
|
||||||
|
const dashboard = makeDashboard({ v1: variable });
|
||||||
|
|
||||||
|
const result = transformDashboardVariables(dashboard);
|
||||||
|
|
||||||
|
expect(result.data.variables.v1.selectedValue).toBe('fallback');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('edge cases', () => {
|
||||||
|
it('returns data unchanged when there are no variables', () => {
|
||||||
|
const { transformDashboardVariables } = setupHook();
|
||||||
|
const dashboard = makeDashboard({});
|
||||||
|
|
||||||
|
const result = transformDashboardVariables(dashboard);
|
||||||
|
|
||||||
|
expect(result.data.variables).toEqual({});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not mutate the original dashboard', () => {
|
||||||
|
const { transformDashboardVariables } = setupHook({
|
||||||
|
env: { selectedValue: 'staging', allSelected: false },
|
||||||
|
});
|
||||||
|
const dashboard = makeDashboard({
|
||||||
|
v1: makeVariable({ id: 'id1', name: 'env', selectedValue: 'prod' }),
|
||||||
|
});
|
||||||
|
const originalValue = dashboard.data.variables.v1.selectedValue;
|
||||||
|
|
||||||
|
transformDashboardVariables(dashboard);
|
||||||
|
|
||||||
|
expect(dashboard.data.variables.v1.selectedValue).toBe(originalValue);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -15,7 +15,7 @@ interface DashboardLocalStorageVariables {
|
|||||||
[id: string]: LocalStoreDashboardVariables;
|
[id: string]: LocalStoreDashboardVariables;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UseDashboardVariablesFromLocalStorageReturn {
|
export interface UseDashboardVariablesFromLocalStorageReturn {
|
||||||
currentDashboard: LocalStoreDashboardVariables;
|
currentDashboard: LocalStoreDashboardVariables;
|
||||||
updateLocalStorageDashboardVariables: (
|
updateLocalStorageDashboardVariables: (
|
||||||
id: string,
|
id: string,
|
||||||
|
|||||||
128
frontend/src/hooks/dashboard/useTransformDashboardVariables.ts
Normal file
128
frontend/src/hooks/dashboard/useTransformDashboardVariables.ts
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
import { ALL_SELECTED_VALUE } from 'components/NewSelect/utils';
|
||||||
|
import {
|
||||||
|
useDashboardVariablesFromLocalStorage,
|
||||||
|
UseDashboardVariablesFromLocalStorageReturn,
|
||||||
|
} from 'hooks/dashboard/useDashboardFromLocalStorage';
|
||||||
|
import useVariablesFromUrl, {
|
||||||
|
UseVariablesFromUrlReturn,
|
||||||
|
} from 'hooks/dashboard/useVariablesFromUrl';
|
||||||
|
import { isEmpty } from 'lodash-es';
|
||||||
|
import { normalizeUrlValueForVariable } from 'providers/Dashboard/normalizeUrlValue';
|
||||||
|
import { Dashboard, IDashboardVariable } from 'types/api/dashboard/getAll';
|
||||||
|
import { v4 as generateUUID } from 'uuid';
|
||||||
|
|
||||||
|
export function useTransformDashboardVariables(
|
||||||
|
dashboardId: string,
|
||||||
|
): Pick<UseVariablesFromUrlReturn, 'getUrlVariables' | 'updateUrlVariable'> &
|
||||||
|
UseDashboardVariablesFromLocalStorageReturn & {
|
||||||
|
transformDashboardVariables: (data: Dashboard) => Dashboard;
|
||||||
|
} {
|
||||||
|
const {
|
||||||
|
currentDashboard,
|
||||||
|
updateLocalStorageDashboardVariables,
|
||||||
|
} = useDashboardVariablesFromLocalStorage(dashboardId);
|
||||||
|
const { getUrlVariables, updateUrlVariable } = useVariablesFromUrl();
|
||||||
|
|
||||||
|
const mergeDBWithLocalStorage = (
|
||||||
|
data: Dashboard,
|
||||||
|
localStorageVariables: any,
|
||||||
|
): Dashboard => {
|
||||||
|
const updatedData = data;
|
||||||
|
if (data && localStorageVariables) {
|
||||||
|
const updatedVariables = data.data.variables;
|
||||||
|
const variablesFromUrl = getUrlVariables();
|
||||||
|
Object.keys(data.data.variables).forEach((variable) => {
|
||||||
|
const variableData = data.data.variables[variable];
|
||||||
|
|
||||||
|
// values from url
|
||||||
|
const urlVariable = variableData?.name
|
||||||
|
? variablesFromUrl[variableData?.name] || variablesFromUrl[variableData.id]
|
||||||
|
: variablesFromUrl[variableData.id];
|
||||||
|
|
||||||
|
let updatedVariable = {
|
||||||
|
...data.data.variables[variable],
|
||||||
|
...localStorageVariables[variableData.name as any],
|
||||||
|
};
|
||||||
|
|
||||||
|
// respect the url variable if it is set, override the others
|
||||||
|
if (!isEmpty(urlVariable)) {
|
||||||
|
if (urlVariable === ALL_SELECTED_VALUE) {
|
||||||
|
updatedVariable = {
|
||||||
|
...updatedVariable,
|
||||||
|
allSelected: true,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// Normalize URL value to match variable's multiSelect configuration
|
||||||
|
const normalizedValue = normalizeUrlValueForVariable(
|
||||||
|
urlVariable,
|
||||||
|
variableData,
|
||||||
|
);
|
||||||
|
|
||||||
|
updatedVariable = {
|
||||||
|
...updatedVariable,
|
||||||
|
selectedValue: normalizedValue,
|
||||||
|
// Only set allSelected to false if showALLOption is available
|
||||||
|
...(updatedVariable?.showALLOption && { allSelected: false }),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updatedVariables[variable] = updatedVariable;
|
||||||
|
});
|
||||||
|
updatedData.data.variables = updatedVariables;
|
||||||
|
}
|
||||||
|
return updatedData;
|
||||||
|
};
|
||||||
|
|
||||||
|
// As we do not have order and ID's in the variables object, we have to process variables to add order and ID if they do not exist in the variables object
|
||||||
|
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||||
|
const transformDashboardVariables = (data: Dashboard): Dashboard => {
|
||||||
|
if (data && data.data && data.data.variables) {
|
||||||
|
const clonedDashboardData = mergeDBWithLocalStorage(
|
||||||
|
JSON.parse(JSON.stringify(data)),
|
||||||
|
currentDashboard,
|
||||||
|
);
|
||||||
|
const { variables } = clonedDashboardData.data;
|
||||||
|
const existingOrders: Set<number> = new Set();
|
||||||
|
|
||||||
|
for (const key in variables) {
|
||||||
|
// eslint-disable-next-line no-prototype-builtins
|
||||||
|
if (variables.hasOwnProperty(key)) {
|
||||||
|
const variable: IDashboardVariable = variables[key];
|
||||||
|
|
||||||
|
// Check if 'order' property doesn't exist or is undefined
|
||||||
|
if (variable.order === undefined) {
|
||||||
|
// Find a unique order starting from 0
|
||||||
|
let order = 0;
|
||||||
|
while (existingOrders.has(order)) {
|
||||||
|
order += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
variable.order = order;
|
||||||
|
existingOrders.add(order);
|
||||||
|
// ! BWC - Specific case for backward compatibility where textboxValue was used instead of defaultValue
|
||||||
|
if (variable.type === 'TEXTBOX' && !variable.defaultValue) {
|
||||||
|
variable.defaultValue = variable.textboxValue || '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (variable.id === undefined) {
|
||||||
|
variable.id = generateUUID();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return clonedDashboardData;
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
transformDashboardVariables,
|
||||||
|
getUrlVariables,
|
||||||
|
updateUrlVariable,
|
||||||
|
currentDashboard,
|
||||||
|
updateLocalStorageDashboardVariables,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -11,7 +11,7 @@ export interface LocalStoreDashboardVariables {
|
|||||||
| IDashboardVariable['selectedValue'];
|
| IDashboardVariable['selectedValue'];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UseVariablesFromUrlReturn {
|
export interface UseVariablesFromUrlReturn {
|
||||||
getUrlVariables: () => LocalStoreDashboardVariables;
|
getUrlVariables: () => LocalStoreDashboardVariables;
|
||||||
setUrlVariables: (variables: LocalStoreDashboardVariables) => void;
|
setUrlVariables: (variables: LocalStoreDashboardVariables) => void;
|
||||||
updateUrlVariable: (
|
updateUrlVariable: (
|
||||||
|
|||||||
@@ -0,0 +1,143 @@
|
|||||||
|
import { Route } from 'react-router-dom';
|
||||||
|
import * as getDashboardModule from 'api/v1/dashboards/id/get';
|
||||||
|
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
|
import { rest, server } from 'mocks-server/server';
|
||||||
|
import { render, screen, waitFor } from 'tests/test-utils';
|
||||||
|
|
||||||
|
import DashboardWidget from '../index';
|
||||||
|
|
||||||
|
const DASHBOARD_ID = 'dash-1';
|
||||||
|
const WIDGET_ID = 'widget-abc';
|
||||||
|
|
||||||
|
const mockDashboardResponse = {
|
||||||
|
status: 'success',
|
||||||
|
data: {
|
||||||
|
id: DASHBOARD_ID,
|
||||||
|
createdAt: '2024-01-01T00:00:00Z',
|
||||||
|
createdBy: 'test',
|
||||||
|
updatedAt: '2024-01-01T00:00:00Z',
|
||||||
|
updatedBy: 'test',
|
||||||
|
isLocked: false,
|
||||||
|
data: {
|
||||||
|
collapsableRowsMigrated: true,
|
||||||
|
description: '',
|
||||||
|
name: '',
|
||||||
|
panelMap: {},
|
||||||
|
tags: [],
|
||||||
|
title: 'Test Dashboard',
|
||||||
|
uploadedGrafana: false,
|
||||||
|
uuid: '',
|
||||||
|
version: '',
|
||||||
|
variables: {},
|
||||||
|
widgets: [],
|
||||||
|
layout: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockSafeNavigate = jest.fn();
|
||||||
|
|
||||||
|
jest.mock('hooks/useSafeNavigate', () => ({
|
||||||
|
useSafeNavigate: (): { safeNavigate: jest.Mock } => ({
|
||||||
|
safeNavigate: mockSafeNavigate,
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('container/NewWidget', () => ({
|
||||||
|
__esModule: true,
|
||||||
|
default: (): JSX.Element => <div data-testid="new-widget">NewWidget</div>,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Wrap component in a Route so useParams can resolve dashboardId.
|
||||||
|
// Query params are passed via the URL so useUrlQuery (react-router) can read them.
|
||||||
|
function renderAtRoute(
|
||||||
|
queryState: Record<string, string | null> = {},
|
||||||
|
): ReturnType<typeof render> {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
Object.entries(queryState).forEach(([k, v]) => {
|
||||||
|
if (v !== null) {
|
||||||
|
params.set(k, v);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const search = params.toString() ? `?${params.toString()}` : '';
|
||||||
|
return render(
|
||||||
|
<Route path="/dashboard/:dashboardId/new">
|
||||||
|
<DashboardWidget />
|
||||||
|
</Route>,
|
||||||
|
undefined,
|
||||||
|
{ initialRoute: `/dashboard/${DASHBOARD_ID}/new${search}` },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mockSafeNavigate.mockClear();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('DashboardWidget', () => {
|
||||||
|
it('redirects to dashboard when widgetId is missing', async () => {
|
||||||
|
renderAtRoute({ graphType: PANEL_TYPES.TIME_SERIES });
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(mockSafeNavigate).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
const [navigatedTo] = mockSafeNavigate.mock.calls[0];
|
||||||
|
expect(navigatedTo).toContain(`/dashboard/${DASHBOARD_ID}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('redirects to dashboard when graphType is missing', async () => {
|
||||||
|
renderAtRoute({ widgetId: WIDGET_ID });
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(mockSafeNavigate).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
const [navigatedTo] = mockSafeNavigate.mock.calls[0];
|
||||||
|
expect(navigatedTo).toContain(`/dashboard/${DASHBOARD_ID}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows spinner while dashboard is loading', () => {
|
||||||
|
// Spy instead of MSW delay('infinite') to avoid leaving an open network handle.
|
||||||
|
jest
|
||||||
|
.spyOn(getDashboardModule, 'default')
|
||||||
|
.mockReturnValue(new Promise(() => {}));
|
||||||
|
|
||||||
|
renderAtRoute({ widgetId: WIDGET_ID, graphType: PANEL_TYPES.TIME_SERIES });
|
||||||
|
|
||||||
|
expect(screen.getByRole('img', { name: 'loading' })).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows error message when dashboard fetch fails', async () => {
|
||||||
|
server.use(
|
||||||
|
rest.get(
|
||||||
|
`http://localhost/api/v1/dashboards/${DASHBOARD_ID}`,
|
||||||
|
(_req, res, ctx) => res(ctx.status(500), ctx.json({ status: 'error' })),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
renderAtRoute({ widgetId: WIDGET_ID, graphType: PANEL_TYPES.TIME_SERIES });
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Something went wrong')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders NewWidget when dashboard loads successfully', async () => {
|
||||||
|
server.use(
|
||||||
|
rest.get(
|
||||||
|
`http://localhost/api/v1/dashboards/${DASHBOARD_ID}`,
|
||||||
|
(_req, res, ctx) => res(ctx.status(200), ctx.json(mockDashboardResponse)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
renderAtRoute({ widgetId: WIDGET_ID, graphType: PANEL_TYPES.TIME_SERIES });
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByTestId('new-widget')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,29 +1,34 @@
|
|||||||
import { useEffect, useMemo } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
import { useQuery } from 'react-query';
|
import { useQuery } from 'react-query';
|
||||||
import { generatePath, useParams } from 'react-router-dom';
|
import { generatePath, useParams } from 'react-router-dom';
|
||||||
import { Card, Typography } from 'antd';
|
import { Card, Typography } from 'antd';
|
||||||
import getDashboard from 'api/v1/dashboards/id/get';
|
import getDashboard from 'api/v1/dashboards/id/get';
|
||||||
import Spinner from 'components/Spinner';
|
import Spinner from 'components/Spinner';
|
||||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||||
|
import { QueryParams } from 'constants/query';
|
||||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
import { DASHBOARD_CACHE_TIME } from 'constants/queryCacheTime';
|
import { DASHBOARD_CACHE_TIME } from 'constants/queryCacheTime';
|
||||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||||
import ROUTES from 'constants/routes';
|
import ROUTES from 'constants/routes';
|
||||||
import NewWidget from 'container/NewWidget';
|
import NewWidget from 'container/NewWidget';
|
||||||
import { isDrilldownEnabled } from 'container/QueryTable/Drilldown/drilldownUtils';
|
import { isDrilldownEnabled } from 'container/QueryTable/Drilldown/drilldownUtils';
|
||||||
|
import { useTransformDashboardVariables } from 'hooks/dashboard/useTransformDashboardVariables';
|
||||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||||
import { parseAsStringEnum, useQueryState } from 'nuqs';
|
import useUrlQuery from 'hooks/useUrlQuery';
|
||||||
import { setDashboardVariablesStore } from 'providers/Dashboard/store/dashboardVariables/dashboardVariablesStore';
|
import { setDashboardVariablesStore } from 'providers/Dashboard/store/dashboardVariables/dashboardVariablesStore';
|
||||||
|
import { Dashboard } from 'types/api/dashboard/getAll';
|
||||||
|
|
||||||
function DashboardWidget(): JSX.Element | null {
|
function DashboardWidget(): JSX.Element | null {
|
||||||
const { dashboardId } = useParams<{
|
const { dashboardId } = useParams<{
|
||||||
dashboardId: string;
|
dashboardId: string;
|
||||||
}>();
|
}>();
|
||||||
const [widgetId] = useQueryState('widgetId');
|
const query = useUrlQuery();
|
||||||
const [graphType] = useQueryState(
|
const { graphType, widgetId } = useMemo(() => {
|
||||||
'graphType',
|
return {
|
||||||
parseAsStringEnum<PANEL_TYPES>(Object.values(PANEL_TYPES)),
|
graphType: query.get(QueryParams.graphType) as PANEL_TYPES,
|
||||||
);
|
widgetId: query.get(QueryParams.widgetId),
|
||||||
|
};
|
||||||
|
}, [query]);
|
||||||
|
|
||||||
const { safeNavigate } = useSafeNavigate();
|
const { safeNavigate } = useSafeNavigate();
|
||||||
|
|
||||||
@@ -57,8 +62,15 @@ function DashboardWidgetInternal({
|
|||||||
widgetId: string;
|
widgetId: string;
|
||||||
graphType: PANEL_TYPES;
|
graphType: PANEL_TYPES;
|
||||||
}): JSX.Element | null {
|
}): JSX.Element | null {
|
||||||
|
const [selectedDashboard, setSelectedDashboard] = useState<
|
||||||
|
Dashboard | undefined
|
||||||
|
>(undefined);
|
||||||
|
|
||||||
|
const { transformDashboardVariables } = useTransformDashboardVariables(
|
||||||
|
dashboardId,
|
||||||
|
);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: dashboardResponse,
|
|
||||||
isFetching: isFetchingDashboardResponse,
|
isFetching: isFetchingDashboardResponse,
|
||||||
isError: isErrorDashboardResponse,
|
isError: isErrorDashboardResponse,
|
||||||
} = useQuery([REACT_QUERY_KEY.DASHBOARD_BY_ID, dashboardId, widgetId], {
|
} = useQuery([REACT_QUERY_KEY.DASHBOARD_BY_ID, dashboardId, widgetId], {
|
||||||
@@ -70,17 +82,15 @@ function DashboardWidgetInternal({
|
|||||||
refetchOnWindowFocus: false,
|
refetchOnWindowFocus: false,
|
||||||
cacheTime: DASHBOARD_CACHE_TIME,
|
cacheTime: DASHBOARD_CACHE_TIME,
|
||||||
onSuccess: (response) => {
|
onSuccess: (response) => {
|
||||||
|
const updatedDashboardData = transformDashboardVariables(response.data);
|
||||||
|
setSelectedDashboard(updatedDashboardData);
|
||||||
setDashboardVariablesStore({
|
setDashboardVariablesStore({
|
||||||
dashboardId,
|
dashboardId,
|
||||||
variables: response.data.data.variables,
|
variables: updatedDashboardData.data.variables,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const selectedDashboard = useMemo(() => dashboardResponse?.data, [
|
|
||||||
dashboardResponse?.data,
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (isFetchingDashboardResponse) {
|
if (isFetchingDashboardResponse) {
|
||||||
return <Spinner tip="Loading.." />;
|
return <Spinner tip="Loading.." />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,21 +17,18 @@ import { useDispatch, useSelector } from 'react-redux';
|
|||||||
import { Modal } from 'antd';
|
import { Modal } from 'antd';
|
||||||
import getDashboard from 'api/v1/dashboards/id/get';
|
import getDashboard from 'api/v1/dashboards/id/get';
|
||||||
import locked from 'api/v1/dashboards/id/lock';
|
import locked from 'api/v1/dashboards/id/lock';
|
||||||
import { ALL_SELECTED_VALUE } from 'components/NewSelect/utils';
|
|
||||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||||
import dayjs, { Dayjs } from 'dayjs';
|
import dayjs, { Dayjs } from 'dayjs';
|
||||||
import { useDashboardVariablesFromLocalStorage } from 'hooks/dashboard/useDashboardFromLocalStorage';
|
import { useTransformDashboardVariables } from 'hooks/dashboard/useTransformDashboardVariables';
|
||||||
import useVariablesFromUrl from 'hooks/dashboard/useVariablesFromUrl';
|
|
||||||
import useTabVisibility from 'hooks/useTabFocus';
|
import useTabVisibility from 'hooks/useTabFocus';
|
||||||
import { getUpdatedLayout } from 'lib/dashboard/getUpdatedLayout';
|
import { getUpdatedLayout } from 'lib/dashboard/getUpdatedLayout';
|
||||||
import { getMinMaxForSelectedTime } from 'lib/getMinMax';
|
import { getMinMaxForSelectedTime } from 'lib/getMinMax';
|
||||||
import { defaultTo, isEmpty } from 'lodash-es';
|
import { defaultTo } from 'lodash-es';
|
||||||
import isEqual from 'lodash-es/isEqual';
|
import isEqual from 'lodash-es/isEqual';
|
||||||
import isUndefined from 'lodash-es/isUndefined';
|
import isUndefined from 'lodash-es/isUndefined';
|
||||||
import omitBy from 'lodash-es/omitBy';
|
import omitBy from 'lodash-es/omitBy';
|
||||||
import { useAppContext } from 'providers/App/App';
|
import { useAppContext } from 'providers/App/App';
|
||||||
import { initializeDefaultVariables } from 'providers/Dashboard/initializeDefaultVariables';
|
import { initializeDefaultVariables } from 'providers/Dashboard/initializeDefaultVariables';
|
||||||
import { normalizeUrlValueForVariable } from 'providers/Dashboard/normalizeUrlValue';
|
|
||||||
import { useErrorModal } from 'providers/ErrorModalProvider';
|
import { useErrorModal } from 'providers/ErrorModalProvider';
|
||||||
// eslint-disable-next-line no-restricted-imports
|
// eslint-disable-next-line no-restricted-imports
|
||||||
import { Dispatch } from 'redux';
|
import { Dispatch } from 'redux';
|
||||||
@@ -39,10 +36,9 @@ import { AppState } from 'store/reducers';
|
|||||||
import AppActions from 'types/actions';
|
import AppActions from 'types/actions';
|
||||||
import { UPDATE_TIME_INTERVAL } from 'types/actions/globalTime';
|
import { UPDATE_TIME_INTERVAL } from 'types/actions/globalTime';
|
||||||
import { SuccessResponseV2 } from 'types/api';
|
import { SuccessResponseV2 } from 'types/api';
|
||||||
import { Dashboard, IDashboardVariable } from 'types/api/dashboard/getAll';
|
import { Dashboard } from 'types/api/dashboard/getAll';
|
||||||
import APIError from 'types/api/error';
|
import APIError from 'types/api/error';
|
||||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||||
import { v4 as generateUUID } from 'uuid';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DASHBOARD_CACHE_TIME,
|
DASHBOARD_CACHE_TIME,
|
||||||
@@ -137,9 +133,10 @@ export function DashboardProvider({
|
|||||||
const {
|
const {
|
||||||
currentDashboard,
|
currentDashboard,
|
||||||
updateLocalStorageDashboardVariables,
|
updateLocalStorageDashboardVariables,
|
||||||
} = useDashboardVariablesFromLocalStorage(dashboardId);
|
getUrlVariables,
|
||||||
|
updateUrlVariable,
|
||||||
const { getUrlVariables, updateUrlVariable } = useVariablesFromUrl();
|
transformDashboardVariables,
|
||||||
|
} = useTransformDashboardVariables(dashboardId);
|
||||||
|
|
||||||
const updatedTimeRef = useRef<Dayjs | null>(null); // Using ref to store the updated time
|
const updatedTimeRef = useRef<Dayjs | null>(null); // Using ref to store the updated time
|
||||||
const modalRef = useRef<any>(null);
|
const modalRef = useRef<any>(null);
|
||||||
@@ -151,99 +148,6 @@ export function DashboardProvider({
|
|||||||
|
|
||||||
const [isDashboardFetching, setIsDashboardFetching] = useState<boolean>(false);
|
const [isDashboardFetching, setIsDashboardFetching] = useState<boolean>(false);
|
||||||
|
|
||||||
const mergeDBWithLocalStorage = (
|
|
||||||
data: Dashboard,
|
|
||||||
localStorageVariables: any,
|
|
||||||
): Dashboard => {
|
|
||||||
const updatedData = data;
|
|
||||||
if (data && localStorageVariables) {
|
|
||||||
const updatedVariables = data.data.variables;
|
|
||||||
const variablesFromUrl = getUrlVariables();
|
|
||||||
Object.keys(data.data.variables).forEach((variable) => {
|
|
||||||
const variableData = data.data.variables[variable];
|
|
||||||
|
|
||||||
// values from url
|
|
||||||
const urlVariable = variableData?.name
|
|
||||||
? variablesFromUrl[variableData?.name] || variablesFromUrl[variableData.id]
|
|
||||||
: variablesFromUrl[variableData.id];
|
|
||||||
|
|
||||||
let updatedVariable = {
|
|
||||||
...data.data.variables[variable],
|
|
||||||
...localStorageVariables[variableData.name as any],
|
|
||||||
};
|
|
||||||
|
|
||||||
// respect the url variable if it is set, override the others
|
|
||||||
if (!isEmpty(urlVariable)) {
|
|
||||||
if (urlVariable === ALL_SELECTED_VALUE) {
|
|
||||||
updatedVariable = {
|
|
||||||
...updatedVariable,
|
|
||||||
allSelected: true,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
// Normalize URL value to match variable's multiSelect configuration
|
|
||||||
const normalizedValue = normalizeUrlValueForVariable(
|
|
||||||
urlVariable,
|
|
||||||
variableData,
|
|
||||||
);
|
|
||||||
|
|
||||||
updatedVariable = {
|
|
||||||
...updatedVariable,
|
|
||||||
selectedValue: normalizedValue,
|
|
||||||
// Only set allSelected to false if showALLOption is available
|
|
||||||
...(updatedVariable?.showALLOption && { allSelected: false }),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updatedVariables[variable] = updatedVariable;
|
|
||||||
});
|
|
||||||
updatedData.data.variables = updatedVariables;
|
|
||||||
}
|
|
||||||
return updatedData;
|
|
||||||
};
|
|
||||||
// As we do not have order and ID's in the variables object, we have to process variables to add order and ID if they do not exist in the variables object
|
|
||||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
|
||||||
const transformDashboardVariables = (data: Dashboard): Dashboard => {
|
|
||||||
if (data && data.data && data.data.variables) {
|
|
||||||
const clonedDashboardData = mergeDBWithLocalStorage(
|
|
||||||
JSON.parse(JSON.stringify(data)),
|
|
||||||
currentDashboard,
|
|
||||||
);
|
|
||||||
const { variables } = clonedDashboardData.data;
|
|
||||||
const existingOrders: Set<number> = new Set();
|
|
||||||
|
|
||||||
for (const key in variables) {
|
|
||||||
// eslint-disable-next-line no-prototype-builtins
|
|
||||||
if (variables.hasOwnProperty(key)) {
|
|
||||||
const variable: IDashboardVariable = variables[key];
|
|
||||||
|
|
||||||
// Check if 'order' property doesn't exist or is undefined
|
|
||||||
if (variable.order === undefined) {
|
|
||||||
// Find a unique order starting from 0
|
|
||||||
let order = 0;
|
|
||||||
while (existingOrders.has(order)) {
|
|
||||||
order += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
variable.order = order;
|
|
||||||
existingOrders.add(order);
|
|
||||||
// ! BWC - Specific case for backward compatibility where textboxValue was used instead of defaultValue
|
|
||||||
if (variable.type === 'TEXTBOX' && !variable.defaultValue) {
|
|
||||||
variable.defaultValue = variable.textboxValue || '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (variable.id === undefined) {
|
|
||||||
variable.id = generateUUID();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return clonedDashboardData;
|
|
||||||
}
|
|
||||||
|
|
||||||
return data;
|
|
||||||
};
|
|
||||||
const dashboardResponse = useQuery(
|
const dashboardResponse = useQuery(
|
||||||
[
|
[
|
||||||
REACT_QUERY_KEY.DASHBOARD_BY_ID,
|
REACT_QUERY_KEY.DASHBOARD_BY_ID,
|
||||||
@@ -274,13 +178,14 @@ export function DashboardProvider({
|
|||||||
},
|
},
|
||||||
|
|
||||||
onSuccess: (data: SuccessResponseV2<Dashboard>) => {
|
onSuccess: (data: SuccessResponseV2<Dashboard>) => {
|
||||||
// if the url variable is not set for any variable, set it to the default value
|
const updatedDashboardData = transformDashboardVariables(data?.data);
|
||||||
const variables = data?.data?.data?.variables;
|
|
||||||
|
// initialize URL variables after dashboard state is set to avoid race conditions
|
||||||
|
const variables = updatedDashboardData?.data?.variables;
|
||||||
if (variables) {
|
if (variables) {
|
||||||
initializeDefaultVariables(variables, getUrlVariables, updateUrlVariable);
|
initializeDefaultVariables(variables, getUrlVariables, updateUrlVariable);
|
||||||
}
|
}
|
||||||
|
|
||||||
const updatedDashboardData = transformDashboardVariables(data?.data);
|
|
||||||
const updatedDate = dayjs(updatedDashboardData?.updatedAt);
|
const updatedDate = dayjs(updatedDashboardData?.updatedAt);
|
||||||
|
|
||||||
setIsDashboardLocked(updatedDashboardData?.locked || false);
|
setIsDashboardLocked(updatedDashboardData?.locked || false);
|
||||||
|
|||||||
@@ -381,6 +381,7 @@ describe('Dashboard Provider - URL Variables Integration', () => {
|
|||||||
multiSelect: false,
|
multiSelect: false,
|
||||||
allSelected: false,
|
allSelected: false,
|
||||||
showALLOption: true,
|
showALLOption: true,
|
||||||
|
order: 0,
|
||||||
},
|
},
|
||||||
services: {
|
services: {
|
||||||
id: 'svc-id',
|
id: 'svc-id',
|
||||||
@@ -388,6 +389,7 @@ describe('Dashboard Provider - URL Variables Integration', () => {
|
|||||||
multiSelect: true,
|
multiSelect: true,
|
||||||
allSelected: false,
|
allSelected: false,
|
||||||
showALLOption: true,
|
showALLOption: true,
|
||||||
|
order: 1,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
mockGetUrlVariables,
|
mockGetUrlVariables,
|
||||||
|
|||||||
81
pkg/modules/cloudintegration/cloudintegration.go
Normal file
81
pkg/modules/cloudintegration/cloudintegration.go
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
package cloudintegration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/SigNoz/signoz/pkg/types/cloudintegrationtypes"
|
||||||
|
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
|
||||||
|
"github.com/SigNoz/signoz/pkg/valuer"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Module interface {
|
||||||
|
// CreateConnectionArtifact generates cloud provider specific connection information,
|
||||||
|
// client side handles how this information is shown
|
||||||
|
CreateConnectionArtifact(
|
||||||
|
ctx context.Context,
|
||||||
|
orgID valuer.UUID,
|
||||||
|
provider cloudintegrationtypes.CloudProviderType,
|
||||||
|
request *cloudintegrationtypes.ConnectionArtifactRequest,
|
||||||
|
) (*cloudintegrationtypes.ConnectionArtifact, error)
|
||||||
|
|
||||||
|
// GetAccountStatus returns agent connection status for a cloud integration account
|
||||||
|
GetAccountStatus(ctx context.Context, orgID, accountID valuer.UUID) (*cloudintegrationtypes.AccountStatus, error)
|
||||||
|
|
||||||
|
// ListConnectedAccounts lists accounts where agent is connected
|
||||||
|
ListConnectedAccounts(ctx context.Context, orgID valuer.UUID) (*cloudintegrationtypes.ConnectedAccounts, error)
|
||||||
|
|
||||||
|
// DisconnectAccount soft deletes/removes a cloud integration account.
|
||||||
|
DisconnectAccount(ctx context.Context, orgID, accountID valuer.UUID) error
|
||||||
|
|
||||||
|
// UpdateAccountConfig updates the configuration of an existing cloud account for a specific organization.
|
||||||
|
UpdateAccountConfig(
|
||||||
|
ctx context.Context,
|
||||||
|
orgId,
|
||||||
|
accountId valuer.UUID,
|
||||||
|
config *cloudintegrationtypes.UpdateAccountConfigRequest,
|
||||||
|
) (*cloudintegrationtypes.Account, error)
|
||||||
|
|
||||||
|
// ListServicesSummary return list of services for a cloud provider attached with the accountID.
|
||||||
|
// This just returns a summary of the service and not the whole service definition
|
||||||
|
ListServicesSummary(ctx context.Context, orgID valuer.UUID, accountID *valuer.UUID) (*cloudintegrationtypes.ServicesSummary, error)
|
||||||
|
|
||||||
|
// GetService returns service definition details for a serviceId. This returns config and
|
||||||
|
// other details required to show in service details page on web client.
|
||||||
|
GetService(ctx context.Context, orgID valuer.UUID, serviceID string, accountID *valuer.UUID) (*cloudintegrationtypes.Service, error)
|
||||||
|
|
||||||
|
// UpdateServiceConfig updates cloud integration service config
|
||||||
|
UpdateServiceConfig(
|
||||||
|
ctx context.Context,
|
||||||
|
serviceId string,
|
||||||
|
orgID valuer.UUID,
|
||||||
|
config *cloudintegrationtypes.UpdateServiceConfigRequest,
|
||||||
|
) (*cloudintegrationtypes.ServiceSummary, error)
|
||||||
|
|
||||||
|
// AgentCheckIn is called by agent to heartbeat and get latest config in response.
|
||||||
|
AgentCheckIn(
|
||||||
|
ctx context.Context,
|
||||||
|
orgID valuer.UUID,
|
||||||
|
req *cloudintegrationtypes.AgentCheckInRequest,
|
||||||
|
) (cloudintegrationtypes.AgentCheckInResponse, error)
|
||||||
|
|
||||||
|
// GetDashboardByID returns dashboard JSON for a given dashboard id.
|
||||||
|
// this only returns the dashboard when the service (embedded in dashboard id) is enabled for
|
||||||
|
GetDashboardByID(ctx context.Context, id string, orgID valuer.UUID) (*dashboardtypes.Dashboard, error)
|
||||||
|
|
||||||
|
// GetAllDashboards returns list of dashboards across all connected cloud integration accounts
|
||||||
|
// and enabled services in the org. This list gets added to dashboard list page
|
||||||
|
GetAllDashboards(ctx context.Context, orgID valuer.UUID) ([]*dashboardtypes.Dashboard, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Handler interface {
|
||||||
|
AgentCheckIn(http.ResponseWriter, *http.Request)
|
||||||
|
GenerateConnectionArtifact(http.ResponseWriter, *http.Request)
|
||||||
|
ListConnectedAccounts(http.ResponseWriter, *http.Request)
|
||||||
|
GetAccountStatus(http.ResponseWriter, *http.Request)
|
||||||
|
ListServices(http.ResponseWriter, *http.Request)
|
||||||
|
GetServiceDetails(http.ResponseWriter, *http.Request)
|
||||||
|
UpdateAccountConfig(http.ResponseWriter, *http.Request)
|
||||||
|
UpdateServiceConfig(http.ResponseWriter, *http.Request)
|
||||||
|
DisconnectAccount(http.ResponseWriter, *http.Request)
|
||||||
|
}
|
||||||
118
pkg/modules/cloudintegration/implcloudintegration/store.go
Normal file
118
pkg/modules/cloudintegration/implcloudintegration/store.go
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
package implcloudintegration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||||
|
"github.com/SigNoz/signoz/pkg/types/cloudintegrationtypes"
|
||||||
|
"github.com/SigNoz/signoz/pkg/valuer"
|
||||||
|
)
|
||||||
|
|
||||||
|
type store struct {
|
||||||
|
store sqlstore.SQLStore
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewStore(sqlStore sqlstore.SQLStore) cloudintegrationtypes.Store {
|
||||||
|
return &store{store: sqlStore}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *store) GetAccountByID(ctx context.Context, orgID, id valuer.UUID, provider cloudintegrationtypes.CloudProviderType) (*cloudintegrationtypes.StorableCloudIntegration, error) {
|
||||||
|
account := new(cloudintegrationtypes.StorableCloudIntegration)
|
||||||
|
err := s.store.BunDB().NewSelect().Model(account).
|
||||||
|
Where("id = ?", id).
|
||||||
|
Where("org_id = ?", orgID).
|
||||||
|
Where("provider = ?", provider).
|
||||||
|
Scan(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, s.store.WrapNotFoundErrf(err, cloudintegrationtypes.ErrCodeCloudIntegrationNotFound, "cloud integration account with id %s not found", id)
|
||||||
|
}
|
||||||
|
return account, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *store) UpsertAccount(ctx context.Context, account *cloudintegrationtypes.StorableCloudIntegration) error {
|
||||||
|
account.UpdatedAt = time.Now()
|
||||||
|
_, err := s.store.BunDBCtx(ctx).NewInsert().Model(account).
|
||||||
|
On("CONFLICT (id, provider, org_id) DO UPDATE").
|
||||||
|
Set("config = EXCLUDED.config").
|
||||||
|
Set("account_id = EXCLUDED.account_id").
|
||||||
|
Set("last_agent_report = EXCLUDED.last_agent_report").
|
||||||
|
Set("removed_at = EXCLUDED.removed_at").
|
||||||
|
Set("updated_at = EXCLUDED.updated_at").
|
||||||
|
Exec(ctx)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *store) RemoveAccount(ctx context.Context, orgID, id valuer.UUID, provider cloudintegrationtypes.CloudProviderType) error {
|
||||||
|
_, err := s.store.BunDBCtx(ctx).NewUpdate().Model((*cloudintegrationtypes.StorableCloudIntegration)(nil)).
|
||||||
|
Set("removed_at = ?", time.Now()).
|
||||||
|
Where("id = ?", id).
|
||||||
|
Where("org_id = ?", orgID).
|
||||||
|
Where("provider = ?", provider).
|
||||||
|
Exec(ctx)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *store) GetConnectedAccounts(ctx context.Context, orgID valuer.UUID, provider cloudintegrationtypes.CloudProviderType) ([]*cloudintegrationtypes.StorableCloudIntegration, error) {
|
||||||
|
var accounts []*cloudintegrationtypes.StorableCloudIntegration
|
||||||
|
err := s.store.BunDB().NewSelect().Model(&accounts).
|
||||||
|
Where("org_id = ?", orgID).
|
||||||
|
Where("provider = ?", provider).
|
||||||
|
Where("removed_at IS NULL").
|
||||||
|
Where("account_id IS NOT NULL").
|
||||||
|
Where("last_agent_report IS NOT NULL").
|
||||||
|
Order("created_at ASC").
|
||||||
|
Scan(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return accounts, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *store) GetConnectedAccount(ctx context.Context, orgID valuer.UUID, provider cloudintegrationtypes.CloudProviderType, providerAccountID string) (*cloudintegrationtypes.StorableCloudIntegration, error) {
|
||||||
|
account := new(cloudintegrationtypes.StorableCloudIntegration)
|
||||||
|
err := s.store.BunDB().NewSelect().Model(account).
|
||||||
|
Where("org_id = ?", orgID).
|
||||||
|
Where("provider = ?", provider).
|
||||||
|
Where("account_id = ?", providerAccountID).
|
||||||
|
Where("last_agent_report IS NOT NULL").
|
||||||
|
Where("removed_at IS NULL").
|
||||||
|
Scan(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, s.store.WrapNotFoundErrf(err, cloudintegrationtypes.ErrCodeCloudIntegrationNotFound, "connected account with provider account id %s not found", providerAccountID)
|
||||||
|
}
|
||||||
|
return account, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *store) GetServiceByType(ctx context.Context, cloudIntegrationID valuer.UUID, serviceType string) (*cloudintegrationtypes.StorableCloudIntegrationService, error) {
|
||||||
|
service := new(cloudintegrationtypes.StorableCloudIntegrationService)
|
||||||
|
err := s.store.BunDB().NewSelect().Model(service).
|
||||||
|
Where("cloud_integration_id = ?", cloudIntegrationID).
|
||||||
|
Where("type = ?", serviceType).
|
||||||
|
Scan(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, s.store.WrapNotFoundErrf(err, cloudintegrationtypes.ErrCodeCloudIntegrationNotFound, "cloud integration service with type %s not found", serviceType)
|
||||||
|
}
|
||||||
|
return service, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *store) UpsertService(ctx context.Context, service *cloudintegrationtypes.StorableCloudIntegrationService) error {
|
||||||
|
service.UpdatedAt = time.Now()
|
||||||
|
_, err := s.store.BunDBCtx(ctx).NewInsert().Model(service).
|
||||||
|
On("CONFLICT (cloud_integration_id, type) DO UPDATE").
|
||||||
|
Set("config = EXCLUDED.config").
|
||||||
|
Set("updated_at = EXCLUDED.updated_at").
|
||||||
|
Exec(ctx)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *store) GetServices(ctx context.Context, cloudIntegrationID valuer.UUID) ([]*cloudintegrationtypes.StorableCloudIntegrationService, error) {
|
||||||
|
var services []*cloudintegrationtypes.StorableCloudIntegrationService
|
||||||
|
err := s.store.BunDB().NewSelect().Model(&services).
|
||||||
|
Where("cloud_integration_id = ?", cloudIntegrationID).
|
||||||
|
Scan(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return services, nil
|
||||||
|
}
|
||||||
49
pkg/types/cloudintegrationtypes/account.go
Normal file
49
pkg/types/cloudintegrationtypes/account.go
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
package cloudintegrationtypes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/SigNoz/signoz/pkg/valuer"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
ConnectedAccounts struct {
|
||||||
|
Accounts []*Account `json:"accounts"`
|
||||||
|
}
|
||||||
|
|
||||||
|
GettableConnectedAccounts = ConnectedAccounts
|
||||||
|
|
||||||
|
UpdateAccountConfigRequest struct {
|
||||||
|
AWS *AWSAccountConfig `json:"aws"`
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdatableAccountConfig = UpdateAccountConfigRequest
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
Account struct {
|
||||||
|
Id string `json:"id"`
|
||||||
|
ProviderAccountId *string `json:"providerAccountID,omitempty"`
|
||||||
|
Provider CloudProviderType `json:"provider"`
|
||||||
|
RemovedAt *time.Time `json:"removedAt,omitempty"`
|
||||||
|
AgentReport *AgentReport `json:"agentReport,omitempty"`
|
||||||
|
OrgID valuer.UUID `json:"orgID"`
|
||||||
|
Config *AccountConfig `json:"accountConfig,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
GettableAccount = Account
|
||||||
|
)
|
||||||
|
|
||||||
|
// AgentReport represents heartbeats sent by the agent.
|
||||||
|
type AgentReport struct {
|
||||||
|
TimestampMillis int64 `json:"timestampMillis"`
|
||||||
|
Data map[string]any `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AccountConfig struct {
|
||||||
|
AWS *AWSAccountConfig `json:"aws,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AWSAccountConfig struct {
|
||||||
|
Regions []string `json:"regions"`
|
||||||
|
}
|
||||||
82
pkg/types/cloudintegrationtypes/cloudintegration.go
Normal file
82
pkg/types/cloudintegrationtypes/cloudintegration.go
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
package cloudintegrationtypes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql/driver"
|
||||||
|
"encoding/json"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/SigNoz/signoz/pkg/errors"
|
||||||
|
"github.com/uptrace/bun"
|
||||||
|
|
||||||
|
"github.com/SigNoz/signoz/pkg/types"
|
||||||
|
"github.com/SigNoz/signoz/pkg/valuer"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrCodeCloudIntegrationNotFound = errors.MustNewCode("cloud_integration_not_found")
|
||||||
|
)
|
||||||
|
|
||||||
|
// StorableCloudIntegration represents a cloud integration stored in the database.
|
||||||
|
// This is also referred as "Account" in the context of cloud integrations.
|
||||||
|
type StorableCloudIntegration struct {
|
||||||
|
bun.BaseModel `bun:"table:cloud_integration"`
|
||||||
|
|
||||||
|
types.Identifiable
|
||||||
|
types.TimeAuditable
|
||||||
|
Provider CloudProviderType `json:"provider" bun:"provider,type:text"`
|
||||||
|
// Config is provider specific data in JSON string format
|
||||||
|
Config string `json:"config" bun:"config,type:text"`
|
||||||
|
AccountID *string `json:"account_id" bun:"account_id,type:text"`
|
||||||
|
LastAgentReport *StorableAgentReport `json:"last_agent_report" bun:"last_agent_report,type:text"`
|
||||||
|
RemovedAt *time.Time `json:"removed_at" bun:"removed_at,type:timestamp,nullzero"`
|
||||||
|
OrgID valuer.UUID `bun:"org_id,type:text"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// StorableAgentReport represents the last heartbeat and arbitrary data sent by the agent
|
||||||
|
// as of now there is no use case for Data field, but keeping it for backwards compatibility with older structure.
|
||||||
|
type StorableAgentReport struct {
|
||||||
|
TimestampMillis int64 `json:"timestamp_millis"`
|
||||||
|
Data map[string]any `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// StorableCloudIntegrationService is to store service config for a cloud integration, which is a cloud provider specific configuration.
|
||||||
|
type StorableCloudIntegrationService struct {
|
||||||
|
bun.BaseModel `bun:"table:cloud_integration_service,alias:cis"`
|
||||||
|
|
||||||
|
types.Identifiable
|
||||||
|
types.TimeAuditable
|
||||||
|
Type valuer.String `bun:"type,type:text,notnull,unique:cloud_integration_id_type"`
|
||||||
|
// Config is cloud provider's service specific data in JSON string format
|
||||||
|
Config string `bun:"config,type:text"`
|
||||||
|
CloudIntegrationID valuer.UUID `bun:"cloud_integration_id,type:text,notnull,unique:cloud_integration_id_type,references:cloud_integration(id),on_delete:cascade"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scan scans value from DB
|
||||||
|
func (r *StorableAgentReport) Scan(src any) error {
|
||||||
|
var data []byte
|
||||||
|
switch v := src.(type) {
|
||||||
|
case []byte:
|
||||||
|
data = v
|
||||||
|
case string:
|
||||||
|
data = []byte(v)
|
||||||
|
default:
|
||||||
|
return errors.NewInternalf(errors.CodeInternal, "tried to scan from %T instead of string or bytes", src)
|
||||||
|
}
|
||||||
|
return json.Unmarshal(data, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Value creates value to be stored in DB
|
||||||
|
func (r *StorableAgentReport) Value() (driver.Value, error) {
|
||||||
|
if r == nil {
|
||||||
|
return nil, errors.NewInternalf(errors.CodeInternal, "agent report is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
serialized, err := json.Marshal(r)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.WrapInternalf(
|
||||||
|
err, errors.CodeInternal, "couldn't serialize agent report to JSON",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// Return as string instead of []byte to ensure PostgreSQL stores as text, not bytes
|
||||||
|
return string(serialized), nil
|
||||||
|
}
|
||||||
41
pkg/types/cloudintegrationtypes/cloudprovider.go
Normal file
41
pkg/types/cloudintegrationtypes/cloudprovider.go
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
package cloudintegrationtypes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/SigNoz/signoz/pkg/errors"
|
||||||
|
"github.com/SigNoz/signoz/pkg/valuer"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CloudProviderType type alias
|
||||||
|
type CloudProviderType struct{ valuer.String }
|
||||||
|
|
||||||
|
var (
|
||||||
|
// cloud providers
|
||||||
|
CloudProviderTypeAWS = CloudProviderType{valuer.NewString("aws")}
|
||||||
|
CloudProviderTypeAzure = CloudProviderType{valuer.NewString("azure")}
|
||||||
|
|
||||||
|
// errors
|
||||||
|
ErrCodeCloudProviderInvalidInput = errors.MustNewCode("invalid_cloud_provider")
|
||||||
|
|
||||||
|
AWSIntegrationUserEmail = valuer.MustNewEmail("aws-integration@signoz.io")
|
||||||
|
AzureIntegrationUserEmail = valuer.MustNewEmail("azure-integration@signoz.io")
|
||||||
|
)
|
||||||
|
|
||||||
|
// CloudIntegrationUserEmails is the list of valid emails for Cloud One Click integrations.
|
||||||
|
// This is used for validation and restrictions in different contexts, across codebase.
|
||||||
|
var CloudIntegrationUserEmails = []valuer.Email{
|
||||||
|
AWSIntegrationUserEmail,
|
||||||
|
AzureIntegrationUserEmail,
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewCloudProvider returns a new CloudProviderType from a string.
|
||||||
|
// It validates the input and returns an error if the input is not valid cloud provider.
|
||||||
|
func NewCloudProvider(provider string) (CloudProviderType, error) {
|
||||||
|
switch provider {
|
||||||
|
case CloudProviderTypeAWS.StringValue():
|
||||||
|
return CloudProviderTypeAWS, nil
|
||||||
|
case CloudProviderTypeAzure.StringValue():
|
||||||
|
return CloudProviderTypeAzure, nil
|
||||||
|
default:
|
||||||
|
return CloudProviderType{}, errors.NewInvalidInputf(ErrCodeCloudProviderInvalidInput, "invalid cloud provider: %s", provider)
|
||||||
|
}
|
||||||
|
}
|
||||||
95
pkg/types/cloudintegrationtypes/connection.go
Normal file
95
pkg/types/cloudintegrationtypes/connection.go
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
package cloudintegrationtypes
|
||||||
|
|
||||||
|
import "github.com/SigNoz/signoz/pkg/types/integrationtypes"
|
||||||
|
|
||||||
|
// request for creating connection artifact
|
||||||
|
type (
|
||||||
|
PostableConnectionArtifact = ConnectionArtifactRequest
|
||||||
|
|
||||||
|
ConnectionArtifactRequest struct {
|
||||||
|
Aws *AWSConnectionArtifactRequest `json:"aws"`
|
||||||
|
}
|
||||||
|
|
||||||
|
AWSConnectionArtifactRequest struct {
|
||||||
|
DeploymentRegion string `json:"deploymentRegion"`
|
||||||
|
Regions []string `json:"regions"`
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
ConnectionArtifact struct {
|
||||||
|
Aws *AWSConnectionArtifact `json:"aws"`
|
||||||
|
}
|
||||||
|
|
||||||
|
AWSConnectionArtifact struct {
|
||||||
|
ConnectionUrl string `json:"connectionURL"`
|
||||||
|
}
|
||||||
|
|
||||||
|
GettableConnectionArtifact = ConnectionArtifact
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
AccountStatus struct {
|
||||||
|
Id string `json:"id"`
|
||||||
|
ProviderAccountId *string `json:"providerAccountID,omitempty"`
|
||||||
|
Status integrationtypes.AccountStatus `json:"status"`
|
||||||
|
}
|
||||||
|
|
||||||
|
GettableAccountStatus = AccountStatus
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
AgentCheckInRequest struct {
|
||||||
|
// older backward compatible fields are mapped to new fields
|
||||||
|
// CloudIntegrationId string `json:"cloudIntegrationId"`
|
||||||
|
// AccountId string `json:"accountId"`
|
||||||
|
|
||||||
|
// New fields
|
||||||
|
ProviderAccountId string `json:"providerAccountId"`
|
||||||
|
CloudAccountId string `json:"cloudAccountId"`
|
||||||
|
|
||||||
|
Data map[string]any `json:"data,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
PostableAgentCheckInRequest struct {
|
||||||
|
AgentCheckInRequest
|
||||||
|
// following are backward compatible fields for older running agents
|
||||||
|
CloudIntegrationId string `json:"cloud_integration_id"`
|
||||||
|
CloudAccountId string `json:"cloud_account_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
GettableAgentCheckInResponse struct {
|
||||||
|
AgentCheckInResponse
|
||||||
|
|
||||||
|
CloudIntegrationId string `json:"cloud_integration_id"`
|
||||||
|
AccountId string `json:"account_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
AgentCheckInResponse struct {
|
||||||
|
// Older fields for backward compatibility are mapped to new fields below
|
||||||
|
// CloudIntegrationId string `json:"cloud_integration_id"`
|
||||||
|
// AccountId string `json:"account_id"`
|
||||||
|
|
||||||
|
// New fields
|
||||||
|
ProviderAccountId string `json:"providerAccountId"`
|
||||||
|
CloudAccountId string `json:"cloudAccountId"`
|
||||||
|
|
||||||
|
// IntegrationConfig populates data related to integration that is required for an agent
|
||||||
|
// to start collecting telemetry data
|
||||||
|
// keeping JSON key snake_case for backward compatibility
|
||||||
|
IntegrationConfig *IntegrationConfig `json:"integration_config,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
IntegrationConfig struct {
|
||||||
|
EnabledRegions []string `json:"enabledRegions"` // backward compatible
|
||||||
|
Telemetry *AWSCollectionStrategy `json:"telemetry,omitempty"` // backward compatible
|
||||||
|
|
||||||
|
// new fields
|
||||||
|
AWS *AWSIntegrationConfig `json:"aws,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
AWSIntegrationConfig struct {
|
||||||
|
EnabledRegions []string `json:"enabledRegions"`
|
||||||
|
Telemetry *AWSCollectionStrategy `json:"telemetry,omitempty"`
|
||||||
|
}
|
||||||
|
)
|
||||||
103
pkg/types/cloudintegrationtypes/regions.go
Normal file
103
pkg/types/cloudintegrationtypes/regions.go
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
package cloudintegrationtypes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/SigNoz/signoz/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
CodeInvalidCloudRegion = errors.MustNewCode("invalid_cloud_region")
|
||||||
|
CodeMismatchCloudProvider = errors.MustNewCode("cloud_provider_mismatch")
|
||||||
|
)
|
||||||
|
|
||||||
|
// List of all valid cloud regions on Amazon Web Services
|
||||||
|
var ValidAWSRegions = map[string]struct{}{
|
||||||
|
"af-south-1": {}, // Africa (Cape Town).
|
||||||
|
"ap-east-1": {}, // Asia Pacific (Hong Kong).
|
||||||
|
"ap-northeast-1": {}, // Asia Pacific (Tokyo).
|
||||||
|
"ap-northeast-2": {}, // Asia Pacific (Seoul).
|
||||||
|
"ap-northeast-3": {}, // Asia Pacific (Osaka).
|
||||||
|
"ap-south-1": {}, // Asia Pacific (Mumbai).
|
||||||
|
"ap-south-2": {}, // Asia Pacific (Hyderabad).
|
||||||
|
"ap-southeast-1": {}, // Asia Pacific (Singapore).
|
||||||
|
"ap-southeast-2": {}, // Asia Pacific (Sydney).
|
||||||
|
"ap-southeast-3": {}, // Asia Pacific (Jakarta).
|
||||||
|
"ap-southeast-4": {}, // Asia Pacific (Melbourne).
|
||||||
|
"ca-central-1": {}, // Canada (Central).
|
||||||
|
"ca-west-1": {}, // Canada West (Calgary).
|
||||||
|
"eu-central-1": {}, // Europe (Frankfurt).
|
||||||
|
"eu-central-2": {}, // Europe (Zurich).
|
||||||
|
"eu-north-1": {}, // Europe (Stockholm).
|
||||||
|
"eu-south-1": {}, // Europe (Milan).
|
||||||
|
"eu-south-2": {}, // Europe (Spain).
|
||||||
|
"eu-west-1": {}, // Europe (Ireland).
|
||||||
|
"eu-west-2": {}, // Europe (London).
|
||||||
|
"eu-west-3": {}, // Europe (Paris).
|
||||||
|
"il-central-1": {}, // Israel (Tel Aviv).
|
||||||
|
"me-central-1": {}, // Middle East (UAE).
|
||||||
|
"me-south-1": {}, // Middle East (Bahrain).
|
||||||
|
"sa-east-1": {}, // South America (Sao Paulo).
|
||||||
|
"us-east-1": {}, // US East (N. Virginia).
|
||||||
|
"us-east-2": {}, // US East (Ohio).
|
||||||
|
"us-west-1": {}, // US West (N. California).
|
||||||
|
"us-west-2": {}, // US West (Oregon).
|
||||||
|
}
|
||||||
|
|
||||||
|
// List of all valid cloud regions for Microsoft Azure
|
||||||
|
var ValidAzureRegions = map[string]struct{}{
|
||||||
|
"australiacentral": {}, // Australia Central
|
||||||
|
"australiacentral2": {}, // Australia Central 2
|
||||||
|
"australiaeast": {}, // Australia East
|
||||||
|
"australiasoutheast": {}, // Australia Southeast
|
||||||
|
"austriaeast": {}, // Austria East
|
||||||
|
"belgiumcentral": {}, // Belgium Central
|
||||||
|
"brazilsouth": {}, // Brazil South
|
||||||
|
"brazilsoutheast": {}, // Brazil Southeast
|
||||||
|
"canadacentral": {}, // Canada Central
|
||||||
|
"canadaeast": {}, // Canada East
|
||||||
|
"centralindia": {}, // Central India
|
||||||
|
"centralus": {}, // Central US
|
||||||
|
"chilecentral": {}, // Chile Central
|
||||||
|
"denmarkeast": {}, // Denmark East
|
||||||
|
"eastasia": {}, // East Asia
|
||||||
|
"eastus": {}, // East US
|
||||||
|
"eastus2": {}, // East US 2
|
||||||
|
"francecentral": {}, // France Central
|
||||||
|
"francesouth": {}, // France South
|
||||||
|
"germanynorth": {}, // Germany North
|
||||||
|
"germanywestcentral": {}, // Germany West Central
|
||||||
|
"indonesiacentral": {}, // Indonesia Central
|
||||||
|
"israelcentral": {}, // Israel Central
|
||||||
|
"italynorth": {}, // Italy North
|
||||||
|
"japaneast": {}, // Japan East
|
||||||
|
"japanwest": {}, // Japan West
|
||||||
|
"koreacentral": {}, // Korea Central
|
||||||
|
"koreasouth": {}, // Korea South
|
||||||
|
"malaysiawest": {}, // Malaysia West
|
||||||
|
"mexicocentral": {}, // Mexico Central
|
||||||
|
"newzealandnorth": {}, // New Zealand North
|
||||||
|
"northcentralus": {}, // North Central US
|
||||||
|
"northeurope": {}, // North Europe
|
||||||
|
"norwayeast": {}, // Norway East
|
||||||
|
"norwaywest": {}, // Norway West
|
||||||
|
"polandcentral": {}, // Poland Central
|
||||||
|
"qatarcentral": {}, // Qatar Central
|
||||||
|
"southafricanorth": {}, // South Africa North
|
||||||
|
"southafricawest": {}, // South Africa West
|
||||||
|
"southcentralus": {}, // South Central US
|
||||||
|
"southindia": {}, // South India
|
||||||
|
"southeastasia": {}, // Southeast Asia
|
||||||
|
"spaincentral": {}, // Spain Central
|
||||||
|
"swedencentral": {}, // Sweden Central
|
||||||
|
"switzerlandnorth": {}, // Switzerland North
|
||||||
|
"switzerlandwest": {}, // Switzerland West
|
||||||
|
"uaecentral": {}, // UAE Central
|
||||||
|
"uaenorth": {}, // UAE North
|
||||||
|
"uksouth": {}, // UK South
|
||||||
|
"ukwest": {}, // UK West
|
||||||
|
"westcentralus": {}, // West Central US
|
||||||
|
"westeurope": {}, // West Europe
|
||||||
|
"westindia": {}, // West India
|
||||||
|
"westus": {}, // West US
|
||||||
|
"westus2": {}, // West US 2
|
||||||
|
"westus3": {}, // West US 3
|
||||||
|
}
|
||||||
203
pkg/types/cloudintegrationtypes/service.go
Normal file
203
pkg/types/cloudintegrationtypes/service.go
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
package cloudintegrationtypes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/SigNoz/signoz/pkg/types"
|
||||||
|
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
|
||||||
|
"github.com/SigNoz/signoz/pkg/valuer"
|
||||||
|
)
|
||||||
|
|
||||||
|
var S3Sync = valuer.NewString("s3sync")
|
||||||
|
|
||||||
|
type (
|
||||||
|
ServicesSummary struct {
|
||||||
|
Services []*ServiceSummary `json:"services"`
|
||||||
|
}
|
||||||
|
|
||||||
|
ServiceSummary struct {
|
||||||
|
ServiceDefinitionMetadata
|
||||||
|
ServiceConfig *ServiceConfig `json:"serviceConfig"`
|
||||||
|
}
|
||||||
|
|
||||||
|
GettableServiceSummary = ServiceSummary
|
||||||
|
|
||||||
|
GettableServicesSummary = ServicesSummary
|
||||||
|
|
||||||
|
Service struct {
|
||||||
|
ServiceDefinition
|
||||||
|
ServiceConfig *ServiceConfig `json:"serviceConfig"`
|
||||||
|
}
|
||||||
|
|
||||||
|
GettableService = Service
|
||||||
|
|
||||||
|
UpdateServiceConfigRequest struct {
|
||||||
|
ProviderAccountID string `json:"providerAccountId"`
|
||||||
|
ServiceConfig *ServiceConfig `json:"serviceConfig"`
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
type ServiceConfig struct {
|
||||||
|
AWS *AWSServiceConfig `json:"aws,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AWSServiceConfig struct {
|
||||||
|
Logs *AWSServiceLogsConfig `json:"logs"`
|
||||||
|
Metrics *AWSServiceMetricsConfig `json:"metrics"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// AWSServiceLogsConfig is AWS specific logs config for a service
|
||||||
|
// NOTE: the JSON keys are snake case for backward compatibility with existing agents
|
||||||
|
type AWSServiceLogsConfig struct {
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
S3Buckets map[string][]string `json:"s3_buckets,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AWSServiceMetricsConfig struct {
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefinitionMetadata represents service definition metadata. This is useful for showing service overview
|
||||||
|
type ServiceDefinitionMetadata struct {
|
||||||
|
Id string `json:"id"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Icon string `json:"icon"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ServiceDefinition struct {
|
||||||
|
ServiceDefinitionMetadata
|
||||||
|
Overview string `json:"overview"` // markdown
|
||||||
|
Assets Assets `json:"assets"`
|
||||||
|
SupportedSignals SupportedSignals `json:"supported_signals"`
|
||||||
|
DataCollected DataCollected `json:"dataCollected"`
|
||||||
|
Strategy *CollectionStrategy `json:"telemetryCollectionStrategy"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CollectionStrategy is cloud provider specific configuration for signal collection,
|
||||||
|
// this is used by agent to understand the nitty-gritty for collecting telemetry for the cloud provider.
|
||||||
|
type CollectionStrategy struct {
|
||||||
|
AWS *AWSCollectionStrategy `json:"aws,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assets represents the collection of dashboards
|
||||||
|
type Assets struct {
|
||||||
|
Dashboards []Dashboard `json:"dashboards"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SupportedSignals for cloud provider's service
|
||||||
|
type SupportedSignals struct {
|
||||||
|
Logs bool `json:"logs"`
|
||||||
|
Metrics bool `json:"metrics"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DataCollected is curated static list of metrics and logs, this is shown as part of service overview
|
||||||
|
type DataCollected struct {
|
||||||
|
Logs []CollectedLogAttribute `json:"logs"`
|
||||||
|
Metrics []CollectedMetric `json:"metrics"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CollectedLogAttribute represents a log attribute that is present in all log entries for a service,
|
||||||
|
// this is shown as part of service overview
|
||||||
|
type CollectedLogAttribute struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Path string `json:"path"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CollectedMetric represents a metric that is collected for a service, this is shown as part of service overview
|
||||||
|
type CollectedMetric struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Unit string `json:"unit"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// AWSCollectionStrategy represents signal collection strategy for AWS services.
|
||||||
|
// this is AWS specific.
|
||||||
|
// NOTE: this structure is still using snake case, for backward compatibility,
|
||||||
|
// with existing agents
|
||||||
|
type AWSCollectionStrategy struct {
|
||||||
|
Metrics *AWSMetricsStrategy `json:"aws_metrics,omitempty"`
|
||||||
|
Logs *AWSLogsStrategy `json:"aws_logs,omitempty"`
|
||||||
|
S3Buckets map[string][]string `json:"s3_buckets,omitempty"` // Only available in S3 Sync Service Type in AWS
|
||||||
|
}
|
||||||
|
|
||||||
|
// AWSMetricsStrategy represents metrics collection strategy for AWS services.
|
||||||
|
// this is AWS specific.
|
||||||
|
// NOTE: this structure is still using snake case, for backward compatibility,
|
||||||
|
// with existing agents
|
||||||
|
type AWSMetricsStrategy struct {
|
||||||
|
// to be used as https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-cloudwatch-metricstream.html#cfn-cloudwatch-metricstream-includefilters
|
||||||
|
StreamFilters []struct {
|
||||||
|
// json tags here are in the shape expected by AWS API as detailed at
|
||||||
|
// https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-cloudwatch-metricstream-metricstreamfilter.html
|
||||||
|
Namespace string `json:"Namespace"`
|
||||||
|
MetricNames []string `json:"MetricNames,omitempty"`
|
||||||
|
} `json:"cloudwatch_metric_stream_filters"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// AWSLogsStrategy represents logs collection strategy for AWS services.
|
||||||
|
// this is AWS specific.
|
||||||
|
// NOTE: this structure is still using snake case, for backward compatibility,
|
||||||
|
// with existing agents
|
||||||
|
type AWSLogsStrategy struct {
|
||||||
|
Subscriptions []struct {
|
||||||
|
// subscribe to all logs groups with specified prefix.
|
||||||
|
// eg: `/aws/rds/`
|
||||||
|
LogGroupNamePrefix string `json:"log_group_name_prefix"`
|
||||||
|
|
||||||
|
// https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/FilterAndPatternSyntax.html
|
||||||
|
// "" implies no filtering is required.
|
||||||
|
FilterPattern string `json:"filter_pattern"`
|
||||||
|
} `json:"cloudwatch_logs_subscriptions"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dashboard represents a dashboard definition for cloud integration.
|
||||||
|
type Dashboard struct {
|
||||||
|
Id string `json:"id"`
|
||||||
|
Url string `json:"url"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
Image string `json:"image"`
|
||||||
|
Definition *dashboardtypes.StorableDashboardData `json:"definition,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// UTILS
|
||||||
|
|
||||||
|
// GetCloudIntegrationDashboardID returns the dashboard id for a cloud integration, given the cloud provider, service id, and dashboard id.
|
||||||
|
// This is used to generate unique dashboard ids for cloud integration, and also to parse the dashboard id to get the cloud provider and service id when needed.
|
||||||
|
func GetCloudIntegrationDashboardID(cloudProvider CloudProviderType, svcId, dashboardId string) string {
|
||||||
|
return fmt.Sprintf("cloud-integration--%s--%s--%s", cloudProvider, svcId, dashboardId)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDashboardsFromAssets returns the list of dashboards for the cloud provider service from definition
|
||||||
|
func GetDashboardsFromAssets(
|
||||||
|
svcId string,
|
||||||
|
orgID valuer.UUID,
|
||||||
|
cloudProvider CloudProviderType,
|
||||||
|
createdAt *time.Time,
|
||||||
|
assets Assets,
|
||||||
|
) []*dashboardtypes.Dashboard {
|
||||||
|
dashboards := make([]*dashboardtypes.Dashboard, 0)
|
||||||
|
|
||||||
|
for _, d := range assets.Dashboards {
|
||||||
|
author := fmt.Sprintf("%s-integration", cloudProvider)
|
||||||
|
dashboards = append(dashboards, &dashboardtypes.Dashboard{
|
||||||
|
ID: GetCloudIntegrationDashboardID(cloudProvider, svcId, d.Id),
|
||||||
|
Locked: true,
|
||||||
|
OrgID: orgID,
|
||||||
|
Data: *d.Definition,
|
||||||
|
TimeAuditable: types.TimeAuditable{
|
||||||
|
CreatedAt: *createdAt,
|
||||||
|
UpdatedAt: *createdAt,
|
||||||
|
},
|
||||||
|
UserAuditable: types.UserAuditable{
|
||||||
|
CreatedBy: author,
|
||||||
|
UpdatedBy: author,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return dashboards
|
||||||
|
}
|
||||||
35
pkg/types/cloudintegrationtypes/store.go
Normal file
35
pkg/types/cloudintegrationtypes/store.go
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
package cloudintegrationtypes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/SigNoz/signoz/pkg/valuer"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Store interface {
|
||||||
|
// GetAccountByID returns a cloud integration account by id
|
||||||
|
GetAccountByID(ctx context.Context, orgID, id valuer.UUID, provider CloudProviderType) (*StorableCloudIntegration, error)
|
||||||
|
|
||||||
|
// UpsertAccount creates or updates a cloud integration account
|
||||||
|
UpsertAccount(ctx context.Context, account *StorableCloudIntegration) error
|
||||||
|
|
||||||
|
// RemoveAccount marks a cloud integration account as removed by setting the RemovedAt field
|
||||||
|
RemoveAccount(ctx context.Context, orgID, id valuer.UUID, provider CloudProviderType) error
|
||||||
|
|
||||||
|
// GetConnectedAccounts returns all the cloud integration accounts for the org and cloud provider
|
||||||
|
GetConnectedAccounts(ctx context.Context, orgID valuer.UUID, provider CloudProviderType) ([]*StorableCloudIntegration, error)
|
||||||
|
|
||||||
|
// GetConnectedAccount for given provider
|
||||||
|
GetConnectedAccount(ctx context.Context, orgID valuer.UUID, provider CloudProviderType, providerAccountID string) (*StorableCloudIntegration, error)
|
||||||
|
|
||||||
|
// cloud_integration_service related methods
|
||||||
|
|
||||||
|
// GetServiceByType returns the cloud integration service for the given cloud integration id and service type
|
||||||
|
GetServiceByType(ctx context.Context, cloudIntegrationID valuer.UUID, serviceType string) (*StorableCloudIntegrationService, error)
|
||||||
|
|
||||||
|
// UpsertService creates or updates a cloud integration service for the given cloud integration id and service type
|
||||||
|
UpsertService(ctx context.Context, service *StorableCloudIntegrationService) error
|
||||||
|
|
||||||
|
// GetServices returns all the cloud integration services for the given cloud integration id
|
||||||
|
GetServices(ctx context.Context, cloudIntegrationID valuer.UUID) ([]*StorableCloudIntegrationService, error)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user