mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-13 10:49:06 +08:00
commit
aeee8b4cb2
@ -131,7 +131,7 @@ services:
|
||||
# - ./data/clickhouse-3/:/var/lib/clickhouse/
|
||||
|
||||
alertmanager:
|
||||
image: signoz/alertmanager:0.23.2
|
||||
image: signoz/alertmanager:0.23.3
|
||||
volumes:
|
||||
- ./data/alertmanager:/data
|
||||
command:
|
||||
@ -144,7 +144,7 @@ services:
|
||||
condition: on-failure
|
||||
|
||||
query-service:
|
||||
image: signoz/query-service:0.27.0
|
||||
image: signoz/query-service:0.28.0
|
||||
command: [ "-config=/root/config/prometheus.yml" ]
|
||||
# ports:
|
||||
# - "6060:6060" # pprof port
|
||||
@ -180,7 +180,7 @@ services:
|
||||
<<: *clickhouse-depend
|
||||
|
||||
frontend:
|
||||
image: signoz/frontend:0.27.0
|
||||
image: signoz/frontend:0.28.0
|
||||
deploy:
|
||||
restart_policy:
|
||||
condition: on-failure
|
||||
|
@ -24,10 +24,6 @@ server {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
location /api/alertmanager {
|
||||
proxy_pass http://alertmanager:9093/api/v2;
|
||||
}
|
||||
|
||||
location ~ ^/api/(v1|v3)/logs/(tail|livetail){
|
||||
proxy_pass http://query-service:8080;
|
||||
proxy_http_version 1.1;
|
||||
|
@ -34,7 +34,7 @@ services:
|
||||
|
||||
alertmanager:
|
||||
container_name: signoz-alertmanager
|
||||
image: signoz/alertmanager:0.23.2
|
||||
image: signoz/alertmanager:0.23.3
|
||||
volumes:
|
||||
- ./data/alertmanager:/data
|
||||
depends_on:
|
||||
|
@ -147,7 +147,7 @@ services:
|
||||
# - ./user_scripts:/var/lib/clickhouse/user_scripts/
|
||||
|
||||
alertmanager:
|
||||
image: signoz/alertmanager:${ALERTMANAGER_TAG:-0.23.2}
|
||||
image: signoz/alertmanager:${ALERTMANAGER_TAG:-0.23.3}
|
||||
container_name: signoz-alertmanager
|
||||
volumes:
|
||||
- ./data/alertmanager:/data
|
||||
@ -162,7 +162,7 @@ services:
|
||||
# Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md`
|
||||
|
||||
query-service:
|
||||
image: signoz/query-service:${DOCKER_TAG:-0.27.0}
|
||||
image: signoz/query-service:${DOCKER_TAG:-0.28.0}
|
||||
container_name: signoz-query-service
|
||||
command: [ "-config=/root/config/prometheus.yml" ]
|
||||
# ports:
|
||||
@ -197,7 +197,7 @@ services:
|
||||
<<: *clickhouse-depend
|
||||
|
||||
frontend:
|
||||
image: signoz/frontend:${DOCKER_TAG:-0.27.0}
|
||||
image: signoz/frontend:${DOCKER_TAG:-0.28.0}
|
||||
container_name: signoz-frontend
|
||||
restart: on-failure
|
||||
depends_on:
|
||||
|
@ -24,10 +24,6 @@ server {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
location /api/alertmanager {
|
||||
proxy_pass http://alertmanager:9093/api/v2;
|
||||
}
|
||||
|
||||
location ~ ^/api/(v1|v3)/logs/(tail|livetail){
|
||||
proxy_pass http://query-service:8080;
|
||||
proxy_http_version 1.1;
|
||||
|
@ -81,6 +81,13 @@ var BasicPlan = basemodel.FeatureSet{
|
||||
UsageLimit: -1,
|
||||
Route: "",
|
||||
},
|
||||
basemodel.Feature{
|
||||
Name: basemodel.AlertChannelOpsgenie,
|
||||
Active: true,
|
||||
Usage: 0,
|
||||
UsageLimit: -1,
|
||||
Route: "",
|
||||
},
|
||||
basemodel.Feature{
|
||||
Name: basemodel.AlertChannelMsTeams,
|
||||
Active: false,
|
||||
@ -161,6 +168,13 @@ var ProPlan = basemodel.FeatureSet{
|
||||
UsageLimit: -1,
|
||||
Route: "",
|
||||
},
|
||||
basemodel.Feature{
|
||||
Name: basemodel.AlertChannelOpsgenie,
|
||||
Active: true,
|
||||
Usage: 0,
|
||||
UsageLimit: -1,
|
||||
Route: "",
|
||||
},
|
||||
basemodel.Feature{
|
||||
Name: basemodel.AlertChannelMsTeams,
|
||||
Active: true,
|
||||
@ -241,6 +255,13 @@ var EnterprisePlan = basemodel.FeatureSet{
|
||||
UsageLimit: -1,
|
||||
Route: "",
|
||||
},
|
||||
basemodel.Feature{
|
||||
Name: basemodel.AlertChannelOpsgenie,
|
||||
Active: true,
|
||||
Usage: 0,
|
||||
UsageLimit: -1,
|
||||
Route: "",
|
||||
},
|
||||
basemodel.Feature{
|
||||
Name: basemodel.AlertChannelMsTeams,
|
||||
Active: true,
|
||||
|
@ -185,7 +185,7 @@
|
||||
"react-resizable": "3.0.4",
|
||||
"ts-jest": "^27.1.4",
|
||||
"ts-node": "^10.2.1",
|
||||
"typescript-plugin-css-modules": "^3.4.0",
|
||||
"typescript-plugin-css-modules": "5.0.1",
|
||||
"webpack-bundle-analyzer": "^4.5.0",
|
||||
"webpack-cli": "^4.9.2"
|
||||
},
|
||||
|
1
frontend/public/locales/en-GB/logs.json
Normal file
1
frontend/public/locales/en-GB/logs.json
Normal file
@ -0,0 +1 @@
|
||||
{ "fetching_log_lines": "Fetching log lines" }
|
@ -29,6 +29,7 @@
|
||||
"NOT_FOUND": "SigNoz | Page Not Found",
|
||||
"LOGS": "SigNoz | Logs",
|
||||
"LOGS_EXPLORER": "SigNoz | Logs Explorer",
|
||||
"LIVE_LOGS": "SigNoz | Live Logs",
|
||||
"HOME_PAGE": "Open source Observability Platform | SigNoz",
|
||||
"PASSWORD_RESET": "SigNoz | Password Reset",
|
||||
"LIST_LICENSES": "SigNoz | List of Licenses",
|
||||
|
@ -20,6 +20,9 @@
|
||||
"field_slack_recipient": "Recipient",
|
||||
"field_slack_title": "Title",
|
||||
"field_slack_description": "Description",
|
||||
"field_opsgenie_api_key": "API Key",
|
||||
"field_opsgenie_description": "Description",
|
||||
"placeholder_opsgenie_description": "Description",
|
||||
"field_webhook_username": "User Name (optional)",
|
||||
"field_webhook_password": "Password (optional)",
|
||||
"field_pager_routing_key": "Routing Key",
|
||||
@ -31,8 +34,12 @@
|
||||
"field_pager_class": "Class",
|
||||
"field_pager_client": "Client",
|
||||
"field_pager_client_url": "Client URL",
|
||||
"field_opsgenie_message": "Message",
|
||||
"field_opsgenie_priority": "Priority",
|
||||
"placeholder_slack_description": "Description",
|
||||
"placeholder_pager_description": "Description",
|
||||
"placeholder_opsgenie_message": "Message",
|
||||
"placeholder_opsgenie_priority": "Priority",
|
||||
"help_pager_client": "Shows up as event source in Pagerduty",
|
||||
"help_pager_client_url": "Shows up as event source link in Pagerduty",
|
||||
"help_pager_class": "The class/type of the event",
|
||||
@ -43,6 +50,9 @@
|
||||
"help_webhook_username": "Leave empty for bearer auth or when authentication is not necessary.",
|
||||
"help_webhook_password": "Specify a password or bearer token",
|
||||
"help_pager_description": "Shows up as description in pagerduty",
|
||||
"help_opsgenie_message": "Shows up as message in opsgenie",
|
||||
"help_opsgenie_priority": "Priority of the incident",
|
||||
"help_opsgenie_description": "Shows up as description in opsgenie",
|
||||
"channel_creation_done": "Successfully created the channel",
|
||||
"channel_creation_failed": "An unexpected error occurred while creating this channel",
|
||||
"channel_edit_done": "Channels Edited Successfully",
|
||||
|
1
frontend/public/locales/en/logs.json
Normal file
1
frontend/public/locales/en/logs.json
Normal file
@ -0,0 +1 @@
|
||||
{ "fetching_log_lines": "Fetching log lines" }
|
@ -29,6 +29,7 @@
|
||||
"NOT_FOUND": "SigNoz | Page Not Found",
|
||||
"LOGS": "SigNoz | Logs",
|
||||
"LOGS_EXPLORER": "SigNoz | Logs Explorer",
|
||||
"LIVE_LOGS": "SigNoz | Live Logs",
|
||||
"HOME_PAGE": "Open source Observability Platform | SigNoz",
|
||||
"PASSWORD_RESET": "SigNoz | Password Reset",
|
||||
"LIST_LICENSES": "SigNoz | List of Licenses",
|
||||
|
@ -110,6 +110,10 @@ export const LogsExplorer = Loadable(
|
||||
() => import(/* webpackChunkName: "Logs Explorer" */ 'pages/LogsExplorer'),
|
||||
);
|
||||
|
||||
export const LiveLogs = Loadable(
|
||||
() => import(/* webpackChunkName: "Live Logs" */ 'pages/LiveLogs'),
|
||||
);
|
||||
|
||||
export const Login = Loadable(
|
||||
() => import(/* webpackChunkName: "Login" */ 'pages/Login'),
|
||||
);
|
||||
|
@ -14,6 +14,7 @@ import {
|
||||
GettingStarted,
|
||||
LicensePage,
|
||||
ListAllALertsPage,
|
||||
LiveLogs,
|
||||
Login,
|
||||
Logs,
|
||||
LogsExplorer,
|
||||
@ -234,6 +235,13 @@ const routes: AppRoutes[] = [
|
||||
key: 'LOGS_EXPLORER',
|
||||
isPrivate: true,
|
||||
},
|
||||
{
|
||||
path: ROUTES.LIVE_LOGS,
|
||||
exact: true,
|
||||
component: LiveLogs,
|
||||
key: 'LIVE_LOGS',
|
||||
isPrivate: true,
|
||||
},
|
||||
{
|
||||
path: ROUTES.LOGIN,
|
||||
exact: true,
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { AxiosAlertManagerInstance } from 'api';
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { AxiosError } from 'axios';
|
||||
import convertObjectIntoParams from 'lib/query/convertObjectIntoParams';
|
||||
@ -11,15 +11,15 @@ const getTriggered = async (
|
||||
try {
|
||||
const queryParams = convertObjectIntoParams(props);
|
||||
|
||||
const response = await AxiosAlertManagerInstance.get(
|
||||
`/alerts?${queryParams}`,
|
||||
);
|
||||
const response = await axios.get(`/alerts?${queryParams}`);
|
||||
|
||||
const amData = JSON.parse(response.data.data);
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: response.data.status,
|
||||
payload: response.data,
|
||||
payload: amData.data,
|
||||
};
|
||||
} catch (error) {
|
||||
return ErrorResponseHandler(error as AxiosError);
|
||||
|
37
frontend/src/api/channels/createOpsgenie.ts
Normal file
37
frontend/src/api/channels/createOpsgenie.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { PayloadProps, Props } from 'types/api/channels/createOpsgenie';
|
||||
|
||||
const create = async (
|
||||
props: Props,
|
||||
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
||||
try {
|
||||
const response = await axios.post('/channels', {
|
||||
name: props.name,
|
||||
opsgenie_configs: [
|
||||
{
|
||||
api_key: props.api_key,
|
||||
description: props.description,
|
||||
priority: props.priority,
|
||||
message: props.message,
|
||||
details: {
|
||||
...props.detailsArray,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: 'Success',
|
||||
payload: response.data.data,
|
||||
};
|
||||
} catch (error) {
|
||||
return ErrorResponseHandler(error as AxiosError);
|
||||
}
|
||||
};
|
||||
|
||||
export default create;
|
38
frontend/src/api/channels/editOpsgenie.ts
Normal file
38
frontend/src/api/channels/editOpsgenie.ts
Normal file
@ -0,0 +1,38 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { PayloadProps, Props } from 'types/api/channels/editOpsgenie';
|
||||
|
||||
const editOpsgenie = async (
|
||||
props: Props,
|
||||
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
||||
try {
|
||||
const response = await axios.put(`/channels/${props.id}`, {
|
||||
name: props.name,
|
||||
opsgenie_configs: [
|
||||
{
|
||||
send_resolved: true,
|
||||
api_key: props.api_key,
|
||||
description: props.description,
|
||||
priority: props.priority,
|
||||
message: props.message,
|
||||
details: {
|
||||
...props.detailsArray,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: 'Success',
|
||||
payload: response.data.data,
|
||||
};
|
||||
} catch (error) {
|
||||
return ErrorResponseHandler(error as AxiosError);
|
||||
}
|
||||
};
|
||||
|
||||
export default editOpsgenie;
|
37
frontend/src/api/channels/testOpsgenie.ts
Normal file
37
frontend/src/api/channels/testOpsgenie.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { PayloadProps, Props } from 'types/api/channels/createOpsgenie';
|
||||
|
||||
const testOpsgenie = async (
|
||||
props: Props,
|
||||
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
||||
try {
|
||||
const response = await axios.post('/testChannel', {
|
||||
name: props.name,
|
||||
opsgenie_configs: [
|
||||
{
|
||||
api_key: props.api_key,
|
||||
description: props.description,
|
||||
priority: props.priority,
|
||||
message: props.message,
|
||||
details: {
|
||||
...props.detailsArray,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: 'Success',
|
||||
payload: response.data.data,
|
||||
};
|
||||
} catch (error) {
|
||||
return ErrorResponseHandler(error as AxiosError);
|
||||
}
|
||||
};
|
||||
|
||||
export default testOpsgenie;
|
@ -4,11 +4,11 @@ import { AxiosError } from 'axios';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import {
|
||||
MetricRangePayloadV3,
|
||||
MetricsRangeProps,
|
||||
QueryRangePayload,
|
||||
} from 'types/api/metrics/getQueryRange';
|
||||
|
||||
export const getMetricsQueryRange = async (
|
||||
props: MetricsRangeProps,
|
||||
props: QueryRangePayload,
|
||||
): Promise<SuccessResponse<MetricRangePayloadV3> | ErrorResponse> => {
|
||||
try {
|
||||
const response = await axios.post('/query_range', props);
|
||||
|
5
frontend/src/api/saveView/deleteView.ts
Normal file
5
frontend/src/api/saveView/deleteView.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import axios from 'api';
|
||||
import { DeleteViewPayloadProps } from 'types/api/saveViews/types';
|
||||
|
||||
export const deleteView = (uuid: string): Promise<DeleteViewPayloadProps> =>
|
||||
axios.delete(`explorer/views/${uuid}`);
|
9
frontend/src/api/saveView/getAllViews.ts
Normal file
9
frontend/src/api/saveView/getAllViews.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import axios from 'api';
|
||||
import { AxiosResponse } from 'axios';
|
||||
import { AllViewsProps } from 'types/api/saveViews/types';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
export const getAllViews = (
|
||||
sourcepage: DataSource,
|
||||
): Promise<AxiosResponse<AllViewsProps>> =>
|
||||
axios.get(`explorer/views?sourcePage=${sourcepage}`);
|
16
frontend/src/api/saveView/saveView.ts
Normal file
16
frontend/src/api/saveView/saveView.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import axios from 'api';
|
||||
import { AxiosResponse } from 'axios';
|
||||
import { SaveViewPayloadProps, SaveViewProps } from 'types/api/saveViews/types';
|
||||
|
||||
export const saveView = ({
|
||||
compositeQuery,
|
||||
sourcePage,
|
||||
viewName,
|
||||
extraData,
|
||||
}: SaveViewProps): Promise<AxiosResponse<SaveViewPayloadProps>> =>
|
||||
axios.post('explorer/views', {
|
||||
name: viewName,
|
||||
sourcePage,
|
||||
compositeQuery,
|
||||
extraData,
|
||||
});
|
19
frontend/src/api/saveView/updateView.ts
Normal file
19
frontend/src/api/saveView/updateView.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import axios from 'api';
|
||||
import {
|
||||
UpdateViewPayloadProps,
|
||||
UpdateViewProps,
|
||||
} from 'types/api/saveViews/types';
|
||||
|
||||
export const updateView = ({
|
||||
compositeQuery,
|
||||
viewName,
|
||||
extraData,
|
||||
sourcePage,
|
||||
viewKey,
|
||||
}: UpdateViewProps): Promise<UpdateViewPayloadProps> =>
|
||||
axios.put(`explorer/views/${viewKey}`, {
|
||||
name: viewName,
|
||||
compositeQuery,
|
||||
extraData,
|
||||
sourcePage,
|
||||
});
|
278
frontend/src/components/ExplorerCard/ExplorerCard.tsx
Normal file
278
frontend/src/components/ExplorerCard/ExplorerCard.tsx
Normal file
@ -0,0 +1,278 @@
|
||||
import {
|
||||
DeleteOutlined,
|
||||
DownOutlined,
|
||||
MoreOutlined,
|
||||
SaveOutlined,
|
||||
ShareAltOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Col,
|
||||
Dropdown,
|
||||
MenuProps,
|
||||
Popover,
|
||||
Row,
|
||||
Space,
|
||||
Typography,
|
||||
} from 'antd';
|
||||
import axios from 'axios';
|
||||
import TextToolTip from 'components/TextToolTip';
|
||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||
import { querySearchParams } from 'constants/queryBuilderQueryNames';
|
||||
import { useGetSearchQueryParam } from 'hooks/queryBuilder/useGetSearchQueryParam';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useDeleteView } from 'hooks/saveViews/useDeleteView';
|
||||
import { useGetAllViews } from 'hooks/saveViews/useGetAllViews';
|
||||
import { useUpdateView } from 'hooks/saveViews/useUpdateView';
|
||||
import useErrorNotification from 'hooks/useErrorNotification';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { mapCompositeQueryFromQuery } from 'lib/newQueryBuilder/queryBuilderMappers/mapCompositeQueryFromQuery';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useCopyToClipboard } from 'react-use';
|
||||
|
||||
import { ExploreHeaderToolTip, SaveButtonText } from './constants';
|
||||
import MenuItemGenerator from './MenuItemGenerator';
|
||||
import SaveViewWithName from './SaveViewWithName';
|
||||
import {
|
||||
DropDownOverlay,
|
||||
ExplorerCardHeadContainer,
|
||||
OffSetCol,
|
||||
} from './styles';
|
||||
import { ExplorerCardProps } from './types';
|
||||
import { deleteViewHandler, isQueryUpdatedInView } from './utils';
|
||||
|
||||
function ExplorerCard({
|
||||
sourcepage,
|
||||
children,
|
||||
}: ExplorerCardProps): JSX.Element {
|
||||
const [isOpen, setIsOpen] = useState<boolean>(false);
|
||||
const [, setCopyUrl] = useCopyToClipboard();
|
||||
const [isQueryUpdated, setIsQueryUpdated] = useState<boolean>(false);
|
||||
const { notifications } = useNotifications();
|
||||
|
||||
const onCopyUrlHandler = (): void => {
|
||||
setCopyUrl(window.location.href);
|
||||
notifications.success({
|
||||
message: 'Copied to clipboard',
|
||||
});
|
||||
};
|
||||
|
||||
const {
|
||||
stagedQuery,
|
||||
currentQuery,
|
||||
panelType,
|
||||
redirectWithQueryBuilderData,
|
||||
} = useQueryBuilder();
|
||||
|
||||
const {
|
||||
data: viewsData,
|
||||
isLoading,
|
||||
error,
|
||||
isRefetching,
|
||||
refetch: refetchAllView,
|
||||
} = useGetAllViews(sourcepage);
|
||||
|
||||
useErrorNotification(error);
|
||||
|
||||
const handlePopOverClose = (): void => {
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
const handleOpenChange = (newOpen: boolean): void => {
|
||||
setIsOpen(newOpen);
|
||||
};
|
||||
|
||||
const viewName =
|
||||
useGetSearchQueryParam(querySearchParams.viewName) || 'Query Builder';
|
||||
|
||||
const viewKey = useGetSearchQueryParam(querySearchParams.viewKey) || '';
|
||||
|
||||
const { mutateAsync: updateViewAsync } = useUpdateView({
|
||||
compositeQuery: mapCompositeQueryFromQuery(currentQuery, panelType),
|
||||
viewKey,
|
||||
extraData: '',
|
||||
sourcePage: sourcepage,
|
||||
viewName,
|
||||
});
|
||||
|
||||
const { mutateAsync: deleteViewAsync } = useDeleteView(viewKey);
|
||||
|
||||
const showErrorNotification = (err: Error): void => {
|
||||
notifications.error({
|
||||
message: axios.isAxiosError(err) ? err.message : SOMETHING_WENT_WRONG,
|
||||
});
|
||||
};
|
||||
|
||||
const onDeleteHandler = useCallback(() => {
|
||||
deleteViewHandler({
|
||||
deleteViewAsync,
|
||||
notifications,
|
||||
panelType,
|
||||
redirectWithQueryBuilderData,
|
||||
refetchAllView,
|
||||
viewId: viewKey,
|
||||
viewKey,
|
||||
});
|
||||
}, [
|
||||
deleteViewAsync,
|
||||
notifications,
|
||||
panelType,
|
||||
redirectWithQueryBuilderData,
|
||||
refetchAllView,
|
||||
viewKey,
|
||||
]);
|
||||
|
||||
const onUpdateQueryHandler = (): void => {
|
||||
updateViewAsync(
|
||||
{
|
||||
compositeQuery: mapCompositeQueryFromQuery(currentQuery, panelType),
|
||||
viewKey,
|
||||
extraData: '',
|
||||
sourcePage: sourcepage,
|
||||
viewName,
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
setIsQueryUpdated(false);
|
||||
notifications.success({
|
||||
message: 'View Updated Successfully',
|
||||
});
|
||||
refetchAllView();
|
||||
},
|
||||
onError: (err) => {
|
||||
showErrorNotification(err);
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setIsQueryUpdated(
|
||||
isQueryUpdatedInView({
|
||||
data: viewsData?.data?.data,
|
||||
stagedQuery,
|
||||
viewKey,
|
||||
currentPanelType: panelType,
|
||||
}),
|
||||
);
|
||||
}, [
|
||||
currentQuery,
|
||||
viewsData?.data?.data,
|
||||
stagedQuery,
|
||||
stagedQuery?.builder.queryData,
|
||||
viewKey,
|
||||
panelType,
|
||||
]);
|
||||
|
||||
const menu = useMemo(
|
||||
(): MenuProps => ({
|
||||
items: viewsData?.data?.data?.map((view) => ({
|
||||
key: view.uuid,
|
||||
label: (
|
||||
<MenuItemGenerator
|
||||
viewName={view.name}
|
||||
viewKey={viewKey}
|
||||
createdBy={view.createdBy}
|
||||
uuid={view.uuid}
|
||||
refetchAllView={refetchAllView}
|
||||
viewData={viewsData.data.data}
|
||||
/>
|
||||
),
|
||||
})),
|
||||
}),
|
||||
[refetchAllView, viewKey, viewsData?.data?.data],
|
||||
);
|
||||
|
||||
const moreOptionMenu = useMemo(
|
||||
(): MenuProps => ({
|
||||
items: [
|
||||
{
|
||||
key: 'delete',
|
||||
label: <Typography.Text strong>Delete</Typography.Text>,
|
||||
onClick: onDeleteHandler,
|
||||
icon: <DeleteOutlined />,
|
||||
},
|
||||
],
|
||||
}),
|
||||
[onDeleteHandler],
|
||||
);
|
||||
|
||||
const saveButtonType = isQueryUpdated ? 'default' : 'primary';
|
||||
const saveButtonIcon = isQueryUpdated ? null : <SaveOutlined />;
|
||||
|
||||
return (
|
||||
<>
|
||||
<ExplorerCardHeadContainer size="small">
|
||||
<Row align="middle">
|
||||
<Col span={6}>
|
||||
<Space>
|
||||
<Typography>{viewName}</Typography>
|
||||
<TextToolTip
|
||||
url={ExploreHeaderToolTip.url}
|
||||
text={ExploreHeaderToolTip.text}
|
||||
useFilledIcon={false}
|
||||
/>
|
||||
</Space>
|
||||
</Col>
|
||||
<OffSetCol span={10} offset={8}>
|
||||
<Space size="large">
|
||||
{viewsData?.data.data && viewsData?.data.data.length && (
|
||||
<Space>
|
||||
{/* <Typography.Text>Saved Views</Typography.Text> */}
|
||||
<Dropdown.Button
|
||||
menu={menu}
|
||||
loading={isLoading || isRefetching}
|
||||
icon={<DownOutlined />}
|
||||
trigger={['click']}
|
||||
overlayStyle={DropDownOverlay}
|
||||
>
|
||||
Select View
|
||||
</Dropdown.Button>
|
||||
</Space>
|
||||
)}
|
||||
{isQueryUpdated && (
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<SaveOutlined />}
|
||||
onClick={onUpdateQueryHandler}
|
||||
>
|
||||
Save changes
|
||||
</Button>
|
||||
)}
|
||||
<Popover
|
||||
placement="bottomLeft"
|
||||
trigger="click"
|
||||
content={
|
||||
<SaveViewWithName
|
||||
sourcePage={sourcepage}
|
||||
handlePopOverClose={handlePopOverClose}
|
||||
refetchAllView={refetchAllView}
|
||||
/>
|
||||
}
|
||||
showArrow={false}
|
||||
open={isOpen}
|
||||
onOpenChange={handleOpenChange}
|
||||
>
|
||||
<Button type={saveButtonType} icon={saveButtonIcon}>
|
||||
{isQueryUpdated
|
||||
? SaveButtonText.SAVE_AS_NEW_VIEW
|
||||
: SaveButtonText.SAVE_VIEW}
|
||||
</Button>
|
||||
</Popover>
|
||||
<ShareAltOutlined onClick={onCopyUrlHandler} />
|
||||
{viewKey && (
|
||||
<Dropdown trigger={['click']} menu={moreOptionMenu}>
|
||||
<MoreOutlined />
|
||||
</Dropdown>
|
||||
)}
|
||||
</Space>
|
||||
</OffSetCol>
|
||||
</Row>
|
||||
</ExplorerCardHeadContainer>
|
||||
<Card>{children}</Card>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default ExplorerCard;
|
87
frontend/src/components/ExplorerCard/MenuItemGenerator.tsx
Normal file
87
frontend/src/components/ExplorerCard/MenuItemGenerator.tsx
Normal file
@ -0,0 +1,87 @@
|
||||
import { DeleteOutlined } from '@ant-design/icons';
|
||||
import { Col, Row, Typography } from 'antd';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useDeleteView } from 'hooks/saveViews/useDeleteView';
|
||||
import { useHandleExplorerTabChange } from 'hooks/useHandleExplorerTabChange';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { MouseEvent, useCallback } from 'react';
|
||||
|
||||
import { MenuItemContainer } from './styles';
|
||||
import { MenuItemLabelGeneratorProps } from './types';
|
||||
import { deleteViewHandler, getViewDetailsUsingViewKey } from './utils';
|
||||
|
||||
function MenuItemGenerator({
|
||||
viewName,
|
||||
viewKey,
|
||||
createdBy,
|
||||
uuid,
|
||||
viewData,
|
||||
refetchAllView,
|
||||
}: MenuItemLabelGeneratorProps): JSX.Element {
|
||||
const { panelType, redirectWithQueryBuilderData } = useQueryBuilder();
|
||||
const { handleExplorerTabChange } = useHandleExplorerTabChange();
|
||||
const { notifications } = useNotifications();
|
||||
|
||||
const { mutateAsync: deleteViewAsync } = useDeleteView(uuid);
|
||||
|
||||
const onDeleteHandler = (event: MouseEvent<HTMLElement>): void => {
|
||||
event.stopPropagation();
|
||||
deleteViewHandler({
|
||||
deleteViewAsync,
|
||||
notifications,
|
||||
panelType,
|
||||
redirectWithQueryBuilderData,
|
||||
refetchAllView,
|
||||
viewId: uuid,
|
||||
viewKey,
|
||||
});
|
||||
};
|
||||
|
||||
const onMenuItemSelectHandler = useCallback(
|
||||
({ key }: { key: string }): void => {
|
||||
const currentViewDetails = getViewDetailsUsingViewKey(key, viewData);
|
||||
if (!currentViewDetails) return;
|
||||
const {
|
||||
query,
|
||||
name,
|
||||
uuid,
|
||||
panelType: currentPanelType,
|
||||
} = currentViewDetails;
|
||||
|
||||
handleExplorerTabChange(currentPanelType, {
|
||||
query,
|
||||
name,
|
||||
uuid,
|
||||
});
|
||||
},
|
||||
[viewData, handleExplorerTabChange],
|
||||
);
|
||||
|
||||
const onLabelClickHandler = (): void => {
|
||||
onMenuItemSelectHandler({
|
||||
key: uuid,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<MenuItemContainer onClick={onLabelClickHandler}>
|
||||
<Row justify="space-between">
|
||||
<Col span={22}>
|
||||
<Row>
|
||||
<Typography.Text strong>{viewName}</Typography.Text>
|
||||
</Row>
|
||||
<Row>
|
||||
<Typography.Text type="secondary">Created by {createdBy}</Typography.Text>
|
||||
</Row>
|
||||
</Col>
|
||||
<Col span={2}>
|
||||
<Typography.Link>
|
||||
<DeleteOutlined onClick={onDeleteHandler} />
|
||||
</Typography.Link>
|
||||
</Col>
|
||||
</Row>
|
||||
</MenuItemContainer>
|
||||
);
|
||||
}
|
||||
|
||||
export default MenuItemGenerator;
|
68
frontend/src/components/ExplorerCard/SaveViewWithName.tsx
Normal file
68
frontend/src/components/ExplorerCard/SaveViewWithName.tsx
Normal file
@ -0,0 +1,68 @@
|
||||
import { Card, Input, Typography } from 'antd';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useSaveView } from 'hooks/saveViews/useSaveView';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { mapCompositeQueryFromQuery } from 'lib/newQueryBuilder/queryBuilderMappers/mapCompositeQueryFromQuery';
|
||||
import { ChangeEvent, useCallback, useState } from 'react';
|
||||
|
||||
import { SaveButton } from './styles';
|
||||
import { SaveViewWithNameProps } from './types';
|
||||
import { saveViewHandler } from './utils';
|
||||
|
||||
function SaveViewWithName({
|
||||
sourcePage,
|
||||
handlePopOverClose,
|
||||
refetchAllView,
|
||||
}: SaveViewWithNameProps): JSX.Element {
|
||||
const [name, setName] = useState('');
|
||||
const {
|
||||
currentQuery,
|
||||
panelType,
|
||||
redirectWithQueryBuilderData,
|
||||
} = useQueryBuilder();
|
||||
const { notifications } = useNotifications();
|
||||
const compositeQuery = mapCompositeQueryFromQuery(currentQuery, panelType);
|
||||
|
||||
const { isLoading, mutateAsync: saveViewAsync } = useSaveView({
|
||||
viewName: name,
|
||||
compositeQuery,
|
||||
sourcePage,
|
||||
extraData: '',
|
||||
});
|
||||
|
||||
const onChangeHandler = useCallback(
|
||||
(e: ChangeEvent<HTMLInputElement>): void => {
|
||||
setName(e.target.value);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const onSaveHandler = (): void => {
|
||||
saveViewHandler({
|
||||
compositeQuery,
|
||||
handlePopOverClose,
|
||||
extraData: '',
|
||||
notifications,
|
||||
panelType: panelType || PANEL_TYPES.LIST,
|
||||
redirectWithQueryBuilderData,
|
||||
refetchAllView,
|
||||
saveViewAsync,
|
||||
sourcePage,
|
||||
viewName: name,
|
||||
setName,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<Typography>Name of the View</Typography>
|
||||
<Input placeholder="Enter Name" onChange={onChangeHandler} />
|
||||
<SaveButton onClick={onSaveHandler} type="primary" loading={isLoading}>
|
||||
Save
|
||||
</SaveButton>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default SaveViewWithName;
|
32
frontend/src/components/ExplorerCard/__mock__/viewData.ts
Normal file
32
frontend/src/components/ExplorerCard/__mock__/viewData.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import { ICompositeMetricQuery } from 'types/api/alerts/compositeQuery';
|
||||
import { ViewProps } from 'types/api/saveViews/types';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
export const viewMockData: ViewProps[] = [
|
||||
{
|
||||
uuid: 'view1',
|
||||
name: 'View 1',
|
||||
createdBy: 'User 1',
|
||||
category: 'category 1',
|
||||
compositeQuery: {} as ICompositeMetricQuery,
|
||||
createdAt: '2021-07-07T06:31:00.000Z',
|
||||
updatedAt: '2021-07-07T06:33:00.000Z',
|
||||
extraData: '',
|
||||
sourcePage: DataSource.TRACES,
|
||||
tags: [],
|
||||
updatedBy: 'User 1',
|
||||
},
|
||||
{
|
||||
uuid: 'view2',
|
||||
name: 'View 2',
|
||||
createdBy: 'User 2',
|
||||
category: 'category 2',
|
||||
compositeQuery: {} as ICompositeMetricQuery,
|
||||
createdAt: '2021-07-07T06:30:00.000Z',
|
||||
updatedAt: '2021-07-07T06:30:00.000Z',
|
||||
extraData: '',
|
||||
sourcePage: DataSource.TRACES,
|
||||
tags: [],
|
||||
updatedBy: 'User 2',
|
||||
},
|
||||
];
|
10
frontend/src/components/ExplorerCard/constants.ts
Normal file
10
frontend/src/components/ExplorerCard/constants.ts
Normal file
@ -0,0 +1,10 @@
|
||||
export const ExploreHeaderToolTip = {
|
||||
url:
|
||||
'https://signoz.io/docs/userguide/query-builder/?utm_source=product&utm_medium=new-query-builder',
|
||||
text: 'More details on how to use query builder',
|
||||
};
|
||||
|
||||
export const SaveButtonText = {
|
||||
SAVE_AS_NEW_VIEW: 'Save as new view',
|
||||
SAVE_VIEW: 'Save view',
|
||||
};
|
@ -1,27 +0,0 @@
|
||||
import { Card, Space, Typography } from 'antd';
|
||||
import TextToolTip from 'components/TextToolTip';
|
||||
|
||||
function ExplorerCard({ children }: Props): JSX.Element {
|
||||
return (
|
||||
<Card
|
||||
size="small"
|
||||
title={
|
||||
<Space>
|
||||
<Typography>Query Builder</Typography>
|
||||
<TextToolTip
|
||||
url="https://signoz.io/docs/userguide/query-builder/?utm_source=product&utm_medium=new-query-builder"
|
||||
text="More details on how to use query builder"
|
||||
/>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
{children}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
interface Props {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export default ExplorerCard;
|
28
frontend/src/components/ExplorerCard/styles.ts
Normal file
28
frontend/src/components/ExplorerCard/styles.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import { Button, Card, Col } from 'antd';
|
||||
import styled, { CSSProperties } from 'styled-components';
|
||||
|
||||
export const ExplorerCardHeadContainer = styled(Card)`
|
||||
margin: 1rem 0;
|
||||
`;
|
||||
|
||||
export const OffSetCol = styled(Col)`
|
||||
text-align: right;
|
||||
`;
|
||||
|
||||
export const SaveButton = styled(Button)`
|
||||
&&& {
|
||||
margin: 1rem 0;
|
||||
width: 5rem;
|
||||
}
|
||||
`;
|
||||
|
||||
export const DropDownOverlay: CSSProperties = {
|
||||
maxHeight: '20rem',
|
||||
overflowY: 'auto',
|
||||
width: '20rem',
|
||||
padding: 0,
|
||||
};
|
||||
|
||||
export const MenuItemContainer = styled(Card)`
|
||||
padding: 0;
|
||||
`;
|
@ -0,0 +1,76 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import ROUTES from 'constants/routes';
|
||||
import MockQueryClientProvider from 'providers/test/MockQueryClientProvider';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import { viewMockData } from '../__mock__/viewData';
|
||||
import ExplorerCard from '../ExplorerCard';
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useLocation: (): { pathname: string } => ({
|
||||
pathname: `${process.env.FRONTEND_API_ENDPOINT}/${ROUTES.TRACES_EXPLORER}/`,
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('hooks/queryBuilder/useGetPanelTypesQueryParam', () => ({
|
||||
useGetPanelTypesQueryParam: jest.fn(() => 'mockedPanelType'),
|
||||
}));
|
||||
|
||||
jest.mock('hooks/saveViews/useGetAllViews', () => ({
|
||||
useGetAllViews: jest.fn(() => ({
|
||||
data: { data: { data: viewMockData } },
|
||||
isLoading: false,
|
||||
error: null,
|
||||
isRefetching: false,
|
||||
refetch: jest.fn(),
|
||||
})),
|
||||
}));
|
||||
|
||||
jest.mock('hooks/saveViews/useUpdateView', () => ({
|
||||
useUpdateView: jest.fn(() => ({
|
||||
mutateAsync: jest.fn(),
|
||||
})),
|
||||
}));
|
||||
|
||||
jest.mock('hooks/saveViews/useDeleteView', () => ({
|
||||
useDeleteView: jest.fn(() => ({
|
||||
mutateAsync: jest.fn(),
|
||||
})),
|
||||
}));
|
||||
|
||||
describe('ExplorerCard', () => {
|
||||
it('renders a card with a title and a description', () => {
|
||||
render(
|
||||
<MockQueryClientProvider>
|
||||
<ExplorerCard sourcepage={DataSource.TRACES}>child</ExplorerCard>
|
||||
</MockQueryClientProvider>,
|
||||
);
|
||||
expect(screen.getByText('Query Builder')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders a save view button', () => {
|
||||
render(
|
||||
<MockQueryClientProvider>
|
||||
<ExplorerCard sourcepage={DataSource.TRACES}>child</ExplorerCard>
|
||||
</MockQueryClientProvider>,
|
||||
);
|
||||
expect(screen.getByText('Save view')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should see all the view listed in dropdown', async () => {
|
||||
const screen = render(
|
||||
<ExplorerCard sourcepage={DataSource.TRACES}>Mock Children</ExplorerCard>,
|
||||
);
|
||||
const selectButton = screen.getByText('Select View');
|
||||
|
||||
fireEvent.click(selectButton);
|
||||
|
||||
const spanElement = screen.getByRole('img', {
|
||||
name: 'down',
|
||||
});
|
||||
fireEvent.click(spanElement);
|
||||
const viewNameText = await screen.findByText('View 2');
|
||||
expect(viewNameText).toBeInTheDocument();
|
||||
});
|
||||
});
|
@ -0,0 +1,53 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import ROUTES from 'constants/routes';
|
||||
import MockQueryClientProvider from 'providers/test/MockQueryClientProvider';
|
||||
|
||||
import { viewMockData } from '../__mock__/viewData';
|
||||
import MenuItemGenerator from '../MenuItemGenerator';
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useLocation: (): { pathname: string } => ({
|
||||
pathname: `${process.env.FRONTEND_API_ENDPOINT}${ROUTES.APPLICATION}/`,
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('MenuItemGenerator', () => {
|
||||
it('should render MenuItemGenerator component', () => {
|
||||
const screen = render(
|
||||
<MockQueryClientProvider>
|
||||
<MenuItemGenerator
|
||||
viewName={viewMockData[0].name}
|
||||
viewKey={viewMockData[0].uuid}
|
||||
createdBy={viewMockData[0].createdBy}
|
||||
uuid={viewMockData[0].uuid}
|
||||
refetchAllView={jest.fn()}
|
||||
viewData={viewMockData}
|
||||
/>
|
||||
</MockQueryClientProvider>,
|
||||
);
|
||||
|
||||
expect(screen.getByText(viewMockData[0].name)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should call onMenuItemSelectHandler on click of MenuItemGenerator', () => {
|
||||
render(
|
||||
<MockQueryClientProvider>
|
||||
<MenuItemGenerator
|
||||
viewName={viewMockData[0].name}
|
||||
viewKey={viewMockData[0].uuid}
|
||||
createdBy={viewMockData[0].createdBy}
|
||||
uuid={viewMockData[0].uuid}
|
||||
refetchAllView={jest.fn()}
|
||||
viewData={viewMockData}
|
||||
/>
|
||||
</MockQueryClientProvider>,
|
||||
);
|
||||
|
||||
const spanElement = screen.getByRole('img', {
|
||||
name: 'delete',
|
||||
});
|
||||
|
||||
expect(spanElement).toBeInTheDocument();
|
||||
});
|
||||
});
|
@ -0,0 +1,63 @@
|
||||
import { fireEvent, render } from '@testing-library/react';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { QueryClient, QueryClientProvider } from 'react-query';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import SaveViewWithName from '../SaveViewWithName';
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useLocation: (): { pathname: string } => ({
|
||||
pathname: `${process.env.FRONTEND_API_ENDPOINT}${ROUTES.APPLICATION}/`,
|
||||
}),
|
||||
}));
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
refetchOnWindowFocus: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
jest.mock('hooks/queryBuilder/useGetPanelTypesQueryParam', () => ({
|
||||
useGetPanelTypesQueryParam: jest.fn(() => 'mockedPanelType'),
|
||||
}));
|
||||
|
||||
jest.mock('hooks/saveViews/useSaveView', () => ({
|
||||
useSaveView: jest.fn(() => ({
|
||||
mutateAsync: jest.fn(),
|
||||
})),
|
||||
}));
|
||||
|
||||
describe('SaveViewWithName', () => {
|
||||
it('should render SaveViewWithName component', () => {
|
||||
const screen = render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<SaveViewWithName
|
||||
sourcePage={DataSource.TRACES}
|
||||
handlePopOverClose={jest.fn()}
|
||||
refetchAllView={jest.fn()}
|
||||
/>
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
|
||||
expect(screen.getByText('Save')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should call saveViewAsync on click of Save button', () => {
|
||||
const screen = render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<SaveViewWithName
|
||||
sourcePage={DataSource.TRACES}
|
||||
handlePopOverClose={jest.fn()}
|
||||
refetchAllView={jest.fn()}
|
||||
/>
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByText('Save'));
|
||||
|
||||
expect(screen.getByText('Save')).toBeInTheDocument();
|
||||
});
|
||||
});
|
77
frontend/src/components/ExplorerCard/types.ts
Normal file
77
frontend/src/components/ExplorerCard/types.ts
Normal file
@ -0,0 +1,77 @@
|
||||
import { NotificationInstance } from 'antd/es/notification/interface';
|
||||
import { AxiosResponse } from 'axios';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { SetStateAction } from 'react';
|
||||
import { UseMutateAsyncFunction } from 'react-query';
|
||||
import { ICompositeMetricQuery } from 'types/api/alerts/compositeQuery';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import {
|
||||
DeleteViewPayloadProps,
|
||||
SaveViewPayloadProps,
|
||||
SaveViewProps,
|
||||
ViewProps,
|
||||
} from 'types/api/saveViews/types';
|
||||
import { DataSource, QueryBuilderContextType } from 'types/common/queryBuilder';
|
||||
|
||||
export interface ExplorerCardProps {
|
||||
sourcepage: DataSource;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export type GetViewDetailsUsingViewKey = (
|
||||
viewKey: string,
|
||||
data: ViewProps[] | undefined,
|
||||
) =>
|
||||
| { query: Query; name: string; uuid: string; panelType: PANEL_TYPES }
|
||||
| undefined;
|
||||
|
||||
export interface IsQueryUpdatedInViewProps {
|
||||
viewKey: string;
|
||||
data: ViewProps[] | undefined;
|
||||
stagedQuery: Query | null;
|
||||
currentPanelType: PANEL_TYPES | null;
|
||||
}
|
||||
|
||||
export interface SaveViewWithNameProps {
|
||||
sourcePage: ExplorerCardProps['sourcepage'];
|
||||
handlePopOverClose: VoidFunction;
|
||||
refetchAllView: VoidFunction;
|
||||
}
|
||||
|
||||
export interface MenuItemLabelGeneratorProps {
|
||||
viewName: string;
|
||||
viewKey: string;
|
||||
createdBy: string;
|
||||
uuid: string;
|
||||
viewData: ViewProps[];
|
||||
refetchAllView: VoidFunction;
|
||||
}
|
||||
|
||||
export interface SaveViewHandlerProps {
|
||||
viewName: string;
|
||||
compositeQuery: ICompositeMetricQuery;
|
||||
sourcePage: ExplorerCardProps['sourcepage'];
|
||||
extraData: string;
|
||||
panelType: PANEL_TYPES | null;
|
||||
notifications: NotificationInstance;
|
||||
refetchAllView: SaveViewWithNameProps['refetchAllView'];
|
||||
saveViewAsync: UseMutateAsyncFunction<
|
||||
AxiosResponse<SaveViewPayloadProps>,
|
||||
Error,
|
||||
SaveViewProps,
|
||||
SaveViewPayloadProps
|
||||
>;
|
||||
handlePopOverClose: SaveViewWithNameProps['handlePopOverClose'];
|
||||
redirectWithQueryBuilderData: QueryBuilderContextType['redirectWithQueryBuilderData'];
|
||||
setName: (value: SetStateAction<string>) => void;
|
||||
}
|
||||
|
||||
export interface DeleteViewHandlerProps {
|
||||
deleteViewAsync: UseMutateAsyncFunction<DeleteViewPayloadProps, Error, string>;
|
||||
refetchAllView: MenuItemLabelGeneratorProps['refetchAllView'];
|
||||
redirectWithQueryBuilderData: QueryBuilderContextType['redirectWithQueryBuilderData'];
|
||||
notifications: NotificationInstance;
|
||||
panelType: PANEL_TYPES | null;
|
||||
viewKey: string;
|
||||
viewId: string;
|
||||
}
|
170
frontend/src/components/ExplorerCard/utils.ts
Normal file
170
frontend/src/components/ExplorerCard/utils.ts
Normal file
@ -0,0 +1,170 @@
|
||||
import { NotificationInstance } from 'antd/es/notification/interface';
|
||||
import axios from 'axios';
|
||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||
import { initialQueriesMap } from 'constants/queryBuilder';
|
||||
import {
|
||||
queryParamNamesMap,
|
||||
querySearchParams,
|
||||
} from 'constants/queryBuilderQueryNames';
|
||||
import { mapQueryDataFromApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataFromApi';
|
||||
import isEqual from 'lodash-es/isEqual';
|
||||
|
||||
import {
|
||||
DeleteViewHandlerProps,
|
||||
GetViewDetailsUsingViewKey,
|
||||
IsQueryUpdatedInViewProps,
|
||||
SaveViewHandlerProps,
|
||||
} from './types';
|
||||
|
||||
const showErrorNotification = (
|
||||
notifications: NotificationInstance,
|
||||
err: Error,
|
||||
): void => {
|
||||
notifications.error({
|
||||
message: axios.isAxiosError(err) ? err.message : SOMETHING_WENT_WRONG,
|
||||
});
|
||||
};
|
||||
|
||||
export const getViewDetailsUsingViewKey: GetViewDetailsUsingViewKey = (
|
||||
viewKey,
|
||||
data,
|
||||
) => {
|
||||
const selectedView = data?.find((view) => view.uuid === viewKey);
|
||||
if (selectedView) {
|
||||
const { compositeQuery, name, uuid } = selectedView;
|
||||
const query = mapQueryDataFromApi(compositeQuery);
|
||||
return { query, name, uuid, panelType: compositeQuery.panelType };
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const isQueryUpdatedInView = ({
|
||||
viewKey,
|
||||
data,
|
||||
stagedQuery,
|
||||
currentPanelType,
|
||||
}: IsQueryUpdatedInViewProps): boolean => {
|
||||
const currentViewDetails = getViewDetailsUsingViewKey(viewKey, data);
|
||||
if (!currentViewDetails) {
|
||||
return false;
|
||||
}
|
||||
const { query, panelType } = currentViewDetails;
|
||||
|
||||
// Omitting id from aggregateAttribute and groupBy
|
||||
const updatedCurrentQuery = {
|
||||
...stagedQuery,
|
||||
builder: {
|
||||
...stagedQuery?.builder,
|
||||
queryData: stagedQuery?.builder.queryData.map((queryData) => {
|
||||
const { id, ...rest } = queryData.aggregateAttribute;
|
||||
const newAggregateAttribute = rest;
|
||||
const newGroupByAttributes = queryData.groupBy.map((groupByAttribute) => {
|
||||
const { id, ...rest } = groupByAttribute;
|
||||
return rest;
|
||||
});
|
||||
const newItems = queryData.filters.items.map((item) => {
|
||||
const { id, ...newItem } = item;
|
||||
if (item.key) {
|
||||
const { id, ...rest } = item.key;
|
||||
return {
|
||||
...newItem,
|
||||
key: rest,
|
||||
};
|
||||
}
|
||||
return newItem;
|
||||
});
|
||||
return {
|
||||
...queryData,
|
||||
aggregateAttribute: newAggregateAttribute,
|
||||
groupBy: newGroupByAttributes,
|
||||
filters: {
|
||||
...queryData.filters,
|
||||
items: newItems,
|
||||
},
|
||||
limit: queryData.limit ? queryData.limit : 0,
|
||||
offset: queryData.offset ? queryData.offset : 0,
|
||||
pageSize: queryData.pageSize ? queryData.pageSize : 0,
|
||||
};
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
panelType !== currentPanelType ||
|
||||
!isEqual(query.builder, updatedCurrentQuery?.builder) ||
|
||||
!isEqual(query.clickhouse_sql, updatedCurrentQuery?.clickhouse_sql) ||
|
||||
!isEqual(query.promql, updatedCurrentQuery?.promql)
|
||||
);
|
||||
};
|
||||
|
||||
export const saveViewHandler = ({
|
||||
saveViewAsync,
|
||||
refetchAllView,
|
||||
notifications,
|
||||
handlePopOverClose,
|
||||
viewName,
|
||||
compositeQuery,
|
||||
sourcePage,
|
||||
extraData,
|
||||
redirectWithQueryBuilderData,
|
||||
panelType,
|
||||
setName,
|
||||
}: SaveViewHandlerProps): void => {
|
||||
saveViewAsync(
|
||||
{
|
||||
viewName,
|
||||
compositeQuery,
|
||||
sourcePage,
|
||||
extraData,
|
||||
},
|
||||
{
|
||||
onSuccess: (data) => {
|
||||
refetchAllView();
|
||||
redirectWithQueryBuilderData(mapQueryDataFromApi(compositeQuery), {
|
||||
[queryParamNamesMap.panelTypes]: panelType,
|
||||
[querySearchParams.viewName]: viewName,
|
||||
[querySearchParams.viewKey]: data.data.data,
|
||||
});
|
||||
notifications.success({
|
||||
message: 'View Saved Successfully',
|
||||
});
|
||||
},
|
||||
onError: (err) => {
|
||||
showErrorNotification(notifications, err);
|
||||
},
|
||||
onSettled: () => {
|
||||
handlePopOverClose();
|
||||
setName('');
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
export const deleteViewHandler = ({
|
||||
deleteViewAsync,
|
||||
refetchAllView,
|
||||
redirectWithQueryBuilderData,
|
||||
notifications,
|
||||
panelType,
|
||||
viewKey,
|
||||
viewId,
|
||||
}: DeleteViewHandlerProps): void => {
|
||||
deleteViewAsync(viewKey, {
|
||||
onSuccess: () => {
|
||||
if (viewId === viewKey) {
|
||||
redirectWithQueryBuilderData(initialQueriesMap.traces, {
|
||||
[querySearchParams.viewName]: 'Query Builder',
|
||||
[queryParamNamesMap.panelTypes]: panelType,
|
||||
[querySearchParams.viewKey]: '',
|
||||
});
|
||||
}
|
||||
notifications.success({
|
||||
message: 'View Deleted Successfully',
|
||||
});
|
||||
refetchAllView();
|
||||
},
|
||||
onError: (err) => {
|
||||
showErrorNotification(notifications, err);
|
||||
},
|
||||
});
|
||||
};
|
@ -21,8 +21,6 @@ import {
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
// interfaces
|
||||
import { ILog } from 'types/api/logs/log';
|
||||
|
||||
// styles
|
||||
import {
|
||||
@ -31,19 +29,17 @@ import {
|
||||
RawLogContent,
|
||||
RawLogViewContainer,
|
||||
} from './styles';
|
||||
import { RawLogViewProps } from './types';
|
||||
|
||||
const convert = new Convert();
|
||||
|
||||
interface RawLogViewProps {
|
||||
isActiveLog?: boolean;
|
||||
isReadOnly?: boolean;
|
||||
data: ILog;
|
||||
linesPerRow: number;
|
||||
}
|
||||
|
||||
function RawLogView(props: RawLogViewProps): JSX.Element {
|
||||
const { isActiveLog = false, isReadOnly = false, data, linesPerRow } = props;
|
||||
|
||||
function RawLogView({
|
||||
isActiveLog,
|
||||
isReadOnly,
|
||||
data,
|
||||
linesPerRow,
|
||||
isTextOverflowEllipsisDisabled,
|
||||
}: RawLogViewProps): JSX.Element {
|
||||
const { isHighlighted, isLogsExplorerPage, onLogCopy } = useCopyLogLink(
|
||||
data.id,
|
||||
);
|
||||
@ -143,6 +139,7 @@ function RawLogView(props: RawLogViewProps): JSX.Element {
|
||||
<RawLogContent
|
||||
$isReadOnly={isReadOnly}
|
||||
$isActiveLog={isActiveLog}
|
||||
$isTextOverflowEllipsisDisabled={isTextOverflowEllipsisDisabled}
|
||||
linesPerRow={linesPerRow}
|
||||
dangerouslySetInnerHTML={html}
|
||||
/>
|
||||
@ -181,6 +178,7 @@ function RawLogView(props: RawLogViewProps): JSX.Element {
|
||||
RawLogView.defaultProps = {
|
||||
isActiveLog: false,
|
||||
isReadOnly: false,
|
||||
isTextOverflowEllipsisDisabled: false,
|
||||
};
|
||||
|
||||
export default RawLogView;
|
||||
|
@ -3,10 +3,12 @@ import { Col, Row, Space } from 'antd';
|
||||
import styled from 'styled-components';
|
||||
import { getActiveLogBackground, getDefaultLogBackground } from 'utils/logs';
|
||||
|
||||
import { RawLogContentProps } from './types';
|
||||
|
||||
export const RawLogViewContainer = styled(Row)<{
|
||||
$isDarkMode: boolean;
|
||||
$isReadOnly: boolean;
|
||||
$isActiveLog: boolean;
|
||||
$isReadOnly?: boolean;
|
||||
$isActiveLog?: boolean;
|
||||
}>`
|
||||
position: relative;
|
||||
width: 100%;
|
||||
@ -31,32 +33,29 @@ export const ExpandIconWrapper = styled(Col)`
|
||||
font-size: 12px;
|
||||
`;
|
||||
|
||||
interface RawLogContentProps {
|
||||
linesPerRow: number;
|
||||
$isReadOnly: boolean;
|
||||
$isActiveLog: boolean;
|
||||
}
|
||||
|
||||
export const RawLogContent = styled.div<RawLogContentProps>`
|
||||
margin-bottom: 0;
|
||||
font-family: Fira Code, monospace;
|
||||
font-weight: 300;
|
||||
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: ${(props): number => props.linesPerRow};
|
||||
line-clamp: ${(props): number => props.linesPerRow};
|
||||
-webkit-box-orient: vertical;
|
||||
${({ $isTextOverflowEllipsisDisabled, linesPerRow }): string =>
|
||||
$isTextOverflowEllipsisDisabled
|
||||
? 'white-space: nowrap'
|
||||
: `overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: ${linesPerRow};
|
||||
line-clamp: ${linesPerRow};
|
||||
-webkit-box-orient: vertical;`};
|
||||
|
||||
font-size: 1rem;
|
||||
line-height: 2rem;
|
||||
|
||||
cursor: ${(props): string =>
|
||||
props.$isActiveLog || props.$isReadOnly ? 'initial' : 'pointer'};
|
||||
cursor: ${({ $isActiveLog, $isReadOnly }): string =>
|
||||
$isActiveLog || $isReadOnly ? 'initial' : 'pointer'};
|
||||
|
||||
${(props): string =>
|
||||
props.$isReadOnly && !props.$isActiveLog ? 'padding: 0 1.5rem;' : ''}
|
||||
${({ $isActiveLog, $isReadOnly }): string =>
|
||||
$isReadOnly && $isActiveLog ? 'padding: 0 1.5rem;' : ''}
|
||||
`;
|
||||
|
||||
export const ActionButtonsWrapper = styled(Space)`
|
||||
|
16
frontend/src/components/Logs/RawLogView/types.ts
Normal file
16
frontend/src/components/Logs/RawLogView/types.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { ILog } from 'types/api/logs/log';
|
||||
|
||||
export interface RawLogViewProps {
|
||||
isActiveLog?: boolean;
|
||||
isReadOnly?: boolean;
|
||||
isTextOverflowEllipsisDisabled?: boolean;
|
||||
data: ILog;
|
||||
linesPerRow: number;
|
||||
}
|
||||
|
||||
export interface RawLogContentProps {
|
||||
linesPerRow: number;
|
||||
$isReadOnly?: boolean;
|
||||
$isActiveLog?: boolean;
|
||||
$isTextOverflowEllipsisDisabled?: boolean;
|
||||
}
|
@ -18,12 +18,24 @@ function TextToolTip({
|
||||
}: TextToolTipProps): JSX.Element {
|
||||
const isDarkMode = useIsDarkMode();
|
||||
|
||||
const onClickHandler = (
|
||||
event: React.MouseEvent<HTMLAnchorElement, MouseEvent>,
|
||||
): void => {
|
||||
event.stopPropagation();
|
||||
};
|
||||
|
||||
const overlay = useMemo(
|
||||
() => (
|
||||
<div>
|
||||
{`${text} `}
|
||||
{url && (
|
||||
<a href={url} rel="noopener noreferrer" target="_blank">
|
||||
<a
|
||||
// Stopping event propagation on click so that parent click listener are not triggered
|
||||
onClick={onClickHandler}
|
||||
href={url}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
{urlText || 'here'}
|
||||
</a>
|
||||
)}
|
||||
|
@ -6,6 +6,7 @@ export enum FeatureKeys {
|
||||
ALERT_CHANNEL_SLACK = 'ALERT_CHANNEL_SLACK',
|
||||
ALERT_CHANNEL_WEBHOOK = 'ALERT_CHANNEL_WEBHOOK',
|
||||
ALERT_CHANNEL_PAGERDUTY = 'ALERT_CHANNEL_PAGERDUTY',
|
||||
ALERT_CHANNEL_OPSGENIE = 'ALERT_CHANNEL_OPSGENIE',
|
||||
ALERT_CHANNEL_MSTEAMS = 'ALERT_CHANNEL_MSTEAMS',
|
||||
DurationSort = 'DurationSort',
|
||||
TimestampSort = 'TimestampSort',
|
||||
|
5
frontend/src/constants/liveTail.ts
Normal file
5
frontend/src/constants/liveTail.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export const LIVE_TAIL_HEARTBEAT_TIMEOUT = 600000;
|
||||
|
||||
export const LIVE_TAIL_GRAPH_INTERVAL = 60000;
|
||||
|
||||
export const MAX_LOGS_LIST_SIZE = 1000;
|
@ -12,3 +12,8 @@ export const PANEL_TYPES_COMPONENT_MAP = {
|
||||
[PANEL_TYPES.LIST]: null,
|
||||
[PANEL_TYPES.EMPTY_WIDGET]: null,
|
||||
} as const;
|
||||
|
||||
export const AVAILABLE_EXPORT_PANEL_TYPES = [
|
||||
PANEL_TYPES.TIME_SERIES,
|
||||
PANEL_TYPES.TABLE,
|
||||
];
|
||||
|
@ -6,6 +6,8 @@ type QueryParamNames =
|
||||
| 'selectedFields'
|
||||
| 'linesPerRow';
|
||||
|
||||
export type QuerySearchParamNames = 'viewName' | 'viewKey';
|
||||
|
||||
export const queryParamNamesMap: Record<QueryParamNames, QueryParamNames> = {
|
||||
compositeQuery: 'compositeQuery',
|
||||
panelTypes: 'panelTypes',
|
||||
@ -14,3 +16,11 @@ export const queryParamNamesMap: Record<QueryParamNames, QueryParamNames> = {
|
||||
selectedFields: 'selectedFields',
|
||||
linesPerRow: 'linesPerRow',
|
||||
};
|
||||
|
||||
export const querySearchParams: Record<
|
||||
QuerySearchParamNames,
|
||||
QuerySearchParamNames
|
||||
> = {
|
||||
viewName: 'viewName',
|
||||
viewKey: 'viewKey',
|
||||
};
|
||||
|
@ -29,6 +29,7 @@ const ROUTES = {
|
||||
NOT_FOUND: '/not-found',
|
||||
LOGS: '/logs',
|
||||
LOGS_EXPLORER: '/logs-explorer',
|
||||
LIVE_LOGS: '/logs-explorer/live',
|
||||
HOME_PAGE: '/',
|
||||
PASSWORD_RESET: '/password-reset',
|
||||
LIST_LICENSES: '/licenses',
|
||||
|
@ -52,6 +52,8 @@ const themeColors = {
|
||||
gamboge: '#D89614',
|
||||
bckgGrey: '#1d1d1d',
|
||||
lightBlue: '#177ddc',
|
||||
buttonSuccessRgb: '73, 170, 25',
|
||||
red: '#E84749',
|
||||
};
|
||||
|
||||
export { themeColors };
|
||||
|
@ -40,6 +40,30 @@ export interface PagerChannel extends Channel {
|
||||
details?: string;
|
||||
detailsArray?: Record<string, string>;
|
||||
}
|
||||
|
||||
// OpsgenieChannel configures alert manager to send
|
||||
// events to opsgenie
|
||||
export interface OpsgenieChannel extends Channel {
|
||||
// ref: https://prometheus.io/docs/alerting/latest/configuration/#opsgenie_config
|
||||
api_key: string;
|
||||
|
||||
message?: string;
|
||||
|
||||
// A description of the incident
|
||||
description?: string;
|
||||
|
||||
// A backlink to the sender of the notification.
|
||||
source?: string;
|
||||
|
||||
// A set of arbitrary key/value pairs that provide further detail
|
||||
// about the alert.
|
||||
details?: string;
|
||||
detailsArray?: Record<string, string>;
|
||||
|
||||
// Priority level of alert. Possible values are P1, P2, P3, P4, and P5.
|
||||
priority?: string;
|
||||
}
|
||||
|
||||
export const ValidatePagerChannel = (p: PagerChannel): string => {
|
||||
if (!p) {
|
||||
return 'Received unexpected input for this channel, please contact your administrator ';
|
||||
@ -63,16 +87,14 @@ export const ValidatePagerChannel = (p: PagerChannel): string => {
|
||||
return '';
|
||||
};
|
||||
|
||||
export type ChannelType =
|
||||
| 'slack'
|
||||
| 'email'
|
||||
| 'webhook'
|
||||
| 'pagerduty'
|
||||
| 'msteams';
|
||||
export const SlackType: ChannelType = 'slack';
|
||||
export const WebhookType: ChannelType = 'webhook';
|
||||
export const PagerType: ChannelType = 'pagerduty';
|
||||
export const MsTeamsType: ChannelType = 'msteams';
|
||||
export enum ChannelType {
|
||||
Slack = 'slack',
|
||||
Email = 'email',
|
||||
Webhook = 'webhook',
|
||||
Pagerduty = 'pagerduty',
|
||||
Opsgenie = 'opsgenie',
|
||||
MsTeams = 'msteams',
|
||||
}
|
||||
|
||||
// LabelFilterStatement will be used for preparing filter conditions / matchers
|
||||
export interface LabelFilterStatement {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { PagerChannel } from './config';
|
||||
import { OpsgenieChannel, PagerChannel } from './config';
|
||||
|
||||
export const PagerInitialConfig: Partial<PagerChannel> = {
|
||||
description: `[{{ .Status | toUpper }}{{ if eq .Status "firing" }}:{{ .Alerts.Firing | len }}{{ end }}] {{ .CommonLabels.alertname }} for {{ .CommonLabels.job }}
|
||||
@ -22,3 +22,31 @@ export const PagerInitialConfig: Partial<PagerChannel> = {
|
||||
num_resolved: '{{ .Alerts.Resolved | len }}',
|
||||
}),
|
||||
};
|
||||
|
||||
export const OpsgenieInitialConfig: Partial<OpsgenieChannel> = {
|
||||
message: '{{ .CommonLabels.alertname }}',
|
||||
description: `{{ if gt (len .Alerts.Firing) 0 -}}
|
||||
Alerts Firing:
|
||||
{{ range .Alerts.Firing }}
|
||||
- Message: {{ .Annotations.description }}
|
||||
Labels:
|
||||
{{ range .Labels.SortedPairs }} - {{ .Name }} = {{ .Value }}
|
||||
{{ end }} Annotations:
|
||||
{{ range .Annotations.SortedPairs }} - {{ .Name }} = {{ .Value }}
|
||||
{{ end }} Source: {{ .GeneratorURL }}
|
||||
{{ end }}
|
||||
{{- end }}
|
||||
{{ if gt (len .Alerts.Resolved) 0 -}}
|
||||
Alerts Resolved:
|
||||
{{ range .Alerts.Resolved }}
|
||||
- Message: {{ .Annotations.description }}
|
||||
Labels:
|
||||
{{ range .Labels.SortedPairs }} - {{ .Name }} = {{ .Value }}
|
||||
{{ end }} Annotations:
|
||||
{{ range .Annotations.SortedPairs }} - {{ .Name }} = {{ .Value }}
|
||||
{{ end }} Source: {{ .GeneratorURL }}
|
||||
{{ end }}
|
||||
{{- end }}`,
|
||||
priority:
|
||||
'{{ if eq (index .Alerts 0).Labels.severity "critical" }}P1{{ else if eq (index .Alerts 0).Labels.severity "warning" }}P2{{ else if eq (index .Alerts 0).Labels.severity "info" }}P3{{ else }}P4{{ end }}',
|
||||
};
|
||||
|
@ -1,9 +1,11 @@
|
||||
import { Form } from 'antd';
|
||||
import createMsTeamsApi from 'api/channels/createMsTeams';
|
||||
import createOpsgenie from 'api/channels/createOpsgenie';
|
||||
import createPagerApi from 'api/channels/createPager';
|
||||
import createSlackApi from 'api/channels/createSlack';
|
||||
import createWebhookApi from 'api/channels/createWebhook';
|
||||
import testMsTeamsApi from 'api/channels/testMsTeams';
|
||||
import testOpsGenie from 'api/channels/testOpsgenie';
|
||||
import testPagerApi from 'api/channels/testPager';
|
||||
import testSlackApi from 'api/channels/testSlack';
|
||||
import testWebhookApi from 'api/channels/testWebhook';
|
||||
@ -17,19 +19,17 @@ import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
ChannelType,
|
||||
MsTeamsChannel,
|
||||
MsTeamsType,
|
||||
OpsgenieChannel,
|
||||
PagerChannel,
|
||||
PagerType,
|
||||
SlackChannel,
|
||||
SlackType,
|
||||
ValidatePagerChannel,
|
||||
WebhookChannel,
|
||||
WebhookType,
|
||||
} from './config';
|
||||
import { PagerInitialConfig } from './defaults';
|
||||
import { OpsgenieInitialConfig, PagerInitialConfig } from './defaults';
|
||||
import { isChannelType } from './utils';
|
||||
|
||||
function CreateAlertChannels({
|
||||
preType = 'slack',
|
||||
preType = ChannelType.Slack,
|
||||
}: CreateAlertChannelsProps): JSX.Element {
|
||||
// init namespace for translations
|
||||
const { t } = useTranslation('channels');
|
||||
@ -37,7 +37,13 @@ function CreateAlertChannels({
|
||||
const [formInstance] = Form.useForm();
|
||||
|
||||
const [selectedConfig, setSelectedConfig] = useState<
|
||||
Partial<SlackChannel & WebhookChannel & PagerChannel & MsTeamsChannel>
|
||||
Partial<
|
||||
SlackChannel &
|
||||
WebhookChannel &
|
||||
PagerChannel &
|
||||
MsTeamsChannel &
|
||||
OpsgenieChannel
|
||||
>
|
||||
>({
|
||||
text: `{{ range .Alerts -}}
|
||||
*Alert:* {{ .Labels.alertname }}{{ if .Labels.severity }} - {{ .Labels.severity }}{{ end }}
|
||||
@ -71,7 +77,7 @@ function CreateAlertChannels({
|
||||
const currentType = type;
|
||||
setType(value as ChannelType);
|
||||
|
||||
if (value === PagerType && currentType !== value) {
|
||||
if (value === ChannelType.Pagerduty && currentType !== value) {
|
||||
// reset config to pager defaults
|
||||
setSelectedConfig({
|
||||
name: selectedConfig?.name,
|
||||
@ -79,6 +85,13 @@ function CreateAlertChannels({
|
||||
...PagerInitialConfig,
|
||||
});
|
||||
}
|
||||
|
||||
if (value === ChannelType.Opsgenie && currentType !== value) {
|
||||
setSelectedConfig((selectedConfig) => ({
|
||||
...selectedConfig,
|
||||
...OpsgenieInitialConfig,
|
||||
}));
|
||||
}
|
||||
},
|
||||
[type, selectedConfig],
|
||||
);
|
||||
@ -239,6 +252,45 @@ function CreateAlertChannels({
|
||||
setSavingState(false);
|
||||
}, [t, notifications, preparePagerRequest]);
|
||||
|
||||
const prepareOpsgenieRequest = useCallback(
|
||||
() => ({
|
||||
api_key: selectedConfig?.api_key || '',
|
||||
name: selectedConfig?.name || '',
|
||||
send_resolved: true,
|
||||
description: selectedConfig?.description || '',
|
||||
message: selectedConfig?.message || '',
|
||||
priority: selectedConfig?.priority || '',
|
||||
}),
|
||||
[selectedConfig],
|
||||
);
|
||||
|
||||
const onOpsgenieHandler = useCallback(async () => {
|
||||
setSavingState(true);
|
||||
|
||||
try {
|
||||
const response = await createOpsgenie(prepareOpsgenieRequest());
|
||||
|
||||
if (response.statusCode === 200) {
|
||||
notifications.success({
|
||||
message: 'Success',
|
||||
description: t('channel_creation_done'),
|
||||
});
|
||||
history.replace(ROUTES.ALL_CHANNELS);
|
||||
} else {
|
||||
notifications.error({
|
||||
message: 'Error',
|
||||
description: response.error || t('channel_creation_failed'),
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
notifications.error({
|
||||
message: 'Error',
|
||||
description: t('channel_creation_failed'),
|
||||
});
|
||||
}
|
||||
setSavingState(false);
|
||||
}, [prepareOpsgenieRequest, t, notifications]);
|
||||
|
||||
const prepareMsTeamsRequest = useCallback(
|
||||
() => ({
|
||||
webhook_url: selectedConfig?.webhook_url || '',
|
||||
@ -280,26 +332,31 @@ function CreateAlertChannels({
|
||||
const onSaveHandler = useCallback(
|
||||
async (value: ChannelType) => {
|
||||
const functionMapper = {
|
||||
[SlackType]: onSlackHandler,
|
||||
[WebhookType]: onWebhookHandler,
|
||||
[PagerType]: onPagerHandler,
|
||||
[MsTeamsType]: onMsTeamsHandler,
|
||||
[ChannelType.Slack]: onSlackHandler,
|
||||
[ChannelType.Webhook]: onWebhookHandler,
|
||||
[ChannelType.Pagerduty]: onPagerHandler,
|
||||
[ChannelType.Opsgenie]: onOpsgenieHandler,
|
||||
[ChannelType.MsTeams]: onMsTeamsHandler,
|
||||
};
|
||||
const functionToCall = functionMapper[value];
|
||||
|
||||
if (functionToCall) {
|
||||
functionToCall();
|
||||
} else {
|
||||
notifications.error({
|
||||
message: 'Error',
|
||||
description: t('selected_channel_invalid'),
|
||||
});
|
||||
if (isChannelType(value)) {
|
||||
const functionToCall = functionMapper[value as keyof typeof functionMapper];
|
||||
|
||||
if (functionToCall) {
|
||||
functionToCall();
|
||||
} else {
|
||||
notifications.error({
|
||||
message: 'Error',
|
||||
description: t('selected_channel_invalid'),
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
[
|
||||
onSlackHandler,
|
||||
onWebhookHandler,
|
||||
onPagerHandler,
|
||||
onOpsgenieHandler,
|
||||
onMsTeamsHandler,
|
||||
notifications,
|
||||
t,
|
||||
@ -313,22 +370,26 @@ function CreateAlertChannels({
|
||||
let request;
|
||||
let response;
|
||||
switch (channelType) {
|
||||
case WebhookType:
|
||||
case ChannelType.Webhook:
|
||||
request = prepareWebhookRequest();
|
||||
response = await testWebhookApi(request);
|
||||
break;
|
||||
case SlackType:
|
||||
case ChannelType.Slack:
|
||||
request = prepareSlackRequest();
|
||||
response = await testSlackApi(request);
|
||||
break;
|
||||
case PagerType:
|
||||
case ChannelType.Pagerduty:
|
||||
request = preparePagerRequest();
|
||||
if (request) response = await testPagerApi(request);
|
||||
break;
|
||||
case MsTeamsType:
|
||||
case ChannelType.MsTeams:
|
||||
request = prepareMsTeamsRequest();
|
||||
response = await testMsTeamsApi(request);
|
||||
break;
|
||||
case ChannelType.Opsgenie:
|
||||
request = prepareOpsgenieRequest();
|
||||
response = await testOpsGenie(request);
|
||||
break;
|
||||
default:
|
||||
notifications.error({
|
||||
message: 'Error',
|
||||
@ -361,6 +422,7 @@ function CreateAlertChannels({
|
||||
prepareWebhookRequest,
|
||||
t,
|
||||
preparePagerRequest,
|
||||
prepareOpsgenieRequest,
|
||||
prepareSlackRequest,
|
||||
prepareMsTeamsRequest,
|
||||
notifications,
|
||||
@ -390,6 +452,7 @@ function CreateAlertChannels({
|
||||
type,
|
||||
...selectedConfig,
|
||||
...PagerInitialConfig,
|
||||
...OpsgenieInitialConfig,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
4
frontend/src/container/CreateAlertChannels/utils.ts
Normal file
4
frontend/src/container/CreateAlertChannels/utils.ts
Normal file
@ -0,0 +1,4 @@
|
||||
import { ChannelType } from './config';
|
||||
|
||||
export const isChannelType = (type: string): type is ChannelType =>
|
||||
Object.values(ChannelType).includes(type as ChannelType);
|
@ -1,9 +1,11 @@
|
||||
import { Form } from 'antd';
|
||||
import editMsTeamsApi from 'api/channels/editMsTeams';
|
||||
import editOpsgenie from 'api/channels/editOpsgenie';
|
||||
import editPagerApi from 'api/channels/editPager';
|
||||
import editSlackApi from 'api/channels/editSlack';
|
||||
import editWebhookApi from 'api/channels/editWebhook';
|
||||
import testMsTeamsApi from 'api/channels/testMsTeams';
|
||||
import testOpsgenie from 'api/channels/testOpsgenie';
|
||||
import testPagerApi from 'api/channels/testPager';
|
||||
import testSlackApi from 'api/channels/testSlack';
|
||||
import testWebhookApi from 'api/channels/testWebhook';
|
||||
@ -11,14 +13,11 @@ import ROUTES from 'constants/routes';
|
||||
import {
|
||||
ChannelType,
|
||||
MsTeamsChannel,
|
||||
MsTeamsType,
|
||||
OpsgenieChannel,
|
||||
PagerChannel,
|
||||
PagerType,
|
||||
SlackChannel,
|
||||
SlackType,
|
||||
ValidatePagerChannel,
|
||||
WebhookChannel,
|
||||
WebhookType,
|
||||
} from 'container/CreateAlertChannels/config';
|
||||
import FormAlertChannels from 'container/FormAlertChannels';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
@ -35,7 +34,13 @@ function EditAlertChannels({
|
||||
|
||||
const [formInstance] = Form.useForm();
|
||||
const [selectedConfig, setSelectedConfig] = useState<
|
||||
Partial<SlackChannel & WebhookChannel & PagerChannel & MsTeamsChannel>
|
||||
Partial<
|
||||
SlackChannel &
|
||||
WebhookChannel &
|
||||
PagerChannel &
|
||||
MsTeamsChannel &
|
||||
OpsgenieChannel
|
||||
>
|
||||
>({
|
||||
...initialValue,
|
||||
});
|
||||
@ -45,7 +50,7 @@ function EditAlertChannels({
|
||||
const { id } = useParams<{ id: string }>();
|
||||
|
||||
const [type, setType] = useState<ChannelType>(
|
||||
initialValue?.type ? (initialValue.type as ChannelType) : SlackType,
|
||||
initialValue?.type ? (initialValue.type as ChannelType) : ChannelType.Slack,
|
||||
);
|
||||
|
||||
const onTypeChangeHandler = useCallback((value: string) => {
|
||||
@ -193,6 +198,48 @@ function EditAlertChannels({
|
||||
setSavingState(false);
|
||||
}, [preparePagerRequest, notifications, selectedConfig, t]);
|
||||
|
||||
const prepareOpsgenieRequest = useCallback(
|
||||
() => ({
|
||||
name: selectedConfig.name || '',
|
||||
api_key: selectedConfig.api_key || '',
|
||||
message: selectedConfig.message || '',
|
||||
description: selectedConfig.description || '',
|
||||
priority: selectedConfig.priority || '',
|
||||
id,
|
||||
}),
|
||||
[id, selectedConfig],
|
||||
);
|
||||
|
||||
const onOpsgenieEditHandler = useCallback(async () => {
|
||||
setSavingState(true);
|
||||
|
||||
if (selectedConfig?.api_key === '') {
|
||||
notifications.error({
|
||||
message: 'Error',
|
||||
description: t('api_key_required'),
|
||||
});
|
||||
setSavingState(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await editOpsgenie(prepareOpsgenieRequest());
|
||||
|
||||
if (response.statusCode === 200) {
|
||||
notifications.success({
|
||||
message: 'Success',
|
||||
description: t('channel_edit_done'),
|
||||
});
|
||||
|
||||
history.replace(ROUTES.ALL_CHANNELS);
|
||||
} else {
|
||||
notifications.error({
|
||||
message: 'Error',
|
||||
description: response.error || t('channel_edit_failed'),
|
||||
});
|
||||
}
|
||||
setSavingState(false);
|
||||
}, [prepareOpsgenieRequest, t, notifications, selectedConfig]);
|
||||
|
||||
const prepareMsTeamsRequest = useCallback(
|
||||
() => ({
|
||||
webhook_url: selectedConfig?.webhook_url || '',
|
||||
@ -237,14 +284,16 @@ function EditAlertChannels({
|
||||
|
||||
const onSaveHandler = useCallback(
|
||||
(value: ChannelType) => {
|
||||
if (value === SlackType) {
|
||||
if (value === ChannelType.Slack) {
|
||||
onSlackEditHandler();
|
||||
} else if (value === WebhookType) {
|
||||
} else if (value === ChannelType.Webhook) {
|
||||
onWebhookEditHandler();
|
||||
} else if (value === PagerType) {
|
||||
} else if (value === ChannelType.Pagerduty) {
|
||||
onPagerEditHandler();
|
||||
} else if (value === MsTeamsType) {
|
||||
} else if (value === ChannelType.MsTeams) {
|
||||
onMsTeamsEditHandler();
|
||||
} else if (value === ChannelType.Opsgenie) {
|
||||
onOpsgenieEditHandler();
|
||||
}
|
||||
},
|
||||
[
|
||||
@ -252,6 +301,7 @@ function EditAlertChannels({
|
||||
onWebhookEditHandler,
|
||||
onPagerEditHandler,
|
||||
onMsTeamsEditHandler,
|
||||
onOpsgenieEditHandler,
|
||||
],
|
||||
);
|
||||
|
||||
@ -262,22 +312,26 @@ function EditAlertChannels({
|
||||
let request;
|
||||
let response;
|
||||
switch (channelType) {
|
||||
case WebhookType:
|
||||
case ChannelType.Webhook:
|
||||
request = prepareWebhookRequest();
|
||||
response = await testWebhookApi(request);
|
||||
break;
|
||||
case SlackType:
|
||||
case ChannelType.Slack:
|
||||
request = prepareSlackRequest();
|
||||
response = await testSlackApi(request);
|
||||
break;
|
||||
case PagerType:
|
||||
case ChannelType.Pagerduty:
|
||||
request = preparePagerRequest();
|
||||
if (request) response = await testPagerApi(request);
|
||||
break;
|
||||
case MsTeamsType:
|
||||
case ChannelType.MsTeams:
|
||||
request = prepareMsTeamsRequest();
|
||||
if (request) response = await testMsTeamsApi(request);
|
||||
break;
|
||||
case ChannelType.Opsgenie:
|
||||
request = prepareOpsgenieRequest();
|
||||
if (request) response = await testOpsgenie(request);
|
||||
break;
|
||||
default:
|
||||
notifications.error({
|
||||
message: 'Error',
|
||||
@ -312,6 +366,7 @@ function EditAlertChannels({
|
||||
preparePagerRequest,
|
||||
prepareSlackRequest,
|
||||
prepareMsTeamsRequest,
|
||||
prepareOpsgenieRequest,
|
||||
notifications,
|
||||
],
|
||||
);
|
||||
|
@ -0,0 +1,74 @@
|
||||
import { Form, Input } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { OpsgenieChannel } from '../../CreateAlertChannels/config';
|
||||
|
||||
const { TextArea } = Input;
|
||||
|
||||
function OpsgenieForm({ setSelectedConfig }: OpsgenieFormProps): JSX.Element {
|
||||
const { t } = useTranslation('channels');
|
||||
|
||||
const handleInputChange = (field: string) => (
|
||||
event: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
|
||||
): void => {
|
||||
setSelectedConfig((value) => ({
|
||||
...value,
|
||||
[field]: event.target.value,
|
||||
}));
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Form.Item name="api_key" label={t('field_opsgenie_api_key')} required>
|
||||
<Input onChange={handleInputChange('api_key')} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="message"
|
||||
help={t('help_opsgenie_message')}
|
||||
label={t('field_opsgenie_message')}
|
||||
required
|
||||
>
|
||||
<TextArea
|
||||
rows={4}
|
||||
onChange={handleInputChange('message')}
|
||||
placeholder={t('placeholder_opsgenie_message')}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="description"
|
||||
help={t('help_opsgenie_description')}
|
||||
label={t('field_opsgenie_description')}
|
||||
required
|
||||
>
|
||||
<TextArea
|
||||
rows={4}
|
||||
onChange={handleInputChange('description')}
|
||||
placeholder={t('placeholder_opsgenie_description')}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="priority"
|
||||
help={t('help_opsgenie_priority')}
|
||||
label={t('field_opsgenie_priority')}
|
||||
required
|
||||
>
|
||||
<TextArea
|
||||
rows={4}
|
||||
onChange={handleInputChange('priority')}
|
||||
placeholder={t('placeholder_opsgenie_priority')}
|
||||
/>
|
||||
</Form.Item>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
interface OpsgenieFormProps {
|
||||
setSelectedConfig: React.Dispatch<
|
||||
React.SetStateAction<Partial<OpsgenieChannel>>
|
||||
>;
|
||||
}
|
||||
|
||||
export default OpsgenieForm;
|
@ -5,13 +5,10 @@ import { FeatureKeys } from 'constants/features';
|
||||
import ROUTES from 'constants/routes';
|
||||
import {
|
||||
ChannelType,
|
||||
MsTeamsType,
|
||||
OpsgenieChannel,
|
||||
PagerChannel,
|
||||
PagerType,
|
||||
SlackChannel,
|
||||
SlackType,
|
||||
WebhookChannel,
|
||||
WebhookType,
|
||||
} from 'container/CreateAlertChannels/config';
|
||||
import useFeatureFlags from 'hooks/useFeatureFlag';
|
||||
import { isFeatureKeys } from 'hooks/useFeatureFlag/utils';
|
||||
@ -20,6 +17,7 @@ import { Dispatch, ReactElement, SetStateAction } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import MsTeamsSettings from './Settings/MsTeams';
|
||||
import OpsgenieSettings from './Settings/Opsgenie';
|
||||
import PagerSettings from './Settings/Pager';
|
||||
import SlackSettings from './Settings/Slack';
|
||||
import WebhookSettings from './Settings/Webhook';
|
||||
@ -61,14 +59,16 @@ function FormAlertChannels({
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
case SlackType:
|
||||
case ChannelType.Slack:
|
||||
return <SlackSettings setSelectedConfig={setSelectedConfig} />;
|
||||
case WebhookType:
|
||||
case ChannelType.Webhook:
|
||||
return <WebhookSettings setSelectedConfig={setSelectedConfig} />;
|
||||
case PagerType:
|
||||
case ChannelType.Pagerduty:
|
||||
return <PagerSettings setSelectedConfig={setSelectedConfig} />;
|
||||
case MsTeamsType:
|
||||
case ChannelType.MsTeams:
|
||||
return <MsTeamsSettings setSelectedConfig={setSelectedConfig} />;
|
||||
case ChannelType.Opsgenie:
|
||||
return <OpsgenieSettings setSelectedConfig={setSelectedConfig} />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
@ -102,6 +102,9 @@ function FormAlertChannels({
|
||||
<Select.Option value="pagerduty" key="pagerduty">
|
||||
Pagerduty
|
||||
</Select.Option>
|
||||
<Select.Option value="opsgenie" key="opsgenie">
|
||||
Opsgenie
|
||||
</Select.Option>
|
||||
{!isOssFeature?.active && (
|
||||
<Select.Option value="msteams" key="msteams">
|
||||
<div>
|
||||
@ -147,7 +150,9 @@ interface FormAlertChannelsProps {
|
||||
formInstance: FormInstance;
|
||||
type: ChannelType;
|
||||
setSelectedConfig: Dispatch<
|
||||
SetStateAction<Partial<SlackChannel & WebhookChannel & PagerChannel>>
|
||||
SetStateAction<
|
||||
Partial<SlackChannel & WebhookChannel & PagerChannel & OpsgenieChannel>
|
||||
>
|
||||
>;
|
||||
onTypeChangeHandler: (value: ChannelType) => void;
|
||||
onSaveHandler: (props: ChannelType) => void;
|
||||
|
@ -134,7 +134,7 @@ function GridCardGraph({
|
||||
return (
|
||||
<span ref={graphRef}>
|
||||
<WidgetGraphComponent
|
||||
enableModel={false}
|
||||
enableModel
|
||||
enableWidgetHeader
|
||||
widget={widget}
|
||||
queryResponse={queryResponse}
|
||||
|
55
frontend/src/container/LiveLogs/BackButton/index.tsx
Normal file
55
frontend/src/container/LiveLogs/BackButton/index.tsx
Normal file
@ -0,0 +1,55 @@
|
||||
import { ArrowLeftOutlined } from '@ant-design/icons';
|
||||
import { Button } from 'antd';
|
||||
import {
|
||||
initialQueryBuilderFormValuesMap,
|
||||
PANEL_TYPES,
|
||||
} from 'constants/queryBuilder';
|
||||
import { queryParamNamesMap } from 'constants/queryBuilderQueryNames';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { useGetCompositeQueryParam } from 'hooks/queryBuilder/useGetCompositeQueryParam';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useCallback } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import { constructCompositeQuery } from '../constants';
|
||||
|
||||
function BackButton(): JSX.Element {
|
||||
const history = useHistory();
|
||||
|
||||
const { updateAllQueriesOperators, resetQuery } = useQueryBuilder();
|
||||
|
||||
const compositeQuery = useGetCompositeQueryParam();
|
||||
|
||||
const handleBack = useCallback(() => {
|
||||
if (!compositeQuery) return;
|
||||
|
||||
const nextCompositeQuery = constructCompositeQuery({
|
||||
query: compositeQuery,
|
||||
initialQueryData: initialQueryBuilderFormValuesMap.logs,
|
||||
customQueryData: { disabled: false },
|
||||
});
|
||||
|
||||
const updatedQuery = updateAllQueriesOperators(
|
||||
nextCompositeQuery,
|
||||
PANEL_TYPES.LIST,
|
||||
DataSource.LOGS,
|
||||
);
|
||||
|
||||
resetQuery(updatedQuery);
|
||||
|
||||
const JSONCompositeQuery = encodeURIComponent(JSON.stringify(updatedQuery));
|
||||
|
||||
const path = `${ROUTES.LOGS_EXPLORER}?${queryParamNamesMap.compositeQuery}=${JSONCompositeQuery}`;
|
||||
|
||||
history.push(path);
|
||||
}, [history, compositeQuery, resetQuery, updateAllQueriesOperators]);
|
||||
|
||||
return (
|
||||
<Button icon={<ArrowLeftOutlined />} onClick={handleBack}>
|
||||
Exit live view
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
export default BackButton;
|
78
frontend/src/container/LiveLogs/FiltersInput/index.tsx
Normal file
78
frontend/src/container/LiveLogs/FiltersInput/index.tsx
Normal file
@ -0,0 +1,78 @@
|
||||
import { Col } from 'antd';
|
||||
import { initialQueriesMap } from 'constants/queryBuilder';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useEventSource } from 'providers/EventSource';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import {
|
||||
IBuilderQuery,
|
||||
Query,
|
||||
TagFilter,
|
||||
} from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import { getQueryWithoutFilterId } from '../utils';
|
||||
import {
|
||||
ContainerStyled,
|
||||
FilterSearchInputStyled,
|
||||
SearchButtonStyled,
|
||||
} from './styles';
|
||||
|
||||
function FiltersInput(): JSX.Element {
|
||||
const {
|
||||
stagedQuery,
|
||||
handleSetQueryData,
|
||||
redirectWithQueryBuilderData,
|
||||
currentQuery,
|
||||
} = useQueryBuilder();
|
||||
|
||||
const { initialLoading, handleSetInitialLoading } = useEventSource();
|
||||
|
||||
const handleChange = useCallback(
|
||||
(filters: TagFilter) => {
|
||||
const listQueryData = stagedQuery?.builder.queryData[0];
|
||||
|
||||
if (!listQueryData) return;
|
||||
|
||||
const queryData: IBuilderQuery = {
|
||||
...listQueryData,
|
||||
filters,
|
||||
};
|
||||
|
||||
handleSetQueryData(0, queryData);
|
||||
},
|
||||
[stagedQuery, handleSetQueryData],
|
||||
);
|
||||
|
||||
const query = useMemo(() => {
|
||||
if (stagedQuery && stagedQuery.builder.queryData.length > 0) {
|
||||
return stagedQuery?.builder.queryData[0];
|
||||
}
|
||||
|
||||
return initialQueriesMap.logs.builder.queryData[0];
|
||||
}, [stagedQuery]);
|
||||
|
||||
const handleSearch = useCallback(() => {
|
||||
if (initialLoading) {
|
||||
handleSetInitialLoading(false);
|
||||
}
|
||||
|
||||
const preparedQuery: Query = getQueryWithoutFilterId(currentQuery);
|
||||
|
||||
redirectWithQueryBuilderData(preparedQuery);
|
||||
}, [
|
||||
initialLoading,
|
||||
currentQuery,
|
||||
redirectWithQueryBuilderData,
|
||||
handleSetInitialLoading,
|
||||
]);
|
||||
|
||||
return (
|
||||
<ContainerStyled>
|
||||
<Col flex={1}>
|
||||
<FilterSearchInputStyled query={query} onChange={handleChange} />
|
||||
</Col>
|
||||
<SearchButtonStyled onSearch={handleSearch} />
|
||||
</ContainerStyled>
|
||||
);
|
||||
}
|
||||
|
||||
export default FiltersInput;
|
24
frontend/src/container/LiveLogs/FiltersInput/styles.ts
Normal file
24
frontend/src/container/LiveLogs/FiltersInput/styles.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import { Input, Row } from 'antd';
|
||||
import { themeColors } from 'constants/theme';
|
||||
import QueryBuilderSearch from 'container/QueryBuilder/filters/QueryBuilderSearch';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const FilterSearchInputStyled = styled(QueryBuilderSearch)`
|
||||
z-index: 1;
|
||||
.ant-select-selector {
|
||||
width: 100%;
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
export const ContainerStyled = styled(Row)`
|
||||
color: ${themeColors.white};
|
||||
`;
|
||||
|
||||
export const SearchButtonStyled = styled(Input.Search)`
|
||||
width: 2rem;
|
||||
.ant-input {
|
||||
display: none;
|
||||
}
|
||||
`;
|
69
frontend/src/container/LiveLogs/ListViewPanel/index.tsx
Normal file
69
frontend/src/container/LiveLogs/ListViewPanel/index.tsx
Normal file
@ -0,0 +1,69 @@
|
||||
import { Button, Popover, Select } from 'antd';
|
||||
import Spinner from 'components/Spinner';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import { useOptionsMenu } from 'container/OptionsMenu';
|
||||
import {
|
||||
defaultSelectStyle,
|
||||
logsOptions,
|
||||
viewModeOptionList,
|
||||
} from 'pages/Logs/config';
|
||||
import PopoverContent from 'pages/Logs/PopoverContent';
|
||||
import { useEventSource } from 'providers/EventSource';
|
||||
import { useCallback } from 'react';
|
||||
import { DataSource, StringOperators } from 'types/common/queryBuilder';
|
||||
|
||||
import { SpinnerWrapper, Wrapper } from './styles';
|
||||
|
||||
function ListViewPanel(): JSX.Element {
|
||||
const { config } = useOptionsMenu({
|
||||
storageKey: LOCALSTORAGE.LOGS_LIST_OPTIONS,
|
||||
dataSource: DataSource.LOGS,
|
||||
aggregateOperator: StringOperators.NOOP,
|
||||
});
|
||||
|
||||
const { isConnectionLoading } = useEventSource();
|
||||
|
||||
const isFormatButtonVisible = logsOptions.includes(config.format?.value);
|
||||
|
||||
const renderPopoverContent = useCallback(() => {
|
||||
if (!config.maxLines) return null;
|
||||
const linedPerRow = config.maxLines.value as number;
|
||||
const handleLinesPerRowChange = config.maxLines.onChange as (
|
||||
value: unknown,
|
||||
) => void;
|
||||
|
||||
return (
|
||||
<PopoverContent
|
||||
linesPerRow={linedPerRow}
|
||||
handleLinesPerRowChange={handleLinesPerRowChange}
|
||||
/>
|
||||
);
|
||||
}, [config]);
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<Select
|
||||
style={defaultSelectStyle}
|
||||
value={config.format?.value}
|
||||
onChange={config.format?.onChange}
|
||||
>
|
||||
{viewModeOptionList.map((option) => (
|
||||
<Select.Option key={option.value}>{option.label}</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
|
||||
{isFormatButtonVisible && (
|
||||
<Popover placement="right" content={renderPopoverContent}>
|
||||
<Button>Format</Button>
|
||||
</Popover>
|
||||
)}
|
||||
{isConnectionLoading && (
|
||||
<SpinnerWrapper>
|
||||
<Spinner style={{ height: 'auto' }} />
|
||||
</SpinnerWrapper>
|
||||
)}
|
||||
</Wrapper>
|
||||
);
|
||||
}
|
||||
|
||||
export default ListViewPanel;
|
11
frontend/src/container/LiveLogs/ListViewPanel/styles.ts
Normal file
11
frontend/src/container/LiveLogs/ListViewPanel/styles.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const Wrapper = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1.5rem;
|
||||
`;
|
||||
|
||||
export const SpinnerWrapper = styled.div`
|
||||
margin-left: auto;
|
||||
`;
|
203
frontend/src/container/LiveLogs/LiveLogsContainer/index.tsx
Normal file
203
frontend/src/container/LiveLogs/LiveLogsContainer/index.tsx
Normal file
@ -0,0 +1,203 @@
|
||||
import { Col } from 'antd';
|
||||
import Spinner from 'components/Spinner';
|
||||
import { MAX_LOGS_LIST_SIZE } from 'constants/liveTail';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { themeColors } from 'constants/theme';
|
||||
import GoToTop from 'container/GoToTop';
|
||||
import FiltersInput from 'container/LiveLogs/FiltersInput';
|
||||
import LiveLogsTopNav from 'container/LiveLogsTopNav';
|
||||
import { useGetCompositeQueryParam } from 'hooks/queryBuilder/useGetCompositeQueryParam';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import useDebouncedFn from 'hooks/useDebouncedFunction';
|
||||
import { useEventSourceEvent } from 'hooks/useEventSourceEvent';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { useEventSource } from 'providers/EventSource';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { prepareQueryRangePayload } from 'store/actions/dashboard/prepareQueryRangePayload';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { ILog } from 'types/api/logs/log';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
|
||||
import { idObject } from '../constants';
|
||||
import ListViewPanel from '../ListViewPanel';
|
||||
import LiveLogsList from '../LiveLogsList';
|
||||
import { QueryHistoryState } from '../types';
|
||||
import { prepareQueryByFilter } from '../utils';
|
||||
import { ContentWrapper, LiveLogsChart, Wrapper } from './styles';
|
||||
|
||||
function LiveLogsContainer(): JSX.Element {
|
||||
const location = useLocation();
|
||||
const [logs, setLogs] = useState<ILog[]>([]);
|
||||
|
||||
const { stagedQuery } = useQueryBuilder();
|
||||
|
||||
const queryLocationState = location.state as QueryHistoryState;
|
||||
|
||||
const batchedEventsRef = useRef<ILog[]>([]);
|
||||
|
||||
const { notifications } = useNotifications();
|
||||
|
||||
const { selectedTime: globalSelectedTime } = useSelector<
|
||||
AppState,
|
||||
GlobalReducer
|
||||
>((state) => state.globalTime);
|
||||
|
||||
const {
|
||||
handleStartOpenConnection,
|
||||
handleCloseConnection,
|
||||
initialLoading,
|
||||
isConnectionLoading,
|
||||
} = useEventSource();
|
||||
|
||||
const compositeQuery = useGetCompositeQueryParam();
|
||||
|
||||
const updateLogs = useCallback((newLogs: ILog[]) => {
|
||||
setLogs((prevState) =>
|
||||
[...newLogs, ...prevState].slice(0, MAX_LOGS_LIST_SIZE),
|
||||
);
|
||||
|
||||
batchedEventsRef.current = [];
|
||||
}, []);
|
||||
|
||||
const debouncedUpdateLogs = useDebouncedFn(() => {
|
||||
const reversedData = batchedEventsRef.current.reverse();
|
||||
updateLogs(reversedData);
|
||||
}, 500);
|
||||
|
||||
const batchLiveLog = useCallback(
|
||||
(log: ILog): void => {
|
||||
batchedEventsRef.current.push(log);
|
||||
|
||||
debouncedUpdateLogs();
|
||||
},
|
||||
[debouncedUpdateLogs],
|
||||
);
|
||||
|
||||
const handleGetLiveLogs = useCallback(
|
||||
(event: MessageEvent<string>) => {
|
||||
const data: ILog = JSON.parse(event.data);
|
||||
|
||||
batchLiveLog(data);
|
||||
},
|
||||
[batchLiveLog],
|
||||
);
|
||||
|
||||
const handleError = useCallback(() => {
|
||||
notifications.error({ message: 'Sorry, something went wrong' });
|
||||
}, [notifications]);
|
||||
|
||||
useEventSourceEvent('message', handleGetLiveLogs);
|
||||
useEventSourceEvent('error', handleError);
|
||||
|
||||
const getPreparedQuery = useCallback(
|
||||
(query: Query): Query => {
|
||||
const firstLogId: string | null = logs.length ? logs[0].id : null;
|
||||
|
||||
const preparedQuery: Query = prepareQueryByFilter(
|
||||
query,
|
||||
idObject,
|
||||
firstLogId,
|
||||
);
|
||||
|
||||
return preparedQuery;
|
||||
},
|
||||
[logs],
|
||||
);
|
||||
|
||||
const openConnection = useCallback(
|
||||
(query: Query) => {
|
||||
const { queryPayload } = prepareQueryRangePayload({
|
||||
query,
|
||||
graphType: PANEL_TYPES.LIST,
|
||||
selectedTime: 'GLOBAL_TIME',
|
||||
globalSelectedInterval: globalSelectedTime,
|
||||
});
|
||||
|
||||
const encodedQueryPayload = encodeURIComponent(JSON.stringify(queryPayload));
|
||||
const queryString = `q=${encodedQueryPayload}`;
|
||||
|
||||
handleStartOpenConnection({ queryString });
|
||||
},
|
||||
[globalSelectedTime, handleStartOpenConnection],
|
||||
);
|
||||
|
||||
const handleStartNewConnection = useCallback(
|
||||
(query: Query) => {
|
||||
handleCloseConnection();
|
||||
|
||||
const preparedQuery = getPreparedQuery(query);
|
||||
|
||||
openConnection(preparedQuery);
|
||||
},
|
||||
[getPreparedQuery, handleCloseConnection, openConnection],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!compositeQuery) return;
|
||||
|
||||
if (
|
||||
(initialLoading && !isConnectionLoading) ||
|
||||
compositeQuery.id !== stagedQuery?.id
|
||||
) {
|
||||
handleStartNewConnection(compositeQuery);
|
||||
}
|
||||
}, [
|
||||
compositeQuery,
|
||||
initialLoading,
|
||||
stagedQuery,
|
||||
isConnectionLoading,
|
||||
openConnection,
|
||||
handleStartNewConnection,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
const prefetchedList = queryLocationState?.listQueryPayload[0]?.list;
|
||||
|
||||
if (prefetchedList) {
|
||||
const prefetchedLogs: ILog[] = prefetchedList
|
||||
.map((item) => ({
|
||||
...item.data,
|
||||
timestamp: item.timestamp,
|
||||
}))
|
||||
.reverse();
|
||||
|
||||
updateLogs(prefetchedLogs);
|
||||
}
|
||||
}, [queryLocationState, updateLogs]);
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<LiveLogsTopNav />
|
||||
<ContentWrapper gutter={[0, 20]} style={{ color: themeColors.lightWhite }}>
|
||||
<Col span={24}>
|
||||
<FiltersInput />
|
||||
</Col>
|
||||
{initialLoading && logs.length === 0 ? (
|
||||
<Col span={24}>
|
||||
<Spinner style={{ height: 'auto' }} tip="Fetching Logs" />
|
||||
</Col>
|
||||
) : (
|
||||
<>
|
||||
<Col span={24}>
|
||||
<LiveLogsChart
|
||||
initialData={queryLocationState?.graphQueryPayload || null}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<ListViewPanel />
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<LiveLogsList logs={logs} />
|
||||
</Col>
|
||||
</>
|
||||
)}
|
||||
<GoToTop />
|
||||
</ContentWrapper>
|
||||
</Wrapper>
|
||||
);
|
||||
}
|
||||
|
||||
export default LiveLogsContainer;
|
17
frontend/src/container/LiveLogs/LiveLogsContainer/styles.ts
Normal file
17
frontend/src/container/LiveLogs/LiveLogsContainer/styles.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { Row } from 'antd';
|
||||
import { themeColors } from 'constants/theme';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import LiveLogsListChart from '../LiveLogsListChart';
|
||||
|
||||
export const LiveLogsChart = styled(LiveLogsListChart)`
|
||||
margin-bottom: 0.5rem;
|
||||
`;
|
||||
|
||||
export const ContentWrapper = styled(Row)`
|
||||
color: rgba(${(themeColors.white, 0.85)});
|
||||
`;
|
||||
|
||||
export const Wrapper = styled.div`
|
||||
padding-bottom: 4rem;
|
||||
`;
|
131
frontend/src/container/LiveLogs/LiveLogsList/index.tsx
Normal file
131
frontend/src/container/LiveLogs/LiveLogsList/index.tsx
Normal file
@ -0,0 +1,131 @@
|
||||
import { Card, Typography } from 'antd';
|
||||
import ListLogView from 'components/Logs/ListLogView';
|
||||
import RawLogView from 'components/Logs/RawLogView';
|
||||
import Spinner from 'components/Spinner';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import { OptionFormatTypes } from 'constants/optionsFormatTypes';
|
||||
import InfinityTableView from 'container/LogsExplorerList/InfinityTableView';
|
||||
import { InfinityWrapperStyled } from 'container/LogsExplorerList/styles';
|
||||
import { convertKeysToColumnFields } from 'container/LogsExplorerList/utils';
|
||||
import { Heading } from 'container/LogsTable/styles';
|
||||
import { useOptionsMenu } from 'container/OptionsMenu';
|
||||
import { contentStyle } from 'container/Trace/Search/config';
|
||||
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
|
||||
import useFontFaceObserver from 'hooks/useFontObserver';
|
||||
import { useEventSource } from 'providers/EventSource';
|
||||
import { memo, useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Virtuoso, VirtuosoHandle } from 'react-virtuoso';
|
||||
// interfaces
|
||||
import { ILog } from 'types/api/logs/log';
|
||||
import { DataSource, StringOperators } from 'types/common/queryBuilder';
|
||||
|
||||
import { LiveLogsListProps } from './types';
|
||||
|
||||
function LiveLogsList({ logs }: LiveLogsListProps): JSX.Element {
|
||||
const ref = useRef<VirtuosoHandle>(null);
|
||||
|
||||
const { t } = useTranslation(['logs']);
|
||||
|
||||
const { isConnectionLoading } = useEventSource();
|
||||
|
||||
const { activeLogId } = useCopyLogLink();
|
||||
|
||||
const { options } = useOptionsMenu({
|
||||
storageKey: LOCALSTORAGE.LOGS_LIST_OPTIONS,
|
||||
dataSource: DataSource.LOGS,
|
||||
aggregateOperator: StringOperators.NOOP,
|
||||
});
|
||||
|
||||
const activeLogIndex = useMemo(
|
||||
() => logs.findIndex(({ id }) => id === activeLogId),
|
||||
[logs, activeLogId],
|
||||
);
|
||||
|
||||
useFontFaceObserver(
|
||||
[
|
||||
{
|
||||
family: 'Fira Code',
|
||||
weight: '300',
|
||||
},
|
||||
],
|
||||
options.format === 'raw',
|
||||
{
|
||||
timeout: 5000,
|
||||
},
|
||||
);
|
||||
|
||||
const selectedFields = convertKeysToColumnFields(options.selectColumns);
|
||||
|
||||
const getItemContent = useCallback(
|
||||
(_: number, log: ILog): JSX.Element => {
|
||||
if (options.format === 'raw') {
|
||||
return (
|
||||
<RawLogView key={log.id} data={log} linesPerRow={options.maxLines} />
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ListLogView key={log.id} logData={log} selectedFields={selectedFields} />
|
||||
);
|
||||
},
|
||||
[options.format, options.maxLines, selectedFields],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!activeLogId || activeLogIndex < 0) return;
|
||||
|
||||
ref?.current?.scrollToIndex({
|
||||
index: activeLogIndex,
|
||||
align: 'start',
|
||||
behavior: 'smooth',
|
||||
});
|
||||
}, [activeLogId, activeLogIndex]);
|
||||
|
||||
const isLoadingList = isConnectionLoading && logs.length === 0;
|
||||
|
||||
if (isLoadingList) {
|
||||
return <Spinner style={{ height: 'auto' }} tip="Fetching Logs" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{options.format !== OptionFormatTypes.TABLE && (
|
||||
<Heading>
|
||||
<Typography.Text>Event</Typography.Text>
|
||||
</Heading>
|
||||
)}
|
||||
|
||||
{logs.length === 0 && <Typography>{t('fetching_log_lines')}</Typography>}
|
||||
|
||||
{logs.length !== 0 && (
|
||||
<InfinityWrapperStyled>
|
||||
{options.format === 'table' ? (
|
||||
<InfinityTableView
|
||||
ref={ref}
|
||||
isLoading={false}
|
||||
tableViewProps={{
|
||||
logs,
|
||||
fields: selectedFields,
|
||||
linesPerRow: options.maxLines,
|
||||
appendTo: 'end',
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Card style={{ width: '100%' }} bodyStyle={{ ...contentStyle }}>
|
||||
<Virtuoso
|
||||
ref={ref}
|
||||
useWindowScroll
|
||||
data={logs}
|
||||
totalCount={logs.length}
|
||||
itemContent={getItemContent}
|
||||
/>
|
||||
</Card>
|
||||
)}
|
||||
</InfinityWrapperStyled>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(LiveLogsList);
|
5
frontend/src/container/LiveLogs/LiveLogsList/types.ts
Normal file
5
frontend/src/container/LiveLogs/LiveLogsList/types.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { ILog } from 'types/api/logs/log';
|
||||
|
||||
export type LiveLogsListProps = {
|
||||
logs: ILog[];
|
||||
};
|
70
frontend/src/container/LiveLogs/LiveLogsListChart/index.tsx
Normal file
70
frontend/src/container/LiveLogs/LiveLogsListChart/index.tsx
Normal file
@ -0,0 +1,70 @@
|
||||
import { LIVE_TAIL_GRAPH_INTERVAL } from 'constants/liveTail';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import LogsExplorerChart from 'container/LogsExplorerChart';
|
||||
import { useGetExplorerQueryRange } from 'hooks/queryBuilder/useGetExplorerQueryRange';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useEventSource } from 'providers/EventSource';
|
||||
import { useMemo } from 'react';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { QueryData } from 'types/api/widgets/getQuery';
|
||||
import { DataSource, LogsAggregatorOperator } from 'types/common/queryBuilder';
|
||||
|
||||
import { LiveLogsListChartProps } from './types';
|
||||
|
||||
function LiveLogsListChart({
|
||||
className,
|
||||
initialData,
|
||||
}: LiveLogsListChartProps): JSX.Element {
|
||||
const { stagedQuery } = useQueryBuilder();
|
||||
const { isConnectionOpen } = useEventSource();
|
||||
|
||||
const listChartQuery: Query | null = useMemo(() => {
|
||||
if (!stagedQuery) return null;
|
||||
|
||||
return {
|
||||
...stagedQuery,
|
||||
builder: {
|
||||
...stagedQuery.builder,
|
||||
queryData: stagedQuery.builder.queryData.map((item) => ({
|
||||
...item,
|
||||
disabled: false,
|
||||
aggregateOperator: LogsAggregatorOperator.COUNT,
|
||||
filters: {
|
||||
...item.filters,
|
||||
items: item.filters.items.filter((item) => item.key?.key !== 'id'),
|
||||
},
|
||||
})),
|
||||
},
|
||||
};
|
||||
}, [stagedQuery]);
|
||||
|
||||
const { data, isFetching } = useGetExplorerQueryRange(
|
||||
listChartQuery,
|
||||
PANEL_TYPES.TIME_SERIES,
|
||||
{
|
||||
enabled: isConnectionOpen,
|
||||
refetchInterval: LIVE_TAIL_GRAPH_INTERVAL,
|
||||
keepPreviousData: true,
|
||||
},
|
||||
{ dataSource: DataSource.LOGS },
|
||||
);
|
||||
|
||||
const chartData: QueryData[] = useMemo(() => {
|
||||
if (initialData) return initialData;
|
||||
|
||||
if (!data) return [];
|
||||
|
||||
return data.payload.data.result;
|
||||
}, [data, initialData]);
|
||||
|
||||
return (
|
||||
<LogsExplorerChart
|
||||
isLoading={initialData ? false : isFetching}
|
||||
data={chartData}
|
||||
isLabelEnabled={false}
|
||||
className={className}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default LiveLogsListChart;
|
@ -0,0 +1,6 @@
|
||||
import { QueryData } from 'types/api/widgets/getQuery';
|
||||
|
||||
export type LiveLogsListChartProps = {
|
||||
className?: string;
|
||||
initialData: QueryData[] | null;
|
||||
};
|
50
frontend/src/container/LiveLogs/constants.ts
Normal file
50
frontend/src/container/LiveLogs/constants.ts
Normal file
@ -0,0 +1,50 @@
|
||||
import {
|
||||
initialQueriesMap,
|
||||
initialQueryBuilderFormValuesMap,
|
||||
} from 'constants/queryBuilder';
|
||||
import { FILTERS } from 'container/QueryBuilder/filters/OrderByFilter/config';
|
||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { IBuilderQuery, Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { LogsAggregatorOperator } from 'types/common/queryBuilder';
|
||||
|
||||
export const defaultLiveQueryDataConfig: Partial<IBuilderQuery> = {
|
||||
aggregateOperator: LogsAggregatorOperator.NOOP,
|
||||
disabled: true,
|
||||
pageSize: 10,
|
||||
orderBy: [{ columnName: 'timestamp', order: FILTERS.DESC }],
|
||||
};
|
||||
|
||||
type GetDefaultCompositeQueryParams = {
|
||||
query: Query;
|
||||
initialQueryData: IBuilderQuery;
|
||||
customQueryData?: Partial<IBuilderQuery>;
|
||||
};
|
||||
|
||||
export const constructCompositeQuery = ({
|
||||
query,
|
||||
initialQueryData,
|
||||
customQueryData,
|
||||
}: GetDefaultCompositeQueryParams): Query => ({
|
||||
...query,
|
||||
builder: {
|
||||
...query.builder,
|
||||
queryData: query.builder.queryData.map((item) => ({
|
||||
...initialQueryData,
|
||||
...item,
|
||||
...customQueryData,
|
||||
})),
|
||||
},
|
||||
});
|
||||
|
||||
export const liveLogsCompositeQuery = constructCompositeQuery({
|
||||
query: initialQueriesMap.logs,
|
||||
initialQueryData: initialQueryBuilderFormValuesMap.logs,
|
||||
customQueryData: defaultLiveQueryDataConfig,
|
||||
});
|
||||
|
||||
export const idObject: BaseAutocompleteData = {
|
||||
key: 'id',
|
||||
type: '',
|
||||
dataType: 'string',
|
||||
isColumn: true,
|
||||
};
|
6
frontend/src/container/LiveLogs/types.ts
Normal file
6
frontend/src/container/LiveLogs/types.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { QueryData, QueryDataV3 } from 'types/api/widgets/getQuery';
|
||||
|
||||
export type QueryHistoryState = {
|
||||
graphQueryPayload: QueryData[];
|
||||
listQueryPayload: QueryDataV3[];
|
||||
};
|
71
frontend/src/container/LiveLogs/utils.ts
Normal file
71
frontend/src/container/LiveLogs/utils.ts
Normal file
@ -0,0 +1,71 @@
|
||||
import { OPERATORS } from 'constants/queryBuilder';
|
||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import {
|
||||
Query,
|
||||
TagFilter,
|
||||
TagFilterItem,
|
||||
} from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
const getIdFilter = (filtersItems: TagFilterItem[]): TagFilterItem | null =>
|
||||
filtersItems.find((item) => item.key?.key === 'id') || null;
|
||||
|
||||
const getFilter = (
|
||||
filters: TagFilter,
|
||||
tagFilter: BaseAutocompleteData,
|
||||
value: string,
|
||||
): TagFilter => {
|
||||
let newItems = filters.items;
|
||||
|
||||
const isExistIdFilter = getIdFilter(newItems);
|
||||
|
||||
if (isExistIdFilter) {
|
||||
newItems = newItems.map((item) =>
|
||||
item.key?.key === 'id' ? { ...item, value } : item,
|
||||
);
|
||||
} else {
|
||||
newItems = [
|
||||
...newItems,
|
||||
{ value, key: tagFilter, op: OPERATORS['>'], id: uuid() },
|
||||
];
|
||||
}
|
||||
|
||||
return { items: newItems, op: filters.op };
|
||||
};
|
||||
|
||||
export const prepareQueryByFilter = (
|
||||
query: Query,
|
||||
tagFilter: BaseAutocompleteData,
|
||||
value: string | null,
|
||||
): Query => {
|
||||
const preparedQuery: Query = {
|
||||
...query,
|
||||
builder: {
|
||||
...query.builder,
|
||||
queryData: query.builder.queryData.map((item) => ({
|
||||
...item,
|
||||
filters: value ? getFilter(item.filters, tagFilter, value) : item.filters,
|
||||
})),
|
||||
},
|
||||
};
|
||||
|
||||
return preparedQuery;
|
||||
};
|
||||
|
||||
export const getQueryWithoutFilterId = (query: Query): Query => {
|
||||
const preparedQuery: Query = {
|
||||
...query,
|
||||
builder: {
|
||||
...query.builder,
|
||||
queryData: query.builder.queryData.map((item) => ({
|
||||
...item,
|
||||
filters: {
|
||||
...item.filters,
|
||||
items: item.filters.items.filter((item) => item.key?.key !== 'id'),
|
||||
},
|
||||
})),
|
||||
},
|
||||
};
|
||||
|
||||
return preparedQuery;
|
||||
};
|
71
frontend/src/container/LiveLogsTopNav/index.tsx
Normal file
71
frontend/src/container/LiveLogsTopNav/index.tsx
Normal file
@ -0,0 +1,71 @@
|
||||
import { PauseCircleFilled, PlayCircleFilled } from '@ant-design/icons';
|
||||
import { Space } from 'antd';
|
||||
import BackButton from 'container/LiveLogs/BackButton';
|
||||
import { getQueryWithoutFilterId } from 'container/LiveLogs/utils';
|
||||
import LocalTopNav from 'container/LocalTopNav';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useEventSource } from 'providers/EventSource';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
|
||||
import { LiveButtonStyled } from './styles';
|
||||
|
||||
function LiveLogsTopNav(): JSX.Element {
|
||||
const {
|
||||
isConnectionOpen,
|
||||
isConnectionLoading,
|
||||
initialLoading,
|
||||
handleCloseConnection,
|
||||
handleSetInitialLoading,
|
||||
} = useEventSource();
|
||||
|
||||
const { redirectWithQueryBuilderData, currentQuery } = useQueryBuilder();
|
||||
|
||||
const isPlaying = isConnectionOpen || isConnectionLoading || initialLoading;
|
||||
|
||||
const onLiveButtonClick = useCallback(() => {
|
||||
if (initialLoading) {
|
||||
handleSetInitialLoading(false);
|
||||
}
|
||||
|
||||
if ((!isConnectionOpen && isConnectionLoading) || isConnectionOpen) {
|
||||
handleCloseConnection();
|
||||
} else {
|
||||
const preparedQuery = getQueryWithoutFilterId(currentQuery);
|
||||
redirectWithQueryBuilderData(preparedQuery);
|
||||
}
|
||||
}, [
|
||||
initialLoading,
|
||||
isConnectionOpen,
|
||||
isConnectionLoading,
|
||||
currentQuery,
|
||||
handleSetInitialLoading,
|
||||
handleCloseConnection,
|
||||
redirectWithQueryBuilderData,
|
||||
]);
|
||||
|
||||
const liveButton = useMemo(
|
||||
() => (
|
||||
<Space size={16}>
|
||||
<LiveButtonStyled
|
||||
icon={isPlaying ? <PauseCircleFilled /> : <PlayCircleFilled />}
|
||||
danger={isPlaying}
|
||||
onClick={onLiveButtonClick}
|
||||
type="primary"
|
||||
>
|
||||
{isPlaying ? 'Pause' : 'Resume'}
|
||||
</LiveButtonStyled>
|
||||
<BackButton />
|
||||
</Space>
|
||||
),
|
||||
[isPlaying, onLiveButtonClick],
|
||||
);
|
||||
|
||||
return (
|
||||
<LocalTopNav
|
||||
actions={liveButton}
|
||||
renderPermissions={{ isDateTimeEnabled: false }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(LiveLogsTopNav);
|
20
frontend/src/container/LiveLogsTopNav/styles.ts
Normal file
20
frontend/src/container/LiveLogsTopNav/styles.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { Button, ButtonProps } from 'antd';
|
||||
import { themeColors } from 'constants/theme';
|
||||
import styled, { css, FlattenSimpleInterpolation } from 'styled-components';
|
||||
|
||||
export const LiveButtonStyled = styled(Button)<ButtonProps>`
|
||||
background-color: rgba(${themeColors.buttonSuccessRgb}, 0.9);
|
||||
|
||||
${({ danger }): FlattenSimpleInterpolation =>
|
||||
!danger
|
||||
? css`
|
||||
&:hover {
|
||||
background-color: rgba(${themeColors.buttonSuccessRgb}, 1) !important;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: rgba(${themeColors.buttonSuccessRgb}, 0.7) !important;
|
||||
}
|
||||
`
|
||||
: css``}
|
||||
`;
|
34
frontend/src/container/LocalTopNav/index.tsx
Normal file
34
frontend/src/container/LocalTopNav/index.tsx
Normal file
@ -0,0 +1,34 @@
|
||||
import { Col, Row, Space } from 'antd';
|
||||
|
||||
import ShowBreadcrumbs from '../TopNav/Breadcrumbs';
|
||||
import DateTimeSelector from '../TopNav/DateTimeSelection';
|
||||
import { Container } from './styles';
|
||||
import { LocalTopNavProps } from './types';
|
||||
|
||||
function LocalTopNav({
|
||||
actions,
|
||||
renderPermissions,
|
||||
}: LocalTopNavProps): JSX.Element | null {
|
||||
return (
|
||||
<Container>
|
||||
<Col span={16}>
|
||||
<ShowBreadcrumbs />
|
||||
</Col>
|
||||
|
||||
<Col span={8}>
|
||||
<Row justify="end">
|
||||
<Space align="start" size={30} direction="horizontal">
|
||||
{actions}
|
||||
{renderPermissions?.isDateTimeEnabled && (
|
||||
<div>
|
||||
<DateTimeSelector />
|
||||
</div>
|
||||
)}
|
||||
</Space>
|
||||
</Row>
|
||||
</Col>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
export default LocalTopNav;
|
9
frontend/src/container/LocalTopNav/styles.ts
Normal file
9
frontend/src/container/LocalTopNav/styles.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { Row } from 'antd';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const Container = styled(Row)`
|
||||
&&& {
|
||||
margin-top: 2rem;
|
||||
min-height: 8vh;
|
||||
}
|
||||
`;
|
6
frontend/src/container/LocalTopNav/types.ts
Normal file
6
frontend/src/container/LocalTopNav/types.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
export type LocalTopNavProps = {
|
||||
actions?: ReactNode;
|
||||
renderPermissions?: { isDateTimeEnabled: boolean };
|
||||
};
|
@ -91,7 +91,7 @@ function TableView({
|
||||
const columns: ColumnsType<DataType> = [
|
||||
{
|
||||
title: 'Action',
|
||||
width: 15,
|
||||
width: 30,
|
||||
render: (fieldData: Record<string, string>): JSX.Element | null => {
|
||||
const fieldKey = fieldData.field.split('.').slice(-1);
|
||||
if (!RESTRICTED_FIELDS.includes(fieldKey[0])) {
|
||||
|
@ -154,7 +154,13 @@ function LogsContextList({
|
||||
|
||||
const getItemContent = useCallback(
|
||||
(_: number, log: ILog): JSX.Element => (
|
||||
<RawLogView isReadOnly key={log.id} data={log} linesPerRow={1} />
|
||||
<RawLogView
|
||||
isReadOnly
|
||||
isTextOverflowEllipsisDisabled
|
||||
key={log.id}
|
||||
data={log}
|
||||
linesPerRow={1}
|
||||
/>
|
||||
),
|
||||
[],
|
||||
);
|
||||
|
@ -3,4 +3,6 @@ import { QueryData } from 'types/api/widgets/getQuery';
|
||||
export type LogsExplorerChartProps = {
|
||||
data: QueryData[];
|
||||
isLoading: boolean;
|
||||
isLabelEnabled?: boolean;
|
||||
className?: string;
|
||||
};
|
||||
|
@ -1,8 +1,9 @@
|
||||
import Graph from 'components/Graph';
|
||||
import Spinner from 'components/Spinner';
|
||||
import { themeColors } from 'constants/theme';
|
||||
import getChartData, { GetChartDataProps } from 'lib/getChartData';
|
||||
import { colors } from 'lib/getRandomColor';
|
||||
import { memo, useMemo } from 'react';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
|
||||
import { LogsExplorerChartProps } from './LogsExplorerChart.interfaces';
|
||||
import { CardStyled } from './LogsExplorerChart.styled';
|
||||
@ -10,17 +11,22 @@ import { CardStyled } from './LogsExplorerChart.styled';
|
||||
function LogsExplorerChart({
|
||||
data,
|
||||
isLoading,
|
||||
isLabelEnabled = true,
|
||||
className,
|
||||
}: LogsExplorerChartProps): JSX.Element {
|
||||
const handleCreateDatasets: Required<GetChartDataProps>['createDataset'] = (
|
||||
element,
|
||||
index,
|
||||
allLabels,
|
||||
) => ({
|
||||
label: allLabels[index],
|
||||
data: element,
|
||||
backgroundColor: colors[index % colors.length] || 'red',
|
||||
borderColor: colors[index % colors.length] || 'red',
|
||||
});
|
||||
const handleCreateDatasets: Required<GetChartDataProps>['createDataset'] = useCallback(
|
||||
(element, index, allLabels) => ({
|
||||
data: element,
|
||||
backgroundColor: colors[index % colors.length] || themeColors.red,
|
||||
borderColor: colors[index % colors.length] || themeColors.red,
|
||||
...(isLabelEnabled
|
||||
? {
|
||||
label: allLabels[index],
|
||||
}
|
||||
: {}),
|
||||
}),
|
||||
[isLabelEnabled],
|
||||
);
|
||||
|
||||
const graphData = useMemo(
|
||||
() =>
|
||||
@ -32,11 +38,11 @@ function LogsExplorerChart({
|
||||
],
|
||||
createDataset: handleCreateDatasets,
|
||||
}),
|
||||
[data],
|
||||
[data, handleCreateDatasets],
|
||||
);
|
||||
|
||||
return (
|
||||
<CardStyled>
|
||||
<CardStyled className={className}>
|
||||
{isLoading ? (
|
||||
<Spinner size="default" height="100%" />
|
||||
) : (
|
||||
|
@ -9,7 +9,7 @@ import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { memo, useCallback, useMemo, useState } from 'react';
|
||||
import { Query, TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import { EditButton, TitleWrapper } from './styles';
|
||||
import { EditButton, LogContainer, TitleWrapper } from './styles';
|
||||
import { LogsExplorerContextProps } from './types';
|
||||
import useInitialQuery from './useInitialQuery';
|
||||
|
||||
@ -96,7 +96,15 @@ function LogsExplorerContext({
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...contextListParams}
|
||||
/>
|
||||
<RawLogView isActiveLog isReadOnly data={log} linesPerRow={1} />
|
||||
<LogContainer>
|
||||
<RawLogView
|
||||
isActiveLog
|
||||
isReadOnly
|
||||
isTextOverflowEllipsisDisabled
|
||||
data={log}
|
||||
linesPerRow={1}
|
||||
/>
|
||||
</LogContainer>
|
||||
<LogsContextList
|
||||
order={FILTERS.DESC}
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
|
@ -28,3 +28,7 @@ export const EditButton = styled(Button)<{ $isDarkMode: boolean }>`
|
||||
? getAlphaColor(themeColors.white)[45]
|
||||
: getAlphaColor(themeColors.black)[45]};
|
||||
`;
|
||||
|
||||
export const LogContainer = styled.div`
|
||||
overflow-x: auto;
|
||||
`;
|
||||
|
@ -10,6 +10,7 @@ import { getDraggedColumns } from 'hooks/useDragColumns/utils';
|
||||
import {
|
||||
cloneElement,
|
||||
forwardRef,
|
||||
memo,
|
||||
ReactElement,
|
||||
ReactNode,
|
||||
useCallback,
|
||||
@ -67,7 +68,6 @@ const InfinityTable = forwardRef<TableVirtuosoHandle, InfinityTableProps>(
|
||||
onAddToQuery,
|
||||
} = useActiveLog();
|
||||
|
||||
const { onEndReached } = infitiyTableProps;
|
||||
const { dataSource, columns } = useTableView({
|
||||
...tableViewProps,
|
||||
onClickExpand: onSetActiveLog,
|
||||
@ -158,8 +158,11 @@ const InfinityTable = forwardRef<TableVirtuosoHandle, InfinityTableProps>(
|
||||
}}
|
||||
itemContent={itemContent}
|
||||
fixedHeaderContent={tableHeader}
|
||||
endReached={onEndReached}
|
||||
totalCount={dataSource.length}
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...(infitiyTableProps?.onEndReached
|
||||
? { endReached: infitiyTableProps.onEndReached }
|
||||
: {})}
|
||||
/>
|
||||
|
||||
{activeContextLog && (
|
||||
@ -179,4 +182,4 @@ const InfinityTable = forwardRef<TableVirtuosoHandle, InfinityTableProps>(
|
||||
},
|
||||
);
|
||||
|
||||
export default InfinityTable;
|
||||
export default memo(InfinityTable);
|
||||
|
@ -3,7 +3,7 @@ import { UseTableViewProps } from 'components/Logs/TableView/types';
|
||||
export type InfinityTableProps = {
|
||||
isLoading?: boolean;
|
||||
tableViewProps: Omit<UseTableViewProps, 'onOpenLogsContext' | 'onClickExpand'>;
|
||||
infitiyTableProps: {
|
||||
infitiyTableProps?: {
|
||||
onEndReached: (index: number) => void;
|
||||
};
|
||||
};
|
||||
|
@ -1,22 +1,19 @@
|
||||
import { Tabs, TabsProps } from 'antd';
|
||||
import TabLabel from 'components/TabLabel';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { AVAILABLE_EXPORT_PANEL_TYPES } from 'constants/panelTypes';
|
||||
import {
|
||||
initialAutocompleteData,
|
||||
initialFilters,
|
||||
initialQueriesMap,
|
||||
initialQueryBuilderFormValues,
|
||||
PANEL_TYPES,
|
||||
} from 'constants/queryBuilder';
|
||||
import { queryParamNamesMap } from 'constants/queryBuilderQueryNames';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { DEFAULT_PER_PAGE_VALUE } from 'container/Controls/config';
|
||||
import ExportPanel from 'container/ExportPanel';
|
||||
import GoToTop from 'container/GoToTop';
|
||||
import LogsExplorerChart from 'container/LogsExplorerChart';
|
||||
import LogsExplorerList from 'container/LogsExplorerList';
|
||||
import LogsExplorerTable from 'container/LogsExplorerTable';
|
||||
import { SIGNOZ_VALUE } from 'container/QueryBuilder/filters/OrderByFilter/constants';
|
||||
import TimeSeriesView from 'container/TimeSeriesView/TimeSeriesView';
|
||||
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
|
||||
import { addEmptyWidgetInDashboardJSONWithQuery } from 'hooks/dashboard/utils';
|
||||
@ -25,12 +22,13 @@ import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
|
||||
import { useGetExplorerQueryRange } from 'hooks/queryBuilder/useGetExplorerQueryRange';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import useAxiosError from 'hooks/useAxiosError';
|
||||
import { useHandleExplorerTabChange } from 'hooks/useHandleExplorerTabChange';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import useUrlQueryData from 'hooks/useUrlQueryData';
|
||||
import { getPaginationQueryData } from 'lib/newQueryBuilder/getPaginationQueryData';
|
||||
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { generatePath, useHistory } from 'react-router-dom';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { Dashboard } from 'types/api/dashboard/getAll';
|
||||
import { ILog } from 'types/api/logs/log';
|
||||
@ -40,8 +38,9 @@ import {
|
||||
Query,
|
||||
TagFilter,
|
||||
} from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource, StringOperators } from 'types/common/queryBuilder';
|
||||
import { DataSource, LogsAggregatorOperator } from 'types/common/queryBuilder';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { generateExportToDashboardLink } from 'utils/dashboard/generateExportToDashboardLink';
|
||||
|
||||
import { ActionsWrapper } from './LogsExplorerViews.styled';
|
||||
|
||||
@ -67,10 +66,10 @@ function LogsExplorerViews(): JSX.Element {
|
||||
stagedQuery,
|
||||
panelType,
|
||||
updateAllQueriesOperators,
|
||||
updateQueriesData,
|
||||
redirectWithQueryBuilderData,
|
||||
} = useQueryBuilder();
|
||||
|
||||
const { handleExplorerTabChange } = useHandleExplorerTabChange();
|
||||
|
||||
// State
|
||||
const [page, setPage] = useState<number>(1);
|
||||
const [logs, setLogs] = useState<ILog[]>([]);
|
||||
@ -120,7 +119,7 @@ function LogsExplorerViews(): JSX.Element {
|
||||
|
||||
const modifiedQueryData: IBuilderQuery = {
|
||||
...listQuery,
|
||||
aggregateOperator: StringOperators.COUNT,
|
||||
aggregateOperator: LogsAggregatorOperator.COUNT,
|
||||
};
|
||||
|
||||
const modifiedQuery: Query = {
|
||||
@ -172,42 +171,6 @@ function LogsExplorerViews(): JSX.Element {
|
||||
},
|
||||
);
|
||||
|
||||
const getUpdateQuery = useCallback(
|
||||
(newPanelType: PANEL_TYPES): Query => {
|
||||
let query = updateAllQueriesOperators(
|
||||
currentQuery,
|
||||
newPanelType,
|
||||
DataSource.TRACES,
|
||||
);
|
||||
|
||||
if (newPanelType === PANEL_TYPES.LIST) {
|
||||
query = updateQueriesData(query, 'queryData', (item) => ({
|
||||
...item,
|
||||
orderBy: item.orderBy.filter((item) => item.columnName !== SIGNOZ_VALUE),
|
||||
aggregateAttribute: initialAutocompleteData,
|
||||
}));
|
||||
}
|
||||
|
||||
return query;
|
||||
},
|
||||
[currentQuery, updateAllQueriesOperators, updateQueriesData],
|
||||
);
|
||||
|
||||
const handleChangeView = useCallback(
|
||||
(type: string) => {
|
||||
const newPanelType = type as PANEL_TYPES;
|
||||
|
||||
if (newPanelType === panelType) return;
|
||||
|
||||
const query = getUpdateQuery(newPanelType);
|
||||
|
||||
redirectWithQueryBuilderData(query, {
|
||||
[queryParamNamesMap.panelTypes]: newPanelType,
|
||||
});
|
||||
},
|
||||
[panelType, getUpdateQuery, redirectWithQueryBuilderData],
|
||||
);
|
||||
|
||||
const getRequestData = useCallback(
|
||||
(
|
||||
query: Query | null,
|
||||
@ -299,11 +262,16 @@ function LogsExplorerViews(): JSX.Element {
|
||||
|
||||
const handleExport = useCallback(
|
||||
(dashboard: Dashboard | null): void => {
|
||||
if (!dashboard) return;
|
||||
if (!dashboard || !panelType) return;
|
||||
|
||||
const panelTypeParam = AVAILABLE_EXPORT_PANEL_TYPES.includes(panelType)
|
||||
? panelType
|
||||
: PANEL_TYPES.TIME_SERIES;
|
||||
|
||||
const updatedDashboard = addEmptyWidgetInDashboardJSONWithQuery(
|
||||
dashboard,
|
||||
exportDefaultQuery,
|
||||
panelTypeParam,
|
||||
);
|
||||
|
||||
updateDashboard(updatedDashboard, {
|
||||
@ -332,11 +300,11 @@ function LogsExplorerViews(): JSX.Element {
|
||||
return;
|
||||
}
|
||||
|
||||
const dashboardEditView = `${generatePath(ROUTES.DASHBOARD, {
|
||||
dashboardId: data?.payload?.uuid,
|
||||
})}/new?${QueryParams.graphType}=graph&${QueryParams.widgetId}=empty&${
|
||||
queryParamNamesMap.compositeQuery
|
||||
}=${encodeURIComponent(JSON.stringify(exportDefaultQuery))}`;
|
||||
const dashboardEditView = generateExportToDashboardLink({
|
||||
query: exportDefaultQuery,
|
||||
panelType: panelTypeParam,
|
||||
dashboardId: data.payload?.uuid || '',
|
||||
});
|
||||
|
||||
history.push(dashboardEditView);
|
||||
},
|
||||
@ -347,6 +315,7 @@ function LogsExplorerViews(): JSX.Element {
|
||||
exportDefaultQuery,
|
||||
history,
|
||||
notifications,
|
||||
panelType,
|
||||
updateDashboard,
|
||||
handleAxisError,
|
||||
],
|
||||
@ -356,9 +325,9 @@ function LogsExplorerViews(): JSX.Element {
|
||||
const shouldChangeView = isMultipleQueries || isGroupByExist;
|
||||
|
||||
if (panelType === PANEL_TYPES.LIST && shouldChangeView) {
|
||||
handleChangeView(PANEL_TYPES.TIME_SERIES);
|
||||
handleExplorerTabChange(PANEL_TYPES.TIME_SERIES);
|
||||
}
|
||||
}, [panelType, isMultipleQueries, isGroupByExist, handleChangeView]);
|
||||
}, [panelType, isMultipleQueries, isGroupByExist, handleExplorerTabChange]);
|
||||
|
||||
useEffect(() => {
|
||||
const currentParams = data?.params as Omit<LogTimeRange, 'pageSize'>;
|
||||
@ -512,7 +481,7 @@ function LogsExplorerViews(): JSX.Element {
|
||||
items={tabsItems}
|
||||
defaultActiveKey={panelType || PANEL_TYPES.LIST}
|
||||
activeKey={panelType || PANEL_TYPES.LIST}
|
||||
onChange={handleChangeView}
|
||||
onChange={handleExplorerTabChange}
|
||||
destroyInactiveTabPane
|
||||
/>
|
||||
|
||||
|
91
frontend/src/container/LogsTopNav/index.tsx
Normal file
91
frontend/src/container/LogsTopNav/index.tsx
Normal file
@ -0,0 +1,91 @@
|
||||
import { PlayCircleFilled } from '@ant-design/icons';
|
||||
import {
|
||||
initialQueryBuilderFormValuesMap,
|
||||
PANEL_TYPES,
|
||||
} from 'constants/queryBuilder';
|
||||
import { queryParamNamesMap } from 'constants/queryBuilderQueryNames';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import ROUTES from 'constants/routes';
|
||||
import {
|
||||
constructCompositeQuery,
|
||||
defaultLiveQueryDataConfig,
|
||||
} from 'container/LiveLogs/constants';
|
||||
import { QueryHistoryState } from 'container/LiveLogs/types';
|
||||
import LocalTopNav from 'container/LocalTopNav';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useQueryClient } from 'react-query';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
|
||||
import { LiveButtonStyled } from './styles';
|
||||
|
||||
function LogsTopNav(): JSX.Element {
|
||||
const history = useHistory();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { stagedQuery, panelType } = useQueryBuilder();
|
||||
|
||||
const handleGoLive = useCallback(() => {
|
||||
if (!stagedQuery) return;
|
||||
|
||||
let queryHistoryState: QueryHistoryState | null = null;
|
||||
|
||||
const compositeQuery = constructCompositeQuery({
|
||||
query: stagedQuery,
|
||||
initialQueryData: initialQueryBuilderFormValuesMap.logs,
|
||||
customQueryData: defaultLiveQueryDataConfig,
|
||||
});
|
||||
|
||||
const isListView =
|
||||
panelType === PANEL_TYPES.LIST && stagedQuery.builder.queryData[0];
|
||||
|
||||
if (isListView) {
|
||||
const [graphQuery, listQuery] = queryClient.getQueriesData<
|
||||
SuccessResponse<MetricRangePayloadProps> | ErrorResponse
|
||||
>({
|
||||
queryKey: REACT_QUERY_KEY.GET_QUERY_RANGE,
|
||||
active: true,
|
||||
});
|
||||
|
||||
queryHistoryState = {
|
||||
graphQueryPayload:
|
||||
graphQuery && graphQuery[1]
|
||||
? graphQuery[1].payload?.data.result || []
|
||||
: [],
|
||||
listQueryPayload:
|
||||
listQuery && listQuery[1]
|
||||
? listQuery[1].payload?.data.newResult.data.result || []
|
||||
: [],
|
||||
};
|
||||
}
|
||||
|
||||
const JSONCompositeQuery = encodeURIComponent(JSON.stringify(compositeQuery));
|
||||
|
||||
const path = `${ROUTES.LIVE_LOGS}?${queryParamNamesMap.compositeQuery}=${JSONCompositeQuery}`;
|
||||
|
||||
history.push(path, queryHistoryState);
|
||||
}, [history, panelType, queryClient, stagedQuery]);
|
||||
|
||||
const liveButton = useMemo(
|
||||
() => (
|
||||
<LiveButtonStyled
|
||||
icon={<PlayCircleFilled />}
|
||||
onClick={handleGoLive}
|
||||
type="primary"
|
||||
>
|
||||
Go Live
|
||||
</LiveButtonStyled>
|
||||
),
|
||||
[handleGoLive],
|
||||
);
|
||||
return (
|
||||
<LocalTopNav
|
||||
actions={liveButton}
|
||||
renderPermissions={{ isDateTimeEnabled: true }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default LogsTopNav;
|
20
frontend/src/container/LogsTopNav/styles.ts
Normal file
20
frontend/src/container/LogsTopNav/styles.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { Button, ButtonProps } from 'antd';
|
||||
import { themeColors } from 'constants/theme';
|
||||
import styled, { css, FlattenSimpleInterpolation } from 'styled-components';
|
||||
|
||||
export const LiveButtonStyled = styled(Button)<ButtonProps>`
|
||||
background-color: rgba(${themeColors.buttonSuccessRgb}, 0.9);
|
||||
|
||||
${({ danger }): FlattenSimpleInterpolation =>
|
||||
!danger
|
||||
? css`
|
||||
&:hover {
|
||||
background-color: rgba(${themeColors.buttonSuccessRgb}, 1) !important;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: rgba(${themeColors.buttonSuccessRgb}, 0.7) !important;
|
||||
}
|
||||
`
|
||||
: css``}
|
||||
`;
|
@ -1,3 +1,5 @@
|
||||
import { RadioChangeEvent } from 'antd';
|
||||
import { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { FieldTitle } from '../styles';
|
||||
@ -7,6 +9,15 @@ import { FormatFieldWrapper, RadioButton, RadioGroup } from './styles';
|
||||
function FormatField({ config }: FormatFieldProps): JSX.Element | null {
|
||||
const { t } = useTranslation(['trace']);
|
||||
|
||||
const onChange = useCallback(
|
||||
(event: RadioChangeEvent) => {
|
||||
if (!config) return;
|
||||
|
||||
config.onChange(event.target.value);
|
||||
},
|
||||
[config],
|
||||
);
|
||||
|
||||
if (!config) return null;
|
||||
|
||||
return (
|
||||
@ -16,7 +27,7 @@ function FormatField({ config }: FormatFieldProps): JSX.Element | null {
|
||||
size="small"
|
||||
buttonStyle="solid"
|
||||
value={config.value}
|
||||
onChange={config.onChange}
|
||||
onChange={onChange}
|
||||
>
|
||||
<RadioButton value="raw">{t('options_menu.raw')}</RadioButton>
|
||||
<RadioButton value="list">{t('options_menu.default')}</RadioButton>
|
||||
|
@ -14,7 +14,9 @@ export interface InitialOptions
|
||||
}
|
||||
|
||||
export type OptionsMenuConfig = {
|
||||
format?: Pick<RadioProps, 'value' | 'onChange'>;
|
||||
format?: Pick<RadioProps, 'value'> & {
|
||||
onChange: (value: LogViewMode) => void;
|
||||
};
|
||||
maxLines?: Pick<InputNumberProps, 'value' | 'onChange'>;
|
||||
addColumn?: Pick<
|
||||
SelectProps,
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { RadioChangeEvent } from 'antd';
|
||||
import getFromLocalstorage from 'api/browser/localstorage/get';
|
||||
import setToLocalstorage from 'api/browser/localstorage/set';
|
||||
import { getAggregateKeys } from 'api/queryBuilder/getAttributeKeys';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import { LogViewMode } from 'container/LogsTable';
|
||||
import { useGetAggregateKeys } from 'hooks/queryBuilder/useGetAggregateKeys';
|
||||
import useDebounce from 'hooks/useDebounce';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
@ -213,10 +213,10 @@ const useOptionsMenu = ({
|
||||
);
|
||||
|
||||
const handleFormatChange = useCallback(
|
||||
(event: RadioChangeEvent) => {
|
||||
(value: LogViewMode) => {
|
||||
const optionsData: OptionsQuery = {
|
||||
...optionsQueryData,
|
||||
format: event.target.value,
|
||||
format: value,
|
||||
};
|
||||
|
||||
handleRedirectWithOptionsData(optionsData);
|
||||
|
@ -35,6 +35,7 @@ function QueryBuilderSearch({
|
||||
query,
|
||||
onChange,
|
||||
whereClauseConfig,
|
||||
className,
|
||||
}: QueryBuilderSearchProps): JSX.Element {
|
||||
const {
|
||||
updateTag,
|
||||
@ -163,6 +164,7 @@ function QueryBuilderSearch({
|
||||
placeholder={PLACEHOLDER}
|
||||
value={queryTags}
|
||||
searchValue={searchValue}
|
||||
className={className}
|
||||
disabled={isMetricsDataSource && !query.aggregateAttribute.key}
|
||||
style={selectStyle}
|
||||
onSearch={handleSearch}
|
||||
@ -186,10 +188,12 @@ interface QueryBuilderSearchProps {
|
||||
query: IBuilderQuery;
|
||||
onChange: (value: TagFilter) => void;
|
||||
whereClauseConfig?: WhereClauseConfig;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
QueryBuilderSearch.defaultProps = {
|
||||
whereClauseConfig: undefined,
|
||||
className: '',
|
||||
};
|
||||
|
||||
export interface CustomTagProps {
|
||||
|
@ -21,6 +21,7 @@ const breadcrumbNameMap = {
|
||||
[ROUTES.ALL_DASHBOARD]: 'Dashboard',
|
||||
[ROUTES.LOGS]: 'Logs',
|
||||
[ROUTES.LOGS_EXPLORER]: 'Logs Explorer',
|
||||
[ROUTES.LIVE_LOGS]: 'Live View',
|
||||
[ROUTES.PIPELINES]: 'Pipelines',
|
||||
};
|
||||
|
||||
|
@ -84,3 +84,5 @@ export const routesToSkip = [
|
||||
ROUTES.LIST_ALL_ALERT,
|
||||
ROUTES.PIPELINES,
|
||||
];
|
||||
|
||||
export const routesToDisable = [ROUTES.LOGS_EXPLORER, ROUTES.LIVE_LOGS];
|
||||
|
@ -3,10 +3,10 @@ import ROUTES from 'constants/routes';
|
||||
import { useMemo } from 'react';
|
||||
import { matchPath, useHistory } from 'react-router-dom';
|
||||
|
||||
import NewExplorerCTA from '../NewExplorerCTA';
|
||||
import ShowBreadcrumbs from './Breadcrumbs';
|
||||
import DateTimeSelector from './DateTimeSelection';
|
||||
import { routesToSkip } from './DateTimeSelection/config';
|
||||
import NewExplorerCTA from './NewExplorerCTA';
|
||||
import { routesToDisable, routesToSkip } from './DateTimeSelection/config';
|
||||
import { Container } from './styles';
|
||||
|
||||
function TopNav(): JSX.Element | null {
|
||||
@ -20,12 +20,20 @@ function TopNav(): JSX.Element | null {
|
||||
[location.pathname],
|
||||
);
|
||||
|
||||
const isDisabled = useMemo(
|
||||
() =>
|
||||
routesToDisable.some((route) =>
|
||||
matchPath(location.pathname, { path: route, exact: true }),
|
||||
),
|
||||
[location.pathname],
|
||||
);
|
||||
|
||||
const isSignUpPage = useMemo(
|
||||
() => matchPath(location.pathname, { path: ROUTES.SIGN_UP, exact: true }),
|
||||
[location.pathname],
|
||||
);
|
||||
|
||||
if (isSignUpPage) {
|
||||
if (isSignUpPage || isDisabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -40,7 +48,6 @@ function TopNav(): JSX.Element | null {
|
||||
<Row justify="end">
|
||||
<Space align="start" size={60} direction="horizontal">
|
||||
<NewExplorerCTA />
|
||||
|
||||
<div>
|
||||
<DateTimeSelector />
|
||||
</div>
|
||||
|
@ -5,6 +5,7 @@ import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
export const addEmptyWidgetInDashboardJSONWithQuery = (
|
||||
dashboard: Dashboard,
|
||||
query: Query,
|
||||
panelTypes?: PANEL_TYPES,
|
||||
): Dashboard => ({
|
||||
...dashboard,
|
||||
data: {
|
||||
@ -30,7 +31,7 @@ export const addEmptyWidgetInDashboardJSONWithQuery = (
|
||||
opacity: '',
|
||||
title: '',
|
||||
timePreferance: 'GLOBAL_TIME',
|
||||
panelTypes: PANEL_TYPES.TIME_SERIES,
|
||||
panelTypes: panelTypes || PANEL_TYPES.TIME_SERIES,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
15
frontend/src/hooks/queryBuilder/useGetSearchQueryParam.ts
Normal file
15
frontend/src/hooks/queryBuilder/useGetSearchQueryParam.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { QuerySearchParamNames } from 'constants/queryBuilderQueryNames';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
export const useGetSearchQueryParam = (
|
||||
searchParams: QuerySearchParamNames,
|
||||
): string | null => {
|
||||
const urlQuery = useUrlQuery();
|
||||
|
||||
return useMemo(() => {
|
||||
const searchQuery = urlQuery.get(searchParams);
|
||||
|
||||
return searchQuery ? JSON.parse(searchQuery) : null;
|
||||
}, [urlQuery, searchParams]);
|
||||
};
|
@ -8,7 +8,7 @@ import { useQueryBuilder } from './useQueryBuilder';
|
||||
export type UseShareBuilderUrlParams = { defaultValue: Query };
|
||||
|
||||
export const useShareBuilderUrl = (defaultQuery: Query): void => {
|
||||
const { redirectWithQueryBuilderData, resetStagedQuery } = useQueryBuilder();
|
||||
const { redirectWithQueryBuilderData } = useQueryBuilder();
|
||||
const urlQuery = useUrlQuery();
|
||||
|
||||
const compositeQuery = useGetCompositeQueryParam();
|
||||
@ -18,11 +18,4 @@ export const useShareBuilderUrl = (defaultQuery: Query): void => {
|
||||
redirectWithQueryBuilderData(defaultQuery);
|
||||
}
|
||||
}, [defaultQuery, urlQuery, redirectWithQueryBuilderData, compositeQuery]);
|
||||
|
||||
useEffect(
|
||||
() => (): void => {
|
||||
resetStagedQuery();
|
||||
},
|
||||
[resetStagedQuery],
|
||||
);
|
||||
};
|
||||
|
11
frontend/src/hooks/saveViews/useDeleteView.ts
Normal file
11
frontend/src/hooks/saveViews/useDeleteView.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { deleteView } from 'api/saveView/deleteView';
|
||||
import { useMutation, UseMutationResult } from 'react-query';
|
||||
import { DeleteViewPayloadProps } from 'types/api/saveViews/types';
|
||||
|
||||
export const useDeleteView = (
|
||||
uuid: string,
|
||||
): UseMutationResult<DeleteViewPayloadProps, Error, string> =>
|
||||
useMutation({
|
||||
mutationKey: [uuid],
|
||||
mutationFn: () => deleteView(uuid),
|
||||
});
|
13
frontend/src/hooks/saveViews/useGetAllViews.ts
Normal file
13
frontend/src/hooks/saveViews/useGetAllViews.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { getAllViews } from 'api/saveView/getAllViews';
|
||||
import { AxiosError, AxiosResponse } from 'axios';
|
||||
import { useQuery, UseQueryResult } from 'react-query';
|
||||
import { AllViewsProps } from 'types/api/saveViews/types';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
export const useGetAllViews = (
|
||||
sourcepage: DataSource,
|
||||
): UseQueryResult<AxiosResponse<AllViewsProps>, AxiosError> =>
|
||||
useQuery<AxiosResponse<AllViewsProps>, AxiosError>({
|
||||
queryKey: [{ sourcepage }],
|
||||
queryFn: () => getAllViews(sourcepage),
|
||||
});
|
26
frontend/src/hooks/saveViews/useSaveView.ts
Normal file
26
frontend/src/hooks/saveViews/useSaveView.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import { saveView } from 'api/saveView/saveView';
|
||||
import { AxiosResponse } from 'axios';
|
||||
import { useMutation, UseMutationResult } from 'react-query';
|
||||
import { SaveViewPayloadProps, SaveViewProps } from 'types/api/saveViews/types';
|
||||
|
||||
export const useSaveView = ({
|
||||
compositeQuery,
|
||||
sourcePage,
|
||||
viewName,
|
||||
extraData,
|
||||
}: SaveViewProps): UseMutationResult<
|
||||
AxiosResponse<SaveViewPayloadProps>,
|
||||
Error,
|
||||
SaveViewProps,
|
||||
SaveViewPayloadProps
|
||||
> =>
|
||||
useMutation({
|
||||
mutationKey: [viewName, sourcePage, compositeQuery, extraData],
|
||||
mutationFn: () =>
|
||||
saveView({
|
||||
compositeQuery,
|
||||
sourcePage,
|
||||
viewName,
|
||||
extraData,
|
||||
}),
|
||||
});
|
30
frontend/src/hooks/saveViews/useUpdateView.ts
Normal file
30
frontend/src/hooks/saveViews/useUpdateView.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import { updateView } from 'api/saveView/updateView';
|
||||
import { useMutation, UseMutationResult } from 'react-query';
|
||||
import {
|
||||
UpdateViewPayloadProps,
|
||||
UpdateViewProps,
|
||||
} from 'types/api/saveViews/types';
|
||||
|
||||
export const useUpdateView = ({
|
||||
compositeQuery,
|
||||
viewName,
|
||||
extraData,
|
||||
sourcePage,
|
||||
viewKey,
|
||||
}: UpdateViewProps): UseMutationResult<
|
||||
UpdateViewPayloadProps,
|
||||
Error,
|
||||
UpdateViewProps,
|
||||
UpdateViewPayloadProps
|
||||
> =>
|
||||
useMutation({
|
||||
mutationKey: [viewName, sourcePage, compositeQuery, extraData],
|
||||
mutationFn: () =>
|
||||
updateView({
|
||||
compositeQuery,
|
||||
viewName,
|
||||
extraData,
|
||||
sourcePage,
|
||||
viewKey,
|
||||
}),
|
||||
});
|
@ -2,20 +2,29 @@ import { EventListener, EventSourceEventMap } from 'event-source-polyfill';
|
||||
import { useEventSource } from 'providers/EventSource';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
export const useEventSourceEvent = (
|
||||
eventName: keyof EventSourceEventMap,
|
||||
listener: EventListener,
|
||||
type EventMap = {
|
||||
message: MessageEvent;
|
||||
open: Event;
|
||||
error: Event;
|
||||
};
|
||||
|
||||
export const useEventSourceEvent = <T extends keyof EventSourceEventMap>(
|
||||
eventName: T,
|
||||
listener: (event: EventMap[T]) => void,
|
||||
): void => {
|
||||
const { eventSourceInstance } = useEventSource();
|
||||
|
||||
useEffect(() => {
|
||||
if (eventSourceInstance) {
|
||||
eventSourceInstance.addEventListener(eventName, listener);
|
||||
eventSourceInstance.addEventListener(eventName, listener as EventListener);
|
||||
}
|
||||
|
||||
return (): void => {
|
||||
if (eventSourceInstance) {
|
||||
eventSourceInstance.removeEventListener(eventName, listener);
|
||||
eventSourceInstance.removeEventListener(
|
||||
eventName,
|
||||
listener as EventListener,
|
||||
);
|
||||
}
|
||||
};
|
||||
}, [eventName, eventSourceInstance, listener]);
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user