mirror of
https://github.com/SigNoz/signoz.git
synced 2026-03-17 02:12:12 +00:00
Compare commits
38 Commits
debug_time
...
refactor/c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2d2aa02a81 | ||
|
|
dd9723ad13 | ||
|
|
3651469416 | ||
|
|
febce75734 | ||
|
|
e1616f3487 | ||
|
|
4b94287ac7 | ||
|
|
1575c7c54c | ||
|
|
cab4a56694 | ||
|
|
78041fe457 | ||
|
|
09b6382820 | ||
|
|
9689b847f0 | ||
|
|
15e5938e95 | ||
|
|
8def3f835b | ||
|
|
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 |
15
.github/CODEOWNERS
vendored
15
.github/CODEOWNERS
vendored
@@ -1,8 +1,6 @@
|
|||||||
# CODEOWNERS info: https://help.github.com/en/articles/about-code-owners
|
# CODEOWNERS info: https://help.github.com/en/articles/about-code-owners
|
||||||
|
|
||||||
# Owners are automatically requested for review for PRs that changes code
|
# Owners are automatically requested for review for PRs that changes code that they own.
|
||||||
|
|
||||||
# that they own.
|
|
||||||
|
|
||||||
/frontend/ @SigNoz/frontend-maintainers
|
/frontend/ @SigNoz/frontend-maintainers
|
||||||
|
|
||||||
@@ -11,8 +9,10 @@
|
|||||||
/frontend/src/container/OnboardingV2Container/onboarding-configs/onboarding-config-with-links.json @makeavish
|
/frontend/src/container/OnboardingV2Container/onboarding-configs/onboarding-config-with-links.json @makeavish
|
||||||
/frontend/src/container/OnboardingV2Container/AddDataSource/AddDataSource.tsx @makeavish
|
/frontend/src/container/OnboardingV2Container/AddDataSource/AddDataSource.tsx @makeavish
|
||||||
|
|
||||||
/deploy/ @SigNoz/devops
|
# CI
|
||||||
.github @SigNoz/devops
|
/deploy/ @therealpandey
|
||||||
|
.github @therealpandey
|
||||||
|
go.mod @therealpandey
|
||||||
|
|
||||||
# Scaffold Owners
|
# Scaffold Owners
|
||||||
|
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
60
.github/workflows/mergequeueci.yaml
vendored
Normal file
60
.github/workflows/mergequeueci.yaml
vendored
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
name: mergequeueci
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types:
|
||||||
|
- dequeued
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
notify:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: github.event.pull_request.merged == false
|
||||||
|
steps:
|
||||||
|
- name: alert
|
||||||
|
uses: slackapi/slack-github-action@v2.1.1
|
||||||
|
with:
|
||||||
|
webhook: ${{ secrets.SLACK_MERGE_QUEUE_WEBHOOK }}
|
||||||
|
webhook-type: incoming-webhook
|
||||||
|
payload: |
|
||||||
|
{
|
||||||
|
"text": ":x: PR removed from merge queue",
|
||||||
|
"blocks": [
|
||||||
|
{
|
||||||
|
"type": "header",
|
||||||
|
"text": {
|
||||||
|
"type": "plain_text",
|
||||||
|
"text": ":x: PR Removed from Merge Queue"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "section",
|
||||||
|
"text": {
|
||||||
|
"type": "mrkdwn",
|
||||||
|
"text": "*<${{ github.event.pull_request.html_url }}|PR #${{ github.event.pull_request.number }}: ${{ github.event.pull_request.title }}>*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "divider"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "section",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"type": "mrkdwn",
|
||||||
|
"text": "*Author*\n@${{ github.event.pull_request.user.login }}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
- name: comment
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||||
|
PR_AUTHOR: ${{ github.event.pull_request.user.login }}
|
||||||
|
PR_URL: ${{ github.event.pull_request.html_url }}
|
||||||
|
run: |
|
||||||
|
gh api repos/${{ github.repository }}/issues/$PR_NUMBER/comments \
|
||||||
|
-f body="> :x: **PR removed from merge queue**
|
||||||
|
>
|
||||||
|
> @$PR_AUTHOR your PR was removed from the merge queue. Fix the issue and re-queue when ready."
|
||||||
2021
frontend/public/Images/allInOneLightMode.svg
Normal file
2021
frontend/public/Images/allInOneLightMode.svg
Normal file
File diff suppressed because it is too large
Load Diff
|
After Width: | Height: | Size: 214 KiB |
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
// ** Helpers
|
// ** Helpers
|
||||||
import { MetrictypesTypeDTO } from 'api/generated/services/sigNoz.schemas';
|
import { MetrictypesTypeDTO } from 'api/generated/services/sigNoz.schemas';
|
||||||
|
import { defaultTraceSelectedColumns } from 'container/OptionsMenu/constants';
|
||||||
import { createIdFromObjectFields } from 'lib/createIdFromObjectFields';
|
import { createIdFromObjectFields } from 'lib/createIdFromObjectFields';
|
||||||
import { createNewBuilderItemName } from 'lib/newQueryBuilder/createNewBuilderItemName';
|
import { createNewBuilderItemName } from 'lib/newQueryBuilder/createNewBuilderItemName';
|
||||||
import { IAttributeValuesResponse } from 'types/api/queryBuilder/getAttributesValues';
|
import { IAttributeValuesResponse } from 'types/api/queryBuilder/getAttributesValues';
|
||||||
@@ -548,3 +549,49 @@ export const DATA_TYPE_VS_ATTRIBUTE_VALUES_KEY: Record<
|
|||||||
[DataTypes.ArrayBool]: 'boolAttributeValues',
|
[DataTypes.ArrayBool]: 'boolAttributeValues',
|
||||||
[DataTypes.EMPTY]: 'stringAttributeValues',
|
[DataTypes.EMPTY]: 'stringAttributeValues',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const listViewInitialLogQuery: Query = {
|
||||||
|
...initialQueriesMap.logs,
|
||||||
|
builder: {
|
||||||
|
...initialQueriesMap.logs.builder,
|
||||||
|
queryData: [
|
||||||
|
{
|
||||||
|
...initialQueriesMap.logs.builder.queryData[0],
|
||||||
|
aggregateOperator: LogsAggregatorOperator.NOOP,
|
||||||
|
orderBy: [{ columnName: 'timestamp', order: 'desc' }],
|
||||||
|
offset: 0,
|
||||||
|
pageSize: 100,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PANEL_TYPES_INITIAL_QUERY: Record<PANEL_TYPES, Query> = {
|
||||||
|
[PANEL_TYPES.TIME_SERIES]: initialQueriesMap.metrics,
|
||||||
|
[PANEL_TYPES.VALUE]: initialQueriesMap.metrics,
|
||||||
|
[PANEL_TYPES.TABLE]: initialQueriesMap.metrics,
|
||||||
|
[PANEL_TYPES.LIST]: listViewInitialLogQuery,
|
||||||
|
[PANEL_TYPES.TRACE]: initialQueriesMap.traces,
|
||||||
|
[PANEL_TYPES.BAR]: initialQueriesMap.metrics,
|
||||||
|
[PANEL_TYPES.PIE]: initialQueriesMap.metrics,
|
||||||
|
[PANEL_TYPES.HISTOGRAM]: initialQueriesMap.metrics,
|
||||||
|
[PANEL_TYPES.EMPTY_WIDGET]: initialQueriesMap.metrics,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const listViewInitialTraceQuery: Query = {
|
||||||
|
// it should be the above commented query
|
||||||
|
...initialQueriesMap.traces,
|
||||||
|
builder: {
|
||||||
|
...initialQueriesMap.traces.builder,
|
||||||
|
queryData: [
|
||||||
|
{
|
||||||
|
...initialQueriesMap.traces.builder.queryData[0],
|
||||||
|
aggregateOperator: LogsAggregatorOperator.NOOP,
|
||||||
|
orderBy: [{ columnName: 'timestamp', order: 'desc' }],
|
||||||
|
offset: 0,
|
||||||
|
pageSize: 10,
|
||||||
|
selectColumns: defaultTraceSelectedColumns,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,50 +0,0 @@
|
|||||||
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
|
||||||
import { defaultTraceSelectedColumns } from 'container/OptionsMenu/constants';
|
|
||||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
|
||||||
import { LogsAggregatorOperator } from 'types/common/queryBuilder';
|
|
||||||
|
|
||||||
export const PANEL_TYPES_INITIAL_QUERY = {
|
|
||||||
[PANEL_TYPES.TIME_SERIES]: initialQueriesMap.metrics,
|
|
||||||
[PANEL_TYPES.VALUE]: initialQueriesMap.metrics,
|
|
||||||
[PANEL_TYPES.TABLE]: initialQueriesMap.metrics,
|
|
||||||
[PANEL_TYPES.LIST]: initialQueriesMap.logs,
|
|
||||||
[PANEL_TYPES.TRACE]: initialQueriesMap.traces,
|
|
||||||
[PANEL_TYPES.BAR]: initialQueriesMap.metrics,
|
|
||||||
[PANEL_TYPES.PIE]: initialQueriesMap.metrics,
|
|
||||||
[PANEL_TYPES.HISTOGRAM]: initialQueriesMap.metrics,
|
|
||||||
[PANEL_TYPES.EMPTY_WIDGET]: initialQueriesMap.metrics,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const listViewInitialLogQuery: Query = {
|
|
||||||
...initialQueriesMap.logs,
|
|
||||||
builder: {
|
|
||||||
...initialQueriesMap.logs.builder,
|
|
||||||
queryData: [
|
|
||||||
{
|
|
||||||
...initialQueriesMap.logs.builder.queryData[0],
|
|
||||||
aggregateOperator: LogsAggregatorOperator.NOOP,
|
|
||||||
orderBy: [{ columnName: 'timestamp', order: 'desc' }],
|
|
||||||
offset: 0,
|
|
||||||
pageSize: 100,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const listViewInitialTraceQuery: Query = {
|
|
||||||
// it should be the above commented query
|
|
||||||
...initialQueriesMap.traces,
|
|
||||||
builder: {
|
|
||||||
...initialQueriesMap.traces.builder,
|
|
||||||
queryData: [
|
|
||||||
{
|
|
||||||
...initialQueriesMap.traces.builder.queryData[0],
|
|
||||||
aggregateOperator: LogsAggregatorOperator.NOOP,
|
|
||||||
orderBy: [{ columnName: 'timestamp', order: 'desc' }],
|
|
||||||
offset: 0,
|
|
||||||
pageSize: 10,
|
|
||||||
selectColumns: defaultTraceSelectedColumns,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
import { Card, Modal } from 'antd';
|
|
||||||
import logEvent from 'api/common/logEvent';
|
|
||||||
import { QueryParams } from 'constants/query';
|
|
||||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
|
||||||
import createQueryParams from 'lib/createQueryParams';
|
|
||||||
import history from 'lib/history';
|
|
||||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
|
||||||
import { LogsAggregatorOperator } from 'types/common/queryBuilder';
|
|
||||||
import { v4 as uuid } from 'uuid';
|
|
||||||
|
|
||||||
import { PANEL_TYPES_INITIAL_QUERY } from './constants';
|
|
||||||
import menuItems from './menuItems';
|
|
||||||
import { Text } from './styles';
|
|
||||||
|
|
||||||
import './ComponentSlider.styles.scss';
|
|
||||||
|
|
||||||
function DashboardGraphSlider(): JSX.Element {
|
|
||||||
const { handleToggleDashboardSlider, isDashboardSliderOpen } = useDashboard();
|
|
||||||
|
|
||||||
const onClickHandler = (name: PANEL_TYPES) => (): void => {
|
|
||||||
const id = uuid();
|
|
||||||
handleToggleDashboardSlider(false);
|
|
||||||
logEvent('Dashboard Detail: New panel type selected', {
|
|
||||||
// dashboardId: '',
|
|
||||||
// dashboardName: '',
|
|
||||||
// numberOfPanels: 0, // todo - at this point we don't know these attributes
|
|
||||||
panelType: name,
|
|
||||||
widgetId: id,
|
|
||||||
});
|
|
||||||
const queryParamsLog = {
|
|
||||||
graphType: name,
|
|
||||||
widgetId: id,
|
|
||||||
[QueryParams.compositeQuery]: JSON.stringify({
|
|
||||||
...PANEL_TYPES_INITIAL_QUERY[name],
|
|
||||||
builder: {
|
|
||||||
...PANEL_TYPES_INITIAL_QUERY[name].builder,
|
|
||||||
queryData: [
|
|
||||||
{
|
|
||||||
...PANEL_TYPES_INITIAL_QUERY[name].builder.queryData[0],
|
|
||||||
aggregateOperator: LogsAggregatorOperator.NOOP,
|
|
||||||
orderBy: [{ columnName: 'timestamp', order: 'desc' }],
|
|
||||||
offset: 0,
|
|
||||||
pageSize: 100,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
const queryParams = {
|
|
||||||
graphType: name,
|
|
||||||
widgetId: id,
|
|
||||||
[QueryParams.compositeQuery]: JSON.stringify(
|
|
||||||
PANEL_TYPES_INITIAL_QUERY[name],
|
|
||||||
),
|
|
||||||
};
|
|
||||||
if (name === PANEL_TYPES.LIST) {
|
|
||||||
history.push(
|
|
||||||
`${history.location.pathname}/new?${createQueryParams(queryParamsLog)}`,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
history.push(
|
|
||||||
`${history.location.pathname}/new?${createQueryParams(queryParams)}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCardClick = (panelType: PANEL_TYPES): void => {
|
|
||||||
onClickHandler(panelType)();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
open={isDashboardSliderOpen}
|
|
||||||
onCancel={(): void => {
|
|
||||||
handleToggleDashboardSlider(false);
|
|
||||||
}}
|
|
||||||
rootClassName="graph-selection"
|
|
||||||
footer={null}
|
|
||||||
title="New Panel"
|
|
||||||
>
|
|
||||||
<div className="panel-selection">
|
|
||||||
{menuItems.map(({ name, icon, display }) => (
|
|
||||||
<Card onClick={(): void => handleCardClick(name)} id={name} key={name}>
|
|
||||||
{icon}
|
|
||||||
<Text>{display}</Text>
|
|
||||||
</Card>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default DashboardGraphSlider;
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
import { Card as CardComponent, Typography } from 'antd';
|
|
||||||
import styled from 'styled-components';
|
|
||||||
|
|
||||||
export const Container = styled.div`
|
|
||||||
display: flex;
|
|
||||||
justify-content: right;
|
|
||||||
gap: 8px;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const Card = styled(CardComponent)`
|
|
||||||
min-height: 80px;
|
|
||||||
min-width: 120px;
|
|
||||||
overflow-y: auto;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: transform 0.2s;
|
|
||||||
|
|
||||||
.ant-card-body {
|
|
||||||
padding: 12px;
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
.ant-typography {
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
transform: scale(1.05);
|
|
||||||
border: 1px solid var(--bg-robin-400);
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const Text = styled(Typography)`
|
|
||||||
text-align: center;
|
|
||||||
margin-top: 1rem;
|
|
||||||
`;
|
|
||||||
@@ -182,9 +182,7 @@ describe('Dashboard landing page actions header tests', () => {
|
|||||||
(useLocation as jest.Mock).mockReturnValue(mockLocation);
|
(useLocation as jest.Mock).mockReturnValue(mockLocation);
|
||||||
|
|
||||||
const mockContextValue: IDashboardContext = {
|
const mockContextValue: IDashboardContext = {
|
||||||
isDashboardSliderOpen: false,
|
|
||||||
isDashboardLocked: false,
|
isDashboardLocked: false,
|
||||||
handleToggleDashboardSlider: jest.fn(),
|
|
||||||
handleDashboardLockToggle: jest.fn(),
|
handleDashboardLockToggle: jest.fn(),
|
||||||
dashboardResponse: {} as IDashboardContext['dashboardResponse'],
|
dashboardResponse: {} as IDashboardContext['dashboardResponse'],
|
||||||
selectedDashboard: (getDashboardById.data as unknown) as Dashboard,
|
selectedDashboard: (getDashboardById.data as unknown) as Dashboard,
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ import {
|
|||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { useAppContext } from 'providers/App/App';
|
import { useAppContext } from 'providers/App/App';
|
||||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||||
|
import { usePanelTypeSelectionModalStore } from 'providers/Dashboard/helpers/panelTypeSelectionModalHelper';
|
||||||
import { sortLayout } from 'providers/Dashboard/util';
|
import { sortLayout } from 'providers/Dashboard/util';
|
||||||
import { DashboardData } from 'types/api/dashboard/getAll';
|
import { DashboardData } from 'types/api/dashboard/getAll';
|
||||||
import { Props } from 'types/api/dashboard/update';
|
import { Props } from 'types/api/dashboard/update';
|
||||||
@@ -48,10 +49,10 @@ import { ComponentTypes } from 'utils/permission';
|
|||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
|
|
||||||
import DashboardHeader from '../components/DashboardHeader/DashboardHeader';
|
import DashboardHeader from '../components/DashboardHeader/DashboardHeader';
|
||||||
import DashboardGraphSlider from '../ComponentsSlider';
|
|
||||||
import DashboardSettings from '../DashboardSettings';
|
import DashboardSettings from '../DashboardSettings';
|
||||||
import { Base64Icons } from '../DashboardSettings/General/utils';
|
import { Base64Icons } from '../DashboardSettings/General/utils';
|
||||||
import DashboardVariableSelection from '../DashboardVariablesSelection';
|
import DashboardVariableSelection from '../DashboardVariablesSelection';
|
||||||
|
import PanelTypeSelectionModal from '../PanelTypeSelectionModal';
|
||||||
import SettingsDrawer from './SettingsDrawer';
|
import SettingsDrawer from './SettingsDrawer';
|
||||||
import { VariablesSettingsTab } from './types';
|
import { VariablesSettingsTab } from './types';
|
||||||
import {
|
import {
|
||||||
@@ -69,6 +70,9 @@ interface DashboardDescriptionProps {
|
|||||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||||
function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
|
function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
|
||||||
const { handle } = props;
|
const { handle } = props;
|
||||||
|
const setIsPanelTypeSelectionModalOpen = usePanelTypeSelectionModalStore(
|
||||||
|
(s) => s.setIsPanelTypeSelectionModalOpen,
|
||||||
|
);
|
||||||
const {
|
const {
|
||||||
selectedDashboard,
|
selectedDashboard,
|
||||||
panelMap,
|
panelMap,
|
||||||
@@ -77,7 +81,6 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
|
|||||||
setLayouts,
|
setLayouts,
|
||||||
isDashboardLocked,
|
isDashboardLocked,
|
||||||
setSelectedDashboard,
|
setSelectedDashboard,
|
||||||
handleToggleDashboardSlider,
|
|
||||||
handleDashboardLockToggle,
|
handleDashboardLockToggle,
|
||||||
} = useDashboard();
|
} = useDashboard();
|
||||||
|
|
||||||
@@ -145,14 +148,14 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
|
|||||||
const [addPanelPermission] = useComponentPermission(permissions, userRole);
|
const [addPanelPermission] = useComponentPermission(permissions, userRole);
|
||||||
|
|
||||||
const onEmptyWidgetHandler = useCallback(() => {
|
const onEmptyWidgetHandler = useCallback(() => {
|
||||||
handleToggleDashboardSlider(true);
|
setIsPanelTypeSelectionModalOpen(true);
|
||||||
logEvent('Dashboard Detail: Add new panel clicked', {
|
logEvent('Dashboard Detail: Add new panel clicked', {
|
||||||
dashboardId: selectedDashboard?.id,
|
dashboardId: selectedDashboard?.id,
|
||||||
dashboardName: selectedDashboard?.data.title,
|
dashboardName: selectedDashboard?.data.title,
|
||||||
numberOfPanels: selectedDashboard?.data.widgets?.length,
|
numberOfPanels: selectedDashboard?.data.widgets?.length,
|
||||||
});
|
});
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [handleToggleDashboardSlider]);
|
}, [setIsPanelTypeSelectionModalOpen]);
|
||||||
|
|
||||||
const handleLockDashboardToggle = (): void => {
|
const handleLockDashboardToggle = (): void => {
|
||||||
setIsDashbordSettingsOpen(false);
|
setIsDashbordSettingsOpen(false);
|
||||||
@@ -521,7 +524,7 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
|
|||||||
<DashboardVariableSelection />
|
<DashboardVariableSelection />
|
||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
<DashboardGraphSlider />
|
<PanelTypeSelectionModal />
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
open={isRenameDashboardOpen}
|
open={isRenameDashboardOpen}
|
||||||
|
|||||||
@@ -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,4 +1,4 @@
|
|||||||
.graph-selection {
|
.panel-type-selection-modal {
|
||||||
.ant-modal-content {
|
.ant-modal-content {
|
||||||
width: 515px;
|
width: 515px;
|
||||||
max-height: 646px;
|
max-height: 646px;
|
||||||
@@ -76,6 +76,11 @@
|
|||||||
content: none;
|
content: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.panel-type-text {
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,7 +119,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.lightMode {
|
.lightMode {
|
||||||
.graph-selection {
|
.panel-type-selection-modal {
|
||||||
.ant-modal-content {
|
.ant-modal-content {
|
||||||
border: 1px solid var(--bg-vanilla-300);
|
border: 1px solid var(--bg-vanilla-300);
|
||||||
background: var(--bg-vanilla-100);
|
background: var(--bg-vanilla-100);
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import { memo } from 'react';
|
||||||
|
import { Card, Modal, Typography } from 'antd';
|
||||||
|
import logEvent from 'api/common/logEvent';
|
||||||
|
import { QueryParams } from 'constants/query';
|
||||||
|
import { PANEL_TYPES, PANEL_TYPES_INITIAL_QUERY } from 'constants/queryBuilder';
|
||||||
|
import createQueryParams from 'lib/createQueryParams';
|
||||||
|
import history from 'lib/history';
|
||||||
|
import { usePanelTypeSelectionModalStore } from 'providers/Dashboard/helpers/panelTypeSelectionModalHelper';
|
||||||
|
import { v4 as uuid } from 'uuid';
|
||||||
|
|
||||||
|
import { PanelTypesWithData } from './menuItems';
|
||||||
|
|
||||||
|
import './PanelTypeSelectionModal.styles.scss';
|
||||||
|
|
||||||
|
function PanelTypeSelectionModal(): JSX.Element {
|
||||||
|
const {
|
||||||
|
isPanelTypeSelectionModalOpen,
|
||||||
|
setIsPanelTypeSelectionModalOpen,
|
||||||
|
} = usePanelTypeSelectionModalStore();
|
||||||
|
|
||||||
|
const onClickHandler = (name: PANEL_TYPES) => (): void => {
|
||||||
|
const id = uuid();
|
||||||
|
setIsPanelTypeSelectionModalOpen(false);
|
||||||
|
logEvent('Dashboard Detail: New panel type selected', {
|
||||||
|
panelType: name,
|
||||||
|
widgetId: id,
|
||||||
|
});
|
||||||
|
|
||||||
|
const queryParams = {
|
||||||
|
graphType: name,
|
||||||
|
widgetId: id,
|
||||||
|
[QueryParams.compositeQuery]: JSON.stringify(
|
||||||
|
PANEL_TYPES_INITIAL_QUERY[name],
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
history.push(
|
||||||
|
`${history.location.pathname}/new?${createQueryParams(queryParams)}`,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCardClick = (panelType: PANEL_TYPES): void => {
|
||||||
|
onClickHandler(panelType)();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
open={isPanelTypeSelectionModalOpen}
|
||||||
|
onCancel={(): void => {
|
||||||
|
setIsPanelTypeSelectionModalOpen(false);
|
||||||
|
}}
|
||||||
|
rootClassName="panel-type-selection-modal"
|
||||||
|
footer={null}
|
||||||
|
title="New Panel"
|
||||||
|
>
|
||||||
|
<div className="panel-selection">
|
||||||
|
{PanelTypesWithData.map(({ name, icon, display }) => (
|
||||||
|
<Card onClick={(): void => handleCardClick(name)} id={name} key={name}>
|
||||||
|
{icon}
|
||||||
|
<Typography className="panel-type-text">{display}</Typography>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(PanelTypeSelectionModal);
|
||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
Table,
|
Table,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
||||||
const Items: ItemsProps[] = [
|
export const PanelTypesWithData: ItemsProps[] = [
|
||||||
{
|
{
|
||||||
name: PANEL_TYPES.TIME_SERIES,
|
name: PANEL_TYPES.TIME_SERIES,
|
||||||
icon: <LineChart size={16} color={Color.BG_ROBIN_400} />,
|
icon: <LineChart size={16} color={Color.BG_ROBIN_400} />,
|
||||||
@@ -52,5 +52,3 @@ export interface ItemsProps {
|
|||||||
icon: JSX.Element;
|
icon: JSX.Element;
|
||||||
display: string;
|
display: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Items;
|
|
||||||
@@ -9,17 +9,18 @@ import DashboardSettings from 'container/DashboardContainer/DashboardSettings';
|
|||||||
import useComponentPermission from 'hooks/useComponentPermission';
|
import useComponentPermission from 'hooks/useComponentPermission';
|
||||||
import { useAppContext } from 'providers/App/App';
|
import { useAppContext } from 'providers/App/App';
|
||||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||||
|
import { usePanelTypeSelectionModalStore } from 'providers/Dashboard/helpers/panelTypeSelectionModalHelper';
|
||||||
import { ROLES, USER_ROLES } from 'types/roles';
|
import { ROLES, USER_ROLES } from 'types/roles';
|
||||||
import { ComponentTypes } from 'utils/permission';
|
import { ComponentTypes } from 'utils/permission';
|
||||||
|
|
||||||
import './DashboardEmptyState.styles.scss';
|
import './DashboardEmptyState.styles.scss';
|
||||||
|
|
||||||
export default function DashboardEmptyState(): JSX.Element {
|
export default function DashboardEmptyState(): JSX.Element {
|
||||||
const {
|
const setIsPanelTypeSelectionModalOpen = usePanelTypeSelectionModalStore(
|
||||||
selectedDashboard,
|
(s) => s.setIsPanelTypeSelectionModalOpen,
|
||||||
isDashboardLocked,
|
);
|
||||||
handleToggleDashboardSlider,
|
|
||||||
} = useDashboard();
|
const { selectedDashboard, isDashboardLocked } = useDashboard();
|
||||||
|
|
||||||
const variablesSettingsTabHandle = useRef<VariablesSettingsTab>(null);
|
const variablesSettingsTabHandle = useRef<VariablesSettingsTab>(null);
|
||||||
const [isSettingsDrawerOpen, setIsSettingsDrawerOpen] = useState<boolean>(
|
const [isSettingsDrawerOpen, setIsSettingsDrawerOpen] = useState<boolean>(
|
||||||
@@ -41,14 +42,14 @@ export default function DashboardEmptyState(): JSX.Element {
|
|||||||
const [addPanelPermission] = useComponentPermission(permissions, userRole);
|
const [addPanelPermission] = useComponentPermission(permissions, userRole);
|
||||||
|
|
||||||
const onEmptyWidgetHandler = useCallback(() => {
|
const onEmptyWidgetHandler = useCallback(() => {
|
||||||
handleToggleDashboardSlider(true);
|
setIsPanelTypeSelectionModalOpen(true);
|
||||||
logEvent('Dashboard Detail: Add new panel clicked', {
|
logEvent('Dashboard Detail: Add new panel clicked', {
|
||||||
dashboardId: selectedDashboard?.id,
|
dashboardId: selectedDashboard?.id,
|
||||||
dashboardName: selectedDashboard?.data.title,
|
dashboardName: selectedDashboard?.data.title,
|
||||||
numberOfPanels: selectedDashboard?.data.widgets?.length,
|
numberOfPanels: selectedDashboard?.data.widgets?.length,
|
||||||
});
|
});
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [handleToggleDashboardSlider]);
|
}, [setIsPanelTypeSelectionModalOpen]);
|
||||||
|
|
||||||
const onConfigureClick = useCallback((): void => {
|
const onConfigureClick = useCallback((): void => {
|
||||||
setIsSettingsDrawerOpen(true);
|
setIsSettingsDrawerOpen(true);
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { useCallback } from 'react';
|
|||||||
import { Select, Typography } from 'antd';
|
import { Select, Typography } from 'antd';
|
||||||
import { QueryParams } from 'constants/query';
|
import { QueryParams } from 'constants/query';
|
||||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
import GraphTypes from 'container/DashboardContainer/ComponentsSlider/menuItems';
|
import { PanelTypesWithData } from 'container/DashboardContainer/PanelTypeSelectionModal/menuItems';
|
||||||
import { handleQueryChange } from 'container/NewWidget/utils';
|
import { handleQueryChange } from 'container/NewWidget/utils';
|
||||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
@@ -59,7 +59,7 @@ function PanelTypeSelector({
|
|||||||
data-testid="panel-change-select"
|
data-testid="panel-change-select"
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
{GraphTypes.map((item) => (
|
{PanelTypesWithData.map((item) => (
|
||||||
<Option key={item.name} value={item.name}>
|
<Option key={item.name} value={item.name}>
|
||||||
<div className="view-panel-select-option">
|
<div className="view-panel-select-option">
|
||||||
<div className="icon">{item.icon}</div>
|
<div className="icon">{item.icon}</div>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import useComponentPermission from 'hooks/useComponentPermission';
|
|||||||
import { EllipsisIcon, PenLine, Plus, X } from 'lucide-react';
|
import { EllipsisIcon, PenLine, Plus, X } from 'lucide-react';
|
||||||
import { useAppContext } from 'providers/App/App';
|
import { useAppContext } from 'providers/App/App';
|
||||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||||
|
import { usePanelTypeSelectionModalStore } from 'providers/Dashboard/helpers/panelTypeSelectionModalHelper';
|
||||||
import { setSelectedRowWidgetId } from 'providers/Dashboard/helpers/selectedRowWidgetIdHelper';
|
import { setSelectedRowWidgetId } from 'providers/Dashboard/helpers/selectedRowWidgetIdHelper';
|
||||||
import { ROLES, USER_ROLES } from 'types/roles';
|
import { ROLES, USER_ROLES } from 'types/roles';
|
||||||
import { ComponentTypes } from 'utils/permission';
|
import { ComponentTypes } from 'utils/permission';
|
||||||
@@ -34,11 +35,11 @@ export function WidgetRowHeader(props: WidgetRowHeaderProps): JSX.Element {
|
|||||||
} = props;
|
} = props;
|
||||||
const [isRowSettingsOpen, setIsRowSettingsOpen] = useState<boolean>(false);
|
const [isRowSettingsOpen, setIsRowSettingsOpen] = useState<boolean>(false);
|
||||||
|
|
||||||
const {
|
const setIsPanelTypeSelectionModalOpen = usePanelTypeSelectionModalStore(
|
||||||
handleToggleDashboardSlider,
|
(s) => s.setIsPanelTypeSelectionModalOpen,
|
||||||
selectedDashboard,
|
);
|
||||||
isDashboardLocked,
|
|
||||||
} = useDashboard();
|
const { selectedDashboard, isDashboardLocked } = useDashboard();
|
||||||
|
|
||||||
const permissions: ComponentTypes[] = ['add_panel'];
|
const permissions: ComponentTypes[] = ['add_panel'];
|
||||||
const { user } = useAppContext();
|
const { user } = useAppContext();
|
||||||
@@ -87,7 +88,7 @@ export function WidgetRowHeader(props: WidgetRowHeaderProps): JSX.Element {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setSelectedRowWidgetId(selectedDashboard.id, id);
|
setSelectedRowWidgetId(selectedDashboard.id, id);
|
||||||
handleToggleDashboardSlider(true);
|
setIsPanelTypeSelectionModalOpen(true);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
New Panel
|
New Panel
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import ROUTES from 'constants/routes';
|
|||||||
import { getMetricsListQuery } from 'container/MetricsExplorer/Summary/utils';
|
import { getMetricsListQuery } from 'container/MetricsExplorer/Summary/utils';
|
||||||
import { useGetMetricsList } from 'hooks/metricsExplorer/useGetMetricsList';
|
import { useGetMetricsList } from 'hooks/metricsExplorer/useGetMetricsList';
|
||||||
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
|
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
|
||||||
|
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||||
import history from 'lib/history';
|
import history from 'lib/history';
|
||||||
import cloneDeep from 'lodash-es/cloneDeep';
|
import cloneDeep from 'lodash-es/cloneDeep';
|
||||||
import { AnimatePresence } from 'motion/react';
|
import { AnimatePresence } from 'motion/react';
|
||||||
@@ -43,6 +44,7 @@ const homeInterval = 30 * 60 * 1000;
|
|||||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||||
export default function Home(): JSX.Element {
|
export default function Home(): JSX.Element {
|
||||||
const { user } = useAppContext();
|
const { user } = useAppContext();
|
||||||
|
const isDarkMode = useIsDarkMode();
|
||||||
|
|
||||||
const [startTime, setStartTime] = useState<number | null>(null);
|
const [startTime, setStartTime] = useState<number | null>(null);
|
||||||
const [endTime, setEndTime] = useState<number | null>(null);
|
const [endTime, setEndTime] = useState<number | null>(null);
|
||||||
@@ -680,7 +682,11 @@ export default function Home(): JSX.Element {
|
|||||||
|
|
||||||
<div className="checklist-img-container">
|
<div className="checklist-img-container">
|
||||||
<img
|
<img
|
||||||
src="/Images/allInOne.svg"
|
src={
|
||||||
|
isDarkMode
|
||||||
|
? '/Images/allInOne.svg'
|
||||||
|
: '/Images/allInOneLightMode.svg'
|
||||||
|
}
|
||||||
alt="checklist-img"
|
alt="checklist-img"
|
||||||
className="checklist-img"
|
className="checklist-img"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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,23 +14,30 @@ import {
|
|||||||
Input,
|
Input,
|
||||||
InputNumber,
|
InputNumber,
|
||||||
Select,
|
Select,
|
||||||
Space,
|
|
||||||
Switch,
|
Switch,
|
||||||
Typography,
|
Typography,
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
import { PrecisionOption, PrecisionOptionsEnum } from 'components/Graph/types';
|
import { PrecisionOption, PrecisionOptionsEnum } from 'components/Graph/types';
|
||||||
import TimePreference from 'components/TimePreferenceDropDown';
|
import TimePreference from 'components/TimePreferenceDropDown';
|
||||||
import { PANEL_TYPES, PanelDisplay } from 'constants/queryBuilder';
|
import { PANEL_TYPES, PanelDisplay } from 'constants/queryBuilder';
|
||||||
import GraphTypes, {
|
import {
|
||||||
ItemsProps,
|
ItemsProps,
|
||||||
} from 'container/DashboardContainer/ComponentsSlider/menuItems';
|
PanelTypesWithData,
|
||||||
|
} from 'container/DashboardContainer/PanelTypeSelectionModal/menuItems';
|
||||||
import { useDashboardVariables } from 'hooks/dashboard/useDashboardVariables';
|
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 +53,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,
|
||||||
@@ -143,7 +151,7 @@ function RightContainer({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const selectedGraphType =
|
const selectedGraphType =
|
||||||
GraphTypes.find((e) => e.name === selectedGraph)?.display || '';
|
PanelTypesWithData.find((e) => e.name === selectedGraph)?.display || '';
|
||||||
|
|
||||||
const onCreateAlertsHandler = useCreateAlerts(selectedWidget, 'panelView');
|
const onCreateAlertsHandler = useCreateAlerts(selectedWidget, 'panelView');
|
||||||
|
|
||||||
@@ -169,7 +177,7 @@ function RightContainer({
|
|||||||
|
|
||||||
const { currentQuery } = useQueryBuilder();
|
const { currentQuery } = useQueryBuilder();
|
||||||
|
|
||||||
const [graphTypes, setGraphTypes] = useState<ItemsProps[]>(GraphTypes);
|
const [graphTypes, setGraphTypes] = useState<ItemsProps[]>(PanelTypesWithData);
|
||||||
|
|
||||||
const dashboardVariableOptions = useMemo<VariableOption[]>(() => {
|
const dashboardVariableOptions = useMemo<VariableOption[]>(() => {
|
||||||
return Object.entries(dashboardVariables).map(([, value]) => ({
|
return Object.entries(dashboardVariables).map(([, value]) => ({
|
||||||
@@ -178,6 +186,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 +216,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);
|
||||||
@@ -241,7 +273,7 @@ function RightContainer({
|
|||||||
prev.filter((graph) => graph.name !== PANEL_TYPES.LIST),
|
prev.filter((graph) => graph.name !== PANEL_TYPES.LIST),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
setGraphTypes(GraphTypes);
|
setGraphTypes(PanelTypesWithData);
|
||||||
}
|
}
|
||||||
}, [currentQuery]);
|
}, [currentQuery]);
|
||||||
|
|
||||||
@@ -263,9 +295,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 +332,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 +358,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 +371,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 +423,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 +438,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 +473,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 +585,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 +601,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 +627,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;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|||||||
@@ -11,11 +11,8 @@ import { getYAxisCategories } from 'components/YAxisUnitSelector/utils';
|
|||||||
import {
|
import {
|
||||||
initialQueryBuilderFormValuesMap,
|
initialQueryBuilderFormValuesMap,
|
||||||
PANEL_TYPES,
|
PANEL_TYPES,
|
||||||
} from 'constants/queryBuilder';
|
|
||||||
import {
|
|
||||||
listViewInitialLogQuery,
|
|
||||||
PANEL_TYPES_INITIAL_QUERY,
|
PANEL_TYPES_INITIAL_QUERY,
|
||||||
} from 'container/DashboardContainer/ComponentsSlider/constants';
|
} from 'constants/queryBuilder';
|
||||||
import {
|
import {
|
||||||
defaultLogsSelectedColumns,
|
defaultLogsSelectedColumns,
|
||||||
defaultTraceSelectedColumns,
|
defaultTraceSelectedColumns,
|
||||||
@@ -549,10 +546,7 @@ export const getDefaultWidgetData = (
|
|||||||
nullZeroValues: '',
|
nullZeroValues: '',
|
||||||
opacity: '',
|
opacity: '',
|
||||||
panelTypes: name,
|
panelTypes: name,
|
||||||
query:
|
query: PANEL_TYPES_INITIAL_QUERY[name],
|
||||||
name === PANEL_TYPES.LIST
|
|
||||||
? listViewInitialLogQuery
|
|
||||||
: PANEL_TYPES_INITIAL_QUERY[name],
|
|
||||||
timePreferance: 'GLOBAL_TIME',
|
timePreferance: 'GLOBAL_TIME',
|
||||||
softMax: null,
|
softMax: null,
|
||||||
softMin: null,
|
softMin: null,
|
||||||
|
|||||||
@@ -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: (
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ import {
|
|||||||
ATTRIBUTE_TYPES,
|
ATTRIBUTE_TYPES,
|
||||||
initialAutocompleteData,
|
initialAutocompleteData,
|
||||||
initialQueryBuilderFormValuesMap,
|
initialQueryBuilderFormValuesMap,
|
||||||
|
listViewInitialLogQuery,
|
||||||
|
listViewInitialTraceQuery,
|
||||||
mapOfFormulaToFilters,
|
mapOfFormulaToFilters,
|
||||||
mapOfQueryFilters,
|
mapOfQueryFilters,
|
||||||
PANEL_TYPES,
|
PANEL_TYPES,
|
||||||
@@ -23,10 +25,6 @@ import {
|
|||||||
metricsUnknownSpaceAggregateOperatorOptions,
|
metricsUnknownSpaceAggregateOperatorOptions,
|
||||||
metricsUnknownTimeAggregateOperatorOptions,
|
metricsUnknownTimeAggregateOperatorOptions,
|
||||||
} from 'constants/queryBuilderOperators';
|
} from 'constants/queryBuilderOperators';
|
||||||
import {
|
|
||||||
listViewInitialLogQuery,
|
|
||||||
listViewInitialTraceQuery,
|
|
||||||
} from 'container/DashboardContainer/ComponentsSlider/constants';
|
|
||||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||||
import { getMetricsOperatorsByAttributeType } from 'lib/newQueryBuilder/getMetricsOperatorsByAttributeType';
|
import { getMetricsOperatorsByAttributeType } from 'lib/newQueryBuilder/getMetricsOperatorsByAttributeType';
|
||||||
import { getOperatorsBySourceAndPanelType } from 'lib/newQueryBuilder/getOperatorsBySourceAndPanelType';
|
import { getOperatorsBySourceAndPanelType } from 'lib/newQueryBuilder/getOperatorsBySourceAndPanelType';
|
||||||
|
|||||||
@@ -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,
|
||||||
@@ -57,9 +53,7 @@ import { IDashboardContext, WidgetColumnWidths } from './types';
|
|||||||
import { sortLayout } from './util';
|
import { sortLayout } from './util';
|
||||||
|
|
||||||
export const DashboardContext = createContext<IDashboardContext>({
|
export const DashboardContext = createContext<IDashboardContext>({
|
||||||
isDashboardSliderOpen: false,
|
|
||||||
isDashboardLocked: false,
|
isDashboardLocked: false,
|
||||||
handleToggleDashboardSlider: () => {},
|
|
||||||
handleDashboardLockToggle: () => {},
|
handleDashboardLockToggle: () => {},
|
||||||
dashboardResponse: {} as UseQueryResult<
|
dashboardResponse: {} as UseQueryResult<
|
||||||
SuccessResponseV2<Dashboard>,
|
SuccessResponseV2<Dashboard>,
|
||||||
@@ -86,8 +80,6 @@ export function DashboardProvider({
|
|||||||
children,
|
children,
|
||||||
dashboardId,
|
dashboardId,
|
||||||
}: PropsWithChildren<{ dashboardId: string }>): JSX.Element {
|
}: PropsWithChildren<{ dashboardId: string }>): JSX.Element {
|
||||||
const [isDashboardSliderOpen, setIsDashboardSlider] = useState<boolean>(false);
|
|
||||||
|
|
||||||
const [isDashboardLocked, setIsDashboardLocked] = useState<boolean>(false);
|
const [isDashboardLocked, setIsDashboardLocked] = useState<boolean>(false);
|
||||||
|
|
||||||
const [
|
const [
|
||||||
@@ -137,9 +129,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 +144,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 +174,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);
|
||||||
@@ -383,13 +284,8 @@ export function DashboardProvider({
|
|||||||
}
|
}
|
||||||
}, [isVisible]);
|
}, [isVisible]);
|
||||||
|
|
||||||
const handleToggleDashboardSlider = (value: boolean): void => {
|
|
||||||
setIsDashboardSlider(value);
|
|
||||||
};
|
|
||||||
|
|
||||||
const { mutate: lockDashboard } = useMutation(locked, {
|
const { mutate: lockDashboard } = useMutation(locked, {
|
||||||
onSuccess: (_, props) => {
|
onSuccess: (_, props) => {
|
||||||
setIsDashboardSlider(false);
|
|
||||||
setIsDashboardLocked(props.lock);
|
setIsDashboardLocked(props.lock);
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
@@ -414,9 +310,7 @@ export function DashboardProvider({
|
|||||||
|
|
||||||
const value: IDashboardContext = useMemo(
|
const value: IDashboardContext = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
isDashboardSliderOpen,
|
|
||||||
isDashboardLocked,
|
isDashboardLocked,
|
||||||
handleToggleDashboardSlider,
|
|
||||||
handleDashboardLockToggle,
|
handleDashboardLockToggle,
|
||||||
dashboardResponse,
|
dashboardResponse,
|
||||||
selectedDashboard,
|
selectedDashboard,
|
||||||
@@ -436,7 +330,6 @@ export function DashboardProvider({
|
|||||||
}),
|
}),
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
[
|
[
|
||||||
isDashboardSliderOpen,
|
|
||||||
isDashboardLocked,
|
isDashboardLocked,
|
||||||
dashboardResponse,
|
dashboardResponse,
|
||||||
selectedDashboard,
|
selectedDashboard,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { create } from 'zustand';
|
||||||
|
|
||||||
|
interface IPanelTypeSelectionModalState {
|
||||||
|
isPanelTypeSelectionModalOpen: boolean;
|
||||||
|
setIsPanelTypeSelectionModalOpen: (isOpen: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This helper is used for selecting the panel type when creating a new panel in the dashboard.
|
||||||
|
* It uses Zustand for state management to keep track of whether the panel type selection modal is open or closed.
|
||||||
|
*/
|
||||||
|
export const usePanelTypeSelectionModalStore = create<IPanelTypeSelectionModalState>(
|
||||||
|
(set) => ({
|
||||||
|
isPanelTypeSelectionModalOpen: false,
|
||||||
|
setIsPanelTypeSelectionModalOpen: (isOpen): void =>
|
||||||
|
set({ isPanelTypeSelectionModalOpen: isOpen }),
|
||||||
|
}),
|
||||||
|
);
|
||||||
@@ -9,9 +9,7 @@ export type WidgetColumnWidths = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export interface IDashboardContext {
|
export interface IDashboardContext {
|
||||||
isDashboardSliderOpen: boolean;
|
|
||||||
isDashboardLocked: boolean;
|
isDashboardLocked: boolean;
|
||||||
handleToggleDashboardSlider: (value: boolean) => void;
|
|
||||||
handleDashboardLockToggle: (value: boolean) => void;
|
handleDashboardLockToggle: (value: boolean) => void;
|
||||||
dashboardResponse: UseQueryResult<SuccessResponseV2<Dashboard>, unknown>;
|
dashboardResponse: UseQueryResult<SuccessResponseV2<Dashboard>, unknown>;
|
||||||
selectedDashboard: Dashboard | undefined;
|
selectedDashboard: Dashboard | undefined;
|
||||||
|
|||||||
82
pkg/modules/cloudintegration/cloudintegration.go
Normal file
82
pkg/modules/cloudintegration/cloudintegration.go
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
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)
|
||||||
|
|
||||||
|
// ListServicesMetadata returns list of services metadata for a cloud provider attached with the integrationID.
|
||||||
|
// This just returns a summary of the service and not the whole service definition
|
||||||
|
ListServicesMetadata(ctx context.Context, orgID valuer.UUID, integrationID *valuer.UUID) (*cloudintegrationtypes.ServicesMetadata, 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, integrationID *valuer.UUID, serviceID string) (*cloudintegrationtypes.Service, error)
|
||||||
|
|
||||||
|
// UpdateServiceConfig updates cloud integration service config
|
||||||
|
UpdateServiceConfig(
|
||||||
|
ctx context.Context,
|
||||||
|
orgID valuer.UUID,
|
||||||
|
serviceID string,
|
||||||
|
config *cloudintegrationtypes.UpdateServiceConfigRequest,
|
||||||
|
) (*cloudintegrationtypes.UpdateServiceConfigResponse, 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
|
||||||
|
// in the org for any cloud integration account
|
||||||
|
GetDashboardByID(ctx context.Context, orgID valuer.UUID, id string) (*dashboardtypes.Dashboard, error)
|
||||||
|
|
||||||
|
// GetAllDashboards returns list of dashboards across all connected cloud integration accounts
|
||||||
|
// for 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)
|
||||||
|
}
|
||||||
152
pkg/modules/cloudintegration/implcloudintegration/store.go
Normal file
152
pkg/modules/cloudintegration/implcloudintegration/store.go
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
package implcloudintegration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/SigNoz/signoz/pkg/errors"
|
||||||
|
"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) CreateAccount(ctx context.Context, orgID valuer.UUID, account *cloudintegrationtypes.StorableCloudIntegration) (*cloudintegrationtypes.StorableCloudIntegration, error) {
|
||||||
|
now := time.Now()
|
||||||
|
if account.ID.IsZero() {
|
||||||
|
account.ID = valuer.GenerateUUID()
|
||||||
|
}
|
||||||
|
account.OrgID = orgID
|
||||||
|
account.CreatedAt = now
|
||||||
|
account.UpdatedAt = now
|
||||||
|
|
||||||
|
_, err := s.store.BunDBCtx(ctx).NewInsert().Model(account).Exec(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, s.store.WrapAlreadyExistsErrf(err, errors.CodeAlreadyExists, "cloud integration account with id %s already exists", account.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
return account, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *store) UpdateAccount(ctx context.Context, account *cloudintegrationtypes.StorableCloudIntegration) error {
|
||||||
|
account.UpdatedAt = time.Now()
|
||||||
|
_, err := s.store.BunDBCtx(ctx).NewUpdate().Model(account).
|
||||||
|
Where("id = ?", account.ID).
|
||||||
|
Where("org_id = ?", account.OrgID).
|
||||||
|
Where("provider = ?", account.Provider).
|
||||||
|
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) CreateService(ctx context.Context, cloudIntegrationID valuer.UUID, service *cloudintegrationtypes.StorableCloudIntegrationService) (*cloudintegrationtypes.StorableCloudIntegrationService, error) {
|
||||||
|
now := time.Now()
|
||||||
|
if service.ID.IsZero() {
|
||||||
|
service.ID = valuer.GenerateUUID()
|
||||||
|
}
|
||||||
|
service.CloudIntegrationID = cloudIntegrationID
|
||||||
|
if service.CreatedAt.IsZero() {
|
||||||
|
service.CreatedAt = now
|
||||||
|
}
|
||||||
|
service.UpdatedAt = now
|
||||||
|
|
||||||
|
_, err := s.store.BunDBCtx(ctx).NewInsert().Model(service).Exec(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, s.store.WrapAlreadyExistsErrf(err, errors.CodeAlreadyExists, "cloud integration service with type %s already exists", service.Type)
|
||||||
|
}
|
||||||
|
|
||||||
|
return service, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *store) UpdateService(ctx context.Context, cloudIntegrationID valuer.UUID, service *cloudintegrationtypes.StorableCloudIntegrationService) error {
|
||||||
|
service.CloudIntegrationID = cloudIntegrationID
|
||||||
|
service.UpdatedAt = time.Now()
|
||||||
|
_, err := s.store.BunDBCtx(ctx).NewUpdate().Model(service).
|
||||||
|
Where("cloud_integration_id = ?", cloudIntegrationID).
|
||||||
|
Where("type = ?", service.Type).
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
96
pkg/types/cloudintegrationtypes/connection.go
Normal file
96
pkg/types/cloudintegrationtypes/connection.go
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
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
|
||||||
|
// which gets mapped to new fields in AgentCheckInRequest
|
||||||
|
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
|
||||||
|
}
|
||||||
211
pkg/types/cloudintegrationtypes/service.go
Normal file
211
pkg/types/cloudintegrationtypes/service.go
Normal file
@@ -0,0 +1,211 @@
|
|||||||
|
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 (
|
||||||
|
ServicesMetadata struct {
|
||||||
|
Services []*ServiceMetadata `json:"services"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServiceMetadata helps to quickly list available services and whether it is enabled or not.
|
||||||
|
// As getting complete service definition is a heavy operation and the response is also large,
|
||||||
|
// initial integration page load can be very slow.
|
||||||
|
ServiceMetadata struct {
|
||||||
|
ServiceDefinitionMetadata
|
||||||
|
// if the service is enabled for the account
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
}
|
||||||
|
|
||||||
|
GettableServicesMetadata = ServicesMetadata
|
||||||
|
|
||||||
|
Service struct {
|
||||||
|
ServiceDefinition
|
||||||
|
ServiceConfig *ServiceConfig `json:"serviceConfig"`
|
||||||
|
}
|
||||||
|
|
||||||
|
GettableService = Service
|
||||||
|
|
||||||
|
UpdateServiceConfigRequest struct {
|
||||||
|
CloudIntegrationId valuer.UUID `json:"cloudIntegrationId"`
|
||||||
|
ServiceConfig *ServiceConfig `json:"serviceConfig"`
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateServiceConfigResponse struct {
|
||||||
|
Id string `json:"id"` // service id
|
||||||
|
CloudIntegrationId valuer.UUID `json:"cloudIntegrationId"`
|
||||||
|
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.
|
||||||
|
// This is used to show available pre-made dashboards for a service,
|
||||||
|
// hence has additional fields like id, title and description
|
||||||
|
type Dashboard struct {
|
||||||
|
Id string `json:"id"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
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
|
||||||
|
}
|
||||||
41
pkg/types/cloudintegrationtypes/store.go
Normal file
41
pkg/types/cloudintegrationtypes/store.go
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
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)
|
||||||
|
|
||||||
|
// CreateAccount creates a new cloud integration account
|
||||||
|
CreateAccount(ctx context.Context, orgID valuer.UUID, account *StorableCloudIntegration) (*StorableCloudIntegration, error)
|
||||||
|
|
||||||
|
// UpdateAccount updates an existing cloud integration account
|
||||||
|
UpdateAccount(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)
|
||||||
|
|
||||||
|
// CreateService creates a new cloud integration service for the given cloud integration id and service type
|
||||||
|
CreateService(ctx context.Context, cloudIntegrationID valuer.UUID, service *StorableCloudIntegrationService) (*StorableCloudIntegrationService, error)
|
||||||
|
|
||||||
|
// UpdateService updates an existing cloud integration service for the given cloud integration id and service type
|
||||||
|
UpdateService(ctx context.Context, cloudIntegrationID valuer.UUID, 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