Merge pull request #3130 from SigNoz/release/v0.23.0

Release/v0.23.0
This commit is contained in:
Ankit Nayan 2023-07-13 23:06:22 +05:30 committed by GitHub
commit 206e8b8dc3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
160 changed files with 4244 additions and 1681 deletions

View File

@ -37,7 +37,7 @@ jobs:
kubectl create ns sample-application kubectl create ns sample-application
# apply hotrod k8s manifest file # apply hotrod k8s manifest file
kubectl -n sample-application apply -f https://raw.githubusercontent.com/SigNoz/signoz/main/sample-apps/hotrod/hotrod.yaml kubectl -n sample-application apply -f https://raw.githubusercontent.com/SigNoz/signoz/develop/sample-apps/hotrod/hotrod.yaml
# wait for all deployments in sample-application namespace to be READY # wait for all deployments in sample-application namespace to be READY
kubectl -n sample-application get deploy --output name | xargs -r -n1 -t kubectl -n sample-application rollout status --timeout=300s kubectl -n sample-application get deploy --output name | xargs -r -n1 -t kubectl -n sample-application rollout status --timeout=300s

View File

@ -338,7 +338,7 @@ to make SigNoz UI available at [localhost:3301](http://localhost:3301)
**5.1.1 To install the HotROD sample app:** **5.1.1 To install the HotROD sample app:**
```bash ```bash
curl -sL https://github.com/SigNoz/signoz/raw/main/sample-apps/hotrod/hotrod-install.sh \ curl -sL https://github.com/SigNoz/signoz/raw/develop/sample-apps/hotrod/hotrod-install.sh \
| HELM_RELEASE=my-release SIGNOZ_NAMESPACE=platform bash | HELM_RELEASE=my-release SIGNOZ_NAMESPACE=platform bash
``` ```
@ -361,7 +361,7 @@ kubectl -n sample-application run strzal --image=djbingham/curl \
**5.1.4 To delete the HotROD sample app:** **5.1.4 To delete the HotROD sample app:**
```bash ```bash
curl -sL https://github.com/SigNoz/signoz/raw/main/sample-apps/hotrod/hotrod-delete.sh \ curl -sL https://github.com/SigNoz/signoz/raw/develop/sample-apps/hotrod/hotrod-delete.sh \
| HOTROD_NAMESPACE=sample-application bash | HOTROD_NAMESPACE=sample-application bash
``` ```

View File

@ -58,7 +58,7 @@ from the HotROD application, you should see the data generated from hotrod in Si
```sh ```sh
kubectl create ns sample-application kubectl create ns sample-application
kubectl -n sample-application apply -f https://raw.githubusercontent.com/SigNoz/signoz/main/sample-apps/hotrod/hotrod.yaml kubectl -n sample-application apply -f https://raw.githubusercontent.com/SigNoz/signoz/develop/sample-apps/hotrod/hotrod.yaml
``` ```
To generate load: To generate load:
@ -66,7 +66,7 @@ To generate load:
```sh ```sh
kubectl -n sample-application run strzal --image=djbingham/curl \ kubectl -n sample-application run strzal --image=djbingham/curl \
--restart='OnFailure' -i --tty --rm --command -- curl -X POST -F \ --restart='OnFailure' -i --tty --rm --command -- curl -X POST -F \
'locust_count=6' -F 'hatch_rate=2' http://locust-master:8089/swarm 'user_count=6' -F 'spawn_rate=2' http://locust-master:8089/swarm
``` ```
To stop load: To stop load:

View File

@ -137,7 +137,7 @@ services:
condition: on-failure condition: on-failure
query-service: query-service:
image: signoz/query-service:0.22.0 image: signoz/query-service:0.23.0
command: ["-config=/root/config/prometheus.yml"] command: ["-config=/root/config/prometheus.yml"]
# ports: # ports:
# - "6060:6060" # pprof port # - "6060:6060" # pprof port
@ -166,7 +166,7 @@ services:
<<: *clickhouse-depend <<: *clickhouse-depend
frontend: frontend:
image: signoz/frontend:0.22.0 image: signoz/frontend:0.23.0
deploy: deploy:
restart_policy: restart_policy:
condition: on-failure condition: on-failure
@ -179,7 +179,7 @@ services:
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf - ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
otel-collector: otel-collector:
image: signoz/signoz-otel-collector:0.79.2 image: signoz/signoz-otel-collector:0.79.3
command: ["--config=/etc/otel-collector-config.yaml", "--feature-gates=-pkg.translator.prometheus.NormalizeName"] command: ["--config=/etc/otel-collector-config.yaml", "--feature-gates=-pkg.translator.prometheus.NormalizeName"]
user: root # required for reading docker container logs user: root # required for reading docker container logs
volumes: volumes:
@ -208,7 +208,7 @@ services:
<<: *clickhouse-depend <<: *clickhouse-depend
otel-collector-metrics: otel-collector-metrics:
image: signoz/signoz-otel-collector:0.79.2 image: signoz/signoz-otel-collector:0.79.3
command: ["--config=/etc/otel-collector-metrics-config.yaml", "--feature-gates=-pkg.translator.prometheus.NormalizeName"] command: ["--config=/etc/otel-collector-metrics-config.yaml", "--feature-gates=-pkg.translator.prometheus.NormalizeName"]
volumes: volumes:
- ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml - ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml

View File

@ -41,7 +41,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` # Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md`
otel-collector: otel-collector:
container_name: otel-collector container_name: otel-collector
image: signoz/signoz-otel-collector:0.79.2 image: signoz/signoz-otel-collector:0.79.3
command: ["--config=/etc/otel-collector-config.yaml", "--feature-gates=-pkg.translator.prometheus.NormalizeName"] command: ["--config=/etc/otel-collector-config.yaml", "--feature-gates=-pkg.translator.prometheus.NormalizeName"]
# user: root # required for reading docker container logs # user: root # required for reading docker container logs
volumes: volumes:
@ -67,7 +67,7 @@ services:
otel-collector-metrics: otel-collector-metrics:
container_name: otel-collector-metrics container_name: otel-collector-metrics
image: signoz/signoz-otel-collector:0.79.2 image: signoz/signoz-otel-collector:0.79.3
command: ["--config=/etc/otel-collector-metrics-config.yaml", "--feature-gates=-pkg.translator.prometheus.NormalizeName"] command: ["--config=/etc/otel-collector-metrics-config.yaml", "--feature-gates=-pkg.translator.prometheus.NormalizeName"]
volumes: volumes:
- ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml - ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml

View File

@ -153,7 +153,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` # 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: query-service:
image: signoz/query-service:${DOCKER_TAG:-0.22.0} image: signoz/query-service:${DOCKER_TAG:-0.23.0}
container_name: query-service container_name: query-service
command: ["-config=/root/config/prometheus.yml"] command: ["-config=/root/config/prometheus.yml"]
# ports: # ports:
@ -181,7 +181,7 @@ services:
<<: *clickhouse-depend <<: *clickhouse-depend
frontend: frontend:
image: signoz/frontend:${DOCKER_TAG:-0.22.0} image: signoz/frontend:${DOCKER_TAG:-0.23.0}
container_name: frontend container_name: frontend
restart: on-failure restart: on-failure
depends_on: depends_on:
@ -193,7 +193,7 @@ services:
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf - ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
otel-collector: otel-collector:
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.79.2} image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.79.3}
command: ["--config=/etc/otel-collector-config.yaml", "--feature-gates=-pkg.translator.prometheus.NormalizeName"] command: ["--config=/etc/otel-collector-config.yaml", "--feature-gates=-pkg.translator.prometheus.NormalizeName"]
user: root # required for reading docker container logs user: root # required for reading docker container logs
volumes: volumes:
@ -219,7 +219,7 @@ services:
<<: *clickhouse-depend <<: *clickhouse-depend
otel-collector-metrics: otel-collector-metrics:
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.79.2} image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.79.3}
command: ["--config=/etc/otel-collector-metrics-config.yaml", "--feature-gates=-pkg.translator.prometheus.NormalizeName"] command: ["--config=/etc/otel-collector-metrics-config.yaml", "--feature-gates=-pkg.translator.prometheus.NormalizeName"]
volumes: volumes:
- ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml - ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml

View File

@ -17,6 +17,7 @@ import (
type APIHandlerOptions struct { type APIHandlerOptions struct {
DataConnector interfaces.DataConnector DataConnector interfaces.DataConnector
SkipConfig *basemodel.SkipConfig SkipConfig *basemodel.SkipConfig
PreferDelta bool
AppDao dao.ModelDao AppDao dao.ModelDao
RulesManager *rules.Manager RulesManager *rules.Manager
FeatureFlags baseint.FeatureLookup FeatureFlags baseint.FeatureLookup
@ -34,6 +35,7 @@ func NewAPIHandler(opts APIHandlerOptions) (*APIHandler, error) {
baseHandler, err := baseapp.NewAPIHandler(baseapp.APIHandlerOpts{ baseHandler, err := baseapp.NewAPIHandler(baseapp.APIHandlerOpts{
Reader: opts.DataConnector, Reader: opts.DataConnector,
SkipConfig: opts.SkipConfig, SkipConfig: opts.SkipConfig,
PerferDelta: opts.PreferDelta,
AppDao: opts.AppDao, AppDao: opts.AppDao,
RuleManager: opts.RulesManager, RuleManager: opts.RulesManager,
FeatureFlags: opts.FeatureFlags}) FeatureFlags: opts.FeatureFlags})

View File

@ -56,6 +56,7 @@ type ServerOptions struct {
// alert specific params // alert specific params
DisableRules bool DisableRules bool
RuleRepoURL string RuleRepoURL string
PreferDelta bool
} }
// Server runs HTTP api service // Server runs HTTP api service
@ -170,6 +171,7 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
apiOpts := api.APIHandlerOptions{ apiOpts := api.APIHandlerOptions{
DataConnector: reader, DataConnector: reader,
SkipConfig: skipConfig, SkipConfig: skipConfig,
PreferDelta: serverOptions.PreferDelta,
AppDao: modelDao, AppDao: modelDao,
RulesManager: rm, RulesManager: rm,
FeatureFlags: lm, FeatureFlags: lm,

View File

@ -83,10 +83,12 @@ func main() {
var ruleRepoURL string var ruleRepoURL string
var enableQueryServiceLogOTLPExport bool var enableQueryServiceLogOTLPExport bool
var preferDelta bool
flag.StringVar(&promConfigPath, "config", "./config/prometheus.yml", "(prometheus config to read metrics)") flag.StringVar(&promConfigPath, "config", "./config/prometheus.yml", "(prometheus config to read metrics)")
flag.StringVar(&skipTopLvlOpsPath, "skip-top-level-ops", "", "(config file to skip top level operations)") flag.StringVar(&skipTopLvlOpsPath, "skip-top-level-ops", "", "(config file to skip top level operations)")
flag.BoolVar(&disableRules, "rules.disable", false, "(disable rule evaluation)") flag.BoolVar(&disableRules, "rules.disable", false, "(disable rule evaluation)")
flag.BoolVar(&preferDelta, "prefer-delta", false, "(prefer delta over raw metrics)")
flag.StringVar(&ruleRepoURL, "rules.repo-url", baseconst.AlertHelpPage, "(host address used to build rule link in alert messages)") flag.StringVar(&ruleRepoURL, "rules.repo-url", baseconst.AlertHelpPage, "(host address used to build rule link in alert messages)")
flag.BoolVar(&enableQueryServiceLogOTLPExport, "enable.query.service.log.otlp.export", false, "(enable query service log otlp export)") flag.BoolVar(&enableQueryServiceLogOTLPExport, "enable.query.service.log.otlp.export", false, "(enable query service log otlp export)")
flag.Parse() flag.Parse()
@ -102,6 +104,7 @@ func main() {
HTTPHostPort: baseconst.HTTPHostPort, HTTPHostPort: baseconst.HTTPHostPort,
PromConfigPath: promConfigPath, PromConfigPath: promConfigPath,
SkipTopLvlOpsPath: skipTopLvlOpsPath, SkipTopLvlOpsPath: skipTopLvlOpsPath,
PreferDelta: preferDelta,
PrivateHostPort: baseconst.PrivateHostPort, PrivateHostPort: baseconst.PrivateHostPort,
DisableRules: disableRules, DisableRules: disableRules,
RuleRepoURL: ruleRepoURL, RuleRepoURL: ruleRepoURL,

View File

@ -2,7 +2,7 @@
"options_menu": { "options_menu": {
"options": "Options", "options": "Options",
"format": "Format", "format": "Format",
"row": "Row", "raw": "Raw",
"default": "Default", "default": "Default",
"column": "Column", "column": "Column",
"maxLines": "Max lines per Row", "maxLines": "Max lines per Row",

View File

@ -2,7 +2,7 @@
"options_menu": { "options_menu": {
"options": "Options", "options": "Options",
"format": "Format", "format": "Format",
"row": "Row", "raw": "Raw",
"default": "Default", "default": "Default",
"column": "Column", "column": "Column",
"maxLines": "Max lines per Row", "maxLines": "Max lines per Row",

View File

@ -7,7 +7,7 @@ export const ServicesTablePage = Loadable(
export const ServiceMetricsPage = Loadable( export const ServiceMetricsPage = Loadable(
() => () =>
import( import(
/* webpackChunkName: "ServiceMetricsPage" */ 'pages/MetricApplication' /* webpackChunkName: "ServiceMetricsPage" */ 'pages/MetricsApplication'
), ),
); );

View File

@ -1,13 +1,7 @@
import axios from 'api'; 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/metrics/getServiceOverview'; import { PayloadProps, Props } from 'types/api/metrics/getServiceOverview';
const getServiceOverview = async ( const getServiceOverview = async (props: Props): Promise<PayloadProps> => {
props: Props,
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
try {
const response = await axios.post(`/service/overview`, { const response = await axios.post(`/service/overview`, {
start: `${props.start}`, start: `${props.start}`,
end: `${props.end}`, end: `${props.end}`,
@ -16,15 +10,7 @@ const getServiceOverview = async (
tags: props.selectedTags, tags: props.selectedTags,
}); });
return { return response.data;
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
}; };
export default getServiceOverview; export default getServiceOverview;

View File

@ -1,24 +1,12 @@
import axios from 'api'; 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/metrics/getTopLevelOperations';
const getTopLevelOperations = async ( const getTopLevelOperations = async (): Promise<ServiceDataProps> => {
props: Props,
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
try {
const response = await axios.post(`/service/top_level_operations`); const response = await axios.post(`/service/top_level_operations`);
return response.data;
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data[props.service],
}; };
} catch (error) {
return ErrorResponseHandler(error as AxiosError); export type ServiceDataProps = {
} [serviceName: string]: string[];
}; };
export default getTopLevelOperations; export default getTopLevelOperations;

View File

@ -1,13 +1,7 @@
import axios from 'api'; 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/metrics/getTopOperations'; import { PayloadProps, Props } from 'types/api/metrics/getTopOperations';
const getTopOperations = async ( const getTopOperations = async (props: Props): Promise<PayloadProps> => {
props: Props,
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
try {
const response = await axios.post(`/service/top_operations`, { const response = await axios.post(`/service/top_operations`, {
start: `${props.start}`, start: `${props.start}`,
end: `${props.end}`, end: `${props.end}`,
@ -15,15 +9,7 @@ const getTopOperations = async (
tags: props.selectedTags, tags: props.selectedTags,
}); });
return { return response.data;
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
}; };
export default getTopOperations; export default getTopOperations;

View File

@ -0,0 +1,27 @@
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;

View File

@ -0,0 +1,9 @@
import { AddToQueryHOCProps } from 'components/Logs/AddToQueryHOC';
import { ActionItemProps } from 'container/LogDetailedView/ActionItem';
import { ILog } from 'types/api/logs/log';
export type LogDetailProps = {
log: ILog | null;
onClose: () => void;
} & Pick<AddToQueryHOCProps, 'onAddToQuery'> &
Pick<ActionItemProps, 'onClickActionItem'>;

View File

@ -0,0 +1,52 @@
import { Drawer, Tabs } from 'antd';
import JSONView from 'container/LogDetailedView/JsonView';
import TableView from 'container/LogDetailedView/TableView';
import { LogDetailProps } from './LogDetail.interfaces';
function LogDetail({
log,
onClose,
onAddToQuery,
onClickActionItem,
}: LogDetailProps): JSX.Element {
const onDrawerClose = (): void => {
onClose();
};
const items = [
{
label: 'Table',
key: '1',
children: log && (
<TableView
logData={log}
onAddToQuery={onAddToQuery}
onClickActionItem={onClickActionItem}
/>
),
},
{
label: 'JSON',
key: '2',
children: log && <JSONView logData={log} />,
},
];
return (
<Drawer
width="60%"
title="Log Details"
placement="right"
closable
onClose={onDrawerClose}
open={log !== null}
style={{ overscrollBehavior: 'contain' }}
destroyOnClose
>
<Tabs defaultActiveKey="1" items={items} />
</Drawer>
);
}
export default LogDetail;

View File

@ -1,39 +1,18 @@
import { Popover } from 'antd'; import { Popover } from 'antd';
import ROUTES from 'constants/routes'; import { OPERATORS } from 'constants/queryBuilder';
import history from 'lib/history';
import { generateFilterQuery } from 'lib/logs/generateFilterQuery';
import { memo, ReactNode, useCallback, useMemo } from 'react'; import { memo, ReactNode, useCallback, useMemo } from 'react';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { ILogsReducer } from 'types/reducer/logs';
import { ButtonContainer } from './styles'; import { ButtonContainer } from './styles';
function AddToQueryHOC({ function AddToQueryHOC({
fieldKey, fieldKey,
fieldValue, fieldValue,
onAddToQuery,
children, children,
}: AddToQueryHOCProps): JSX.Element { }: AddToQueryHOCProps): JSX.Element {
const {
searchFilter: { queryString },
} = useSelector<AppState, ILogsReducer>((store) => store.logs);
const generatedQuery = useMemo(
() => generateFilterQuery({ fieldKey, fieldValue, type: 'IN' }),
[fieldKey, fieldValue],
);
const handleQueryAdd = useCallback(() => { const handleQueryAdd = useCallback(() => {
let updatedQueryString = queryString || ''; onAddToQuery(fieldKey, fieldValue, OPERATORS.IN);
}, [fieldKey, fieldValue, onAddToQuery]);
if (updatedQueryString.length === 0) {
updatedQueryString += `${generatedQuery}`;
} else {
updatedQueryString += ` AND ${generatedQuery}`;
}
history.replace(`${ROUTES.LOGS}?q=${updatedQueryString}`);
}, [generatedQuery, queryString]);
const popOverContent = useMemo(() => <span>Add to query: {fieldKey}</span>, [ const popOverContent = useMemo(() => <span>Add to query: {fieldKey}</span>, [
fieldKey, fieldKey,
@ -48,9 +27,10 @@ function AddToQueryHOC({
); );
} }
interface AddToQueryHOCProps { export interface AddToQueryHOCProps {
fieldKey: string; fieldKey: string;
fieldValue: string; fieldValue: string;
onAddToQuery: (fieldKey: string, fieldValue: string, operator: string) => void;
children: ReactNode; children: ReactNode;
} }

View File

@ -8,16 +8,13 @@ import { useNotifications } from 'hooks/useNotifications';
// utils // utils
import { FlatLogData } from 'lib/logs/flatLogData'; import { FlatLogData } from 'lib/logs/flatLogData';
import { useCallback, useMemo } from 'react'; import { useCallback, useMemo } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useCopyToClipboard } from 'react-use'; import { useCopyToClipboard } from 'react-use';
// interfaces // interfaces
import { AppState } from 'store/reducers'; import { IField } from 'types/api/logs/fields';
import { SET_DETAILED_LOG_DATA } from 'types/actions/logs';
import { ILog } from 'types/api/logs/log'; import { ILog } from 'types/api/logs/log';
import { ILogsReducer } from 'types/reducer/logs';
// components // components
import AddToQueryHOC from '../AddToQueryHOC'; import AddToQueryHOC, { AddToQueryHOCProps } from '../AddToQueryHOC';
import CopyClipboardHOC from '../CopyClipboardHOC'; import CopyClipboardHOC from '../CopyClipboardHOC';
// styles // styles
import { import {
@ -36,6 +33,10 @@ interface LogFieldProps {
fieldKey: string; fieldKey: string;
fieldValue: string; fieldValue: string;
} }
type LogSelectedFieldProps = LogFieldProps &
Pick<AddToQueryHOCProps, 'onAddToQuery'>;
function LogGeneralField({ fieldKey, fieldValue }: LogFieldProps): JSX.Element { function LogGeneralField({ fieldKey, fieldValue }: LogFieldProps): JSX.Element {
const html = useMemo( const html = useMemo(
() => ({ () => ({
@ -59,10 +60,15 @@ function LogGeneralField({ fieldKey, fieldValue }: LogFieldProps): JSX.Element {
function LogSelectedField({ function LogSelectedField({
fieldKey = '', fieldKey = '',
fieldValue = '', fieldValue = '',
}: LogFieldProps): JSX.Element { onAddToQuery,
}: LogSelectedFieldProps): JSX.Element {
return ( return (
<SelectedLog> <SelectedLog>
<AddToQueryHOC fieldKey={fieldKey} fieldValue={fieldValue}> <AddToQueryHOC
fieldKey={fieldKey}
fieldValue={fieldValue}
onAddToQuery={onAddToQuery}
>
<Typography.Text> <Typography.Text>
<span style={{ color: blue[4] }}>{fieldKey}</span> <span style={{ color: blue[4] }}>{fieldKey}</span>
</Typography.Text> </Typography.Text>
@ -77,26 +83,26 @@ function LogSelectedField({
); );
} }
interface ListLogViewProps { type ListLogViewProps = {
logData: ILog; logData: ILog;
} onOpenDetailedView: (log: ILog) => void;
function ListLogView({ logData }: ListLogViewProps): JSX.Element { selectedFields: IField[];
const { } & Pick<AddToQueryHOCProps, 'onAddToQuery'>;
fields: { selected },
} = useSelector<AppState, ILogsReducer>((state) => state.logs);
const dispatch = useDispatch(); function ListLogView({
logData,
selectedFields,
onOpenDetailedView,
onAddToQuery,
}: ListLogViewProps): JSX.Element {
const flattenLogData = useMemo(() => FlatLogData(logData), [logData]); const flattenLogData = useMemo(() => FlatLogData(logData), [logData]);
const [, setCopy] = useCopyToClipboard(); const [, setCopy] = useCopyToClipboard();
const { notifications } = useNotifications(); const { notifications } = useNotifications();
const handleDetailedView = useCallback(() => { const handleDetailedView = useCallback(() => {
dispatch({ onOpenDetailedView(logData);
type: SET_DETAILED_LOG_DATA, }, [logData, onOpenDetailedView]);
payload: logData,
});
}, [dispatch, logData]);
const handleCopyJSON = (): void => { const handleCopyJSON = (): void => {
setCopy(JSON.stringify(logData, null, 2)); setCopy(JSON.stringify(logData, null, 2));
@ -106,8 +112,16 @@ function ListLogView({ logData }: ListLogViewProps): JSX.Element {
}; };
const updatedSelecedFields = useMemo( const updatedSelecedFields = useMemo(
() => selected.filter((e) => e.name !== 'id'), () => selectedFields.filter((e) => e.name !== 'id'),
[selected], [selectedFields],
);
const timestampValue = useMemo(
() =>
typeof flattenLogData.timestamp === 'string'
? dayjs(flattenLogData.timestamp).format()
: dayjs(flattenLogData.timestamp / 1e6).format(),
[flattenLogData.timestamp],
); );
return ( return (
@ -119,10 +133,7 @@ function ListLogView({ logData }: ListLogViewProps): JSX.Element {
{flattenLogData.stream && ( {flattenLogData.stream && (
<LogGeneralField fieldKey="stream" fieldValue={flattenLogData.stream} /> <LogGeneralField fieldKey="stream" fieldValue={flattenLogData.stream} />
)} )}
<LogGeneralField <LogGeneralField fieldKey="timestamp" fieldValue={timestampValue} />
fieldKey="timestamp"
fieldValue={dayjs((flattenLogData.timestamp as never) / 1e6).format()}
/>
</> </>
</LogContainer> </LogContainer>
<div> <div>
@ -132,6 +143,7 @@ function ListLogView({ logData }: ListLogViewProps): JSX.Element {
key={field.name} key={field.name}
fieldKey={field.name} fieldKey={field.name}
fieldValue={flattenLogData[field.name] as never} fieldValue={flattenLogData[field.name] as never}
onAddToQuery={onAddToQuery}
/> />
) : null, ) : null,
)} )}

View File

@ -1,10 +1,5 @@
import { Card, Typography } from 'antd'; import { Card, Typography } from 'antd';
import styled, { keyframes } from 'styled-components'; import styled from 'styled-components';
const fadeInAnimation = keyframes`
0% { opacity: 0; }
100% { opacity: 1;}
`;
export const Container = styled(Card)` export const Container = styled(Card)`
width: 100% !important; width: 100% !important;
@ -12,9 +7,6 @@ export const Container = styled(Card)`
.ant-card-body { .ant-card-body {
padding: 0.3rem 0.6rem; padding: 0.3rem 0.6rem;
} }
animation-name: ${fadeInAnimation};
animation-duration: 0.2s;
animation-timing-function: ease-in;
`; `;
export const Text = styled(Typography.Text)` export const Text = styled(Typography.Text)`

View File

@ -30,7 +30,10 @@ function RawLogView(props: RawLogViewProps): JSX.Element {
const isDarkMode = useIsDarkMode(); const isDarkMode = useIsDarkMode();
const text = useMemo( const text = useMemo(
() => `${dayjs(data.timestamp / 1e6).format()} | ${data.body}`, () =>
typeof data.timestamp === 'string'
? `${dayjs(data.timestamp).format()} | ${data.body}`
: `${dayjs(data.timestamp / 1e6).format()} | ${data.body}`,
[data.timestamp, data.body], [data.timestamp, data.body],
); );

View File

@ -1,121 +1,18 @@
import { ExpandAltOutlined } from '@ant-design/icons'; import { Table } from 'antd';
import Convert from 'ansi-to-html';
import { Table, Typography } from 'antd';
import { ColumnsType, ColumnType } from 'antd/es/table';
import dayjs from 'dayjs';
import dompurify from 'dompurify';
// utils
import { FlatLogData } from 'lib/logs/flatLogData';
import { useMemo } from 'react';
import { IField } from 'types/api/logs/fields';
// interfaces
import { ILog } from 'types/api/logs/log';
// styles
import { ExpandIconWrapper } from '../RawLogView/styles';
// config // config
import { defaultCellStyle, defaultTableStyle, tableScroll } from './config'; import { tableScroll } from './config';
import { TableBodyContent } from './styles'; import { LogsTableViewProps } from './types';
import { useTableView } from './useTableView';
type ColumnTypeRender<T = unknown> = ReturnType<
NonNullable<ColumnType<T>['render']>
>;
type LogsTableViewProps = {
logs: ILog[];
fields: IField[];
linesPerRow: number;
onClickExpand: (log: ILog) => void;
};
const convert = new Convert();
function LogsTableView(props: LogsTableViewProps): JSX.Element { function LogsTableView(props: LogsTableViewProps): JSX.Element {
const { logs, fields, linesPerRow, onClickExpand } = props; const { dataSource, columns } = useTableView(props);
const flattenLogData = useMemo(() => logs.map((log) => FlatLogData(log)), [
logs,
]);
const columns: ColumnsType<Record<string, unknown>> = useMemo(() => {
const fieldColumns: ColumnsType<Record<string, unknown>> = fields
.filter((e) => e.name !== 'id')
.map(({ name }) => ({
title: name,
dataIndex: name,
key: name,
render: (field): ColumnTypeRender<Record<string, unknown>> => ({
props: {
style: defaultCellStyle,
},
children: (
<Typography.Paragraph ellipsis={{ rows: linesPerRow }}>
{field}
</Typography.Paragraph>
),
}),
}));
return [
{
title: '',
dataIndex: 'id',
key: 'expand',
// https://github.com/ant-design/ant-design/discussions/36886
render: (_, item): ColumnTypeRender<Record<string, unknown>> => ({
props: {
style: defaultCellStyle,
},
children: (
<ExpandIconWrapper
onClick={(): void => {
onClickExpand((item as unknown) as ILog);
}}
>
<ExpandAltOutlined />
</ExpandIconWrapper>
),
}),
},
{
title: 'timestamp',
dataIndex: 'timestamp',
key: 'timestamp',
// https://github.com/ant-design/ant-design/discussions/36886
render: (field): ColumnTypeRender<Record<string, unknown>> => {
const date = dayjs(field / 1e6).format();
return {
children: <Typography.Paragraph ellipsis>{date}</Typography.Paragraph>,
};
},
},
...fieldColumns,
{
title: 'body',
dataIndex: 'body',
key: 'body',
render: (field): ColumnTypeRender<Record<string, unknown>> => ({
props: {
style: defaultTableStyle,
},
children: (
<TableBodyContent
dangerouslySetInnerHTML={{
__html: convert.toHtml(dompurify.sanitize(field)),
}}
linesPerRow={linesPerRow}
/>
),
}),
},
];
}, [fields, linesPerRow, onClickExpand]);
return ( return (
<Table <Table
size="small" size="small"
columns={columns} columns={columns}
dataSource={flattenLogData} dataSource={dataSource}
pagination={false} pagination={false}
rowKey="id" rowKey="id"
bordered bordered

View File

@ -0,0 +1,23 @@
import { ColumnsType, ColumnType } from 'antd/es/table';
import { IField } from 'types/api/logs/fields';
import { ILog } from 'types/api/logs/log';
export type ColumnTypeRender<T = unknown> = ReturnType<
NonNullable<ColumnType<T>['render']>
>;
export type LogsTableViewProps = {
logs: ILog[];
fields: IField[];
linesPerRow: number;
onClickExpand: (log: ILog) => void;
};
export type UseTableViewResult = {
columns: ColumnsType<Record<string, unknown>>;
dataSource: Record<string, string>[];
};
export type UseTableViewProps = {
appendTo?: 'center' | 'end';
} & LogsTableViewProps;

View File

@ -0,0 +1,114 @@
import { ExpandAltOutlined } from '@ant-design/icons';
import Convert from 'ansi-to-html';
import { Typography } from 'antd';
import { ColumnsType } from 'antd/es/table';
import dayjs from 'dayjs';
import dompurify from 'dompurify';
import { FlatLogData } from 'lib/logs/flatLogData';
import { useMemo } from 'react';
import { ILog } from 'types/api/logs/log';
import { ExpandIconWrapper } from '../RawLogView/styles';
import { defaultCellStyle, defaultTableStyle } from './config';
import { TableBodyContent } from './styles';
import {
ColumnTypeRender,
UseTableViewProps,
UseTableViewResult,
} from './types';
const convert = new Convert();
export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
const {
logs,
fields,
linesPerRow,
onClickExpand,
appendTo = 'center',
} = props;
const flattenLogData = useMemo(() => logs.map((log) => FlatLogData(log)), [
logs,
]);
const columns: ColumnsType<Record<string, unknown>> = useMemo(() => {
const fieldColumns: ColumnsType<Record<string, unknown>> = fields
.filter((e) => e.name !== 'id')
.map(({ name }) => ({
title: name,
dataIndex: name,
key: name,
render: (field): ColumnTypeRender<Record<string, unknown>> => ({
props: {
style: defaultCellStyle,
},
children: (
<Typography.Paragraph ellipsis={{ rows: linesPerRow }}>
{field}
</Typography.Paragraph>
),
}),
}));
return [
{
title: '',
dataIndex: 'id',
key: 'expand',
// https://github.com/ant-design/ant-design/discussions/36886
render: (_, item): ColumnTypeRender<Record<string, unknown>> => ({
props: {
style: defaultCellStyle,
},
children: (
<ExpandIconWrapper
onClick={(): void => {
onClickExpand((item as unknown) as ILog);
}}
>
<ExpandAltOutlined />
</ExpandIconWrapper>
),
}),
},
{
title: 'timestamp',
dataIndex: 'timestamp',
key: 'timestamp',
// https://github.com/ant-design/ant-design/discussions/36886
render: (field): ColumnTypeRender<Record<string, unknown>> => {
const date =
typeof field === 'string'
? dayjs(field).format()
: dayjs(field / 1e6).format();
return {
children: <Typography.Paragraph ellipsis>{date}</Typography.Paragraph>,
};
},
},
...(appendTo === 'center' ? fieldColumns : []),
{
title: 'body',
dataIndex: 'body',
key: 'body',
render: (field): ColumnTypeRender<Record<string, unknown>> => ({
props: {
style: defaultTableStyle,
},
children: (
<TableBodyContent
dangerouslySetInnerHTML={{
__html: convert.toHtml(dompurify.sanitize(field)),
}}
linesPerRow={linesPerRow}
/>
),
}),
},
...(appendTo === 'end' ? fieldColumns : []),
];
}, [fields, linesPerRow, appendTo, onClickExpand]);
return { columns, dataSource: flattenLogData };
};

View File

@ -0,0 +1,5 @@
export type TabLabelProps = {
isDisabled: boolean;
label: string;
tooltipText?: string;
};

View File

@ -0,0 +1,29 @@
import { Tooltip } from 'antd';
import { memo } from 'react';
import { TabLabelProps } from './TabLabel.interfaces';
function TabLabel({
label,
isDisabled,
tooltipText,
}: TabLabelProps): JSX.Element {
const currentLabel = <span>{label}</span>;
if (isDisabled) {
return (
<Tooltip
trigger="hover"
autoAdjustOverflow
placement="top"
title={tooltipText}
>
{currentLabel}
</Tooltip>
);
}
return currentLabel;
}
export default memo(TabLabel);

View File

@ -1 +1 @@
export const style = { fontSize: '1.3125rem' }; export const style = { fontSize: '1rem' };

View File

@ -1,3 +1,5 @@
const SOMETHING_WENT_WRONG = 'Something went wrong';
const getVersion = 'version'; const getVersion = 'version';
export { getVersion }; export { getVersion, SOMETHING_WENT_WRONG };

View File

@ -6,4 +6,6 @@ export enum LOCALSTORAGE {
THEME = 'THEME', THEME = 'THEME',
LOGS_VIEW_MODE = 'LOGS_VIEW_MODE', LOGS_VIEW_MODE = 'LOGS_VIEW_MODE',
LOGS_LINES_PER_ROW = 'LOGS_LINES_PER_ROW', LOGS_LINES_PER_ROW = 'LOGS_LINES_PER_ROW',
LOGS_LIST_OPTIONS = 'LOGS_LIST_OPTIONS',
TRACES_LIST_OPTIONS = 'TRACES_LIST_OPTIONS',
} }

View File

@ -3,6 +3,7 @@ import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider';
import { createIdFromObjectFields } from 'lib/createIdFromObjectFields'; import { createIdFromObjectFields } from 'lib/createIdFromObjectFields';
import { createNewBuilderItemName } from 'lib/newQueryBuilder/createNewBuilderItemName'; import { createNewBuilderItemName } from 'lib/newQueryBuilder/createNewBuilderItemName';
import { import {
AutocompleteType,
BaseAutocompleteData, BaseAutocompleteData,
LocalDataType, LocalDataType,
} from 'types/api/queryBuilder/queryAutocompleteResponse'; } from 'types/api/queryBuilder/queryAutocompleteResponse';
@ -14,6 +15,7 @@ import {
IPromQLQuery, IPromQLQuery,
Query, Query,
QueryState, QueryState,
TagFilter,
} from 'types/api/queryBuilder/queryBuilderData'; } from 'types/api/queryBuilder/queryBuilderData';
import { EQueryType } from 'types/common/dashboard'; import { EQueryType } from 'types/common/dashboard';
import { import {
@ -49,6 +51,11 @@ export const baseAutoCompleteIdKeysOrder: (keyof Omit<
'id' 'id'
>)[] = ['key', 'dataType', 'type', 'isColumn']; >)[] = ['key', 'dataType', 'type', 'isColumn'];
export const autocompleteType: Record<AutocompleteType, AutocompleteType> = {
resource: 'resource',
tag: 'tag',
};
export const formulasNames: string[] = Array.from( export const formulasNames: string[] = Array.from(
Array(MAX_FORMULAS), Array(MAX_FORMULAS),
(_, i) => `F${i + 1}`, (_, i) => `F${i + 1}`,
@ -59,7 +66,6 @@ export const alphabet: string[] = alpha.map((str) => String.fromCharCode(str));
export enum QueryBuilderKeys { export enum QueryBuilderKeys {
GET_AGGREGATE_ATTRIBUTE = 'GET_AGGREGATE_ATTRIBUTE', GET_AGGREGATE_ATTRIBUTE = 'GET_AGGREGATE_ATTRIBUTE',
GET_AGGREGATE_KEYS = 'GET_AGGREGATE_KEYS', GET_AGGREGATE_KEYS = 'GET_AGGREGATE_KEYS',
GET_ATTRIBUTE_KEY = 'GET_ATTRIBUTE_KEY',
} }
export const mapOfOperators = { export const mapOfOperators = {
@ -113,6 +119,11 @@ export const initialAutocompleteData: BaseAutocompleteData = {
type: null, type: null,
}; };
export const initialFilters: TagFilter = {
items: [],
op: 'AND',
};
const initialQueryBuilderFormValues: IBuilderQuery = { const initialQueryBuilderFormValues: IBuilderQuery = {
dataSource: DataSource.METRICS, dataSource: DataSource.METRICS,
queryName: createNewBuilderItemName({ existNames: [], sourceNames: alphabet }), queryName: createNewBuilderItemName({ existNames: [], sourceNames: alphabet }),

View File

@ -0,0 +1 @@
export const DEBOUNCE_DELAY = 200;

View File

@ -1,2 +1,16 @@
export const COMPOSITE_QUERY = 'compositeQuery'; type QueryParamNames =
export const PANEL_TYPES_QUERY = 'panelTypes'; | 'compositeQuery'
| 'panelTypes'
| 'pageSize'
| 'viewMode'
| 'selectedFields'
| 'linesPerRow';
export const queryParamNamesMap: Record<QueryParamNames, QueryParamNames> = {
compositeQuery: 'compositeQuery',
panelTypes: 'panelTypes',
pageSize: 'pageSize',
viewMode: 'viewMode',
selectedFields: 'selectedFields',
linesPerRow: 'linesPerRow',
};

View File

@ -3,3 +3,5 @@ export const FORMULA_REGEXP = /F\d+/;
export const HAVING_FILTER_REGEXP = /^[-\d.,\s]+$/; export const HAVING_FILTER_REGEXP = /^[-\d.,\s]+$/;
export const TYPE_ADDON_REGEXP = /_(.+)/; export const TYPE_ADDON_REGEXP = /_(.+)/;
export const SPLIT_FIRST_UNDERSCORE = /(?<!^)_/;

View File

@ -2,6 +2,8 @@ import { CSSProperties } from 'react';
export const ITEMS_PER_PAGE_OPTIONS = [25, 50, 100, 200]; export const ITEMS_PER_PAGE_OPTIONS = [25, 50, 100, 200];
export const DEFAULT_PER_PAGE_VALUE = 100;
export const defaultSelectStyle: CSSProperties = { export const defaultSelectStyle: CSSProperties = {
minWidth: '6rem', minWidth: '6rem',
}; };

View File

@ -0,0 +1,7 @@
import { OptionsMenuConfig } from 'container/OptionsMenu/types';
export type ExplorerControlPanelProps = {
isShowPageSize: boolean;
isLoading: boolean;
optionsMenuConfig?: OptionsMenuConfig;
};

View File

@ -0,0 +1,29 @@
import { Col, Row } from 'antd';
import OptionsMenu from 'container/OptionsMenu';
import PageSizeSelect from 'container/PageSizeSelect';
import { ExplorerControlPanelProps } from './ExplorerControlPanel.interfaces';
import { ContainerStyled } from './styles';
function ExplorerControlPanel({
isLoading,
isShowPageSize,
optionsMenuConfig,
}: ExplorerControlPanelProps): JSX.Element {
return (
<ContainerStyled>
<Row justify="end" gutter={30}>
{optionsMenuConfig && (
<Col>
<OptionsMenu config={optionsMenuConfig} />
</Col>
)}
<Col>
<PageSizeSelect isLoading={isLoading} isShow={isShowPageSize} />
</Col>
</Row>
</ContainerStyled>
);
}
export default ExplorerControlPanel;

View File

@ -0,0 +1,5 @@
import styled from 'styled-components';
export const ContainerStyled = styled.div`
margin-bottom: 0.3rem;
`;

View File

@ -1,8 +1,7 @@
import { Button, Typography } from 'antd'; import { Button, Typography } from 'antd';
import createDashboard from 'api/dashboard/create'; import createDashboard from 'api/dashboard/create';
import axios from 'axios';
import { useGetAllDashboard } from 'hooks/dashboard/useGetAllDashboard'; import { useGetAllDashboard } from 'hooks/dashboard/useGetAllDashboard';
import { useNotifications } from 'hooks/useNotifications'; import useAxiosError from 'hooks/useAxiosError';
import { useCallback, useMemo, useState } from 'react'; import { useCallback, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useMutation } from 'react-query'; import { useMutation } from 'react-query';
@ -15,10 +14,9 @@ import {
Title, Title,
Wrapper, Wrapper,
} from './styles'; } from './styles';
import { getSelectOptions } from './utils'; import { filterOptions, getSelectOptions } from './utils';
function ExportPanel({ isLoading, onExport }: ExportPanelProps): JSX.Element { function ExportPanel({ isLoading, onExport }: ExportPanelProps): JSX.Element {
const { notifications } = useNotifications();
const { t } = useTranslation(['dashboard']); const { t } = useTranslation(['dashboard']);
const [selectedDashboardId, setSelectedDashboardId] = useState<string | null>( const [selectedDashboardId, setSelectedDashboardId] = useState<string | null>(
@ -31,21 +29,19 @@ function ExportPanel({ isLoading, onExport }: ExportPanelProps): JSX.Element {
refetch, refetch,
} = useGetAllDashboard(); } = useGetAllDashboard();
const handleError = useAxiosError();
const { const {
mutate: createNewDashboard, mutate: createNewDashboard,
isLoading: createDashboardLoading, isLoading: createDashboardLoading,
} = useMutation(createDashboard, { } = useMutation(createDashboard, {
onSuccess: (data) => { onSuccess: (data) => {
onExport(data?.payload || null); if (data.payload) {
onExport(data?.payload);
}
refetch(); refetch();
}, },
onError: (error) => { onError: handleError,
if (axios.isAxiosError(error)) {
notifications.error({
message: error.message,
});
}
},
}); });
const options = useMemo(() => getSelectOptions(data?.payload || []), [data]); const options = useMemo(() => getSelectOptions(data?.payload || []), [data]);
@ -90,10 +86,12 @@ function ExportPanel({ isLoading, onExport }: ExportPanelProps): JSX.Element {
<DashboardSelect <DashboardSelect
placeholder="Select Dashboard" placeholder="Select Dashboard"
options={options} options={options}
showSearch
loading={isDashboardLoading} loading={isDashboardLoading}
disabled={isDashboardLoading} disabled={isDashboardLoading}
value={selectedDashboardId} value={selectedDashboardId}
onSelect={handleSelect} onSelect={handleSelect}
filterOption={filterOptions}
/> />
<Button <Button
type="primary" type="primary"

View File

@ -1,9 +0,0 @@
export const MENU_KEY = {
EXPORT: 'export',
CREATE_ALERTS: 'create-alerts',
};
export const MENU_LABEL = {
EXPORT: 'Export Panel',
CREATE_ALERTS: 'Create Alerts',
};

View File

@ -1,12 +1,12 @@
import { Button, Dropdown, MenuProps, Modal } from 'antd'; import { AlertOutlined, AreaChartOutlined } from '@ant-design/icons';
import { COMPOSITE_QUERY } from 'constants/queryBuilderQueryNames'; import { Button, Modal, Space } from 'antd';
import { queryParamNamesMap } from 'constants/queryBuilderQueryNames';
import ROUTES from 'constants/routes'; import ROUTES from 'constants/routes';
import history from 'lib/history'; import history from 'lib/history';
import { useCallback, useMemo, useState } from 'react'; import { useCallback, useState } from 'react';
import { Dashboard } from 'types/api/dashboard/getAll'; import { Dashboard } from 'types/api/dashboard/getAll';
import { Query } from 'types/api/queryBuilder/queryBuilderData'; import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { MENU_KEY, MENU_LABEL } from './config';
import ExportPanelContainer from './ExportPanel'; import ExportPanelContainer from './ExportPanel';
function ExportPanel({ function ExportPanel({
@ -22,57 +22,43 @@ function ExportPanel({
const onCreateAlertsHandler = useCallback(() => { const onCreateAlertsHandler = useCallback(() => {
history.push( history.push(
`${ROUTES.ALERTS_NEW}?${COMPOSITE_QUERY}=${encodeURIComponent( `${ROUTES.ALERTS_NEW}?${
JSON.stringify(query), queryParamNamesMap.compositeQuery
)}`, }=${encodeURIComponent(JSON.stringify(query))}`,
); );
}, [query]); }, [query]);
const onMenuClickHandler: MenuProps['onClick'] = useCallback(
(e: OnClickProps) => {
if (e.key === MENU_KEY.EXPORT) {
onModalToggle(true);
}
if (e.key === MENU_KEY.CREATE_ALERTS) {
onCreateAlertsHandler();
}
},
[onModalToggle, onCreateAlertsHandler],
);
const menu: MenuProps = useMemo(
() => ({
items: [
{
key: MENU_KEY.EXPORT,
label: MENU_LABEL.EXPORT,
},
{
key: MENU_KEY.CREATE_ALERTS,
label: MENU_LABEL.CREATE_ALERTS,
},
],
onClick: onMenuClickHandler,
}),
[onMenuClickHandler],
);
const onCancel = (value: boolean) => (): void => { const onCancel = (value: boolean) => (): void => {
onModalToggle(value); onModalToggle(value);
}; };
const onAddToDashboard = (): void => {
setIsExport(true);
};
return ( return (
<> <>
<Dropdown trigger={['click']} menu={menu}> <Space size={24}>
<Button>Actions</Button> <Button
</Dropdown> icon={<AreaChartOutlined />}
onClick={onAddToDashboard}
type="primary"
>
Add to Dashboard
</Button>
<Button onClick={onCreateAlertsHandler} icon={<AlertOutlined />}>
Setup Alerts
</Button>
</Space>
<Modal <Modal
footer={null} footer={null}
onOk={onCancel(false)} onOk={onCancel(false)}
onCancel={onCancel(false)} onCancel={onCancel(false)}
open={isExport} open={isExport}
centered centered
destroyOnClose
> >
<ExportPanelContainer <ExportPanelContainer
query={query} query={query}
@ -84,18 +70,12 @@ function ExportPanel({
); );
} }
ExportPanel.defaultProps = {
isLoading: false,
};
interface OnClickProps {
key: string;
}
export interface ExportPanelProps { export interface ExportPanelProps {
isLoading?: boolean; isLoading?: boolean;
onExport: (dashboard: Dashboard | null) => void; onExport: (dashboard: Dashboard | null) => void;
query: Query | null; query: Query | null;
} }
ExportPanel.defaultProps = { isLoading: false };
export default ExportPanel; export default ExportPanel;

View File

@ -8,3 +8,11 @@ export const getSelectOptions = (
label: data.title, label: data.title,
value: uuid, value: uuid,
})); }));
export const filterOptions: SelectProps['filterOption'] = (
input,
options,
): boolean =>
(options?.label?.toString() ?? '')
?.toLowerCase()
.includes(input.toLowerCase());

View File

@ -26,6 +26,7 @@ function FullView({
name, name,
yAxisUnit, yAxisUnit,
onDragSelect, onDragSelect,
isDependedDataLoaded = false,
}: FullViewProps): JSX.Element { }: FullViewProps): JSX.Element {
const { selectedTime: globalSelectedTime } = useSelector< const { selectedTime: globalSelectedTime } = useSelector<
AppState, AppState,
@ -61,6 +62,7 @@ function FullView({
}, },
{ {
queryKey, queryKey,
enabled: !isDependedDataLoaded,
}, },
); );
@ -76,9 +78,7 @@ function FullView({
[response], [response],
); );
const isLoading = response.isLoading === true; if (response.status === 'idle' || response.status === 'loading') {
if (isLoading) {
return <Spinner height="100%" size="large" tip="Loading..." />; return <Spinner height="100%" size="large" tip="Loading..." />;
} }
@ -123,6 +123,7 @@ interface FullViewProps {
name: string; name: string;
yAxisUnit?: string; yAxisUnit?: string;
onDragSelect?: (start: number, end: number) => void; onDragSelect?: (start: number, end: number) => void;
isDependedDataLoaded?: boolean;
} }
FullView.defaultProps = { FullView.defaultProps = {
@ -130,6 +131,7 @@ FullView.defaultProps = {
onClickHandler: undefined, onClickHandler: undefined,
yAxisUnit: undefined, yAxisUnit: undefined,
onDragSelect: undefined, onDragSelect: undefined,
isDependedDataLoaded: undefined,
}; };
export default FullView; export default FullView;

View File

@ -83,6 +83,11 @@ function GridCardGraph({
const updatedQuery = useStepInterval(widget?.query); const updatedQuery = useStepInterval(widget?.query);
const isEmptyWidget = useMemo(
() => widget?.id === 'empty' || isEmpty(widget),
[widget],
);
const queryResponse = useGetQueryRange( const queryResponse = useGetQueryRange(
{ {
selectedTime: widget?.timePreferance, selectedTime: widget?.timePreferance,
@ -101,7 +106,7 @@ function GridCardGraph({
variables, variables,
], ],
keepPreviousData: true, keepPreviousData: true,
enabled: isGraphVisible, enabled: isGraphVisible && !isEmptyWidget,
refetchOnMount: false, refetchOnMount: false,
onError: (error) => { onError: (error) => {
setErrorMessage(error.message); setErrorMessage(error.message);
@ -131,8 +136,6 @@ function GridCardGraph({
); );
const onDeleteHandler = useCallback(() => { const onDeleteHandler = useCallback(() => {
const isEmptyWidget = widget?.id === 'empty' || isEmpty(widget);
const widgetId = isEmptyWidget ? layout[0].i : widget?.id; const widgetId = isEmptyWidget ? layout[0].i : widget?.id;
featureResponse featureResponse
@ -147,7 +150,8 @@ function GridCardGraph({
}); });
}); });
}, [ }, [
widget, isEmptyWidget,
widget?.id,
layout, layout,
featureResponse, featureResponse,
deleteWidget, deleteWidget,

View File

@ -9,7 +9,7 @@ import {
import { Dropdown, MenuProps, Tooltip, Typography } from 'antd'; import { Dropdown, MenuProps, Tooltip, Typography } from 'antd';
import { MenuItemType } from 'antd/es/menu/hooks/useItems'; import { MenuItemType } from 'antd/es/menu/hooks/useItems';
import Spinner from 'components/Spinner'; import Spinner from 'components/Spinner';
import { COMPOSITE_QUERY } from 'constants/queryBuilderQueryNames'; import { queryParamNamesMap } from 'constants/queryBuilderQueryNames';
import useComponentPermission from 'hooks/useComponentPermission'; import useComponentPermission from 'hooks/useComponentPermission';
import history from 'lib/history'; import history from 'lib/history';
import { useCallback, useMemo, useState } from 'react'; import { useCallback, useMemo, useState } from 'react';
@ -64,7 +64,9 @@ function WidgetHeader({
history.push( history.push(
`${window.location.pathname}/new?widgetId=${widgetId}&graphType=${ `${window.location.pathname}/new?widgetId=${widgetId}&graphType=${
widget.panelTypes widget.panelTypes
}&${COMPOSITE_QUERY}=${encodeURIComponent(JSON.stringify(widget.query))}`, }&${queryParamNamesMap.compositeQuery}=${encodeURIComponent(
JSON.stringify(widget.query),
)}`,
); );
}, [widget.id, widget.panelTypes, widget.query]); }, [widget.id, widget.panelTypes, widget.query]);

View File

@ -5,7 +5,7 @@ import { ColumnsType } from 'antd/lib/table';
import saveAlertApi from 'api/alerts/save'; import saveAlertApi from 'api/alerts/save';
import { ResizeTable } from 'components/ResizeTable'; import { ResizeTable } from 'components/ResizeTable';
import TextToolTip from 'components/TextToolTip'; import TextToolTip from 'components/TextToolTip';
import { COMPOSITE_QUERY } from 'constants/queryBuilderQueryNames'; import { queryParamNamesMap } from 'constants/queryBuilderQueryNames';
import ROUTES from 'constants/routes'; import ROUTES from 'constants/routes';
import useComponentPermission from 'hooks/useComponentPermission'; import useComponentPermission from 'hooks/useComponentPermission';
import useInterval from 'hooks/useInterval'; import useInterval from 'hooks/useInterval';
@ -75,11 +75,9 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
const compositeQuery = mapQueryDataFromApi(record.condition.compositeQuery); const compositeQuery = mapQueryDataFromApi(record.condition.compositeQuery);
history.push( history.push(
`${ `${ROUTES.EDIT_ALERTS}?ruleId=${record.id.toString()}&${
ROUTES.EDIT_ALERTS queryParamNamesMap.compositeQuery
}?ruleId=${record.id.toString()}&${COMPOSITE_QUERY}=${encodeURIComponent( }=${encodeURIComponent(JSON.stringify(compositeQuery))}`,
JSON.stringify(compositeQuery),
)}`,
); );
}) })
.catch(handleError); .catch(handleError);

View File

@ -83,12 +83,17 @@ function LogControls(): JSX.Element | null {
const flattenLogData = useMemo( const flattenLogData = useMemo(
() => () =>
logs.map((log) => logs.map((log) => {
FlatLogData({ const timestamp =
typeof log.timestamp === 'string'
? dayjs(log.timestamp).format()
: dayjs(log.timestamp / 1e6).format();
return FlatLogData({
...log, ...log,
timestamp: (dayjs(log.timestamp / 1e6).format() as unknown) as number, timestamp,
});
}), }),
),
[logs], [logs],
); );

View File

@ -1,148 +1,43 @@
import { MinusCircleOutlined, PlusCircleOutlined } from '@ant-design/icons'; import { MinusCircleOutlined, PlusCircleOutlined } from '@ant-design/icons';
import { Button, Col, Popover } from 'antd'; import { Button, Col, Popover } from 'antd';
import getStep from 'lib/getStep'; import { OPERATORS } from 'constants/queryBuilder';
import { generateFilterQuery } from 'lib/logs/generateFilterQuery'; import { removeJSONStringifyQuotes } from 'lib/removeJSONStringifyQuotes';
import { getIdConditions } from 'pages/Logs/utils'; import { memo, useCallback, useMemo } from 'react';
import { memo, useMemo } from 'react';
import { connect, useDispatch, useSelector } from 'react-redux';
import { bindActionCreators, Dispatch } from 'redux';
import { ThunkDispatch } from 'redux-thunk';
import { getLogs } from 'store/actions/logs/getLogs';
import { getLogsAggregate } from 'store/actions/logs/getLogsAggregate';
import { AppState } from 'store/reducers';
import AppActions from 'types/actions';
import { SET_SEARCH_QUERY_STRING, TOGGLE_LIVE_TAIL } from 'types/actions/logs';
import { GlobalReducer } from 'types/reducer/globalTime';
import { ILogsReducer } from 'types/reducer/logs';
const removeJSONStringifyQuotes = (s: string): string => {
if (!s || !s.length) {
return s;
}
if (s[0] === '"' && s[s.length - 1] === '"') {
return s.slice(1, s.length - 1);
}
return s;
};
interface ActionItemProps {
fieldKey: string;
fieldValue: string;
getLogs: (props: Parameters<typeof getLogs>[0]) => ReturnType<typeof getLogs>;
getLogsAggregate: (
props: Parameters<typeof getLogsAggregate>[0],
) => ReturnType<typeof getLogsAggregate>;
}
function ActionItem({ function ActionItem({
fieldKey, fieldKey,
fieldValue, fieldValue,
getLogs, onClickActionItem,
getLogsAggregate, }: ActionItemProps): JSX.Element {
}: ActionItemProps): JSX.Element | unknown { const handleClick = useCallback(
const { (operator: string) => {
searchFilter: { queryString },
logLinesPerPage,
idStart,
liveTail,
idEnd,
order,
} = useSelector<AppState, ILogsReducer>((store) => store.logs);
const dispatch = useDispatch<Dispatch<AppActions>>();
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const handleQueryAdd = (newQueryString: string): void => {
let updatedQueryString = queryString || '';
if (updatedQueryString.length === 0) {
updatedQueryString += `${newQueryString}`;
} else {
updatedQueryString += ` AND ${newQueryString}`;
}
dispatch({
type: SET_SEARCH_QUERY_STRING,
payload: {
searchQueryString: updatedQueryString,
},
});
if (liveTail === 'STOPPED') {
getLogs({
q: updatedQueryString,
limit: logLinesPerPage,
orderBy: 'timestamp',
order,
timestampStart: minTime,
timestampEnd: maxTime,
...getIdConditions(idStart, idEnd, order),
});
getLogsAggregate({
timestampStart: minTime,
timestampEnd: maxTime,
step: getStep({
start: minTime,
end: maxTime,
inputFormat: 'ns',
}),
q: updatedQueryString,
});
} else if (liveTail === 'PLAYING') {
dispatch({
type: TOGGLE_LIVE_TAIL,
payload: 'PAUSED',
});
setTimeout(
() =>
dispatch({
type: TOGGLE_LIVE_TAIL,
payload: liveTail,
}),
0,
);
}
};
const validatedFieldValue = removeJSONStringifyQuotes(fieldValue); const validatedFieldValue = removeJSONStringifyQuotes(fieldValue);
onClickActionItem(fieldKey, validatedFieldValue, operator);
},
[onClickActionItem, fieldKey, fieldValue],
);
const onClickHandler = useCallback(
(operator: string) => (): void => {
handleClick(operator);
},
[handleClick],
);
const PopOverMenuContent = useMemo( const PopOverMenuContent = useMemo(
() => ( () => (
<Col> <Col>
<Button <Button type="text" size="small" onClick={onClickHandler(OPERATORS.IN)}>
type="text"
size="small"
onClick={(): void =>
handleQueryAdd(
generateFilterQuery({
fieldKey,
fieldValue: validatedFieldValue,
type: 'IN',
}),
)
}
>
<PlusCircleOutlined /> Filter for value <PlusCircleOutlined /> Filter for value
</Button> </Button>
<br /> <br />
<Button <Button type="text" size="small" onClick={onClickHandler(OPERATORS.NIN)}>
type="text"
size="small"
onClick={(): void =>
handleQueryAdd(
generateFilterQuery({
fieldKey,
fieldValue: validatedFieldValue,
type: 'NIN',
}),
)
}
>
<MinusCircleOutlined /> Filter out value <MinusCircleOutlined /> Filter out value
</Button> </Button>
</Col> </Col>
), ),
// eslint-disable-next-line react-hooks/exhaustive-deps [onClickHandler],
[fieldKey, validatedFieldValue],
); );
return ( return (
<Popover placement="bottomLeft" content={PopOverMenuContent} trigger="click"> <Popover placement="bottomLeft" content={PopOverMenuContent} trigger="click">
@ -152,19 +47,15 @@ function ActionItem({
</Popover> </Popover>
); );
} }
interface DispatchProps {
getLogs: (props: Parameters<typeof getLogs>[0]) => (dispatch: never) => void; export interface ActionItemProps {
getLogsAggregate: ( fieldKey: string;
props: Parameters<typeof getLogsAggregate>[0], fieldValue: string;
) => (dispatch: never) => void; onClickActionItem: (
fieldKey: string,
fieldValue: string,
operator: string,
) => void;
} }
const mapDispatchToProps = ( export default memo(ActionItem);
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
): DispatchProps => ({
getLogs: bindActionCreators(getLogs, dispatch),
getLogsAggregate: bindActionCreators(getLogsAggregate, dispatch),
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export default connect(null, mapDispatchToProps)(memo(ActionItem as any));

View File

@ -3,7 +3,9 @@ import { LinkOutlined } from '@ant-design/icons';
import { Input, Space, Tooltip } from 'antd'; import { Input, Space, Tooltip } from 'antd';
import { ColumnsType } from 'antd/es/table'; import { ColumnsType } from 'antd/es/table';
import Editor from 'components/Editor'; import Editor from 'components/Editor';
import AddToQueryHOC from 'components/Logs/AddToQueryHOC'; import AddToQueryHOC, {
AddToQueryHOCProps,
} from 'components/Logs/AddToQueryHOC';
import CopyClipboardHOC from 'components/Logs/CopyClipboardHOC'; import CopyClipboardHOC from 'components/Logs/CopyClipboardHOC';
import { ResizeTable } from 'components/ResizeTable'; import { ResizeTable } from 'components/ResizeTable';
import ROUTES from 'constants/routes'; import ROUTES from 'constants/routes';
@ -18,7 +20,7 @@ import AppActions from 'types/actions';
import { SET_DETAILED_LOG_DATA } from 'types/actions/logs'; import { SET_DETAILED_LOG_DATA } from 'types/actions/logs';
import { ILog } from 'types/api/logs/log'; import { ILog } from 'types/api/logs/log';
import ActionItem from './ActionItem'; import ActionItem, { ActionItemProps } from './ActionItem';
import { flattenObject, recursiveParseJSON } from './utils'; import { flattenObject, recursiveParseJSON } from './utils';
// Fields which should be restricted from adding it to query // Fields which should be restricted from adding it to query
@ -27,7 +29,16 @@ const RESTRICTED_FIELDS = ['timestamp'];
interface TableViewProps { interface TableViewProps {
logData: ILog; logData: ILog;
} }
function TableView({ logData }: TableViewProps): JSX.Element | null {
type Props = TableViewProps &
Pick<AddToQueryHOCProps, 'onAddToQuery'> &
Pick<ActionItemProps, 'onClickActionItem'>;
function TableView({
logData,
onAddToQuery,
onClickActionItem,
}: Props): JSX.Element | null {
const [fieldSearchInput, setFieldSearchInput] = useState<string>(''); const [fieldSearchInput, setFieldSearchInput] = useState<string>('');
const dispatch = useDispatch<Dispatch<AppActions>>(); const dispatch = useDispatch<Dispatch<AppActions>>();
@ -84,7 +95,13 @@ function TableView({ logData }: TableViewProps): JSX.Element | null {
render: (fieldData: Record<string, string>): JSX.Element | null => { render: (fieldData: Record<string, string>): JSX.Element | null => {
const fieldKey = fieldData.field.split('.').slice(-1); const fieldKey = fieldData.field.split('.').slice(-1);
if (!RESTRICTED_FIELDS.includes(fieldKey[0])) { if (!RESTRICTED_FIELDS.includes(fieldKey[0])) {
return <ActionItem fieldKey={fieldKey} fieldValue={fieldData.value} />; return (
<ActionItem
fieldKey={fieldKey[0]}
fieldValue={fieldData.value}
onClickActionItem={onClickActionItem}
/>
);
} }
return null; return null;
}, },
@ -128,7 +145,11 @@ function TableView({ logData }: TableViewProps): JSX.Element | null {
if (!RESTRICTED_FIELDS.includes(fieldKey[0])) { if (!RESTRICTED_FIELDS.includes(fieldKey[0])) {
return ( return (
<AddToQueryHOC fieldKey={fieldKey[0]} fieldValue={flattenLogData[field]}> <AddToQueryHOC
fieldKey={fieldKey[0]}
fieldValue={flattenLogData[field]}
onAddToQuery={onAddToQuery}
>
{renderedField} {renderedField}
</AddToQueryHOC> </AddToQueryHOC>
); );

View File

@ -1,17 +1,48 @@
import { Drawer, Tabs } from 'antd'; import LogDetail from 'components/LogDetail';
import { useDispatch, useSelector } from 'react-redux'; import ROUTES from 'constants/routes';
import { Dispatch } from 'redux'; import { getGeneratedFilterQueryString } from 'lib/getGeneratedFilterQueryString';
import getStep from 'lib/getStep';
import { getIdConditions } from 'pages/Logs/utils';
import { memo, useCallback } from 'react';
import { connect, useDispatch, useSelector } from 'react-redux';
import { useHistory } from 'react-router-dom';
import { bindActionCreators, Dispatch } from 'redux';
import { ThunkDispatch } from 'redux-thunk';
import { getLogs } from 'store/actions/logs/getLogs';
import { getLogsAggregate } from 'store/actions/logs/getLogsAggregate';
import { AppState } from 'store/reducers'; import { AppState } from 'store/reducers';
import AppActions from 'types/actions'; import AppActions from 'types/actions';
import { SET_DETAILED_LOG_DATA } from 'types/actions/logs'; import {
SET_DETAILED_LOG_DATA,
SET_SEARCH_QUERY_STRING,
TOGGLE_LIVE_TAIL,
} from 'types/actions/logs';
import { GlobalReducer } from 'types/reducer/globalTime';
import { ILogsReducer } from 'types/reducer/logs'; import { ILogsReducer } from 'types/reducer/logs';
import JSONView from './JsonView'; type LogDetailedViewProps = {
import TableView from './TableView'; getLogs: (props: Parameters<typeof getLogs>[0]) => ReturnType<typeof getLogs>;
getLogsAggregate: (
props: Parameters<typeof getLogsAggregate>[0],
) => ReturnType<typeof getLogsAggregate>;
};
function LogDetailedView(): JSX.Element { function LogDetailedView({
const { detailedLog } = useSelector<AppState, ILogsReducer>( getLogs,
(state) => state.logs, getLogsAggregate,
}: LogDetailedViewProps): JSX.Element {
const history = useHistory();
const {
detailedLog,
searchFilter: { queryString },
logLinesPerPage,
idStart,
liveTail,
idEnd,
order,
} = useSelector<AppState, ILogsReducer>((state) => state.logs);
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
); );
const dispatch = useDispatch<Dispatch<AppActions>>(); const dispatch = useDispatch<Dispatch<AppActions>>();
@ -23,33 +54,109 @@ function LogDetailedView(): JSX.Element {
}); });
}; };
const items = [ const handleAddToQuery = useCallback(
{ (fieldKey: string, fieldValue: string, operator: string) => {
label: 'Table', const updatedQueryString = getGeneratedFilterQueryString(
key: '1', fieldKey,
children: detailedLog && <TableView logData={detailedLog} />, fieldValue,
operator,
queryString,
);
history.replace(`${ROUTES.LOGS}?q=${updatedQueryString}`);
}, },
{ [history, queryString],
label: 'JSON', );
key: '2',
children: detailedLog && <JSONView logData={detailedLog} />, const handleClickActionItem = useCallback(
(fieldKey: string, fieldValue: string, operator: string): void => {
const updatedQueryString = getGeneratedFilterQueryString(
fieldKey,
fieldValue,
operator,
queryString,
);
dispatch({
type: SET_SEARCH_QUERY_STRING,
payload: {
searchQueryString: updatedQueryString,
}, },
]; });
if (liveTail === 'STOPPED') {
getLogs({
q: updatedQueryString,
limit: logLinesPerPage,
orderBy: 'timestamp',
order,
timestampStart: minTime,
timestampEnd: maxTime,
...getIdConditions(idStart, idEnd, order),
});
getLogsAggregate({
timestampStart: minTime,
timestampEnd: maxTime,
step: getStep({
start: minTime,
end: maxTime,
inputFormat: 'ns',
}),
q: updatedQueryString,
});
} else if (liveTail === 'PLAYING') {
dispatch({
type: TOGGLE_LIVE_TAIL,
payload: 'PAUSED',
});
setTimeout(
() =>
dispatch({
type: TOGGLE_LIVE_TAIL,
payload: liveTail,
}),
0,
);
}
},
[
dispatch,
getLogs,
getLogsAggregate,
idEnd,
idStart,
liveTail,
logLinesPerPage,
maxTime,
minTime,
order,
queryString,
],
);
return ( return (
<Drawer <LogDetail
width="60%" log={detailedLog}
title="Log Details"
placement="right"
closable
onClose={onDrawerClose} onClose={onDrawerClose}
open={detailedLog !== null} onAddToQuery={handleAddToQuery}
style={{ overscrollBehavior: 'contain' }} onClickActionItem={handleClickActionItem}
destroyOnClose />
>
<Tabs defaultActiveKey="1" items={items} />
</Drawer>
); );
} }
export default LogDetailedView; interface DispatchProps {
getLogs: (props: Parameters<typeof getLogs>[0]) => (dispatch: never) => void;
getLogsAggregate: (
props: Parameters<typeof getLogsAggregate>[0],
) => (dispatch: never) => void;
}
const mapDispatchToProps = (
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
): DispatchProps => ({
getLogs: bindActionCreators(getLogs, dispatch),
getLogsAggregate: bindActionCreators(getLogsAggregate, dispatch),
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export default connect(null, mapDispatchToProps)(memo(LogDetailedView as any));

View File

@ -0,0 +1,51 @@
import { Button } from 'antd';
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
import { QueryBuilder } from 'container/QueryBuilder';
import { QueryBuilderProps } from 'container/QueryBuilder/QueryBuilder.interfaces';
import { useGetPanelTypesQueryParam } from 'hooks/queryBuilder/useGetPanelTypesQueryParam';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
import { ButtonWrapperStyled } from 'pages/LogsExplorer/styles';
import { prepareQueryWithDefaultTimestamp } from 'pages/LogsExplorer/utils';
import { memo, useMemo } from 'react';
import { DataSource } from 'types/common/queryBuilder';
function LogExplorerQuerySection(): JSX.Element {
const { handleRunQuery, updateAllQueriesOperators } = useQueryBuilder();
const panelTypes = useGetPanelTypesQueryParam(PANEL_TYPES.LIST);
const defaultValue = useMemo(() => {
const updatedQuery = updateAllQueriesOperators(
initialQueriesMap.logs,
PANEL_TYPES.LIST,
DataSource.LOGS,
);
return prepareQueryWithDefaultTimestamp(updatedQuery);
}, [updateAllQueriesOperators]);
useShareBuilderUrl(defaultValue);
const filterConfigs: QueryBuilderProps['filterConfigs'] = useMemo(() => {
const isTable = panelTypes === PANEL_TYPES.TABLE;
const config: QueryBuilderProps['filterConfigs'] = {
stepInterval: { isHidden: isTable, isDisabled: true },
};
return config;
}, [panelTypes]);
return (
<QueryBuilder
panelType={panelTypes}
config={{ initialDataSource: DataSource.LOGS, queryVariant: 'static' }}
filterConfigs={filterConfigs}
actions={
<ButtonWrapperStyled>
<Button type="primary" onClick={handleRunQuery}>
Run Query
</Button>
</ButtonWrapperStyled>
}
/>
);
}
export default memo(LogExplorerQuerySection);

View File

@ -0,0 +1,6 @@
import { QueryData } from 'types/api/widgets/getQuery';
export type LogsExplorerChartProps = {
data: QueryData[];
isLoading: boolean;
};

View File

@ -1,48 +1,43 @@
import Graph from 'components/Graph'; import Graph from 'components/Graph';
import Spinner from 'components/Spinner'; import Spinner from 'components/Spinner';
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder'; import getChartData, { GetChartDataProps } from 'lib/getChartData';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys'; import { colors } from 'lib/getRandomColor';
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { getExplorerChartData } from 'lib/explorer/getExplorerChartData';
import { memo, useMemo } from 'react'; import { memo, useMemo } from 'react';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { GlobalReducer } from 'types/reducer/globalTime';
import { LogsExplorerChartProps } from './LogsExplorerChart.interfaces';
import { CardStyled } from './LogsExplorerChart.styled'; import { CardStyled } from './LogsExplorerChart.styled';
function LogsExplorerChart(): JSX.Element { function LogsExplorerChart({
const { stagedQuery, panelType, isEnabledQuery } = useQueryBuilder(); data,
isLoading,
}: 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 { selectedTime } = useSelector<AppState, GlobalReducer>( const graphData = useMemo(
(state) => state.globalTime, () =>
); getChartData({
queryData: [
const { data, isFetching } = useGetQueryRange(
{ {
query: stagedQuery || initialQueriesMap.metrics, queryData: data,
graphType: panelType || PANEL_TYPES.LIST,
globalSelectedInterval: selectedTime,
selectedTime: 'GLOBAL_TIME',
},
{
queryKey: [REACT_QUERY_KEY.GET_QUERY_RANGE, selectedTime, stagedQuery],
enabled: isEnabledQuery,
}, },
],
createDataset: handleCreateDatasets,
}),
[data],
); );
const graphData = useMemo(() => {
if (data?.payload.data && data.payload.data.result.length > 0) {
return getExplorerChartData([data.payload.data.result[0]]);
}
return getExplorerChartData([]);
}, [data]);
return ( return (
<CardStyled> <CardStyled>
{isFetching ? ( {isLoading ? (
<Spinner size="default" height="100%" /> <Spinner size="default" height="100%" />
) : ( ) : (
<Graph <Graph

View File

@ -0,0 +1,6 @@
import { CSSProperties } from 'react';
export const infinityDefaultStyles: CSSProperties = {
height: 'auto',
width: '100%',
};

View File

@ -0,0 +1,98 @@
import { ColumnTypeRender } from 'components/Logs/TableView/types';
import { useTableView } from 'components/Logs/TableView/useTableView';
import { cloneElement, ReactElement, ReactNode, useCallback } from 'react';
import { TableComponents, TableVirtuoso } from 'react-virtuoso';
import { infinityDefaultStyles } from './config';
import {
TableCellStyled,
TableHeaderCellStyled,
TableRowStyled,
TableStyled,
} from './styles';
import { InfinityTableProps } from './types';
// eslint-disable-next-line react/function-component-definition
const CustomTable: TableComponents['Table'] = ({ style, children }) => (
<TableStyled style={style}>{children}</TableStyled>
);
// eslint-disable-next-line react/function-component-definition
const CustomTableRow: TableComponents['TableRow'] = ({
children,
context,
...props
// eslint-disable-next-line react/jsx-props-no-spreading
}) => <TableRowStyled {...props}>{children}</TableRowStyled>;
function InfinityTable({
tableViewProps,
infitiyTableProps,
}: InfinityTableProps): JSX.Element | null {
const { onEndReached } = infitiyTableProps;
const { dataSource, columns } = useTableView(tableViewProps);
const itemContent = useCallback(
(index: number, log: Record<string, unknown>): JSX.Element => (
<>
{columns.map((column) => {
if (!column.render) return <td>Empty</td>;
const element: ColumnTypeRender<Record<string, unknown>> = column.render(
log[column.key as keyof Record<string, unknown>],
log,
index,
);
const elementWithChildren = element as Exclude<
ColumnTypeRender<Record<string, unknown>>,
ReactNode
>;
const children = elementWithChildren.children as ReactElement;
const props = elementWithChildren.props as Record<string, unknown>;
return (
<TableCellStyled key={column.key}>
{cloneElement(children, props)}
</TableCellStyled>
);
})}
</>
),
[columns],
);
const tableHeader = useCallback(
() => (
<tr>
{columns.map((column) => (
<TableHeaderCellStyled key={column.key}>
{column.title as string}
</TableHeaderCellStyled>
))}
</tr>
),
[columns],
);
return (
<TableVirtuoso
style={infinityDefaultStyles}
data={dataSource}
components={{
Table: CustomTable,
// TODO: fix it in the future
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
TableRow: CustomTableRow,
}}
itemContent={itemContent}
fixedHeaderContent={tableHeader}
endReached={onEndReached}
totalCount={dataSource.length}
/>
);
}
export default InfinityTable;

View File

@ -0,0 +1,40 @@
import { themeColors } from 'constants/theme';
import styled from 'styled-components';
export const TableStyled = styled.table`
width: 100%;
border-top: 1px solid rgba(253, 253, 253, 0.12);
border-radius: 2px 2px 0 0;
border-collapse: separate;
border-spacing: 0;
border-inline-start: 1px solid rgba(253, 253, 253, 0.12);
border-inline-end: 1px solid rgba(253, 253, 253, 0.12);
`;
export const TableCellStyled = styled.td`
padding: 0.5rem;
border-inline-end: 1px solid rgba(253, 253, 253, 0.12);
border-top: 1px solid rgba(253, 253, 253, 0.12);
background-color: ${themeColors.lightBlack};
`;
export const TableRowStyled = styled.tr`
&:hover {
${TableCellStyled} {
background-color: #1d1d1d;
}
}
`;
export const TableHeaderCellStyled = styled.th`
padding: 0.5rem;
border-inline-end: 1px solid rgba(253, 253, 253, 0.12);
background-color: #1d1d1d;
&:first-child {
border-start-start-radius: 2px;
}
&:last-child {
border-start-end-radius: 2px;
border-inline-end: none;
}
`;

View File

@ -0,0 +1,8 @@
import { UseTableViewProps } from 'components/Logs/TableView/types';
export type InfinityTableProps = {
tableViewProps: UseTableViewProps;
infitiyTableProps: {
onEndReached: (index: number) => void;
};
};

View File

@ -1,3 +1,12 @@
import { QueryDataV3 } from 'types/api/widgets/getQuery'; import { AddToQueryHOCProps } from 'components/Logs/AddToQueryHOC';
import { ILog } from 'types/api/logs/log';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
export type LogsExplorerListProps = { data: QueryDataV3[]; isLoading: boolean }; export type LogsExplorerListProps = {
isLoading: boolean;
currentStagedQueryData: IBuilderQuery | null;
logs: ILog[];
onEndReached: (index: number) => void;
onExpand: (log: ILog) => void;
onOpenDetailedView: (log: ILog) => void;
} & Pick<AddToQueryHOCProps, 'onAddToQuery'>;

View File

@ -2,38 +2,46 @@ import { Card, Typography } from 'antd';
// components // components
import ListLogView from 'components/Logs/ListLogView'; import ListLogView from 'components/Logs/ListLogView';
import RawLogView from 'components/Logs/RawLogView'; import RawLogView from 'components/Logs/RawLogView';
import LogsTableView from 'components/Logs/TableView';
import Spinner from 'components/Spinner'; import Spinner from 'components/Spinner';
import { LogViewMode } from 'container/LogsTable'; import { LOCALSTORAGE } from 'constants/localStorage';
import { Container, Heading } from 'container/LogsTable/styles'; import ExplorerControlPanel from 'container/ExplorerControlPanel';
import { Heading } from 'container/LogsTable/styles';
import { useOptionsMenu } from 'container/OptionsMenu';
import { contentStyle } from 'container/Trace/Search/config'; import { contentStyle } from 'container/Trace/Search/config';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import useFontFaceObserver from 'hooks/useFontObserver'; import useFontFaceObserver from 'hooks/useFontObserver';
import { memo, useCallback, useMemo, useState } from 'react'; import { memo, useCallback, useMemo } from 'react';
import { Virtuoso } from 'react-virtuoso'; import { Virtuoso } from 'react-virtuoso';
// interfaces // interfaces
import { ILog } from 'types/api/logs/log'; import { ILog } from 'types/api/logs/log';
import { DataSource, StringOperators } from 'types/common/queryBuilder';
import InfinityTableView from './InfinityTableView';
import { LogsExplorerListProps } from './LogsExplorerList.interfaces'; import { LogsExplorerListProps } from './LogsExplorerList.interfaces';
import { InfinityWrapperStyled } from './styles';
import { convertKeysToColumnFields } from './utils';
function LogsExplorerList({ function Footer(): JSX.Element {
data, return <Spinner height={20} tip="Getting Logs" />;
isLoading,
}: LogsExplorerListProps): JSX.Element {
const [viewMode] = useState<LogViewMode>('raw');
const [linesPerRow] = useState<number>(20);
const logs: ILog[] = useMemo(() => {
if (data.length > 0 && data[0].list) {
const logs: ILog[] = data[0].list.map((item) => ({
timestamp: +item.timestamp,
...item.data,
}));
return logs;
} }
return []; function LogsExplorerList({
}, [data]); isLoading,
currentStagedQueryData,
logs,
onOpenDetailedView,
onEndReached,
onExpand,
onAddToQuery,
}: LogsExplorerListProps): JSX.Element {
const { initialDataSource } = useQueryBuilder();
const { options, config } = useOptionsMenu({
storageKey: LOCALSTORAGE.LOGS_LIST_OPTIONS,
dataSource: initialDataSource || DataSource.METRICS,
aggregateOperator:
currentStagedQueryData?.aggregateOperator || StringOperators.NOOP,
});
useFontFaceObserver( useFontFaceObserver(
[ [
@ -42,75 +50,110 @@ function LogsExplorerList({
weight: '300', weight: '300',
}, },
], ],
viewMode === 'raw', options.format === 'raw',
{ {
timeout: 5000, timeout: 5000,
}, },
); );
// TODO: implement here linesPerRow, mode like in useSelectedLogView
const selectedFields = useMemo(
() => convertKeysToColumnFields(options.selectColumns),
[options],
);
const getItemContent = useCallback( const getItemContent = useCallback(
(index: number): JSX.Element => { (_: number, log: ILog): JSX.Element => {
const log = logs[index]; if (options.format === 'raw') {
if (viewMode === 'raw') {
return ( return (
<RawLogView <RawLogView
key={log.id} key={log.id}
data={log} data={log}
linesPerRow={linesPerRow} linesPerRow={options.maxLines}
// TODO: write new onClickExpanded logic onClickExpand={onExpand}
onClickExpand={(): void => {}}
/> />
); );
} }
return <ListLogView key={log.id} logData={log} />; return (
<ListLogView
key={log.id}
logData={log}
selectedFields={selectedFields}
onOpenDetailedView={onOpenDetailedView}
onAddToQuery={onAddToQuery}
/>
);
}, },
[logs, linesPerRow, viewMode], [
options.format,
options.maxLines,
selectedFields,
onOpenDetailedView,
onAddToQuery,
onExpand,
],
); );
const renderContent = useMemo(() => { const renderContent = useMemo(() => {
if (viewMode === 'table') { const components = isLoading
? {
Footer,
}
: {};
if (options.format === 'table') {
return ( return (
<LogsTableView <InfinityTableView
logs={logs} tableViewProps={{
// TODO: write new selected logic logs,
fields={[]} fields: selectedFields,
linesPerRow={linesPerRow} linesPerRow: options.maxLines,
// TODO: write new onClickExpanded logic onClickExpand: onExpand,
onClickExpand={(): void => {}} appendTo: 'end',
}}
infitiyTableProps={{ onEndReached }}
/> />
); );
} }
return ( return (
<Card bodyStyle={contentStyle}> <Card style={{ width: '100%' }} bodyStyle={{ ...contentStyle }}>
<Virtuoso <Virtuoso
useWindowScroll useWindowScroll
data={logs}
endReached={onEndReached}
totalCount={logs.length} totalCount={logs.length}
itemContent={getItemContent} itemContent={getItemContent}
components={components}
/> />
</Card> </Card>
); );
}, [getItemContent, linesPerRow, logs, viewMode]); }, [
isLoading,
if (isLoading) { logs,
return <Spinner height={20} tip="Getting Logs" />; options.format,
} options.maxLines,
onEndReached,
getItemContent,
selectedFields,
onExpand,
]);
return ( return (
<Container> <>
{viewMode !== 'table' && ( <ExplorerControlPanel
isLoading={isLoading}
isShowPageSize={false}
optionsMenuConfig={config}
/>
{options.format !== 'table' && (
<Heading> <Heading>
<Typography.Text>Event</Typography.Text> <Typography.Text>Event</Typography.Text>
</Heading> </Heading>
)} )}
{logs.length === 0 && <Typography>No logs lines found</Typography>} {logs.length === 0 && <Typography>No logs lines found</Typography>}
<InfinityWrapperStyled>{renderContent}</InfinityWrapperStyled>
{renderContent} </>
</Container>
); );
} }

View File

@ -0,0 +1,6 @@
import styled from 'styled-components';
export const InfinityWrapperStyled = styled.div`
min-height: 40rem;
display: flex;
`;

View File

@ -0,0 +1,11 @@
import { IField } from 'types/api/logs/fields';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
export const convertKeysToColumnFields = (
keys: BaseAutocompleteData[],
): IField[] =>
keys.map((item) => ({
dataType: item.dataType as string,
name: item.key,
type: item.type as string,
}));

View File

@ -6,8 +6,8 @@ import { memo } from 'react';
import { LogsExplorerTableProps } from './LogsExplorerTable.interfaces'; import { LogsExplorerTableProps } from './LogsExplorerTable.interfaces';
function LogsExplorerTable({ function LogsExplorerTable({
isLoading,
data, data,
isLoading,
}: LogsExplorerTableProps): JSX.Element { }: LogsExplorerTableProps): JSX.Element {
const { stagedQuery } = useQueryBuilder(); const { stagedQuery } = useQueryBuilder();

View File

@ -7,3 +7,9 @@ export const TabsStyled = styled(Tabs)`
background-color: ${themeColors.lightBlack}; background-color: ${themeColors.lightBlack};
} }
`; `;
export const ActionsWrapper = styled.div`
display: flex;
justify-content: flex-end;
margin-bottom: 1rem;
`;

View File

@ -1,50 +1,100 @@
import { TabsProps } from 'antd'; import { TabsProps } from 'antd';
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder'; import axios from 'axios';
import { PANEL_TYPES_QUERY } from 'constants/queryBuilderQueryNames'; import LogDetail from 'components/LogDetail';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys'; import TabLabel from 'components/TabLabel';
import { QueryParams } from 'constants/query';
import {
initialQueriesMap,
OPERATORS,
PANEL_TYPES,
QueryBuilderKeys,
} 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 LogsExplorerChart from 'container/LogsExplorerChart';
import LogsExplorerList from 'container/LogsExplorerList'; import LogsExplorerList from 'container/LogsExplorerList';
import LogsExplorerTable from 'container/LogsExplorerTable'; // TODO: temporary hide table view
// import LogsExplorerTable from 'container/LogsExplorerTable';
import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider'; import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider';
import TimeSeriesView from 'container/TimeSeriesView/TimeSeriesView'; import TimeSeriesView from 'container/TimeSeriesView/TimeSeriesView';
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange'; import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
import { addEmptyWidgetInDashboardJSONWithQuery } from 'hooks/dashboard/utils';
import { useGetExplorerQueryRange } from 'hooks/queryBuilder/useGetExplorerQueryRange';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { memo, useCallback, useEffect, useMemo } from 'react'; import { useNotifications } from 'hooks/useNotifications';
import useUrlQueryData from 'hooks/useUrlQueryData';
import { chooseAutocompleteFromCustomValue } from 'lib/newQueryBuilder/chooseAutocompleteFromCustomValue';
import { getPaginationQueryData } from 'lib/newQueryBuilder/getPaginationQueryData';
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useQueryClient } from 'react-query';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { generatePath, useHistory } from 'react-router-dom';
import { AppState } from 'store/reducers'; import { AppState } from 'store/reducers';
import { DataSource } from 'types/common/queryBuilder'; import { SuccessResponse } from 'types/api';
import { Dashboard } from 'types/api/dashboard/getAll';
import { ILog } from 'types/api/logs/log';
import {
BaseAutocompleteData,
IQueryAutocompleteResponse,
} from 'types/api/queryBuilder/queryAutocompleteResponse';
import {
IBuilderQuery,
OrderByPayload,
Query,
} from 'types/api/queryBuilder/queryBuilderData';
import { DataSource, StringOperators } from 'types/common/queryBuilder';
import { GlobalReducer } from 'types/reducer/globalTime'; import { GlobalReducer } from 'types/reducer/globalTime';
import { v4 as uuid } from 'uuid';
import { TabsStyled } from './LogsExplorerViews.styled'; import { ActionsWrapper, TabsStyled } from './LogsExplorerViews.styled';
function LogsExplorerViews(): JSX.Element { function LogsExplorerViews(): JSX.Element {
const { notifications } = useNotifications();
const history = useHistory();
const queryClient = useQueryClient();
const { queryData: pageSize } = useUrlQueryData(
queryParamNamesMap.pageSize,
DEFAULT_PER_PAGE_VALUE,
);
const { minTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const currentMinTimeRef = useRef<number>(minTime);
// Context
const { const {
currentQuery, currentQuery,
stagedQuery, stagedQuery,
panelType, panelType,
isEnabledQuery,
updateAllQueriesOperators, updateAllQueriesOperators,
redirectWithQueryBuilderData, redirectWithQueryBuilderData,
} = useQueryBuilder(); } = useQueryBuilder();
const { selectedTime } = useSelector<AppState, GlobalReducer>( // State
(state) => state.globalTime, const [activeLog, setActiveLog] = useState<ILog | null>(null);
const [page, setPage] = useState<number>(1);
const [logs, setLogs] = useState<ILog[]>([]);
const [requestData, setRequestData] = useState<Query | null>(null);
const currentStagedQueryData = useMemo(() => {
if (!stagedQuery || stagedQuery.builder.queryData.length !== 1) return null;
return stagedQuery.builder.queryData[0];
}, [stagedQuery]);
const orderByTimestamp: OrderByPayload | null = useMemo(() => {
const timestampOrderBy = currentStagedQueryData?.orderBy.find(
(item) => item.columnName === 'timestamp',
); );
const { data, isFetching, isError } = useGetQueryRange( return timestampOrderBy || null;
{ }, [currentStagedQueryData]);
query: stagedQuery || initialQueriesMap.metrics,
graphType: panelType || PANEL_TYPES.LIST,
globalSelectedInterval: selectedTime,
selectedTime: 'GLOBAL_TIME',
params: {
dataSource: DataSource.LOGS,
},
},
{
queryKey: [REACT_QUERY_KEY.GET_QUERY_RANGE, selectedTime, stagedQuery],
enabled: isEnabledQuery,
},
);
const isMultipleQueries = useMemo( const isMultipleQueries = useMemo(
() => () =>
@ -62,35 +112,67 @@ function LogsExplorerViews(): JSX.Element {
return groupByCount > 0; return groupByCount > 0;
}, [currentQuery]); }, [currentQuery]);
const currentData = useMemo( const isLimit: boolean = useMemo(() => {
() => data?.payload.data.newResult.data.result || [], if (!currentStagedQueryData) return false;
[data], if (!currentStagedQueryData.limit) return false;
return logs.length >= currentStagedQueryData.limit;
}, [logs.length, currentStagedQueryData]);
const listChartQuery = useMemo(() => {
if (!stagedQuery || !currentStagedQueryData) return null;
const modifiedQueryData: IBuilderQuery = {
...currentStagedQueryData,
aggregateOperator: StringOperators.COUNT,
};
const modifiedQuery: Query = {
...stagedQuery,
builder: {
...stagedQuery.builder,
queryData: stagedQuery.builder.queryData.map((item) => ({
...item,
...modifiedQueryData,
})),
},
};
return modifiedQuery;
}, [stagedQuery, currentStagedQueryData]);
const exportDefaultQuery = useMemo(
() =>
updateAllQueriesOperators(
currentQuery || initialQueriesMap.logs,
PANEL_TYPES.TIME_SERIES,
DataSource.LOGS,
),
[currentQuery, updateAllQueriesOperators],
); );
const tabsItems: TabsProps['items'] = useMemo( const listChartData = useGetExplorerQueryRange(
() => [ listChartQuery,
{ PANEL_TYPES.TIME_SERIES,
label: 'List View',
key: PANEL_TYPES.LIST,
disabled: isMultipleQueries || isGroupByExist,
children: <LogsExplorerList data={currentData} isLoading={isFetching} />,
},
{
label: 'TimeSeries',
key: PANEL_TYPES.TIME_SERIES,
children: (
<TimeSeriesView isLoading={isFetching} data={data} isError={isError} />
),
},
{
label: 'Table',
key: PANEL_TYPES.TABLE,
children: <LogsExplorerTable data={currentData} isLoading={isFetching} />,
},
],
[isMultipleQueries, isGroupByExist, currentData, isFetching, data, isError],
); );
const { data, isFetching, isError } = useGetExplorerQueryRange(
requestData,
panelType,
{
keepPreviousData: true,
enabled: !isLimit,
},
);
const handleSetActiveLog = useCallback((nextActiveLog: ILog) => {
setActiveLog(nextActiveLog);
}, []);
const handleClearActiveLog = useCallback(() => {
setActiveLog(null);
}, []);
const handleChangeView = useCallback( const handleChangeView = useCallback(
(newPanelType: string) => { (newPanelType: string) => {
if (newPanelType === panelType) return; if (newPanelType === panelType) return;
@ -101,7 +183,9 @@ function LogsExplorerViews(): JSX.Element {
DataSource.LOGS, DataSource.LOGS,
); );
redirectWithQueryBuilderData(query, { [PANEL_TYPES_QUERY]: newPanelType }); redirectWithQueryBuilderData(query, {
[queryParamNamesMap.panelTypes]: newPanelType,
});
}, },
[ [
currentQuery, currentQuery,
@ -111,23 +195,325 @@ function LogsExplorerViews(): JSX.Element {
], ],
); );
const getRequestData = useCallback(
(
query: Query | null,
params: { page: number; log: ILog | null; pageSize: number },
): Query | null => {
if (!query) return null;
const paginateData = getPaginationQueryData({
currentStagedQueryData,
listItemId: params.log ? params.log.id : null,
orderByTimestamp,
page: params.page,
pageSize: params.pageSize,
});
const data: Query = {
...query,
builder: {
...query.builder,
queryData: query.builder.queryData.map((item) => ({
...item,
...paginateData,
pageSize: params.pageSize,
})),
},
};
return data;
},
[currentStagedQueryData, orderByTimestamp],
);
const handleAddToQuery = useCallback(
(fieldKey: string, fieldValue: string, operator: string): void => {
const keysAutocomplete: BaseAutocompleteData[] =
queryClient.getQueryData<SuccessResponse<IQueryAutocompleteResponse>>(
[QueryBuilderKeys.GET_AGGREGATE_KEYS],
{ exact: false },
)?.payload.attributeKeys || [];
const existAutocompleteKey = chooseAutocompleteFromCustomValue(
keysAutocomplete,
fieldKey,
);
const currentOperator =
Object.keys(OPERATORS).find((op) => op === operator) || '';
const nextQuery: Query = {
...currentQuery,
builder: {
...currentQuery.builder,
queryData: currentQuery.builder.queryData.map((item) => ({
...item,
filters: {
...item.filters,
items: [
...item.filters.items.filter(
(item) => item.key?.id !== existAutocompleteKey.id,
),
{
id: uuid(),
key: existAutocompleteKey,
op: currentOperator,
value: fieldValue,
},
],
},
})),
},
};
redirectWithQueryBuilderData(nextQuery);
},
[currentQuery, queryClient, redirectWithQueryBuilderData],
);
const handleEndReached = useCallback(
(index: number) => {
if (isLimit) return;
if (logs.length < pageSize) return;
const lastLog = logs[index];
const limit = currentStagedQueryData?.limit;
const nextLogsLenth = logs.length + pageSize;
const nextPageSize =
limit && nextLogsLenth >= limit ? limit - logs.length : pageSize;
if (!stagedQuery) return;
const newRequestData = getRequestData(stagedQuery, {
page: page + 1,
log: orderByTimestamp ? lastLog : null,
pageSize: nextPageSize,
});
setPage((prevPage) => prevPage + 1);
setRequestData(newRequestData);
},
[
isLimit,
logs,
currentStagedQueryData?.limit,
pageSize,
stagedQuery,
getRequestData,
page,
orderByTimestamp,
],
);
const {
mutate: updateDashboard,
isLoading: isUpdateDashboardLoading,
} = useUpdateDashboard();
const handleExport = useCallback(
(dashboard: Dashboard | null): void => {
if (!dashboard) return;
const updatedDashboard = addEmptyWidgetInDashboardJSONWithQuery(
dashboard,
exportDefaultQuery,
);
updateDashboard(updatedDashboard, {
onSuccess: (data) => {
if (data.error) {
const message =
data.error === 'feature usage exceeded' ? (
<span>
Panel limit exceeded for {DataSource.LOGS} in community edition. Please
checkout our paid plans{' '}
<a
href="https://signoz.io/pricing/?utm_source=product&utm_medium=dashboard-limit"
rel="noreferrer noopener"
target="_blank"
>
here
</a>
</span>
) : (
data.error
);
notifications.error({
message,
});
return;
}
const dashboardEditView = `${generatePath(ROUTES.DASHBOARD, {
dashboardId: data?.payload?.uuid,
})}/new?${QueryParams.graphType}=graph&${QueryParams.widgetId}=empty&${
queryParamNamesMap.compositeQuery
}=${encodeURIComponent(JSON.stringify(exportDefaultQuery))}`;
history.push(dashboardEditView);
},
onError: (error) => {
if (axios.isAxiosError(error)) {
notifications.error({
message: error.message,
});
}
},
});
},
[exportDefaultQuery, history, notifications, updateDashboard],
);
useEffect(() => { useEffect(() => {
const shouldChangeView = isMultipleQueries || isGroupByExist; const shouldChangeView = isMultipleQueries || isGroupByExist;
if (panelType === 'list' && shouldChangeView) { if (panelType === PANEL_TYPES.LIST && shouldChangeView) {
handleChangeView(PANEL_TYPES.TIME_SERIES); handleChangeView(PANEL_TYPES.TIME_SERIES);
} }
}, [panelType, isMultipleQueries, isGroupByExist, handleChangeView]); }, [panelType, isMultipleQueries, isGroupByExist, handleChangeView]);
useEffect(() => {
const currentData = data?.payload.data.newResult.data.result || [];
if (currentData.length > 0 && currentData[0].list) {
const currentLogs: ILog[] = currentData[0].list.map((item) => ({
...item.data,
timestamp: item.timestamp,
}));
setLogs((prevLogs) => [...prevLogs, ...currentLogs]);
}
}, [data]);
useEffect(() => {
if (
requestData?.id !== stagedQuery?.id ||
currentMinTimeRef.current !== minTime
) {
const newRequestData = getRequestData(stagedQuery, {
page: 1,
log: null,
pageSize,
});
setLogs([]);
setPage(1);
setRequestData(newRequestData);
currentMinTimeRef.current = minTime;
}
}, [stagedQuery, requestData, getRequestData, pageSize, minTime]);
const tabsItems: TabsProps['items'] = useMemo(
() => [
{
label: (
<TabLabel
label="List View"
tooltipText="Please remove attributes from Group By filter to switch to List View tab"
isDisabled={isMultipleQueries || isGroupByExist}
/>
),
key: PANEL_TYPES.LIST,
disabled: isMultipleQueries || isGroupByExist,
children: (
<LogsExplorerList
isLoading={isFetching}
currentStagedQueryData={currentStagedQueryData}
logs={logs}
onOpenDetailedView={handleSetActiveLog}
onEndReached={handleEndReached}
onExpand={handleSetActiveLog}
onAddToQuery={handleAddToQuery}
/>
),
},
{
label: <TabLabel label="Time Series" isDisabled={false} />,
key: PANEL_TYPES.TIME_SERIES,
children: (
<TimeSeriesView isLoading={isFetching} data={data} isError={isError} />
),
},
// TODO: temporary hide table view
// {
// label: 'Table',
// key: PANEL_TYPES.TABLE,
// children: (
// <LogsExplorerTable
// data={data?.payload.data.newResult.data.result || []}
// isLoading={isFetching}
// />
// ),
// },
],
[
isMultipleQueries,
isGroupByExist,
isFetching,
currentStagedQueryData,
logs,
handleSetActiveLog,
handleEndReached,
handleAddToQuery,
data,
isError,
],
);
const chartData = useMemo(() => {
if (!stagedQuery) return [];
if (panelType === PANEL_TYPES.LIST) {
if (
listChartData &&
listChartData.data &&
listChartData.data.payload.data.result.length > 0
) {
return listChartData.data.payload.data.result;
}
return [];
}
if (!data || data.payload.data.result.length === 0) return [];
const isGroupByExist = stagedQuery.builder.queryData.some(
(queryData) => queryData.groupBy.length > 0,
);
return isGroupByExist
? data.payload.data.result
: [data.payload.data.result[0]];
}, [stagedQuery, data, panelType, listChartData]);
return ( return (
<div> <>
<LogsExplorerChart isLoading={isFetching} data={chartData} />
{stagedQuery && (
<ActionsWrapper>
<ExportPanel
query={exportDefaultQuery}
isLoading={isUpdateDashboardLoading}
onExport={handleExport}
/>
</ActionsWrapper>
)}
<TabsStyled <TabsStyled
items={tabsItems} items={tabsItems}
defaultActiveKey={panelType || PANEL_TYPES.LIST} defaultActiveKey={panelType || PANEL_TYPES.LIST}
activeKey={panelType || PANEL_TYPES.LIST} activeKey={panelType || PANEL_TYPES.LIST}
onChange={handleChangeView} onChange={handleChangeView}
destroyInactiveTabPane
/> />
</div> <LogDetail
log={activeLog}
onClose={handleClearActiveLog}
onAddToQuery={handleAddToQuery}
onClickActionItem={handleAddToQuery}
/>
</>
); );
} }

View File

@ -4,13 +4,17 @@ import ListLogView from 'components/Logs/ListLogView';
import RawLogView from 'components/Logs/RawLogView'; import RawLogView from 'components/Logs/RawLogView';
import LogsTableView from 'components/Logs/TableView'; import LogsTableView from 'components/Logs/TableView';
import Spinner from 'components/Spinner'; import Spinner from 'components/Spinner';
import ROUTES from 'constants/routes';
import { contentStyle } from 'container/Trace/Search/config'; import { contentStyle } from 'container/Trace/Search/config';
import useFontFaceObserver from 'hooks/useFontObserver'; import useFontFaceObserver from 'hooks/useFontObserver';
import { getGeneratedFilterQueryString } from 'lib/getGeneratedFilterQueryString';
import { memo, useCallback, useMemo } from 'react'; import { memo, useCallback, useMemo } from 'react';
import { useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { useHistory } from 'react-router-dom';
import { Virtuoso } from 'react-virtuoso'; import { Virtuoso } from 'react-virtuoso';
// interfaces
import { AppState } from 'store/reducers'; import { AppState } from 'store/reducers';
// interfaces
import { SET_DETAILED_LOG_DATA } from 'types/actions/logs';
import { ILog } from 'types/api/logs/log'; import { ILog } from 'types/api/logs/log';
import { ILogsReducer } from 'types/reducer/logs'; import { ILogsReducer } from 'types/reducer/logs';
@ -28,6 +32,10 @@ type LogsTableProps = {
function LogsTable(props: LogsTableProps): JSX.Element { function LogsTable(props: LogsTableProps): JSX.Element {
const { viewMode, onClickExpand, linesPerRow } = props; const { viewMode, onClickExpand, linesPerRow } = props;
const history = useHistory();
const dispatch = useDispatch();
useFontFaceObserver( useFontFaceObserver(
[ [
{ {
@ -44,6 +52,7 @@ function LogsTable(props: LogsTableProps): JSX.Element {
const { const {
logs, logs,
fields: { selected }, fields: { selected },
searchFilter: { queryString },
isLoading, isLoading,
liveTail, liveTail,
} = useSelector<AppState, ILogsReducer>((state) => state.logs); } = useSelector<AppState, ILogsReducer>((state) => state.logs);
@ -58,6 +67,30 @@ function LogsTable(props: LogsTableProps): JSX.Element {
liveTail, liveTail,
]); ]);
const handleOpenDetailedView = useCallback(
(logData: ILog) => {
dispatch({
type: SET_DETAILED_LOG_DATA,
payload: logData,
});
},
[dispatch],
);
const handleAddToQuery = useCallback(
(fieldKey: string, fieldValue: string, operator: string) => {
const updatedQueryString = getGeneratedFilterQueryString(
fieldKey,
fieldValue,
operator,
queryString,
);
history.replace(`${ROUTES.LOGS}?q=${updatedQueryString}`);
},
[history, queryString],
);
const getItemContent = useCallback( const getItemContent = useCallback(
(index: number): JSX.Element => { (index: number): JSX.Element => {
const log = logs[index]; const log = logs[index];
@ -73,9 +106,25 @@ function LogsTable(props: LogsTableProps): JSX.Element {
); );
} }
return <ListLogView key={log.id} logData={log} />; return (
<ListLogView
key={log.id}
logData={log}
selectedFields={selected}
onOpenDetailedView={handleOpenDetailedView}
onAddToQuery={handleAddToQuery}
/>
);
}, },
[logs, linesPerRow, viewMode, onClickExpand], [
logs,
viewMode,
selected,
linesPerRow,
onClickExpand,
handleOpenDetailedView,
handleAddToQuery,
],
); );
const renderContent = useMemo(() => { const renderContent = useMemo(() => {

View File

@ -1,5 +1,13 @@
import { Typography } from 'antd';
import getServiceOverview from 'api/metrics/getServiceOverview';
import getTopLevelOperations, {
ServiceDataProps,
} from 'api/metrics/getTopLevelOperations';
import getTopOperations from 'api/metrics/getTopOperations';
import axios from 'axios';
import { ActiveElement, Chart, ChartData, ChartEvent } from 'chart.js'; import { ActiveElement, Chart, ChartData, ChartEvent } from 'chart.js';
import Graph from 'components/Graph'; import Graph from 'components/Graph';
import Spinner from 'components/Spinner';
import { QueryParams } from 'constants/query'; import { QueryParams } from 'constants/query';
import ROUTES from 'constants/routes'; import ROUTES from 'constants/routes';
import FullView from 'container/GridGraphLayout/Graph/FullView/index.metricsBuilder'; import FullView from 'container/GridGraphLayout/Graph/FullView/index.metricsBuilder';
@ -12,16 +20,22 @@ import {
} from 'hooks/useResourceAttribute/utils'; } from 'hooks/useResourceAttribute/utils';
import convertToNanoSecondsToSecond from 'lib/convertToNanoSecondsToSecond'; import convertToNanoSecondsToSecond from 'lib/convertToNanoSecondsToSecond';
import { colors } from 'lib/getRandomColor'; import { colors } from 'lib/getRandomColor';
import getStep from 'lib/getStep';
import history from 'lib/history'; import history from 'lib/history';
import { useCallback, useMemo, useState } from 'react'; import { useCallback, useMemo, useState } from 'react';
import { useQueries, UseQueryResult } from 'react-query';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { useLocation, useParams } from 'react-router-dom'; import { useLocation, useParams } from 'react-router-dom';
import { UpdateTimeInterval } from 'store/actions'; import { UpdateTimeInterval } from 'store/actions';
import { AppState } from 'store/reducers'; import { AppState } from 'store/reducers';
import { PayloadProps } from 'types/api/metrics/getServiceOverview';
import { PayloadProps as PayloadPropsTopOpertions } from 'types/api/metrics/getTopOperations';
import { EQueryType } from 'types/common/dashboard'; import { EQueryType } from 'types/common/dashboard';
import MetricReducer from 'types/reducer/metrics'; import { GlobalReducer } from 'types/reducer/globalTime';
import { Tags } from 'types/reducer/trace';
import { v4 as uuid } from 'uuid'; import { v4 as uuid } from 'uuid';
import { SOMETHING_WENT_WRONG } from '../../../constants/api';
import { getWidgetQueryBuilder } from '../MetricsApplication.factory'; import { getWidgetQueryBuilder } from '../MetricsApplication.factory';
import { import {
errorPercentage, errorPercentage,
@ -37,9 +51,17 @@ import {
} from './util'; } from './util';
function Application(): JSX.Element { function Application(): JSX.Element {
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const { servicename } = useParams<{ servicename?: string }>(); const { servicename } = useParams<{ servicename?: string }>();
const [selectedTimeStamp, setSelectedTimeStamp] = useState<number>(0); const [selectedTimeStamp, setSelectedTimeStamp] = useState<number>(0);
const { search } = useLocation(); const { search } = useLocation();
const { queries } = useResourceAttribute();
const selectedTags = useMemo(
() => (convertRawQueriesToTraceSelectedTags(queries) as Tags[]) || [],
[queries],
);
const handleSetTimeStamp = useCallback((selectTime: number) => { const handleSetTimeStamp = useCallback((selectTime: number) => {
setSelectedTimeStamp(selectTime); setSelectedTimeStamp(selectTime);
@ -64,12 +86,53 @@ function Application(): JSX.Element {
[handleSetTimeStamp], [handleSetTimeStamp],
); );
const { topOperations, serviceOverview, topLevelOperations } = useSelector< const queryResult = useQueries<
AppState, [
MetricReducer UseQueryResult<PayloadProps>,
>((state) => state.metrics); UseQueryResult<PayloadPropsTopOpertions>,
UseQueryResult<ServiceDataProps>,
]
>([
{
queryKey: [servicename, selectedTags, minTime, maxTime],
queryFn: (): Promise<PayloadProps> =>
getServiceOverview({
service: servicename || '',
start: minTime,
end: maxTime,
step: getStep({
start: minTime,
end: maxTime,
inputFormat: 'ns',
}),
selectedTags,
}),
},
{
queryKey: [minTime, maxTime, servicename, selectedTags],
queryFn: (): Promise<PayloadPropsTopOpertions> =>
getTopOperations({
service: servicename || '',
start: minTime,
end: maxTime,
selectedTags,
}),
},
{
queryKey: [servicename, minTime, maxTime, selectedTags],
queryFn: (): Promise<ServiceDataProps> => getTopLevelOperations(),
},
]);
const { queries } = useResourceAttribute(); const serviceOverview = queryResult[0].data;
const serviceOverviewError = queryResult[0].error;
const serviceOverviewIsError = queryResult[0].isError;
const serviceOverviewIsLoading = queryResult[0].isLoading;
const topOperations = queryResult[1].data;
const topLevelOperations = queryResult[2].data;
const topLevelOperationsError = queryResult[2].error;
const topLevelOperationsIsError = queryResult[2].isError;
const topLevelOperationsIsLoading = queryResult[2].isLoading;
const selectedTraceTags: string = JSON.stringify( const selectedTraceTags: string = JSON.stringify(
convertRawQueriesToTraceSelectedTags(queries) || [], convertRawQueriesToTraceSelectedTags(queries) || [],
@ -89,7 +152,9 @@ function Application(): JSX.Element {
builder: operationPerSec({ builder: operationPerSec({
servicename, servicename,
tagFilterItems, tagFilterItems,
topLevelOperations, topLevelOperations: topLevelOperations
? topLevelOperations[servicename || '']
: [],
}), }),
clickhouse_sql: [], clickhouse_sql: [],
id: uuid(), id: uuid(),
@ -105,7 +170,9 @@ function Application(): JSX.Element {
builder: errorPercentage({ builder: errorPercentage({
servicename, servicename,
tagFilterItems, tagFilterItems,
topLevelOperations, topLevelOperations: topLevelOperations
? topLevelOperations[servicename || '']
: [],
}), }),
clickhouse_sql: [], clickhouse_sql: [],
id: uuid(), id: uuid(),
@ -158,8 +225,12 @@ function Application(): JSX.Element {
[], [],
); );
const dataSets = useMemo( const dataSets = useMemo(() => {
() => [ if (!serviceOverview) {
return [];
}
return [
{ {
data: serviceOverview.map((e) => data: serviceOverview.map((e) =>
parseFloat(convertToNanoSecondsToSecond(e.p99)), parseFloat(convertToNanoSecondsToSecond(e.p99)),
@ -178,19 +249,25 @@ function Application(): JSX.Element {
), ),
...generalChartDataProperties('p50 Latency', 2), ...generalChartDataProperties('p50 Latency', 2),
}, },
], ];
[generalChartDataProperties, serviceOverview], }, [generalChartDataProperties, serviceOverview]);
);
const data = useMemo( const data = useMemo(() => {
() => ({ if (!serviceOverview) {
return {
datasets: [],
labels: [],
};
}
return {
datasets: dataSets, datasets: dataSets,
labels: serviceOverview.map( labels: serviceOverview.map(
(e) => new Date(parseFloat(convertToNanoSecondsToSecond(e.timestamp))), (e) => new Date(parseFloat(convertToNanoSecondsToSecond(e.timestamp))),
), ),
}), };
[serviceOverview, dataSets], }, [serviceOverview, dataSets]);
);
return ( return (
<> <>
<Row gutter={24}> <Row gutter={24}>
@ -208,7 +285,19 @@ function Application(): JSX.Element {
View Traces View Traces
</Button> </Button>
<Card> <Card>
{serviceOverviewIsError ? (
<Typography>
{axios.isAxiosError(serviceOverviewError)
? serviceOverviewError.response?.data
: SOMETHING_WENT_WRONG}
</Typography>
) : (
<>
<GraphTitle>Latency</GraphTitle> <GraphTitle>Latency</GraphTitle>
{serviceOverviewIsLoading && (
<Spinner size="large" tip="Loading..." height="40vh" />
)}
{!serviceOverviewIsLoading && (
<GraphContainer> <GraphContainer>
<Graph <Graph
animate={false} animate={false}
@ -220,6 +309,9 @@ function Application(): JSX.Element {
onDragSelect={onDragSelect} onDragSelect={onDragSelect}
/> />
</GraphContainer> </GraphContainer>
)}
</>
)}
</Card> </Card>
</Col> </Col>
@ -237,6 +329,14 @@ function Application(): JSX.Element {
View Traces View Traces
</Button> </Button>
<Card> <Card>
{topLevelOperationsIsError ? (
<Typography>
{axios.isAxiosError(topLevelOperationsError)
? topLevelOperationsError.response?.data
: SOMETHING_WENT_WRONG}
</Typography>
) : (
<>
<GraphTitle>Rate (ops/s)</GraphTitle> <GraphTitle>Rate (ops/s)</GraphTitle>
<GraphContainer> <GraphContainer>
<FullView <FullView
@ -246,8 +346,11 @@ function Application(): JSX.Element {
widget={operationPerSecWidget} widget={operationPerSecWidget}
yAxisUnit="ops" yAxisUnit="ops"
onDragSelect={onDragSelect} onDragSelect={onDragSelect}
isDependedDataLoaded={topLevelOperationsIsLoading}
/> />
</GraphContainer> </GraphContainer>
</>
)}
</Card> </Card>
</Col> </Col>
</Row> </Row>
@ -265,6 +368,14 @@ function Application(): JSX.Element {
</Button> </Button>
<Card> <Card>
{topLevelOperationsIsError ? (
<Typography>
{axios.isAxiosError(topLevelOperationsError)
? topLevelOperationsError.response?.data
: SOMETHING_WENT_WRONG}
</Typography>
) : (
<>
<GraphTitle>Error Percentage</GraphTitle> <GraphTitle>Error Percentage</GraphTitle>
<GraphContainer> <GraphContainer>
<FullView <FullView
@ -274,14 +385,17 @@ function Application(): JSX.Element {
widget={errorPercentageWidget} widget={errorPercentageWidget}
yAxisUnit="%" yAxisUnit="%"
onDragSelect={onDragSelect} onDragSelect={onDragSelect}
isDependedDataLoaded={topLevelOperationsIsLoading}
/> />
</GraphContainer> </GraphContainer>
</>
)}
</Card> </Card>
</Col> </Col>
<Col span={12}> <Col span={12}>
<Card> <Card>
<TopOperationsTable data={topOperations} /> <TopOperationsTable data={topOperations || []} />
</Card> </Card>
</Col> </Col>
</Row> </Row>

View File

@ -1,90 +0,0 @@
import RouteTab from 'components/RouteTab';
import ROUTES from 'constants/routes';
import ResourceAttributesFilter from 'container/ResourceAttributesFilter';
import history from 'lib/history';
import { memo, useMemo } from 'react';
import { generatePath, useParams } from 'react-router-dom';
import { useLocation } from 'react-use';
import DBCall from './Tabs/DBCall';
import External from './Tabs/External';
import Overview from './Tabs/Overview';
function OverViewTab(): JSX.Element {
return <Overview />;
}
function DbCallTab(): JSX.Element {
return <DBCall />;
}
function ExternalTab(): JSX.Element {
return <External />;
}
function ServiceMetrics(): JSX.Element {
const { search } = useLocation();
const { servicename } = useParams<{ servicename: string }>();
const searchParams = new URLSearchParams(search);
const tab = searchParams.get('tab');
const overMetrics = 'Overview Metrics';
const dbCallMetrics = 'Database Calls';
const externalMetrics = 'External Calls';
const getActiveKey = (): string => {
switch (tab) {
case null: {
return overMetrics;
}
case dbCallMetrics: {
return dbCallMetrics;
}
case externalMetrics: {
return externalMetrics;
}
default: {
return overMetrics;
}
}
};
const activeKey = getActiveKey();
const routes = useMemo(
() => [
{
Component: OverViewTab,
name: overMetrics,
route: `${generatePath(ROUTES.SERVICE_METRICS, {
servicename,
})}?tab=${overMetrics}`,
},
{
Component: DbCallTab,
name: dbCallMetrics,
route: `${generatePath(ROUTES.SERVICE_METRICS, {
servicename,
})}?tab=${dbCallMetrics}`,
},
{
Component: ExternalTab,
name: externalMetrics,
route: `${generatePath(ROUTES.SERVICE_METRICS, {
servicename,
})}?tab=${externalMetrics}`,
},
],
[servicename],
);
return (
<>
<ResourceAttributesFilter />
<RouteTab routes={routes} history={history} activeKey={activeKey} />
</>
);
}
export default memo(ServiceMetrics);

View File

@ -1,7 +1,7 @@
/* eslint-disable @typescript-eslint/naming-convention */ /* eslint-disable @typescript-eslint/naming-convention */
import { initialQueriesMap } from 'constants/queryBuilder'; import { initialQueriesMap } from 'constants/queryBuilder';
import { COMPOSITE_QUERY } from 'constants/queryBuilderQueryNames'; import { queryParamNamesMap } from 'constants/queryBuilderQueryNames';
import { useIsDarkMode } from 'hooks/useDarkMode'; import { useIsDarkMode } from 'hooks/useDarkMode';
import { useNotifications } from 'hooks/useNotifications'; import { useNotifications } from 'hooks/useNotifications';
import history from 'lib/history'; import history from 'lib/history';
@ -47,7 +47,7 @@ function DashboardGraphSlider({ toggleAddWidget }: Props): JSX.Element {
history.push( history.push(
`${history.location.pathname}/new?graphType=${name}&widgetId=${ `${history.location.pathname}/new?graphType=${name}&widgetId=${
emptyLayout.i emptyLayout.i
}&${COMPOSITE_QUERY}=${encodeURIComponent( }&${queryParamNamesMap.compositeQuery}=${encodeURIComponent(
JSON.stringify(initialQueriesMap.metrics), JSON.stringify(initialQueriesMap.metrics),
)}`, )}`,
); );

View File

@ -26,7 +26,6 @@ function SettingsDrawer(): JSX.Element {
width="70%" width="70%"
onClose={onClose} onClose={onClose}
visible={visible} visible={visible}
maskClosable={false}
> >
<DashboardSettingsContent /> <DashboardSettingsContent />
</DrawerContainer> </DrawerContainer>

View File

@ -3,12 +3,13 @@ import TextToolTip from 'components/TextToolTip';
import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider'; import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider';
import { WidgetGraphProps } from 'container/NewWidget/types'; import { WidgetGraphProps } from 'container/NewWidget/types';
import { QueryBuilder } from 'container/QueryBuilder'; import { QueryBuilder } from 'container/QueryBuilder';
import { QueryBuilderProps } from 'container/QueryBuilder/QueryBuilder.interfaces';
import { useGetWidgetQueryRange } from 'hooks/queryBuilder/useGetWidgetQueryRange'; import { useGetWidgetQueryRange } from 'hooks/queryBuilder/useGetWidgetQueryRange';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl'; import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
import { updateStepInterval } from 'hooks/queryBuilder/useStepInterval'; import { updateStepInterval } from 'hooks/queryBuilder/useStepInterval';
import useUrlQuery from 'hooks/useUrlQuery'; import useUrlQuery from 'hooks/useUrlQuery';
import { useCallback } from 'react'; import { useCallback, useMemo } from 'react';
import { connect, useSelector } from 'react-redux'; import { connect, useSelector } from 'react-redux';
import { bindActionCreators, Dispatch } from 'redux'; import { bindActionCreators, Dispatch } from 'redux';
import { ThunkDispatch } from 'redux-thunk'; import { ThunkDispatch } from 'redux-thunk';
@ -101,12 +102,22 @@ function QuerySection({
handleStageQuery(currentQuery); handleStageQuery(currentQuery);
}; };
const filterConfigs: QueryBuilderProps['filterConfigs'] = useMemo(() => {
const config: QueryBuilderProps['filterConfigs'] = {
stepInterval: { isHidden: false, isDisabled: true },
};
return config;
}, []);
const items = [ const items = [
{ {
key: EQueryType.QUERY_BUILDER, key: EQueryType.QUERY_BUILDER,
label: 'Query Builder', label: 'Query Builder',
tab: <Typography>Query Builder</Typography>, tab: <Typography>Query Builder</Typography>,
children: <QueryBuilder panelType={selectedGraph} />, children: (
<QueryBuilder panelType={selectedGraph} filterConfigs={filterConfigs} />
),
}, },
{ {
key: EQueryType.CLICKHOUSE, key: EQueryType.CLICKHOUSE,

View File

@ -1,5 +1,5 @@
import { SearchOutlined } from '@ant-design/icons'; import { SearchOutlined } from '@ant-design/icons';
import { Input } from 'antd'; import { Input, Spin } from 'antd';
import Typography from 'antd/es/typography/Typography'; import Typography from 'antd/es/typography/Typography';
import { useIsDarkMode } from 'hooks/useDarkMode'; import { useIsDarkMode } from 'hooks/useDarkMode';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@ -26,12 +26,17 @@ function AddColumnField({ config }: AddColumnFieldProps): JSX.Element | null {
<Input.Group compact> <Input.Group compact>
<AddColumnSelect <AddColumnSelect
loading={config.isFetching}
size="small" size="small"
mode="multiple" mode="multiple"
placeholder="Search" placeholder="Search"
options={config.options} options={config.options}
value={[]} value={[]}
onChange={config.onChange} onSelect={config.onSelect}
onSearch={config.onSearch}
onFocus={config.onFocus}
onBlur={config.onBlur}
notFoundContent={config.isFetching ? <Spin size="small" /> : null}
/> />
<SearchIconWrapper $isDarkMode={isDarkMode}> <SearchIconWrapper $isDarkMode={isDarkMode}>
<SearchOutlined /> <SearchOutlined />

View File

@ -18,9 +18,9 @@ function FormatField({ config }: FormatFieldProps): JSX.Element | null {
value={config.value} value={config.value}
onChange={config.onChange} onChange={config.onChange}
> >
<RadioButton value="row">{t('options_menu.row')}</RadioButton> <RadioButton value="raw">{t('options_menu.raw')}</RadioButton>
<RadioButton value="default">{t('options_menu.default')}</RadioButton> <RadioButton value="list">{t('options_menu.default')}</RadioButton>
<RadioButton value="column">{t('options_menu.column')}</RadioButton> <RadioButton value="table">{t('options_menu.column')}</RadioButton>
</RadioGroup> </RadioGroup>
</FormatFieldWrapper> </FormatFieldWrapper>
); );

View File

@ -4,6 +4,6 @@ export const URL_OPTIONS = 'options';
export const defaultOptionsQuery: OptionsQuery = { export const defaultOptionsQuery: OptionsQuery = {
selectColumns: [], selectColumns: [],
maxLines: 0, maxLines: 2,
format: 'default', format: 'list',
}; };

View File

@ -1,10 +1,11 @@
import { InputNumberProps, RadioProps, SelectProps } from 'antd'; import { InputNumberProps, RadioProps, SelectProps } from 'antd';
import { LogViewMode } from 'container/LogsTable';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse'; import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
export interface OptionsQuery { export interface OptionsQuery {
selectColumns: BaseAutocompleteData[]; selectColumns: BaseAutocompleteData[];
maxLines: number; maxLines: number;
format: 'default' | 'row' | 'column'; format: LogViewMode;
} }
export interface InitialOptions export interface InitialOptions
@ -15,7 +16,11 @@ export interface InitialOptions
export type OptionsMenuConfig = { export type OptionsMenuConfig = {
format?: Pick<RadioProps, 'value' | 'onChange'>; format?: Pick<RadioProps, 'value' | 'onChange'>;
maxLines?: Pick<InputNumberProps, 'value' | 'onChange'>; maxLines?: Pick<InputNumberProps, 'value' | 'onChange'>;
addColumn?: Pick<SelectProps, 'options' | 'onChange'> & { addColumn?: Pick<
SelectProps,
'options' | 'onSelect' | 'onFocus' | 'onSearch' | 'onBlur'
> & {
isFetching: boolean;
value: BaseAutocompleteData[]; value: BaseAutocompleteData[];
onRemove: (key: string) => void; onRemove: (key: string) => void;
}; };

View File

@ -1,66 +1,146 @@
import { RadioChangeEvent } from 'antd'; 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 { getAggregateKeys } from 'api/queryBuilder/getAttributeKeys';
import { LOCALSTORAGE } from 'constants/localStorage';
import { QueryBuilderKeys } from 'constants/queryBuilder'; import { QueryBuilderKeys } from 'constants/queryBuilder';
import useDebounce from 'hooks/useDebounce';
import { useNotifications } from 'hooks/useNotifications'; import { useNotifications } from 'hooks/useNotifications';
import useUrlQueryData from 'hooks/useUrlQueryData'; import useUrlQueryData from 'hooks/useUrlQueryData';
import { useCallback, useEffect, useMemo } from 'react'; import { useCallback, useEffect, useMemo, useState } from 'react';
import { useQuery } from 'react-query'; import { useQueries, useQuery } from 'react-query';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse'; import { ErrorResponse, SuccessResponse } from 'types/api';
import {
BaseAutocompleteData,
IQueryAutocompleteResponse,
} from 'types/api/queryBuilder/queryAutocompleteResponse';
import { DataSource } from 'types/common/queryBuilder'; import { DataSource } from 'types/common/queryBuilder';
import { defaultOptionsQuery, URL_OPTIONS } from './constants'; import { defaultOptionsQuery, URL_OPTIONS } from './constants';
import { InitialOptions, OptionsMenuConfig, OptionsQuery } from './types'; import { InitialOptions, OptionsMenuConfig, OptionsQuery } from './types';
import { getInitialColumns, getOptionsFromKeys } from './utils'; import { getOptionsFromKeys } from './utils';
interface UseOptionsMenuProps { interface UseOptionsMenuProps {
dataSource: DataSource; dataSource: DataSource;
aggregateOperator: string; aggregateOperator: string;
initialOptions?: InitialOptions; initialOptions?: InitialOptions;
storageKey: LOCALSTORAGE;
} }
interface UseOptionsMenu { interface UseOptionsMenu {
isLoading: boolean;
options: OptionsQuery; options: OptionsQuery;
config: OptionsMenuConfig; config: OptionsMenuConfig;
} }
const useOptionsMenu = ({ const useOptionsMenu = ({
storageKey,
dataSource, dataSource,
aggregateOperator, aggregateOperator,
initialOptions = {}, initialOptions = {},
}: UseOptionsMenuProps): UseOptionsMenu => { }: UseOptionsMenuProps): UseOptionsMenu => {
const { notifications } = useNotifications(); const { notifications } = useNotifications();
const [searchText, setSearchText] = useState<string>('');
const [isFocused, setIsFocused] = useState<boolean>(false);
const debouncedSearchText = useDebounce(searchText, 300);
const localStorageOptionsQuery = useMemo(
() => getFromLocalstorage(storageKey),
[storageKey],
);
const initialQueryParams = useMemo(
() => ({
searchText: '',
aggregateAttribute: '',
tagType: null,
dataSource,
aggregateOperator,
}),
[dataSource, aggregateOperator],
);
const { const {
query: optionsQuery, query: optionsQuery,
queryData: optionsQueryData, queryData: optionsQueryData,
redirectWithQuery: redirectWithOptionsData, redirectWithQuery: redirectWithOptionsData,
} = useUrlQueryData<OptionsQuery>(URL_OPTIONS); } = useUrlQueryData<OptionsQuery>(URL_OPTIONS, defaultOptionsQuery);
const { data, isFetched, isLoading } = useQuery( const initialQueries = useMemo(
[QueryBuilderKeys.GET_ATTRIBUTE_KEY], () =>
async () => initialOptions?.selectColumns?.map((column) => ({
queryKey: column,
queryFn: (): Promise<
SuccessResponse<IQueryAutocompleteResponse> | ErrorResponse
> =>
getAggregateKeys({ getAggregateKeys({
searchText: '', ...initialQueryParams,
dataSource, searchText: column,
aggregateOperator,
aggregateAttribute: '',
}), }),
enabled: !!column && !optionsQuery,
})) || [],
[initialOptions?.selectColumns, initialQueryParams, optionsQuery],
); );
const attributeKeys = useMemo(() => data?.payload?.attributeKeys || [], [ const initialAttributesResult = useQueries(initialQueries);
data?.payload?.attributeKeys,
const isFetchedInitialAttributes = useMemo(
() => initialAttributesResult.every((result) => result.isFetched),
[initialAttributesResult],
);
const initialSelectedColumns = useMemo(() => {
if (!isFetchedInitialAttributes) return [];
const attributesData = initialAttributesResult?.reduce(
(acc, attributeResponse) => {
const data = attributeResponse?.data?.payload?.attributeKeys || [];
return [...acc, ...data];
},
[] as BaseAutocompleteData[],
);
return (
(initialOptions.selectColumns
?.map((column) => attributesData.find(({ key }) => key === column))
.filter(Boolean) as BaseAutocompleteData[]) || []
);
}, [
isFetchedInitialAttributes,
initialOptions?.selectColumns,
initialAttributesResult,
]); ]);
const {
data: searchedAttributesData,
isFetching: isSearchedAttributesFetching,
} = useQuery(
[QueryBuilderKeys.GET_AGGREGATE_KEYS, debouncedSearchText, isFocused],
async () =>
getAggregateKeys({
...initialQueryParams,
searchText: debouncedSearchText,
}),
{
enabled: isFocused,
},
);
const searchedAttributeKeys = useMemo(
() => searchedAttributesData?.payload?.attributeKeys || [],
[searchedAttributesData?.payload?.attributeKeys],
);
const initialOptionsQuery: OptionsQuery = useMemo( const initialOptionsQuery: OptionsQuery = useMemo(
() => ({ () => ({
...defaultOptionsQuery, ...defaultOptionsQuery,
...initialOptions, ...initialOptions,
selectColumns: initialOptions?.selectColumns selectColumns: initialOptions?.selectColumns
? getInitialColumns(initialOptions?.selectColumns || [], attributeKeys) ? initialSelectedColumns
: defaultOptionsQuery.selectColumns, : defaultOptionsQuery.selectColumns,
}), }),
[initialOptions, attributeKeys], [initialOptions, initialSelectedColumns],
); );
const selectedColumnKeys = useMemo( const selectedColumnKeys = useMemo(
@ -68,29 +148,49 @@ const useOptionsMenu = ({
[optionsQueryData], [optionsQueryData],
); );
const addColumnOptions = useMemo( const optionsFromAttributeKeys = useMemo(() => {
() => getOptionsFromKeys(attributeKeys, selectedColumnKeys), const filteredAttributeKeys = searchedAttributeKeys.filter(
[attributeKeys, selectedColumnKeys], (item) => item.key !== 'body',
); );
const handleSelectedColumnsChange = useCallback( return getOptionsFromKeys(filteredAttributeKeys, selectedColumnKeys);
(value: string[]) => { }, [searchedAttributeKeys, selectedColumnKeys]);
const newSelectedColumnKeys = [
...new Set([...selectedColumnKeys, ...value]), const handleRedirectWithOptionsData = useCallback(
]; (newQueryData: OptionsQuery) => {
redirectWithOptionsData(newQueryData);
setToLocalstorage(storageKey, JSON.stringify(newQueryData));
},
[storageKey, redirectWithOptionsData],
);
const handleSelectColumns = useCallback(
(value: string) => {
const newSelectedColumnKeys = [...new Set([...selectedColumnKeys, value])];
const newSelectedColumns = newSelectedColumnKeys.reduce((acc, key) => { const newSelectedColumns = newSelectedColumnKeys.reduce((acc, key) => {
const column = attributeKeys.find(({ id }) => id === key); const column = [
...searchedAttributeKeys,
...optionsQueryData.selectColumns,
].find(({ id }) => id === key);
if (!column) return acc; if (!column) return acc;
return [...acc, column]; return [...acc, column];
}, [] as BaseAutocompleteData[]); }, [] as BaseAutocompleteData[]);
redirectWithOptionsData({ const optionsData: OptionsQuery = {
...defaultOptionsQuery, ...optionsQueryData,
selectColumns: newSelectedColumns, selectColumns: newSelectedColumns,
}); };
handleRedirectWithOptionsData(optionsData);
}, },
[attributeKeys, selectedColumnKeys, redirectWithOptionsData], [
searchedAttributeKeys,
selectedColumnKeys,
optionsQueryData,
handleRedirectWithOptionsData,
],
); );
const handleRemoveSelectedColumn = useCallback( const handleRemoveSelectedColumn = useCallback(
@ -99,63 +199,88 @@ const useOptionsMenu = ({
({ id }) => id !== columnKey, ({ id }) => id !== columnKey,
); );
if (!newSelectedColumns.length) { if (!newSelectedColumns.length && dataSource !== DataSource.LOGS) {
notifications.error({ notifications.error({
message: 'There must be at least one selected column', message: 'There must be at least one selected column',
}); });
} else { } else {
redirectWithOptionsData({ const optionsData: OptionsQuery = {
...defaultOptionsQuery, ...optionsQueryData,
selectColumns: newSelectedColumns, selectColumns: newSelectedColumns,
}); };
handleRedirectWithOptionsData(optionsData);
} }
}, },
[optionsQueryData, notifications, redirectWithOptionsData], [dataSource, notifications, optionsQueryData, handleRedirectWithOptionsData],
); );
const handleFormatChange = useCallback( const handleFormatChange = useCallback(
(event: RadioChangeEvent) => { (event: RadioChangeEvent) => {
redirectWithOptionsData({ const optionsData: OptionsQuery = {
...defaultOptionsQuery, ...optionsQueryData,
format: event.target.value, format: event.target.value,
}); };
handleRedirectWithOptionsData(optionsData);
}, },
[redirectWithOptionsData], [handleRedirectWithOptionsData, optionsQueryData],
); );
const handleMaxLinesChange = useCallback( const handleMaxLinesChange = useCallback(
(value: string | number | null) => { (value: string | number | null) => {
redirectWithOptionsData({ const optionsData: OptionsQuery = {
...defaultOptionsQuery, ...optionsQueryData,
maxLines: value as number, maxLines: value as number,
}); };
handleRedirectWithOptionsData(optionsData);
}, },
[redirectWithOptionsData], [handleRedirectWithOptionsData, optionsQueryData],
); );
const handleSearchAttribute = useCallback((value: string) => {
setSearchText(value);
}, []);
const handleFocus = (): void => {
setIsFocused(true);
};
const handleBlur = (): void => {
setIsFocused(false);
setSearchText('');
};
const optionsMenuConfig: Required<OptionsMenuConfig> = useMemo( const optionsMenuConfig: Required<OptionsMenuConfig> = useMemo(
() => ({ () => ({
addColumn: { addColumn: {
isFetching: isSearchedAttributesFetching,
value: optionsQueryData?.selectColumns || defaultOptionsQuery.selectColumns, value: optionsQueryData?.selectColumns || defaultOptionsQuery.selectColumns,
options: addColumnOptions || [], options: optionsFromAttributeKeys || [],
onChange: handleSelectedColumnsChange, onFocus: handleFocus,
onBlur: handleBlur,
onSelect: handleSelectColumns,
onRemove: handleRemoveSelectedColumn, onRemove: handleRemoveSelectedColumn,
onSearch: handleSearchAttribute,
}, },
format: { format: {
value: optionsQueryData?.format || defaultOptionsQuery.format, value: optionsQueryData.format || defaultOptionsQuery.format,
onChange: handleFormatChange, onChange: handleFormatChange,
}, },
maxLines: { maxLines: {
value: optionsQueryData?.maxLines || defaultOptionsQuery.maxLines, value: optionsQueryData.maxLines || defaultOptionsQuery.maxLines,
onChange: handleMaxLinesChange, onChange: handleMaxLinesChange,
}, },
}), }),
[ [
addColumnOptions, optionsFromAttributeKeys,
optionsQueryData?.maxLines, optionsQueryData?.maxLines,
optionsQueryData?.format, optionsQueryData?.format,
optionsQueryData?.selectColumns, optionsQueryData?.selectColumns,
handleSelectedColumnsChange, isSearchedAttributesFetching,
handleSearchAttribute,
handleSelectColumns,
handleRemoveSelectedColumn, handleRemoveSelectedColumn,
handleFormatChange, handleFormatChange,
handleMaxLinesChange, handleMaxLinesChange,
@ -163,13 +288,22 @@ const useOptionsMenu = ({
); );
useEffect(() => { useEffect(() => {
if (optionsQuery || !isFetched) return; if (optionsQuery || !isFetchedInitialAttributes) return;
redirectWithOptionsData(initialOptionsQuery); const nextOptionsQuery = localStorageOptionsQuery
}, [isFetched, optionsQuery, initialOptionsQuery, redirectWithOptionsData]); ? JSON.parse(localStorageOptionsQuery)
: initialOptionsQuery;
redirectWithOptionsData(nextOptionsQuery);
}, [
isFetchedInitialAttributes,
optionsQuery,
initialOptionsQuery,
localStorageOptionsQuery,
redirectWithOptionsData,
]);
return { return {
isLoading,
options: optionsQueryData, options: optionsQueryData,
config: optionsMenuConfig, config: optionsMenuConfig,
}; };

View File

@ -14,15 +14,3 @@ export const getOptionsFromKeys = (
({ value }) => !selectedKeys.find((key) => key === value), ({ value }) => !selectedKeys.find((key) => key === value),
); );
}; };
export const getInitialColumns = (
initialColumnTitles: string[],
attributeKeys: BaseAutocompleteData[],
): BaseAutocompleteData[] =>
initialColumnTitles.reduce((acc, title) => {
const initialColumn = attributeKeys.find(({ key }) => title === key);
if (!initialColumn) return acc;
return [...acc, initialColumn];
}, [] as BaseAutocompleteData[]);

View File

@ -0,0 +1,4 @@
export type PageSizeSelectProps = {
isLoading: boolean;
isShow: boolean;
};

View File

@ -0,0 +1,51 @@
import { Col, Row, Select } from 'antd';
import { queryParamNamesMap } from 'constants/queryBuilderQueryNames';
import {
defaultSelectStyle,
ITEMS_PER_PAGE_OPTIONS,
} from 'container/Controls/config';
import useUrlQueryData from 'hooks/useUrlQueryData';
import { useCallback } from 'react';
import { PageSizeSelectProps } from './PageSizeSelect.interfaces';
function PageSizeSelect({
isLoading,
isShow,
}: PageSizeSelectProps): JSX.Element | null {
const { redirectWithQuery, queryData: pageSize } = useUrlQueryData<number>(
queryParamNamesMap.pageSize,
ITEMS_PER_PAGE_OPTIONS[0],
);
const handleChangePageSize = useCallback(
(value: number) => {
redirectWithQuery(value);
},
[redirectWithQuery],
);
if (!isShow) return null;
return (
<Row>
<Col>
<Select
style={defaultSelectStyle}
loading={isLoading}
value={pageSize}
onChange={handleChangePageSize}
>
{ITEMS_PER_PAGE_OPTIONS.map((count) => (
<Select.Option
key={count}
value={count}
>{`${count} / page`}</Select.Option>
))}
</Select>
</Col>
</Row>
);
}
export default PageSizeSelect;

View File

@ -1,5 +1,6 @@
import { ITEMS } from 'container/NewDashboard/ComponentsSlider/menuItems'; import { ITEMS } from 'container/NewDashboard/ComponentsSlider/menuItems';
import { ReactNode } from 'react'; import { ReactNode } from 'react';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder'; import { DataSource } from 'types/common/queryBuilder';
export type QueryBuilderConfig = export type QueryBuilderConfig =
@ -13,4 +14,7 @@ export type QueryBuilderProps = {
config?: QueryBuilderConfig; config?: QueryBuilderConfig;
panelType: ITEMS; panelType: ITEMS;
actions?: ReactNode; actions?: ReactNode;
filterConfigs?: Partial<
Record<keyof IBuilderQuery, { isHidden: boolean; isDisabled: boolean }>
>;
}; };

View File

@ -1,6 +0,0 @@
import { Col } from 'antd';
import styled from 'styled-components';
export const ActionsWrapperStyled = styled(Col)`
padding-right: 1rem;
`;

View File

@ -10,13 +10,12 @@ import { memo, useEffect, useMemo } from 'react';
import { Formula, Query } from './components'; import { Formula, Query } from './components';
// ** Types // ** Types
import { QueryBuilderProps } from './QueryBuilder.interfaces'; import { QueryBuilderProps } from './QueryBuilder.interfaces';
// ** Styles
import { ActionsWrapperStyled } from './QueryBuilder.styled';
export const QueryBuilder = memo(function QueryBuilder({ export const QueryBuilder = memo(function QueryBuilder({
config, config,
panelType: newPanelType, panelType: newPanelType,
actions, actions,
filterConfigs = {},
}: QueryBuilderProps): JSX.Element { }: QueryBuilderProps): JSX.Element {
const { const {
currentQuery, currentQuery,
@ -74,6 +73,7 @@ export const QueryBuilder = memo(function QueryBuilder({
isAvailableToDisable={isAvailableToDisableQuery} isAvailableToDisable={isAvailableToDisableQuery}
queryVariant={config?.queryVariant || 'dropdown'} queryVariant={config?.queryVariant || 'dropdown'}
query={query} query={query}
filterConfigs={filterConfigs}
/> />
</Col> </Col>
))} ))}
@ -85,7 +85,7 @@ export const QueryBuilder = memo(function QueryBuilder({
</Row> </Row>
</Col> </Col>
<ActionsWrapperStyled span={24}> <Col span={24}>
<Row gutter={[20, 0]}> <Row gutter={[20, 0]}>
<Col> <Col>
<Button <Button
@ -109,7 +109,7 @@ export const QueryBuilder = memo(function QueryBuilder({
</Col> </Col>
{actions} {actions}
</Row> </Row>
</ActionsWrapperStyled> </Col>
</Row> </Row>
); );
}); });

View File

@ -1,3 +1,4 @@
import { QueryBuilderProps } from 'container/QueryBuilder/QueryBuilder.interfaces';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData'; import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
export type QueryProps = { export type QueryProps = {
@ -5,4 +6,4 @@ export type QueryProps = {
isAvailableToDisable: boolean; isAvailableToDisable: boolean;
query: IBuilderQuery; query: IBuilderQuery;
queryVariant: 'static' | 'dropdown'; queryVariant: 'static' | 'dropdown';
}; } & Pick<QueryBuilderProps, 'filterConfigs'>;

View File

@ -35,6 +35,7 @@ export const Query = memo(function Query({
isAvailableToDisable, isAvailableToDisable,
queryVariant, queryVariant,
query, query,
filterConfigs,
}: QueryProps): JSX.Element { }: QueryProps): JSX.Element {
const { panelType } = useQueryBuilder(); const { panelType } = useQueryBuilder();
const { const {
@ -47,7 +48,7 @@ export const Query = memo(function Query({
handleChangeQueryData, handleChangeQueryData,
handleChangeOperator, handleChangeOperator,
handleDeleteQuery, handleDeleteQuery,
} = useQueryOperations({ index, query }); } = useQueryOperations({ index, query, filterConfigs });
const handleChangeAggregateEvery = useCallback( const handleChangeAggregateEvery = useCallback(
(value: IBuilderQuery['stepInterval']) => { (value: IBuilderQuery['stepInterval']) => {
@ -109,6 +110,30 @@ export const Query = memo(function Query({
[handleChangeQueryData], [handleChangeQueryData],
); );
const renderAggregateEveryFilter = useCallback(
(): JSX.Element | null =>
!filterConfigs?.stepInterval?.isHidden ? (
<Row gutter={[11, 5]}>
<Col flex="5.93rem">
<FilterLabel label="Aggregate Every" />
</Col>
<Col flex="1 1 6rem">
<AggregateEveryFilter
query={query}
disabled={filterConfigs?.stepInterval?.isDisabled || false}
onChange={handleChangeAggregateEvery}
/>
</Col>
</Row>
) : null,
[
filterConfigs?.stepInterval?.isHidden,
filterConfigs?.stepInterval?.isDisabled,
query,
handleChangeAggregateEvery,
],
);
const renderAdditionalFilters = useCallback((): ReactNode => { const renderAdditionalFilters = useCallback((): ReactNode => {
switch (panelType) { switch (panelType) {
case PANEL_TYPES.TIME_SERIES: { case PANEL_TYPES.TIME_SERIES: {
@ -149,19 +174,7 @@ export const Query = memo(function Query({
</Col> </Col>
)} )}
<Col span={11}> <Col span={11}>{renderAggregateEveryFilter()}</Col>
<Row gutter={[11, 5]}>
<Col flex="5.93rem">
<FilterLabel label="Aggregate Every" />
</Col>
<Col flex="1 1 6rem">
<AggregateEveryFilter
query={query}
onChange={handleChangeAggregateEvery}
/>
</Col>
</Row>
</Col>
</> </>
); );
} }
@ -179,19 +192,7 @@ export const Query = memo(function Query({
</Col> </Col>
</Row> </Row>
</Col> </Col>
<Col span={11}> <Col span={11}>{renderAggregateEveryFilter()}</Col>
<Row gutter={[11, 5]}>
<Col flex="5.93rem">
<FilterLabel label="Aggregate Every" />
</Col>
<Col flex="1 1 6rem">
<AggregateEveryFilter
query={query}
onChange={handleChangeAggregateEvery}
/>
</Col>
</Row>
</Col>
</> </>
); );
} }
@ -230,21 +231,7 @@ export const Query = memo(function Query({
</Row> </Row>
</Col> </Col>
{panelType !== PANEL_TYPES.LIST && ( <Col span={11}>{renderAggregateEveryFilter()}</Col>
<Col span={11}>
<Row gutter={[11, 5]}>
<Col flex="5.93rem">
<FilterLabel label="Aggregate Every" />
</Col>
<Col flex="1 1 6rem">
<AggregateEveryFilter
query={query}
onChange={handleChangeAggregateEvery}
/>
</Col>
</Row>
</Col>
)}
</> </>
); );
} }
@ -253,10 +240,10 @@ export const Query = memo(function Query({
panelType, panelType,
query, query,
isMetricsDataSource, isMetricsDataSource,
handleChangeAggregateEvery,
handleChangeHavingFilter, handleChangeHavingFilter,
handleChangeLimit, handleChangeLimit,
handleChangeOrderByKeys, handleChangeOrderByKeys,
renderAggregateEveryFilter,
]); ]);
return ( return (

View File

@ -8,6 +8,7 @@ import { selectStyle } from '../QueryBuilderSearch/config';
function AggregateEveryFilter({ function AggregateEveryFilter({
onChange, onChange,
query, query,
disabled,
}: AggregateEveryFilterProps): JSX.Element { }: AggregateEveryFilterProps): JSX.Element {
const isMetricsDataSource = useMemo( const isMetricsDataSource = useMemo(
() => query.dataSource === DataSource.METRICS, () => query.dataSource === DataSource.METRICS,
@ -20,7 +21,8 @@ function AggregateEveryFilter({
} }
}; };
const isDisabled = isMetricsDataSource && !query.aggregateAttribute.key; const isDisabled =
(isMetricsDataSource && !query.aggregateAttribute.key) || disabled;
return ( return (
<InputNumber <InputNumber
@ -37,6 +39,7 @@ function AggregateEveryFilter({
interface AggregateEveryFilterProps { interface AggregateEveryFilterProps {
onChange: (values: number) => void; onChange: (values: number) => void;
query: IBuilderQuery; query: IBuilderQuery;
disabled: boolean;
} }
export default AggregateEveryFilter; export default AggregateEveryFilter;

View File

@ -4,20 +4,21 @@ import { AutoComplete, Spin } from 'antd';
import { getAggregateAttribute } from 'api/queryBuilder/getAggregateAttribute'; import { getAggregateAttribute } from 'api/queryBuilder/getAggregateAttribute';
import { import {
baseAutoCompleteIdKeysOrder, baseAutoCompleteIdKeysOrder,
idDivider,
initialAutocompleteData,
QueryBuilderKeys, QueryBuilderKeys,
selectValueDivider, selectValueDivider,
} from 'constants/queryBuilder'; } from 'constants/queryBuilder';
import { DEBOUNCE_DELAY } from 'constants/queryBuilderFilterConfig';
import useDebounce from 'hooks/useDebounce'; import useDebounce from 'hooks/useDebounce';
import { createIdFromObjectFields } from 'lib/createIdFromObjectFields'; import { createIdFromObjectFields } from 'lib/createIdFromObjectFields';
import { chooseAutocompleteFromCustomValue } from 'lib/newQueryBuilder/chooseAutocompleteFromCustomValue';
import { getAutocompleteValueAndType } from 'lib/newQueryBuilder/getAutocompleteValueAndType';
import { transformStringWithPrefix } from 'lib/query/transformStringWithPrefix'; import { transformStringWithPrefix } from 'lib/query/transformStringWithPrefix';
import { memo, useCallback, useMemo, useState } from 'react'; import { memo, useCallback, useMemo, useState } from 'react';
import { useQuery } from 'react-query'; import { useQuery, useQueryClient } from 'react-query';
import { SuccessResponse } from 'types/api';
import { import {
AutocompleteType,
BaseAutocompleteData, BaseAutocompleteData,
DataType, IQueryAutocompleteResponse,
} from 'types/api/queryBuilder/queryAutocompleteResponse'; } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { DataSource } from 'types/common/queryBuilder'; import { DataSource } from 'types/common/queryBuilder';
import { ExtendedSelectOption } from 'types/common/select'; import { ExtendedSelectOption } from 'types/common/select';
@ -32,8 +33,18 @@ export const AggregatorFilter = memo(function AggregatorFilter({
disabled, disabled,
onChange, onChange,
}: AgregatorFilterProps): JSX.Element { }: AgregatorFilterProps): JSX.Element {
const queryClient = useQueryClient();
const [optionsData, setOptionsData] = useState<ExtendedSelectOption[]>([]); const [optionsData, setOptionsData] = useState<ExtendedSelectOption[]>([]);
const debouncedValue = useDebounce(query.aggregateAttribute.key, 300); const [searchText, setSearchText] = useState<string>('');
const debouncedSearchText = useMemo(() => {
// eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/no-unused-vars
const [_, value] = getAutocompleteValueAndType(searchText);
return value;
}, [searchText]);
const debouncedValue = useDebounce(debouncedSearchText, DEBOUNCE_DELAY);
const { isFetching } = useQuery( const { isFetching } = useQuery(
[ [
QueryBuilderKeys.GET_AGGREGATE_ATTRIBUTE, QueryBuilderKeys.GET_AGGREGATE_ATTRIBUTE,
@ -69,46 +80,95 @@ export const AggregatorFilter = memo(function AggregatorFilter({
}, },
); );
const handleChangeAttribute = useCallback( const handleSearchText = useCallback((text: string): void => {
setSearchText(text);
}, []);
const placeholder: string =
query.dataSource === DataSource.METRICS
? `${transformToUpperCase(query.dataSource)} name`
: 'Aggregate attribute';
const getAttributesData = useCallback(
(): BaseAutocompleteData[] =>
queryClient.getQueryData<SuccessResponse<IQueryAutocompleteResponse>>([
QueryBuilderKeys.GET_AGGREGATE_ATTRIBUTE,
debouncedValue,
query.aggregateOperator,
query.dataSource,
])?.payload.attributeKeys || [],
[debouncedValue, query.aggregateOperator, query.dataSource, queryClient],
);
const getResponseAttributes = useCallback(async () => {
const response = await queryClient.fetchQuery(
[
QueryBuilderKeys.GET_AGGREGATE_ATTRIBUTE,
searchText,
query.aggregateOperator,
query.dataSource,
],
async () =>
getAggregateAttribute({
searchText,
aggregateOperator: query.aggregateOperator,
dataSource: query.dataSource,
}),
);
return response.payload?.attributeKeys || [];
}, [query.aggregateOperator, query.dataSource, queryClient, searchText]);
const handleChangeCustomValue = useCallback(
async (value: string, attributes: BaseAutocompleteData[]) => {
const customAttribute: BaseAutocompleteData = chooseAutocompleteFromCustomValue(
attributes,
value,
);
onChange(customAttribute);
},
[onChange],
);
const handleBlur = useCallback(async () => {
if (searchText) {
const aggregateAttributes = await getResponseAttributes();
handleChangeCustomValue(searchText, aggregateAttributes);
}
}, [getResponseAttributes, handleChangeCustomValue, searchText]);
const handleChange = useCallback(
( (
value: string, value: string,
option: ExtendedSelectOption | ExtendedSelectOption[], option: ExtendedSelectOption | ExtendedSelectOption[],
): void => { ): void => {
const currentOption = option as ExtendedSelectOption; const currentOption = option as ExtendedSelectOption;
const aggregateAttributes = getAttributesData();
if (currentOption.key) { if (currentOption.key) {
const [key, dataType, type, isColumn] = currentOption.key.split(idDivider); const attribute = aggregateAttributes.find(
const attribute: BaseAutocompleteData = { (item) => item.id === currentOption.key,
key,
dataType: dataType as DataType,
type: type as AutocompleteType,
isColumn: isColumn === 'true',
};
onChange(attribute);
} else {
const attribute = { ...initialAutocompleteData, key: value };
onChange(attribute);
}
},
[onChange],
); );
const value = useMemo( if (attribute) {
() => onChange(attribute);
transformStringWithPrefix({ }
} else {
handleChangeCustomValue(value, aggregateAttributes);
}
setSearchText('');
},
[getAttributesData, handleChangeCustomValue, onChange],
);
const value = transformStringWithPrefix({
str: query.aggregateAttribute.key, str: query.aggregateAttribute.key,
prefix: query.aggregateAttribute.type || '', prefix: query.aggregateAttribute.type || '',
condition: !query.aggregateAttribute.isColumn, condition: !query.aggregateAttribute.isColumn,
}), });
[query],
);
const placeholder: string =
query.dataSource === DataSource.METRICS
? `${transformToUpperCase(query.dataSource)} name`
: 'Aggregate attribute';
return ( return (
<AutoComplete <AutoComplete
@ -116,10 +176,12 @@ export const AggregatorFilter = memo(function AggregatorFilter({
style={selectStyle} style={selectStyle}
showArrow={false} showArrow={false}
filterOption={false} filterOption={false}
onSearch={handleSearchText}
notFoundContent={isFetching ? <Spin size="small" /> : null} notFoundContent={isFetching ? <Spin size="small" /> : null}
options={optionsData} options={optionsData}
value={value} value={value}
onChange={handleChangeAttribute} onBlur={handleBlur}
onChange={handleChange}
disabled={disabled} disabled={disabled}
/> />
); );

View File

@ -3,22 +3,19 @@ import { getAggregateKeys } from 'api/queryBuilder/getAttributeKeys';
// ** Constants // ** Constants
import { import {
idDivider, idDivider,
initialAutocompleteData,
QueryBuilderKeys, QueryBuilderKeys,
selectValueDivider, selectValueDivider,
} from 'constants/queryBuilder'; } from 'constants/queryBuilder';
import { DEBOUNCE_DELAY } from 'constants/queryBuilderFilterConfig';
import useDebounce from 'hooks/useDebounce'; import useDebounce from 'hooks/useDebounce';
import { chooseAutocompleteFromCustomValue } from 'lib/newQueryBuilder/chooseAutocompleteFromCustomValue';
// ** Components // ** Components
// ** Helpers // ** Helpers
import { transformStringWithPrefix } from 'lib/query/transformStringWithPrefix'; import { transformStringWithPrefix } from 'lib/query/transformStringWithPrefix';
import { isEqual, uniqWith } from 'lodash-es'; import { isEqual, uniqWith } from 'lodash-es';
import { memo, useCallback, useEffect, useState } from 'react'; import { memo, useCallback, useEffect, useState } from 'react';
import { useQuery } from 'react-query'; import { useQuery, useQueryClient } from 'react-query';
import { import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
AutocompleteType,
BaseAutocompleteData,
DataType,
} from 'types/api/queryBuilder/queryAutocompleteResponse';
import { SelectOption } from 'types/common/select'; import { SelectOption } from 'types/common/select';
import { selectStyle } from '../QueryBuilderSearch/config'; import { selectStyle } from '../QueryBuilderSearch/config';
@ -29,6 +26,7 @@ export const GroupByFilter = memo(function GroupByFilter({
onChange, onChange,
disabled, disabled,
}: GroupByFilterProps): JSX.Element { }: GroupByFilterProps): JSX.Element {
const queryClient = useQueryClient();
const [searchText, setSearchText] = useState<string>(''); const [searchText, setSearchText] = useState<string>('');
const [optionsData, setOptionsData] = useState<SelectOption<string, string>[]>( const [optionsData, setOptionsData] = useState<SelectOption<string, string>[]>(
[], [],
@ -38,7 +36,7 @@ export const GroupByFilter = memo(function GroupByFilter({
); );
const [isFocused, setIsFocused] = useState<boolean>(false); const [isFocused, setIsFocused] = useState<boolean>(false);
const debouncedValue = useDebounce(searchText, 300); const debouncedValue = useDebounce(searchText, DEBOUNCE_DELAY);
const { isFetching } = useQuery( const { isFetching } = useQuery(
[QueryBuilderKeys.GET_AGGREGATE_KEYS, debouncedValue, isFocused], [QueryBuilderKeys.GET_AGGREGATE_KEYS, debouncedValue, isFocused],
@ -81,6 +79,28 @@ export const GroupByFilter = memo(function GroupByFilter({
}, },
); );
const getAttributeKeys = useCallback(async () => {
const response = await queryClient.fetchQuery(
[QueryBuilderKeys.GET_AGGREGATE_KEYS, searchText, isFocused],
async () =>
getAggregateKeys({
aggregateAttribute: query.aggregateAttribute.key,
dataSource: query.dataSource,
aggregateOperator: query.aggregateOperator,
searchText,
}),
);
return response.payload?.attributeKeys || [];
}, [
isFocused,
query.aggregateAttribute.key,
query.aggregateOperator,
query.dataSource,
queryClient,
searchText,
]);
const handleSearchKeys = (searchText: string): void => { const handleSearchKeys = (searchText: string): void => {
setSearchText(searchText); setSearchText(searchText);
}; };
@ -94,30 +114,35 @@ export const GroupByFilter = memo(function GroupByFilter({
setIsFocused(true); setIsFocused(true);
}; };
const handleChange = (values: SelectOption<string, string>[]): void => { const handleChange = useCallback(
async (values: SelectOption<string, string>[]): Promise<void> => {
const keys = await getAttributeKeys();
const groupByValues: BaseAutocompleteData[] = values.map((item) => { const groupByValues: BaseAutocompleteData[] = values.map((item) => {
const [currentValue, id] = item.value.split(selectValueDivider); const [currentValue, id] = item.value.split(selectValueDivider);
if (id && id.includes(idDivider)) {
const [key, dataType, type, isColumn] = id.split(idDivider);
return { if (id && id.includes(idDivider)) {
id, const attribute = keys.find((item) => item.id === id);
key: key || currentValue, const existAttribute = query.groupBy.find((item) => item.id === id);
dataType: (dataType as DataType) || initialAutocompleteData.dataType,
type: (type as AutocompleteType) || initialAutocompleteData.type, if (attribute) {
isColumn: isColumn return attribute;
? isColumn === 'true'
: initialAutocompleteData.isColumn,
};
} }
return { ...initialAutocompleteData, key: currentValue }; if (existAttribute) {
return existAttribute;
}
}
return chooseAutocompleteFromCustomValue(keys, currentValue);
}); });
const result = uniqWith(groupByValues, isEqual); const result = uniqWith(groupByValues, isEqual);
onChange(result); onChange(result);
}; },
[getAttributeKeys, onChange, query.groupBy],
);
const clearSearch = useCallback(() => { const clearSearch = useCallback(() => {
setSearchText(''); setSearchText('');

View File

@ -1,8 +1,8 @@
import { InputNumber } from 'antd'; import { InputNumber, Tooltip } from 'antd';
import { useMemo } from 'react'; // import { useMemo } from 'react';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData'; import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
// import { DataSource } from 'types/common/queryBuilder';
import { selectStyle } from '../QueryBuilderSearch/config'; import { selectStyle } from '../QueryBuilderSearch/config';
function LimitFilter({ onChange, query }: LimitFilterProps): JSX.Element { function LimitFilter({ onChange, query }: LimitFilterProps): JSX.Element {
@ -21,21 +21,25 @@ function LimitFilter({ onChange, query }: LimitFilterProps): JSX.Element {
} }
}; };
const isMetricsDataSource = useMemo( // const isMetricsDataSource = useMemo(
() => query.dataSource === DataSource.METRICS, // () => query.dataSource === DataSource.METRICS,
[query.dataSource], // [query.dataSource],
); // );
// const isDisabled = isMetricsDataSource && !query.aggregateAttribute.key;
return ( return (
<Tooltip placement="top" title="coming soon">
<InputNumber <InputNumber
min={1} min={1}
type="number" type="number"
readOnly
value={query.limit} value={query.limit}
disabled={isMetricsDataSource && !query.aggregateAttribute.key}
style={selectStyle} style={selectStyle}
onChange={onChange} onChange={onChange}
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
/> />
</Tooltip>
); );
} }

View File

@ -18,6 +18,7 @@ import {
getLabelFromValue, getLabelFromValue,
mapLabelValuePairs, mapLabelValuePairs,
orderByValueDelimiter, orderByValueDelimiter,
splitOrderByFromString,
transformToOrderByStringValues, transformToOrderByStringValues,
} from './utils'; } from './utils';
@ -27,7 +28,7 @@ export function OrderByFilter({
}: OrderByFilterProps): JSX.Element { }: OrderByFilterProps): JSX.Element {
const [searchText, setSearchText] = useState<string>(''); const [searchText, setSearchText] = useState<string>('');
const [selectedValue, setSelectedValue] = useState<IOption[]>( const [selectedValue, setSelectedValue] = useState<IOption[]>(
transformToOrderByStringValues(query.orderBy) || [], transformToOrderByStringValues(query.orderBy),
); );
const { data, isFetching } = useQuery( const { data, isFetching } = useQuery(
@ -115,7 +116,11 @@ export function OrderByFilter({
if (!match) return { label: item.label, value: item.value }; if (!match) return { label: item.label, value: item.value };
// eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/no-unused-vars
const [_, order] = match.data.flat() as string[]; const [_, order] = match.data.flat() as string[];
if (order) return { label: item.label, value: item.value }; if (order)
return {
label: item.label,
value: item.value,
};
return { return {
label: `${item.value} ${FILTERS.ASC}`, label: `${item.value} ${FILTERS.ASC}`,
@ -131,29 +136,69 @@ export function OrderByFilter({
); );
}, []); }, []);
const handleChange = (values: IOption[]): void => { const getValidResult = useCallback(
const result = getUniqValues(values); (result: IOption[]): IOption[] =>
result.reduce<IOption[]>((acc, item) => {
if (item.value === FILTERS.ASC || item.value === FILTERS.DESC) return acc;
if (item.value.includes(FILTERS.ASC) || item.value.includes(FILTERS.DESC)) {
const splittedOrderBy = splitOrderByFromString(item.value);
if (splittedOrderBy) {
acc.push({
label: `${splittedOrderBy.columnName} ${splittedOrderBy.order}`,
value: `${splittedOrderBy.columnName}${orderByValueDelimiter}${splittedOrderBy.order}`,
});
return acc;
}
}
acc.push(item);
return acc;
}, []),
[],
);
const handleChange = (values: IOption[]): void => {
const validResult = getValidResult(values);
const result = getUniqValues(validResult);
setSelectedValue(result);
const orderByValues: OrderByPayload[] = result.map((item) => { const orderByValues: OrderByPayload[] = result.map((item) => {
const match = Papa.parse(item.value, { delimiter: orderByValueDelimiter }); const match = Papa.parse(item.value, { delimiter: orderByValueDelimiter });
if (match) { if (!match) {
const [columnName, order] = match.data.flat() as string[];
return {
columnName: checkIfKeyPresent(columnName, query.aggregateAttribute.key)
? '#SIGNOZ_VALUE'
: columnName,
order: order ?? 'asc',
};
}
return { return {
columnName: item.value, columnName: item.value,
order: 'asc', order: 'asc',
}; };
}
const [columnName, order] = match.data.flat() as string[];
const columnNameValue = checkIfKeyPresent(
columnName,
query.aggregateAttribute.key,
)
? '#SIGNOZ_VALUE'
: columnName;
const orderValue = order ?? 'asc';
return {
columnName: columnNameValue,
order: orderValue,
};
}); });
const selectedValue: IOption[] = orderByValues.map((item) => ({
label: `${item.columnName} ${item.order}`,
value: `${item.columnName} ${item.order}`,
}));
setSelectedValue(selectedValue);
setSearchText(''); setSearchText('');
onChange(orderByValues); onChange(orderByValues);
}; };

View File

@ -4,15 +4,31 @@ import * as Papa from 'papaparse';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse'; import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { OrderByPayload } from 'types/api/queryBuilder/queryBuilderData'; import { OrderByPayload } from 'types/api/queryBuilder/queryBuilderData';
import { FILTERS } from './config';
export const orderByValueDelimiter = '|'; export const orderByValueDelimiter = '|';
export const transformToOrderByStringValues = ( export const transformToOrderByStringValues = (
orderBy: OrderByPayload[], orderBy: OrderByPayload[],
): IOption[] => ): IOption[] => {
orderBy.map((item) => ({ const prepareSelectedValue: IOption[] = orderBy.reduce<IOption[]>(
(acc, item) => {
if (item.columnName === '#SIGNOZ_VALUE') return acc;
const option: IOption = {
label: `${item.columnName} ${item.order}`, label: `${item.columnName} ${item.order}`,
value: `${item.columnName}${orderByValueDelimiter}${item.order}`, value: `${item.columnName}${orderByValueDelimiter}${item.order}`,
})); };
acc.push(option);
return acc;
},
[],
);
return prepareSelectedValue;
};
export function mapLabelValuePairs( export function mapLabelValuePairs(
arr: BaseAutocompleteData[], arr: BaseAutocompleteData[],
@ -52,3 +68,13 @@ export function getLabelFromValue(arr: IOption[]): string[] {
export function checkIfKeyPresent(str: string, valueToCheck: string): boolean { export function checkIfKeyPresent(str: string, valueToCheck: string): boolean {
return new RegExp(`\\(${valueToCheck}\\)`).test(str); return new RegExp(`\\(${valueToCheck}\\)`).test(str);
} }
export function splitOrderByFromString(str: string): OrderByPayload | null {
const splittedStr = str.split(' ');
const order = splittedStr.pop() || FILTERS.ASC;
const columnName = splittedStr.join(' ');
if (!columnName) return null;
return { columnName, order };
}

View File

@ -0,0 +1,2 @@
export const PLACEHOLDER =
'Search Filter : select options from suggested values, for IN/NOT IN operators - press "Enter" after selecting options';

View File

@ -18,6 +18,7 @@ import { DataSource } from 'types/common/queryBuilder';
import { v4 as uuid } from 'uuid'; import { v4 as uuid } from 'uuid';
import { selectStyle } from './config'; import { selectStyle } from './config';
import { PLACEHOLDER } from './constant';
import { StyledCheckOutlined, TypographyText } from './style'; import { StyledCheckOutlined, TypographyText } from './style';
import { import {
getOperatorValue, getOperatorValue,
@ -155,7 +156,7 @@ function QueryBuilderSearch({
filterOption={false} filterOption={false}
autoClearSearchValue={false} autoClearSearchValue={false}
mode="multiple" mode="multiple"
placeholder="Search Filter" placeholder={PLACEHOLDER}
value={queryTags} value={queryTags}
searchValue={searchValue} searchValue={searchValue}
disabled={isMetricsDataSource && !query.aggregateAttribute.key} disabled={isMetricsDataSource && !query.aggregateAttribute.key}

View File

@ -1,10 +1,5 @@
import type { ColumnsType } from 'antd/es/table';
import { ResizeTable } from 'components/ResizeTable'; import { ResizeTable } from 'components/ResizeTable';
import dayjs from 'dayjs'; import { createTableColumnsFromQuery } from 'lib/query/createTableColumnsFromQuery';
import {
createTableColumnsFromQuery,
RowData,
} from 'lib/query/createTableColumnsFromQuery';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { QueryTableProps } from './QueryTable.intefaces'; import { QueryTableProps } from './QueryTable.intefaces';
@ -26,23 +21,11 @@ export function QueryTable({
[query, queryTableData, renderActionCell], [query, queryTableData, renderActionCell],
); );
const modifiedColumns = useMemo(() => { const filteredColumns = columns.filter((item) => item.key !== 'timestamp');
const currentColumns: ColumnsType<RowData> = columns.map((column) =>
column.key === 'timestamp'
? {
...column,
render: (_, record): string =>
dayjs(new Date(record.timestamp)).format('MMM DD, YYYY, HH:mm:ss'),
}
: column,
);
return currentColumns;
}, [columns]);
const tableColumns = modifyColumns const tableColumns = modifyColumns
? modifyColumns(modifiedColumns) ? modifyColumns(filteredColumns)
: modifiedColumns; : filteredColumns;
return ( return (
<ResizeTable <ResizeTable

View File

@ -11,6 +11,7 @@ function TimeSeriesView({
data, data,
isLoading, isLoading,
isError, isError,
yAxisUnit,
}: TimeSeriesViewProps): JSX.Element { }: TimeSeriesViewProps): JSX.Element {
const chartData = useMemo( const chartData = useMemo(
() => () =>
@ -32,6 +33,7 @@ function TimeSeriesView({
<Graph <Graph
animate={false} animate={false}
data={chartData} data={chartData}
yAxisUnit={yAxisUnit}
name="tracesExplorerGraph" name="tracesExplorerGraph"
type="line" type="line"
/> />
@ -42,12 +44,14 @@ function TimeSeriesView({
interface TimeSeriesViewProps { interface TimeSeriesViewProps {
data?: SuccessResponse<MetricRangePayloadProps>; data?: SuccessResponse<MetricRangePayloadProps>;
yAxisUnit?: string;
isLoading: boolean; isLoading: boolean;
isError: boolean; isError: boolean;
} }
TimeSeriesView.defaultProps = { TimeSeriesView.defaultProps = {
data: undefined, data: undefined,
yAxisUnit: 'short',
}; };
export default TimeSeriesView; export default TimeSeriesView;

View File

@ -2,23 +2,43 @@ import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys'; import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange'; import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useMemo } from 'react';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers'; import { AppState } from 'store/reducers';
import { DataSource } from 'types/common/queryBuilder'; import { DataSource } from 'types/common/queryBuilder';
import { GlobalReducer } from 'types/reducer/globalTime'; import { GlobalReducer } from 'types/reducer/globalTime';
import TimeSeriesView from './TimeSeriesView'; import TimeSeriesView from './TimeSeriesView';
import { convertDataValueToMs } from './utils';
function TimeSeriesViewContainer({ function TimeSeriesViewContainer({
dataSource = DataSource.TRACES, dataSource = DataSource.TRACES,
}: TimeSeriesViewProps): JSX.Element { }: TimeSeriesViewProps): JSX.Element {
const { stagedQuery, panelType } = useQueryBuilder(); const { stagedQuery, currentQuery, panelType } = useQueryBuilder();
const { selectedTime: globalSelectedTime, maxTime, minTime } = useSelector< const { selectedTime: globalSelectedTime, maxTime, minTime } = useSelector<
AppState, AppState,
GlobalReducer GlobalReducer
>((state) => state.globalTime); >((state) => state.globalTime);
const isValidToConvertToMs = useMemo(() => {
const isValid: boolean[] = [];
currentQuery.builder.queryData.forEach(
({ aggregateAttribute, aggregateOperator }) => {
const isExistDurationNanoAttribute =
aggregateAttribute.key === 'durationNano';
const isCountOperator =
aggregateOperator === 'count' || aggregateOperator === 'count_distinct';
isValid.push(!isCountOperator && isExistDurationNanoAttribute);
},
);
return isValid.every(Boolean);
}, [currentQuery]);
const { data, isLoading, isError } = useGetQueryRange( const { data, isLoading, isError } = useGetQueryRange(
{ {
query: stagedQuery || initialQueriesMap[dataSource], query: stagedQuery || initialQueriesMap[dataSource],
@ -41,7 +61,19 @@ function TimeSeriesViewContainer({
}, },
); );
return <TimeSeriesView isError={isError} isLoading={isLoading} data={data} />; const responseData = useMemo(
() => (isValidToConvertToMs ? convertDataValueToMs(data) : data),
[data, isValidToConvertToMs],
);
return (
<TimeSeriesView
isError={isError}
isLoading={isLoading}
data={responseData}
yAxisUnit={isValidToConvertToMs ? 'ms' : 'short'}
/>
);
} }
interface TimeSeriesViewProps { interface TimeSeriesViewProps {

View File

@ -0,0 +1,26 @@
import { SuccessResponse } from 'types/api/index';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import { QueryData } from 'types/api/widgets/getQuery';
export const convertDataValueToMs = (
data?: SuccessResponse<MetricRangePayloadProps>,
): SuccessResponse<MetricRangePayloadProps> | undefined => {
const convertedData = data;
const convertedResult: QueryData[] = data?.payload?.data?.result
? data.payload.data.result.map((item) => {
const values: [number, string][] = item.values.map((value) => {
const [first = 0, second = ''] = value || [];
return [first, String(Number(second) / 1000000)];
});
return { ...item, values };
})
: [];
if (convertedData?.payload?.data?.result && convertedResult) {
convertedData.payload.data.result = convertedResult;
}
return convertedData;
};

View File

@ -0,0 +1,3 @@
export const RIBBON_STYLES = {
top: '-0.75rem',
};

View File

@ -0,0 +1,50 @@
import { CompassOutlined } from '@ant-design/icons';
import { Badge, Button } from 'antd';
import ROUTES from 'constants/routes';
import history from 'lib/history';
import { useMemo } from 'react';
import { useLocation } from 'react-router-dom';
import { RIBBON_STYLES } from './config';
function NewExplorerCTA(): JSX.Element | null {
const location = useLocation();
const isTraceOrLogsExplorerPage = useMemo(
() => location.pathname === ROUTES.LOGS || location.pathname === ROUTES.TRACE,
[location.pathname],
);
const onClickHandler = (): void => {
if (location.pathname === ROUTES.LOGS) {
history.push(ROUTES.LOGS_EXPLORER);
} else if (location.pathname === ROUTES.TRACE) {
history.push(ROUTES.TRACES_EXPLORER);
}
};
const buttonText = useMemo(
() =>
`Try new ${ROUTES.LOGS === location.pathname ? 'Logs' : 'Traces'} Explorer`,
[location.pathname],
);
if (!isTraceOrLogsExplorerPage) {
return null;
}
return (
<Badge.Ribbon style={RIBBON_STYLES} text="New">
<Button
icon={<CompassOutlined />}
onClick={onClickHandler}
danger
type="primary"
>
{buttonText}
</Button>
</Badge.Ribbon>
);
}
export default NewExplorerCTA;

View File

@ -1,4 +1,4 @@
import { Col } from 'antd'; import { Col, Row, Space } from 'antd';
import ROUTES from 'constants/routes'; import ROUTES from 'constants/routes';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { matchPath, useHistory } from 'react-router-dom'; import { matchPath, useHistory } from 'react-router-dom';
@ -6,6 +6,7 @@ import { matchPath, useHistory } from 'react-router-dom';
import ShowBreadcrumbs from './Breadcrumbs'; import ShowBreadcrumbs from './Breadcrumbs';
import DateTimeSelector from './DateTimeSelection'; import DateTimeSelector from './DateTimeSelection';
import { routesToSkip } from './DateTimeSelection/config'; import { routesToSkip } from './DateTimeSelection/config';
import NewExplorerCTA from './NewExplorerCTA';
import { Container } from './styles'; import { Container } from './styles';
function TopNav(): JSX.Element | null { function TopNav(): JSX.Element | null {
@ -36,7 +37,15 @@ function TopNav(): JSX.Element | null {
{!isRouteToSkip && ( {!isRouteToSkip && (
<Col span={8}> <Col span={8}>
<Row justify="end">
<Space align="start" size={60} direction="horizontal">
<NewExplorerCTA />
<div>
<DateTimeSelector /> <DateTimeSelector />
</div>
</Space>
</Row>
</Col> </Col>
)} )}
</Container> </Container>

View File

@ -6,7 +6,6 @@ import {
Dispatch, Dispatch,
SetStateAction, SetStateAction,
useCallback, useCallback,
useEffect,
useMemo, useMemo,
useState, useState,
} from 'react'; } from 'react';
@ -24,13 +23,9 @@ function Tags({
setText, setText,
}: TagsProps): JSX.Element { }: TagsProps): JSX.Element {
const { t } = useTranslation(['traceDetails']); const { t } = useTranslation(['traceDetails']);
const [allRenderedTags, setAllRenderedTags] = useState(tags); const [searchText, setSearchText] = useState('');
const isSearchVisible = useMemo(() => tags.length > 5, [tags]); const isSearchVisible = useMemo(() => tags.length > 5, [tags]);
useEffect(() => {
setAllRenderedTags(tags);
}, [tags]);
const getLink = useCallback( const getLink = useCallback(
(item: Record<string, string>) => (item: Record<string, string>) =>
`${ROUTES.TRACE}/${item.TraceId}${formUrlParams({ `${ROUTES.TRACE}/${item.TraceId}${formUrlParams({
@ -41,14 +36,12 @@ function Tags({
[], [],
); );
const onChangeHandler = useCallback( const onChangeHandler = (e: ChangeEvent<HTMLInputElement>): void => {
(e: ChangeEvent<HTMLInputElement>): void => {
const { value } = e.target; const { value } = e.target;
const filteredTags = tags.filter((tag) => tag.key.includes(value)); setSearchText(value);
setAllRenderedTags(filteredTags); };
},
[tags], const filteredTags = tags.filter((tag) => tag.key.includes(searchText));
);
if (tags.length === 0) { if (tags.length === 0) {
return <Typography>No tags in selected span</Typography>; return <Typography>No tags in selected span</Typography>;
@ -61,9 +54,10 @@ function Tags({
placeholder={t('traceDetails:search_tags')} placeholder={t('traceDetails:search_tags')}
allowClear allowClear
onChange={onChangeHandler} onChange={onChangeHandler}
value={searchText}
/> />
)} )}
{allRenderedTags.map((tag) => ( {filteredTags.map((tag) => (
<Tag <Tag
key={JSON.stringify(tag)} key={JSON.stringify(tag)}
{...{ {...{

View File

@ -1,11 +1,11 @@
import { DEFAULT_PER_PAGE_OPTIONS } from 'hooks/queryPagination'; import { DEFAULT_PER_PAGE_OPTIONS } from 'hooks/queryPagination';
export const defaultSelectedColumns: string[] = [ export const defaultSelectedColumns: string[] = [
'name',
'serviceName', 'serviceName',
'responseStatusCode', 'name',
'httpMethod',
'durationNano', 'durationNano',
'httpMethod',
'responseStatusCode',
]; ];
export const PER_PAGE_OPTIONS: number[] = [10, ...DEFAULT_PER_PAGE_OPTIONS]; export const PER_PAGE_OPTIONS: number[] = [10, ...DEFAULT_PER_PAGE_OPTIONS];

Some files were not shown because too many files have changed in this diff Show More