Merge pull request #3872 from SigNoz/release/v0.33.0

Release/v0.33.0
This commit is contained in:
Ankit Nayan 2023-11-01 23:30:32 +05:30 committed by GitHub
commit 7603e0ebe0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
114 changed files with 2650 additions and 1265 deletions

View File

@ -29,6 +29,7 @@ jobs:
export PATH="/usr/local/go/bin/:$PATH" # needed for Golang to work
docker system prune --force
docker pull signoz/signoz-otel-collector:main
docker pull signoz/signoz/signoz-schema-migrator:main
cd ~/signoz
git status
git add .

View File

@ -146,7 +146,7 @@ services:
condition: on-failure
query-service:
image: signoz/query-service:0.32.1
image: signoz/query-service:0.33.0
command:
[
"-config=/root/config/prometheus.yml",
@ -186,7 +186,7 @@ services:
<<: *db-depend
frontend:
image: signoz/frontend:0.32.1
image: signoz/frontend:0.33.0
deploy:
restart_policy:
condition: on-failure
@ -199,7 +199,7 @@ services:
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
otel-collector:
image: signoz/signoz-otel-collector:0.79.12
image: signoz/signoz-otel-collector:0.79.13
command:
[
"--config=/etc/otel-collector-config.yaml",
@ -237,7 +237,7 @@ services:
- query-service
otel-collector-migrator:
image: signoz/signoz-schema-migrator:0.79.12
image: signoz/signoz-schema-migrator:0.79.13
deploy:
restart_policy:
condition: on-failure
@ -250,7 +250,7 @@ services:
# - clickhouse-3
otel-collector-metrics:
image: signoz/signoz-otel-collector:0.79.12
image: signoz/signoz-otel-collector:0.79.13
command:
[
"--config=/etc/otel-collector-metrics-config.yaml",

View File

@ -63,38 +63,33 @@ receivers:
processors:
logstransform/internal:
operators:
- type: regex_parser
id: traceid
# https://regex101.com/r/MMfNjk/1
regex: '(?i)(trace(-|_||)id("|=| |-|:)*)(?P<trace_id>[A-Fa-f0-9]+)'
parse_from: body
parse_to: attributes.temp_trace
if: 'body matches "(?i)(trace(-|_||)id(\"|=| |-|:)*)(?P<trace_id>[A-Fa-f0-9]+)"'
output: spanid
- type: regex_parser
id: spanid
# https://regex101.com/r/uXSwLc/1
regex: '(?i)(span(-|_||)id("|=| |-|:)*)(?P<span_id>[A-Fa-f0-9]+)'
parse_from: body
parse_to: attributes.temp_trace
if: 'body matches "(?i)(span(-|_||)id(\"|=| |-|:)*)(?P<span_id>[A-Fa-f0-9]+)"'
output: trace_parser
- type: trace_parser
if: '"trace_id" in attributes or "span_id" in attributes'
id: trace_parser
trace_id:
parse_from: attributes.trace_id
parse_from: attributes.temp_trace.trace_id
span_id:
parse_from: attributes.span_id
output: remove_trace_id
- type: trace_parser
if: '"traceId" in attributes or "spanId" in attributes'
trace_id:
parse_from: attributes.traceId
span_id:
parse_from: attributes.spanId
output: remove_traceId
- id: remove_traceId
type: remove
if: '"traceId" in attributes'
field: attributes.traceId
output: remove_spanId
- id: remove_spanId
type: remove
if: '"spanId" in attributes'
field: attributes.spanId
- id: remove_trace_id
type: remove
if: '"trace_id" in attributes'
field: attributes.trace_id
output: remove_span_id
- id: remove_span_id
type: remove
if: '"span_id" in attributes'
field: attributes.span_id
parse_from: attributes.temp_trace.span_id
output: remove_temp
- type: remove
id: remove_temp
field: attributes.temp_trace
if: '"temp_trace" in attributes'
batch:
send_batch_size: 10000
send_batch_max_size: 11000

View File

@ -66,7 +66,7 @@ services:
- --storage.path=/data
otel-collector-migrator:
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.79.12}
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.79.13}
container_name: otel-migrator
command:
- "--dsn=tcp://clickhouse:9000"
@ -81,7 +81,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`
otel-collector:
container_name: signoz-otel-collector
image: signoz/signoz-otel-collector:0.79.12
image: signoz/signoz-otel-collector:0.79.13
command:
[
"--config=/etc/otel-collector-config.yaml",
@ -118,7 +118,7 @@ services:
otel-collector-metrics:
container_name: signoz-otel-collector-metrics
image: signoz/signoz-otel-collector:0.79.12
image: signoz/signoz-otel-collector:0.79.13
command:
[
"--config=/etc/otel-collector-metrics-config.yaml",

View File

@ -164,7 +164,7 @@ services:
# Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md`
query-service:
image: signoz/query-service:${DOCKER_TAG:-0.32.1}
image: signoz/query-service:${DOCKER_TAG:-0.33.0}
container_name: signoz-query-service
command:
[
@ -203,7 +203,7 @@ services:
<<: *db-depend
frontend:
image: signoz/frontend:${DOCKER_TAG:-0.32.1}
image: signoz/frontend:${DOCKER_TAG:-0.33.0}
container_name: signoz-frontend
restart: on-failure
depends_on:
@ -215,7 +215,7 @@ services:
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
otel-collector-migrator:
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.79.12}
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.79.13}
container_name: otel-migrator
command:
- "--dsn=tcp://clickhouse:9000"
@ -229,7 +229,7 @@ services:
otel-collector:
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.79.12}
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.79.13}
container_name: signoz-otel-collector
command:
[
@ -269,7 +269,7 @@ services:
condition: service_healthy
otel-collector-metrics:
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.79.12}
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.79.13}
container_name: signoz-otel-collector-metrics
command:
[

View File

@ -64,38 +64,33 @@ receivers:
processors:
logstransform/internal:
operators:
- type: regex_parser
id: traceid
# https://regex101.com/r/MMfNjk/1
regex: '(?i)(trace(-|_||)id("|=| |-|:)*)(?P<trace_id>[A-Fa-f0-9]+)'
parse_from: body
parse_to: attributes.temp_trace
if: 'body matches "(?i)(trace(-|_||)id(\"|=| |-|:)*)(?P<trace_id>[A-Fa-f0-9]+)"'
output: spanid
- type: regex_parser
id: spanid
# https://regex101.com/r/uXSwLc/1
regex: '(?i)(span(-|_||)id("|=| |-|:)*)(?P<span_id>[A-Fa-f0-9]+)'
parse_from: body
parse_to: attributes.temp_trace
if: 'body matches "(?i)(span(-|_||)id(\"|=| |-|:)*)(?P<span_id>[A-Fa-f0-9]+)"'
output: trace_parser
- type: trace_parser
if: '"trace_id" in attributes or "span_id" in attributes'
id: trace_parser
trace_id:
parse_from: attributes.trace_id
parse_from: attributes.temp_trace.trace_id
span_id:
parse_from: attributes.span_id
output: remove_trace_id
- type: trace_parser
if: '"traceId" in attributes or "spanId" in attributes'
trace_id:
parse_from: attributes.traceId
span_id:
parse_from: attributes.spanId
output: remove_traceId
- id: remove_traceId
type: remove
if: '"traceId" in attributes'
field: attributes.traceId
output: remove_spanId
- id: remove_spanId
type: remove
if: '"spanId" in attributes'
field: attributes.spanId
- id: remove_trace_id
type: remove
if: '"trace_id" in attributes'
field: attributes.trace_id
output: remove_span_id
- id: remove_span_id
type: remove
if: '"span_id" in attributes'
field: attributes.span_id
parse_from: attributes.temp_trace.span_id
output: remove_temp
- type: remove
id: remove_temp
field: attributes.temp_trace
if: '"temp_trace" in attributes'
batch:
send_batch_size: 10000
send_batch_max_size: 11000

View File

@ -1,5 +1,5 @@
# use a minimal alpine image
FROM alpine:3.17
FROM alpine:3.18.3
# Add Maintainer Info
LABEL maintainer="signoz"

View File

@ -37,9 +37,9 @@
"processor_field_placeholder": "Field",
"processor_value_placeholder": "Value",
"processor_description_placeholder": "example rule: %{word:first}",
"processor_trace_id_placeholder": "Trace Id Parce From",
"processor_span_id_placeholder": "Span id Parse From",
"processor_trace_flags_placeholder": "Trace flags parse from",
"processor_trace_id_placeholder": "Parse Trace ID from",
"processor_span_id_placeholder": "Parse Span ID from",
"processor_trace_flags_placeholder": "Parse Trace flags from",
"processor_from_placeholder": "From",
"processor_to_placeholder": "To"
}

View File

@ -32,6 +32,7 @@
"LOGS": "SigNoz | Logs",
"LOGS_EXPLORER": "SigNoz | Logs Explorer",
"LIVE_LOGS": "SigNoz | Live Logs",
"LOGS_PIPELINES": "SigNoz | Logs Pipelines",
"HOME_PAGE": "Open source Observability Platform | SigNoz",
"PASSWORD_RESET": "SigNoz | Password Reset",
"LIST_LICENSES": "SigNoz | List of Licenses",

View File

@ -39,10 +39,12 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
[pathname],
);
const { data: licensesData } = useLicense();
const {
data: licensesData,
isFetching: isFetchingLicensesData,
} = useLicense();
const {
user,
isUserFetching,
isUserFetchingError,
isLoggedIn: isLoggedInState,
@ -116,7 +118,7 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
if (
localStorageUserAuthToken &&
localStorageUserAuthToken.refreshJwt &&
user?.userId === ''
isUserFetching
) {
handleUserLoginIfTokenPresent(key);
} else {
@ -131,28 +133,34 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
if (path && path !== ROUTES.WORKSPACE_LOCKED) {
history.push(ROUTES.WORKSPACE_LOCKED);
}
dispatch({
type: UPDATE_USER_IS_FETCH,
payload: {
isUserFetching: false,
},
});
dispatch({
type: UPDATE_USER_IS_FETCH,
payload: {
isUserFetching: false,
},
});
}
};
useEffect(() => {
if (!isFetchingLicensesData) {
const shouldBlockWorkspace = licensesData?.payload?.workSpaceBlock;
if (shouldBlockWorkspace) {
navigateToWorkSpaceBlocked(currentRoute);
}
}
}, [isFetchingLicensesData]);
// eslint-disable-next-line sonarjs/cognitive-complexity
useEffect(() => {
(async (): Promise<void> => {
try {
const shouldBlockWorkspace = licensesData?.payload?.workSpaceBlock;
if (currentRoute) {
const { isPrivate, key } = currentRoute;
if (shouldBlockWorkspace) {
navigateToWorkSpaceBlocked(currentRoute);
} else if (isPrivate) {
if (isPrivate && key !== ROUTES.WORKSPACE_LOCKED) {
handlePrivateRoutes(key);
} else {
// no need to fetch the user and make user fetching false

View File

@ -13,6 +13,7 @@ import useLicense, { LICENSE_PLAN_KEY } from 'hooks/useLicense';
import { NotificationProvider } from 'hooks/useNotifications';
import { ResourceProvider } from 'hooks/useResourceAttribute';
import history from 'lib/history';
import { identity, pickBy } from 'lodash-es';
import { DashboardProvider } from 'providers/Dashboard/Dashboard';
import { QueryBuilderProvider } from 'providers/QueryBuilder';
import { Suspense, useEffect, useState } from 'react';
@ -90,13 +91,19 @@ function App(): JSX.Element {
const orgName =
org && Array.isArray(org) && org.length > 0 ? org[0].name : '';
const { name, email } = user;
const identifyPayload = {
email: user?.email,
name: user?.name,
email,
name,
company_name: orgName,
role,
source: 'signoz-ui',
};
const domain = extractDomain(user?.email);
const sanitizedIdentifyPayload = pickBy(identifyPayload, identity);
const domain = extractDomain(email);
const hostNameParts = hostname.split('.');
@ -106,13 +113,14 @@ function App(): JSX.Element {
data_region: hostNameParts[1],
tenant_url: hostname,
company_domain: domain,
source: 'signoz-ui',
};
window.analytics.identify(user?.email, identifyPayload);
window.analytics.identify(email, sanitizedIdentifyPayload);
window.analytics.group(domain, groupTraits);
window.clarity('identify', user.email, user.name);
window.clarity('identify', email, name);
};
useEffect(() => {

View File

@ -282,10 +282,10 @@ const routes: AppRoutes[] = [
isPrivate: false,
},
{
path: ROUTES.PIPELINES,
path: ROUTES.LOGS_PIPELINES,
exact: true,
component: PipelinePage,
key: 'PIPELINES',
key: 'LOGS_PIPELINES',
isPrivate: true,
},
{
@ -299,7 +299,7 @@ const routes: AppRoutes[] = [
path: ROUTES.WORKSPACE_LOCKED,
exact: true,
component: WorkspaceBlocked,
isPrivate: false,
isPrivate: true,
key: 'WORKSPACE_LOCKED',
},
];

View File

@ -0,0 +1,11 @@
.dropdown-button {
color: #fff;
}
.dropdown-button--dark {
color: #000;
}
.dropdown-icon {
font-size: 1.2rem;
}

View File

@ -0,0 +1,30 @@
import './DropDown.styles.scss';
import { EllipsisOutlined } from '@ant-design/icons';
import { Button, Dropdown, MenuProps } from 'antd';
import { useIsDarkMode } from 'hooks/useDarkMode';
function DropDown({ element }: { element: JSX.Element[] }): JSX.Element {
const isDarkMode = useIsDarkMode();
const items: MenuProps['items'] = element.map(
(e: JSX.Element, index: number) => ({
label: e,
key: index,
}),
);
return (
<Dropdown menu={{ items }}>
<Button
type="link"
className={!isDarkMode ? 'dropdown-button--dark' : 'dropdown-button'}
onClick={(e): void => e.preventDefault()}
>
<EllipsisOutlined className="dropdown-icon" />
</Button>
</Dropdown>
);
}
export default DropDown;

View File

@ -0,0 +1,25 @@
.DynamicColumnTable {
display: flex;
flex-direction: column;
width: 100%;
.dynamicColumnTable-button {
align-self: flex-end;
margin: 10px 0;
}
}
.dynamicColumnsTable-items {
display: flex;
width: 10.625rem;
justify-content: space-between;
align-items: center;
}
@media (max-width: 768px) {
.dynamicColumnsTable-items {
flex-direction: column;
width: auto;
text-align: center;
}
}

View File

@ -0,0 +1,112 @@
/* eslint-disable react/jsx-props-no-spreading */
import './DynamicColumnTable.syles.scss';
import { SettingOutlined } from '@ant-design/icons';
import { Button, Dropdown, MenuProps, Switch } from 'antd';
import { ColumnsType } from 'antd/lib/table';
import { memo, useEffect, useState } from 'react';
import { popupContainer } from 'utils/selectPopupContainer';
import ResizeTable from './ResizeTable';
import { DynamicColumnTableProps } from './types';
import {
getNewColumnData,
getVisibleColumns,
setVisibleColumns,
} from './utils';
function DynamicColumnTable({
tablesource,
columns,
dynamicColumns,
onDragColumn,
...restProps
}: DynamicColumnTableProps): JSX.Element {
const [columnsData, setColumnsData] = useState<ColumnsType | undefined>(
columns,
);
useEffect(() => {
const visibleColumns = getVisibleColumns({
tablesource,
columnsData: columns,
dynamicColumns,
});
setColumnsData((prevColumns) =>
prevColumns
? [
...prevColumns.slice(0, prevColumns.length - 1),
...visibleColumns,
prevColumns[prevColumns.length - 1],
]
: undefined,
);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const onToggleHandler = (index: number) => (
checked: boolean,
event: React.MouseEvent<HTMLButtonElement>,
): void => {
event.stopPropagation();
setVisibleColumns({
tablesource,
dynamicColumns,
index,
checked,
});
setColumnsData((prevColumns) =>
getNewColumnData({
checked,
index,
prevColumns,
dynamicColumns,
}),
);
};
const items: MenuProps['items'] =
dynamicColumns?.map((column, index) => ({
label: (
<div className="dynamicColumnsTable-items">
<div>{column.title?.toString()}</div>
<Switch
checked={columnsData?.findIndex((c) => c.key === column.key) !== -1}
onChange={onToggleHandler(index)}
/>
</div>
),
key: index,
type: 'checkbox',
})) || [];
return (
<div className="DynamicColumnTable">
{dynamicColumns && (
<Dropdown
getPopupContainer={popupContainer}
menu={{ items }}
trigger={['click']}
>
<Button
className="dynamicColumnTable-button"
size="middle"
icon={<SettingOutlined />}
/>
</Dropdown>
)}
<ResizeTable
columns={columnsData}
onDragColumn={onDragColumn}
{...restProps}
/>
</div>
);
}
DynamicColumnTable.defaultProps = {
onDragColumn: undefined,
};
export default memo(DynamicColumnTable);

View File

@ -0,0 +1,15 @@
import { Typography } from 'antd';
import Time from './Time';
function DateComponent(
CreatedOrUpdateTime: string | number | Date,
): JSX.Element {
if (CreatedOrUpdateTime === null) {
return <Typography> - </Typography>;
}
return <Time CreatedOrUpdateTime={CreatedOrUpdateTime} />;
}
export default DateComponent;

View File

@ -2,16 +2,15 @@ import { Typography } from 'antd';
import convertDateToAmAndPm from 'lib/convertDateToAmAndPm';
import getFormattedDate from 'lib/getFormatedDate';
import { Data } from '..';
function DateComponent(lastUpdatedTime: Data['lastUpdatedTime']): JSX.Element {
const time = new Date(lastUpdatedTime);
function Time({ CreatedOrUpdateTime }: DateProps): JSX.Element {
const time = new Date(CreatedOrUpdateTime);
const date = getFormattedDate(time);
const timeString = `${date} ${convertDateToAmAndPm(time)}`;
return <Typography>{timeString}</Typography>;
}
export default DateComponent;
type DateProps = {
CreatedOrUpdateTime: string | number | Date;
};
export default Time;

View File

@ -0,0 +1,11 @@
export const TableDataSource = {
Alert: 'alert',
Dashboard: 'dashboard',
} as const;
export const DynamicColumnsKey = {
CreatedAt: 'createdAt',
CreatedBy: 'createdBy',
UpdatedAt: 'updatedAt',
UpdatedBy: 'updatedBy',
};

View File

@ -1,6 +1,43 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { TableProps } from 'antd';
import { ColumnsType } from 'antd/es/table';
import { ColumnGroupType, ColumnType } from 'antd/lib/table';
import { TableDataSource } from './contants';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export interface ResizeTableProps extends TableProps<any> {
onDragColumn?: (fromIndex: number, toIndex: number) => void;
}
export interface DynamicColumnTableProps extends TableProps<any> {
tablesource: typeof TableDataSource[keyof typeof TableDataSource];
dynamicColumns: TableProps<any>['columns'];
onDragColumn?: (fromIndex: number, toIndex: number) => void;
}
export type GetVisibleColumnsFunction = (
props: GetVisibleColumnProps,
) => (ColumnGroupType<any> | ColumnType<any>)[];
export type GetVisibleColumnProps = {
tablesource: typeof TableDataSource[keyof typeof TableDataSource];
dynamicColumns?: ColumnsType<any>;
columnsData?: ColumnsType;
};
export type SetVisibleColumnsProps = {
checked: boolean;
index: number;
tablesource: typeof TableDataSource[keyof typeof TableDataSource];
dynamicColumns?: ColumnsType<any>;
};
type GetNewColumnDataProps = {
prevColumns?: ColumnsType;
checked: boolean;
dynamicColumns?: ColumnsType<any>;
index: number;
};
export type GetNewColumnDataFunction = (
props: GetNewColumnDataProps,
) => ColumnsType | undefined;

View File

@ -0,0 +1,77 @@
import { DynamicColumnsKey } from './contants';
import {
GetNewColumnDataFunction,
GetVisibleColumnsFunction,
SetVisibleColumnsProps,
} from './types';
export const getVisibleColumns: GetVisibleColumnsFunction = ({
tablesource,
dynamicColumns,
columnsData,
}) => {
let columnVisibilityData: { [key: string]: boolean };
try {
const storedData = localStorage.getItem(tablesource);
if (typeof storedData === 'string' && dynamicColumns) {
columnVisibilityData = JSON.parse(storedData);
return dynamicColumns.filter((column) => {
if (column.key && !columnsData?.find((c) => c.key === column.key)) {
return columnVisibilityData[column.key];
}
return false;
});
}
const initialColumnVisibility: Record<string, boolean> = {};
Object.values(DynamicColumnsKey).forEach((key) => {
initialColumnVisibility[key] = false;
});
localStorage.setItem(tablesource, JSON.stringify(initialColumnVisibility));
} catch (error) {
console.error(error);
}
return [];
};
export const setVisibleColumns = ({
checked,
index,
tablesource,
dynamicColumns,
}: SetVisibleColumnsProps): void => {
try {
const storedData = localStorage.getItem(tablesource);
if (typeof storedData === 'string' && dynamicColumns) {
const columnVisibilityData = JSON.parse(storedData);
const { key } = dynamicColumns[index];
if (key) {
columnVisibilityData[key] = checked;
}
localStorage.setItem(tablesource, JSON.stringify(columnVisibilityData));
}
} catch (error) {
console.error(error);
}
};
export const getNewColumnData: GetNewColumnDataFunction = ({
prevColumns,
checked,
dynamicColumns,
index,
}) => {
if (checked && dynamicColumns) {
return prevColumns
? [
...prevColumns.slice(0, prevColumns.length - 1),
dynamicColumns[index],
prevColumns[prevColumns.length - 1],
]
: undefined;
}
return prevColumns && dynamicColumns
? prevColumns.filter((column) => dynamicColumns[index].title !== column.title)
: undefined;
};

View File

@ -0,0 +1,10 @@
.label-column {
display: flex;
flex-wrap: wrap;
.label-column--tag {
white-space: normal;
margin: 0.2rem 0.2rem;
}
}

View File

@ -0,0 +1,54 @@
import './LabelColumn.styles.scss';
import { Popover, Tag } from 'antd';
import { popupContainer } from 'utils/selectPopupContainer';
import { LabelColumnProps } from './TableRenderer.types';
import TagWithToolTip from './TagWithToolTip';
function LabelColumn({ labels, value, color }: LabelColumnProps): JSX.Element {
const newLabels = labels.length > 3 ? labels.slice(0, 3) : labels;
const remainingLabels = labels.length > 3 ? labels.slice(3) : [];
return (
<div className="label-column">
{newLabels.map(
(label: string): JSX.Element => (
<TagWithToolTip key={label} label={label} color={color} value={value} />
),
)}
{remainingLabels.length > 0 && (
<Popover
getPopupContainer={popupContainer}
placement="bottomRight"
showArrow={false}
content={
<div>
{labels.map(
(label: string): JSX.Element => (
<TagWithToolTip
key={label}
label={label}
color={color}
value={value}
/>
),
)}
</div>
}
trigger="hover"
>
<Tag className="label-column--tag" color={color}>
+{remainingLabels.length}
</Tag>
</Popover>
)}
</div>
);
}
LabelColumn.defaultProps = {
value: {},
};
export default LabelColumn;

View File

@ -0,0 +1,5 @@
export type LabelColumnProps = {
labels: string[];
color?: string;
value?: { [key: string]: string };
};

View File

@ -0,0 +1,36 @@
import { Tag, Tooltip } from 'antd';
import { getLabelRenderingValue } from './utils';
function TagWithToolTip({
label,
value,
color,
}: TagWithToolTipProps): JSX.Element {
const tooltipTitle =
value && value[label] ? `${label}: ${value[label]}` : label;
return (
<div key={label}>
<Tooltip title={tooltipTitle}>
<Tag className="label-column--tag" color={color}>
{getLabelRenderingValue(label, value && value[label])}
</Tag>
</Tooltip>
</div>
);
}
type TagWithToolTipProps = {
label: string;
color?: string;
value?: {
[key: string]: string;
};
};
TagWithToolTip.defaultProps = {
value: undefined,
color: undefined,
};
export default TagWithToolTip;

View File

@ -16,6 +16,28 @@ export const generatorResizeTableColumns = <T>({
};
});
export const getLabelRenderingValue = (
label: string,
value?: string,
): string => {
const maxLength = 20;
if (label.length > maxLength) {
return `${label.slice(0, maxLength)}...`;
}
if (value) {
const remainingSpace = maxLength - label.length;
let newValue = value;
if (value.length > remainingSpace) {
newValue = `${value.slice(0, remainingSpace)}...`;
}
return `${label}: ${newValue}`;
}
return label;
};
interface GeneratorResizeTableColumnsProp<T> {
baseColumnOptions: ColumnsType<T>;
dynamicColumnOption: { key: string; columnOption: ColumnType<T> }[];

View File

@ -31,13 +31,12 @@ const ROUTES = {
LOGS: '/logs',
LOGS_EXPLORER: '/logs-explorer',
LIVE_LOGS: '/logs-explorer/live',
LOGS_PIPELINES: '/pipelines',
HOME_PAGE: '/',
PASSWORD_RESET: '/password-reset',
LIST_LICENSES: '/licenses',
LOGS_INDEX_FIELDS: '/logs-explorer/index-fields',
LOGS_PIPELINE: '/logs-explorer/pipeline',
TRACE_EXPLORER: '/trace-explorer',
PIPELINES: '/pipelines',
BILLING: '/billing',
SUPPORT: '/support',
WORKSPACE_LOCKED: '/workspace-locked',

View File

@ -0,0 +1,4 @@
.download-button {
display: flex;
align-items: center;
}

View File

@ -0,0 +1,77 @@
import './Download.styles.scss';
import { CloudDownloadOutlined } from '@ant-design/icons';
import { Button, Dropdown, MenuProps } from 'antd';
import { Excel } from 'antd-table-saveas-excel';
import { unparse } from 'papaparse';
import { DownloadProps } from './Download.types';
function Download({ data, isLoading, fileName }: DownloadProps): JSX.Element {
const downloadExcelFile = (): void => {
const headers = Object.keys(Object.assign({}, ...data)).map((item) => {
const updatedTitle = item
.split('_')
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');
return {
title: updatedTitle,
dataIndex: item,
};
});
const excel = new Excel();
excel
.addSheet(fileName)
.addColumns(headers)
.addDataSource(data, {
str2Percent: true,
})
.saveAs(`${fileName}.xlsx`);
};
const downloadCsvFile = (): void => {
const csv = unparse(data);
const csvBlob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
const csvUrl = URL.createObjectURL(csvBlob);
const downloadLink = document.createElement('a');
downloadLink.href = csvUrl;
downloadLink.download = `${fileName}.csv`;
downloadLink.click();
downloadLink.remove();
};
const menu: MenuProps = {
items: [
{
key: 'download-as-excel',
label: 'Excel',
onClick: downloadExcelFile,
},
{
key: 'download-as-csv',
label: 'CSV',
onClick: downloadCsvFile,
},
],
};
return (
<Dropdown menu={menu} trigger={['click']}>
<Button
className="download-button"
loading={isLoading}
size="small"
type="link"
>
<CloudDownloadOutlined />
Download
</Button>
</Dropdown>
);
}
Download.defaultProps = {
isLoading: undefined,
};
export default Download;

View File

@ -0,0 +1,10 @@
export type DownloadOptions = {
isDownloadEnabled: boolean;
fileName: string;
};
export type DownloadProps = {
data: Record<string, string>[];
isLoading?: boolean;
fileName: string;
};

View File

@ -2,6 +2,7 @@ import { Form, Select } from 'antd';
import { useTranslation } from 'react-i18next';
import { AlertDef, Labels } from 'types/api/alerts/def';
import { requireErrorMessage } from 'utils/form/requireErrorMessage';
import { popupContainer } from 'utils/selectPopupContainer';
import ChannelSelect from './ChannelSelect';
import LabelSelect from './labels';
@ -36,6 +37,7 @@ function BasicInfo({ alertDef, setAlertDef }: BasicInfoProps): JSX.Element {
name={['labels', 'severity']}
>
<SeveritySelect
getPopupContainer={popupContainer}
defaultValue="critical"
onChange={(value: unknown | string): void => {
const s = (value as string) || 'critical';

View File

@ -9,9 +9,12 @@ import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
import getChartData from 'lib/getChartData';
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { AlertDef } from 'types/api/alerts/def';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { EQueryType } from 'types/common/dashboard';
import { GlobalReducer } from 'types/reducer/globalTime';
import { ChartContainer, FailedMessageContainer } from './styles';
import { covertIntoDataFormats } from './utils';
@ -41,6 +44,9 @@ function ChartPreview({
}: ChartPreviewProps): JSX.Element | null {
const { t } = useTranslation('alerts');
const threshold = alertDef?.condition.target || 0;
const { minTime, maxTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const thresholdValue = covertIntoDataFormats({
value: threshold,
@ -100,6 +106,8 @@ function ChartPreview({
'chartPreview',
userQueryKey || JSON.stringify(query),
selectedInterval,
minTime,
maxTime,
],
retry: false,
enabled: canQuery,

View File

@ -20,6 +20,7 @@ import {
defaultMatchType,
} from 'types/api/alerts/def';
import { EQueryType } from 'types/common/dashboard';
import { popupContainer } from 'utils/selectPopupContainer';
import { FormContainer, InlineSelect, StepHeading } from './styles';
@ -45,6 +46,7 @@ function RuleOptions({
const renderCompareOps = (): JSX.Element => (
<InlineSelect
getPopupContainer={popupContainer}
defaultValue={defaultCompareOp}
value={alertDef.condition?.op}
style={{ minWidth: '120px' }}
@ -69,6 +71,7 @@ function RuleOptions({
const renderThresholdMatchOpts = (): JSX.Element => (
<InlineSelect
getPopupContainer={popupContainer}
defaultValue={defaultMatchType}
style={{ minWidth: '130px' }}
value={alertDef.condition?.matchType}
@ -83,6 +86,7 @@ function RuleOptions({
const renderPromMatchOpts = (): JSX.Element => (
<InlineSelect
getPopupContainer={popupContainer}
defaultValue={defaultMatchType}
style={{ minWidth: '130px' }}
value={alertDef.condition?.matchType}
@ -94,6 +98,7 @@ function RuleOptions({
const renderEvalWindows = (): JSX.Element => (
<InlineSelect
getPopupContainer={popupContainer}
defaultValue={defaultEvalWindow}
style={{ minWidth: '120px' }}
value={alertDef.evalWindow}
@ -180,6 +185,7 @@ function RuleOptions({
<Form.Item>
<Select
getPopupContainer={popupContainer}
allowClear
showSearch
options={categorySelectOptions}

View File

@ -44,7 +44,6 @@ import {
StyledLeftContainer,
} from './styles';
import UserGuide from './UserGuide';
import { getUpdatedStepInterval, toChartInterval } from './utils';
function FormAlertRules({
alertType,
@ -55,9 +54,10 @@ function FormAlertRules({
// init namespace for translations
const { t } = useTranslation('alerts');
const { minTime, maxTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const { minTime, maxTime, selectedTime: globalSelectedInterval } = useSelector<
AppState,
GlobalReducer
>((state) => state.globalTime);
const {
currentQuery,
@ -354,16 +354,6 @@ function FormAlertRules({
<BasicInfo alertDef={alertDef} setAlertDef={setAlertDef} />
);
const updatedStagedQuery = useMemo((): Query | null => {
const newQuery: Query | null = stagedQuery;
if (newQuery) {
newQuery.builder.queryData[0].stepInterval = getUpdatedStepInterval(
alertDef.evalWindow,
);
}
return newQuery;
}, [alertDef.evalWindow, stagedQuery]);
const renderQBChartPreview = (): JSX.Element => (
<ChartPreview
headline={
@ -373,10 +363,9 @@ function FormAlertRules({
/>
}
name=""
query={updatedStagedQuery}
selectedInterval={toChartInterval(alertDef.evalWindow)}
query={stagedQuery}
selectedInterval={globalSelectedInterval}
alertDef={alertDef}
allowSelectedIntervalForStepGen
/>
);
@ -405,7 +394,7 @@ function FormAlertRules({
name="Chart Preview"
query={stagedQuery}
alertDef={alertDef}
selectedInterval={toChartInterval(alertDef.evalWindow)}
selectedInterval={globalSelectedInterval}
/>
);

View File

@ -1,4 +1,4 @@
import { Typography } from 'antd';
import { Skeleton, Typography } from 'antd';
import { ToggleGraphProps } from 'components/Graph/types';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import GridPanelSwitch from 'container/GridPanelSwitch';
@ -249,20 +249,23 @@ function WidgetGraphComponent({
isWarning={isWarning}
/>
</div>
<GridPanelSwitch
panelType={widget.panelTypes}
data={data}
isStacked={widget.isStacked}
opacity={widget.opacity}
title={' '}
name={name}
yAxisUnit={widget.yAxisUnit}
onClickHandler={onClickHandler}
onDragSelect={onDragSelect}
panelData={queryResponse.data?.payload?.data.newResult.data.result || []}
query={widget.query}
ref={lineChartRef}
/>
{queryResponse.isLoading && <Skeleton />}
{queryResponse.isSuccess && (
<GridPanelSwitch
panelType={widget.panelTypes}
data={data}
isStacked={widget.isStacked}
opacity={widget.opacity}
title={' '}
name={name}
yAxisUnit={widget.yAxisUnit}
onClickHandler={onClickHandler}
onDragSelect={onDragSelect}
panelData={queryResponse.data?.payload?.data.newResult.data.result || []}
query={widget.query}
ref={lineChartRef}
/>
)}
</span>
);
}

View File

@ -1,4 +1,3 @@
import { Skeleton } from 'antd';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
import { useStepInterval } from 'hooks/queryBuilder/useStepInterval';
@ -99,10 +98,6 @@ function GridCardGraph({
const isEmptyLayout = widget?.id === PANEL_TYPES.EMPTY_WIDGET;
if (queryResponse.isLoading) {
return <Skeleton />;
}
return (
<span ref={graphRef}>
<WidgetGraphComponent

View File

@ -3,7 +3,14 @@ import { PlusOutlined } from '@ant-design/icons';
import { Typography } from 'antd';
import { ColumnsType } from 'antd/lib/table';
import saveAlertApi from 'api/alerts/save';
import { ResizeTable } from 'components/ResizeTable';
import DropDown from 'components/DropDown/DropDown';
import {
DynamicColumnsKey,
TableDataSource,
} from 'components/ResizeTable/contants';
import DynamicColumnTable from 'components/ResizeTable/DynamicColumnTable';
import DateComponent from 'components/ResizeTable/TableComponent/DateComponent';
import LabelColumn from 'components/TableRenderer/LabelColumn';
import TextToolTip from 'components/TextToolTip';
import { QueryParams } from 'constants/query';
import ROUTES from 'constants/routes';
@ -22,7 +29,7 @@ import { GettableAlert } from 'types/api/alerts/get';
import AppReducer from 'types/reducer/app';
import DeleteAlert from './DeleteAlert';
import { Button, ButtonContainer, ColumnButton, StyledTag } from './styles';
import { Button, ButtonContainer, ColumnButton } from './styles';
import Status from './TableComponents/Status';
import ToggleAlertState from './ToggleAlertState';
@ -121,6 +128,51 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
}
};
const dynamicColumns: ColumnsType<GettableAlert> = [
{
title: 'Created At',
dataIndex: 'createAt',
width: 80,
key: DynamicColumnsKey.CreatedAt,
align: 'center',
sorter: (a: GettableAlert, b: GettableAlert): number => {
const prev = new Date(a.createAt).getTime();
const next = new Date(b.createAt).getTime();
return prev - next;
},
render: DateComponent,
},
{
title: 'Created By',
dataIndex: 'createBy',
width: 80,
key: DynamicColumnsKey.CreatedBy,
align: 'center',
},
{
title: 'Updated At',
dataIndex: 'updateAt',
width: 80,
key: DynamicColumnsKey.UpdatedAt,
align: 'center',
sorter: (a: GettableAlert, b: GettableAlert): number => {
const prev = new Date(a.updateAt).getTime();
const next = new Date(b.updateAt).getTime();
return prev - next;
},
render: DateComponent,
},
{
title: 'Updated By',
dataIndex: 'updateBy',
width: 80,
key: DynamicColumnsKey.UpdatedBy,
align: 'center',
},
];
const columns: ColumnsType<GettableAlert> = [
{
title: 'Status',
@ -178,13 +230,7 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
}
return (
<>
{withOutSeverityKeys.map((e) => (
<StyledTag key={e} color="magenta">
{e}: {value[e]}
</StyledTag>
))}
</>
<LabelColumn labels={withOutSeverityKeys} value={value} color="magenta" />
);
},
},
@ -195,20 +241,30 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
title: 'Action',
dataIndex: 'id',
key: 'action',
width: 120,
width: 10,
render: (id: GettableAlert['id'], record): JSX.Element => (
<>
<ToggleAlertState disabled={record.disabled} setData={setData} id={id} />
<ColumnButton onClick={onEditHandler(record)} type="link">
Edit
</ColumnButton>
<ColumnButton onClick={onCloneHandler(record)} type="link">
Clone
</ColumnButton>
<DeleteAlert notifications={notificationsApi} setData={setData} id={id} />
</>
<DropDown
element={[
<ToggleAlertState
key="1"
disabled={record.disabled}
setData={setData}
id={id}
/>,
<ColumnButton key="2" onClick={onEditHandler(record)} type="link">
Edit
</ColumnButton>,
<ColumnButton key="3" onClick={onCloneHandler(record)} type="link">
Clone
</ColumnButton>,
<DeleteAlert
key="4"
notifications={notificationsApi}
setData={setData}
id={id}
/>,
]}
/>
),
});
}
@ -229,7 +285,13 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
</Button>
)}
</ButtonContainer>
<ResizeTable columns={columns} rowKey="id" dataSource={data} />
<DynamicColumnTable
tablesource={TableDataSource.Alert}
columns={columns}
rowKey="id"
dataSource={data}
dynamicColumns={dynamicColumns}
/>
</>
);
}

View File

@ -1,4 +1,4 @@
import { Button as ButtonComponent, Tag } from 'antd';
import { Button as ButtonComponent } from 'antd';
import styled from 'styled-components';
export const ButtonContainer = styled.div`
@ -23,9 +23,3 @@ export const ColumnButton = styled(ButtonComponent)`
margin-right: 1.5em;
}
`;
export const StyledTag = styled(Tag)`
&&& {
white-space: normal;
}
`;

View File

@ -10,6 +10,8 @@ describe('executeSearchQueries', () => {
uuid: uuid(),
created_at: '',
updated_at: '',
created_by: '',
updated_by: '',
data: {
title: 'first dashboard',
variables: {},
@ -20,6 +22,8 @@ describe('executeSearchQueries', () => {
uuid: uuid(),
created_at: '',
updated_at: '',
created_by: '',
updated_by: '',
data: {
title: 'second dashboard',
variables: {},
@ -30,6 +34,8 @@ describe('executeSearchQueries', () => {
uuid: uuid(),
created_at: '',
updated_at: '',
created_by: '',
updated_by: '',
data: {
title: 'third dashboard (with special characters +?\\)',
variables: {},

View File

@ -45,18 +45,30 @@ function DeleteButton({ id }: Data): JSX.Element {
// This is to avoid the type collision
function Wrapper(props: Data): JSX.Element {
const { createdBy, description, id, key, lastUpdatedTime, name, tags } = props;
const {
createdAt,
description,
id,
key,
lastUpdatedTime,
name,
tags,
createdBy,
lastUpdatedBy,
} = props;
return (
<DeleteButton
{...{
createdBy,
createdAt,
description,
id,
key,
lastUpdatedTime,
name,
tags,
createdBy,
lastUpdatedBy,
}}
/>
);

View File

@ -10,7 +10,12 @@ import {
import { ItemType } from 'antd/es/menu/hooks/useItems';
import createDashboard from 'api/dashboard/create';
import { AxiosError } from 'axios';
import { ResizeTable } from 'components/ResizeTable';
import {
DynamicColumnsKey,
TableDataSource,
} from 'components/ResizeTable/contants';
import DynamicColumnTable from 'components/ResizeTable/DynamicColumnTable';
import LabelColumn from 'components/TableRenderer/LabelColumn';
import TextToolTip from 'components/TextToolTip';
import ROUTES from 'constants/routes';
import SearchFilter from 'container/ListOfDashboard/SearchFilter';
@ -26,13 +31,11 @@ import { Dashboard } from 'types/api/dashboard/getAll';
import AppReducer from 'types/reducer/app';
import { popupContainer } from 'utils/selectPopupContainer';
import DateComponent from '../../components/ResizeTable/TableComponent/DateComponent';
import ImportJSON from './ImportJSON';
import { ButtonContainer, NewDashboardButton, TableContainer } from './styles';
import Createdby from './TableComponents/CreatedBy';
import DateComponent from './TableComponents/Date';
import DeleteButton from './TableComponents/DeleteButton';
import Name from './TableComponents/Name';
import Tags from './TableComponents/Tags';
function ListOfAllDashboard(): JSX.Element {
const {
@ -71,48 +74,66 @@ function ListOfAllDashboard(): JSX.Element {
errorMessage: '',
});
const dynamicColumns: TableColumnProps<Data>[] = [
{
title: 'Created At',
dataIndex: 'createdAt',
width: 30,
key: DynamicColumnsKey.CreatedAt,
sorter: (a: Data, b: Data): number => {
console.log({ a });
const prev = new Date(a.createdAt).getTime();
const next = new Date(b.createdAt).getTime();
return prev - next;
},
render: DateComponent,
},
{
title: 'Created By',
dataIndex: 'createdBy',
width: 30,
key: DynamicColumnsKey.CreatedBy,
},
{
title: 'Last Updated Time',
width: 30,
dataIndex: 'lastUpdatedTime',
key: DynamicColumnsKey.UpdatedAt,
sorter: (a: Data, b: Data): number => {
const prev = new Date(a.lastUpdatedTime).getTime();
const next = new Date(b.lastUpdatedTime).getTime();
return prev - next;
},
render: DateComponent,
},
{
title: 'Last Updated By',
dataIndex: 'lastUpdatedBy',
width: 30,
key: DynamicColumnsKey.UpdatedBy,
},
];
const columns = useMemo(() => {
const tableColumns: TableColumnProps<Data>[] = [
{
title: 'Name',
dataIndex: 'name',
width: 100,
width: 40,
render: Name,
},
{
title: 'Description',
width: 100,
width: 50,
dataIndex: 'description',
},
{
title: 'Tags (can be multiple)',
dataIndex: 'tags',
width: 80,
render: Tags,
},
{
title: 'Created At',
dataIndex: 'createdBy',
width: 80,
sorter: (a: Data, b: Data): number => {
const prev = new Date(a.createdBy).getTime();
const next = new Date(b.createdBy).getTime();
return prev - next;
},
render: Createdby,
},
{
title: 'Last Updated Time',
width: 90,
dataIndex: 'lastUpdatedTime',
sorter: (a: Data, b: Data): number => {
const prev = new Date(a.lastUpdatedTime).getTime();
const next = new Date(b.lastUpdatedTime).getTime();
return prev - next;
},
render: DateComponent,
width: 50,
render: (value): JSX.Element => <LabelColumn labels={value} />,
},
];
@ -130,13 +151,15 @@ function ListOfAllDashboard(): JSX.Element {
const data: Data[] =
filteredDashboards?.map((e) => ({
createdBy: e.created_at,
createdAt: e.created_at,
description: e.data.description || '',
id: e.uuid,
lastUpdatedTime: e.updated_at,
name: e.data.title,
tags: e.data.tags || [],
key: e.uuid,
createdBy: e.created_by,
lastUpdatedBy: e.updated_by,
refetchDashboardList,
})) || [];
@ -290,7 +313,9 @@ function ListOfAllDashboard(): JSX.Element {
uploadedGrafana={uploadedGrafana}
onModalHandler={(): void => onModalHandler(false)}
/>
<ResizeTable
<DynamicColumnTable
tablesource={TableDataSource.Dashboard}
dynamicColumns={dynamicColumns}
columns={columns}
pagination={{
pageSize: 9,
@ -314,7 +339,9 @@ export interface Data {
description: string;
tags: string[];
createdBy: string;
createdAt: string;
lastUpdatedTime: string;
lastUpdatedBy: string;
id: string;
}

View File

@ -1,15 +1,14 @@
import { CloudDownloadOutlined, FastBackwardOutlined } from '@ant-design/icons';
import { Button, Divider, Dropdown, MenuProps } from 'antd';
import { Excel } from 'antd-table-saveas-excel';
import { FastBackwardOutlined } from '@ant-design/icons';
import { Button, Divider } from 'antd';
import Controls from 'container/Controls';
import Download from 'container/Download/Download';
import { getGlobalTime } from 'container/LogsSearchFilter/utils';
import { getMinMax } from 'container/TopNav/AutoRefresh/config';
import dayjs from 'dayjs';
import { Pagination } from 'hooks/queryPagination';
import { FlatLogData } from 'lib/logs/flatLogData';
import { OrderPreferenceItems } from 'pages/Logs/config';
import { unparse } from 'papaparse';
import { memo, useCallback, useMemo } from 'react';
import { memo, useMemo } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { Dispatch } from 'redux';
import { AppState } from 'store/reducers';
@ -23,7 +22,7 @@ import {
import { GlobalReducer } from 'types/reducer/globalTime';
import { ILogsReducer } from 'types/reducer/logs';
import { Container, DownloadLogButton } from './styles';
import { Container } from './styles';
function LogControls(): JSX.Element | null {
const {
@ -97,58 +96,6 @@ function LogControls(): JSX.Element | null {
[logs],
);
const downloadExcelFile = useCallback((): void => {
const headers = Object.keys(Object.assign({}, ...flattenLogData)).map(
(item) => {
const updatedTitle = item
.split('_')
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');
return {
title: updatedTitle,
dataIndex: item,
};
},
);
const excel = new Excel();
excel
.addSheet('log_data')
.addColumns(headers)
.addDataSource(flattenLogData, {
str2Percent: true,
})
.saveAs('log_data.xlsx');
}, [flattenLogData]);
const downloadCsvFile = useCallback((): void => {
const csv = unparse(flattenLogData);
const csvBlob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
const csvUrl = URL.createObjectURL(csvBlob);
const downloadLink = document.createElement('a');
downloadLink.href = csvUrl;
downloadLink.download = 'log_data.csv';
downloadLink.click();
downloadLink.remove();
}, [flattenLogData]);
const menu: MenuProps = useMemo(
() => ({
items: [
{
key: 'download-as-excel',
label: 'Excel',
onClick: downloadExcelFile,
},
{
key: 'download-as-csv',
label: 'CSV',
onClick: downloadCsvFile,
},
],
}),
[downloadCsvFile, downloadExcelFile],
);
const isLoading = isLogsLoading || isLoadingAggregate;
if (liveTail !== 'STOPPED') {
@ -157,12 +104,7 @@ function LogControls(): JSX.Element | null {
return (
<Container>
<Dropdown menu={menu} trigger={['click']}>
<DownloadLogButton loading={isLoading} size="small" type="link">
<CloudDownloadOutlined />
Download
</DownloadLogButton>
</Dropdown>
<Download data={flattenLogData} isLoading={isLoading} fileName="log_data" />
<Button
loading={isLoading}
size="small"

View File

@ -92,8 +92,8 @@ function Application(): JSX.Element {
const {
data: topLevelOperations,
isLoading: topLevelOperationsLoading,
error: topLevelOperationsError,
isLoading: topLevelOperationsIsLoading,
isError: topLevelOperationsIsError,
} = useQuery<ServiceDataProps>({
queryKey: [servicename, minTime, maxTime, selectedTags],
@ -199,7 +199,7 @@ function Application(): JSX.Element {
selectedTimeStamp={selectedTimeStamp}
selectedTraceTags={selectedTraceTags}
topLevelOperationsRoute={topLevelOperationsRoute}
topLevelOperationsLoading={topLevelOperationsLoading}
topLevelOperationsIsLoading={topLevelOperationsIsLoading}
/>
</Col>
@ -220,11 +220,11 @@ function Application(): JSX.Element {
handleGraphClick={handleGraphClick}
onDragSelect={onDragSelect}
topLevelOperationsError={topLevelOperationsError}
topLevelOperationsLoading={topLevelOperationsLoading}
topLevelOperationsIsError={topLevelOperationsIsError}
name="operations_per_sec"
widget={operationPerSecWidget}
opName="Rate"
topLevelOperationsIsLoading={topLevelOperationsIsLoading}
/>
</Col>
</Row>
@ -264,11 +264,11 @@ function Application(): JSX.Element {
handleGraphClick={handleGraphClick}
onDragSelect={onDragSelect}
topLevelOperationsError={topLevelOperationsError}
topLevelOperationsLoading={topLevelOperationsLoading}
topLevelOperationsIsError={topLevelOperationsIsError}
name="error_percentage_%"
widget={errorPercentageWidget}
opName="Error"
topLevelOperationsIsLoading={topLevelOperationsIsLoading}
/>
</ColErrorContainer>
</Col>

View File

@ -1,4 +1,3 @@
import Spinner from 'components/Spinner';
import { FeatureKeys } from 'constants/features';
import { PANEL_TYPES } from 'constants/queryBuilder';
import Graph from 'container/GridCardLayout/GridCard';
@ -25,7 +24,7 @@ function ServiceOverview({
selectedTraceTags,
selectedTimeStamp,
topLevelOperationsRoute,
topLevelOperationsLoading,
topLevelOperationsIsLoading,
}: ServiceOverviewProps): JSX.Element {
const { servicename } = useParams<IServiceName>();
@ -64,15 +63,8 @@ function ServiceOverview({
[servicename, isSpanMetricEnable, topLevelOperationsRoute, tagFilterItems],
);
const isQueryEnabled = topLevelOperationsRoute.length > 0;
if (topLevelOperationsLoading) {
return (
<Card>
<Spinner height="40vh" tip="Loading..." />
</Card>
);
}
const isQueryEnabled =
!topLevelOperationsIsLoading && topLevelOperationsRoute.length > 0;
return (
<>
@ -109,7 +101,7 @@ interface ServiceOverviewProps {
onDragSelect: (start: number, end: number) => void;
handleGraphClick: (type: string) => ClickHandlerType;
topLevelOperationsRoute: string[];
topLevelOperationsLoading: boolean;
topLevelOperationsIsLoading: boolean;
}
export default ServiceOverview;

View File

@ -1,6 +1,5 @@
import { Typography } from 'antd';
import axios from 'axios';
import Spinner from 'components/Spinner';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import Graph from 'container/GridCardLayout/GridCard';
import { Card, GraphContainer } from 'container/MetricsApplication/styles';
@ -13,10 +12,10 @@ function TopLevelOperation({
opName,
topLevelOperationsIsError,
topLevelOperationsError,
topLevelOperationsLoading,
onDragSelect,
handleGraphClick,
widget,
topLevelOperationsIsLoading,
}: TopLevelOperationProps): JSX.Element {
return (
<Card>
@ -28,17 +27,13 @@ function TopLevelOperation({
</Typography>
) : (
<GraphContainer>
{topLevelOperationsLoading && (
<Spinner size="large" tip="Loading..." height="40vh" />
)}
{!topLevelOperationsLoading && (
<Graph
name={name}
widget={widget}
onClickHandler={handleGraphClick(opName)}
onDragSelect={onDragSelect}
/>
)}
<Graph
name={name}
widget={widget}
onClickHandler={handleGraphClick(opName)}
onDragSelect={onDragSelect}
isQueryEnabled={!topLevelOperationsIsLoading}
/>
</GraphContainer>
)}
</Card>
@ -50,10 +45,10 @@ interface TopLevelOperationProps {
opName: string;
topLevelOperationsIsError: boolean;
topLevelOperationsError: unknown;
topLevelOperationsLoading: boolean;
onDragSelect: (start: number, end: number) => void;
handleGraphClick: (type: string) => ClickHandlerType;
widget: Widgets;
topLevelOperationsIsLoading: boolean;
}
export default TopLevelOperation;

View File

@ -1,5 +1,4 @@
import getTopOperations from 'api/metrics/getTopOperations';
import Spinner from 'components/Spinner';
import TopOperationsTable from 'container/MetricsApplication/TopOperationsTable';
import useResourceAttribute from 'hooks/useResourceAttribute';
import { convertRawQueriesToTraceSelectedTags } from 'hooks/useResourceAttribute/utils';
@ -36,12 +35,7 @@ function TopOperation(): JSX.Element {
const topOperationData = data || [];
return (
<>
{isLoading && <Spinner size="large" tip="Loading..." height="40vh" />}
{!isLoading && <TopOperationsTable data={topOperationData} />}
</>
);
return <TopOperationsTable data={topOperationData} isLoading={isLoading} />;
}
export default TopOperation;

View File

@ -1,4 +1,5 @@
import { PANEL_TYPES } from 'constants/queryBuilder';
import { topOperationMetricsDownloadOptions } from 'container/MetricsApplication/constant';
import { getWidgetQueryBuilder } from 'container/MetricsApplication/MetricsApplication.factory';
import { topOperationQueries } from 'container/MetricsApplication/MetricsPageQueries/TopOperationQueries';
import { QueryTable } from 'container/QueryTable';
@ -109,6 +110,7 @@ function TopOperationMetrics(): JSX.Element {
queryTableData={queryTableData}
loading={isLoading}
renderColumnCell={renderColumnCell}
downloadOption={topOperationMetricsDownloadOptions}
/>
);
}

View File

@ -0,0 +1,9 @@
.top-operation {
position: relative;
.top-operation--download {
position: absolute;
top: 15px;
right: 0px;
z-index: 1;
}
}

View File

@ -1,16 +1,32 @@
import { Tooltip, Typography } from 'antd';
import { ColumnsType } from 'antd/lib/table';
import './TopOperationsTable.styles.scss';
import { SearchOutlined } from '@ant-design/icons';
import { InputRef, Tooltip, Typography } from 'antd';
import { ColumnsType, ColumnType } from 'antd/lib/table';
import { ResizeTable } from 'components/ResizeTable';
import Download from 'container/Download/Download';
import { filterDropdown } from 'container/ServiceApplication/Filter/FilterDropdown';
import useResourceAttribute from 'hooks/useResourceAttribute';
import { convertRawQueriesToTraceSelectedTags } from 'hooks/useResourceAttribute/utils';
import { useRef } from 'react';
import { useSelector } from 'react-redux';
import { useParams } from 'react-router-dom';
import { AppState } from 'store/reducers';
import { GlobalReducer } from 'types/reducer/globalTime';
import { getErrorRate, navigateToTrace } from './utils';
import { IServiceName } from './Tabs/types';
import {
convertedTracesToDownloadData,
getErrorRate,
navigateToTrace,
} from './utils';
function TopOperationsTable({ data }: TopOperationsTableProps): JSX.Element {
function TopOperationsTable({
data,
isLoading,
}: TopOperationsTableProps): JSX.Element {
const searchInput = useRef<InputRef>(null);
const { servicename } = useParams<IServiceName>();
const { minTime, maxTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
@ -34,19 +50,35 @@ function TopOperationsTable({ data }: TopOperationsTableProps): JSX.Element {
});
};
const getSearchOption = (): ColumnType<TopOperationList> => ({
filterDropdown,
filterIcon: <SearchOutlined />,
onFilter: (value, record): boolean =>
record.name
.toString()
.toLowerCase()
.includes((value as string).toLowerCase()),
onFilterDropdownOpenChange: (visible): void => {
if (visible) {
setTimeout(() => searchInput.current?.select(), 100);
}
},
render: (text: string): JSX.Element => (
<Tooltip placement="topLeft" title={text}>
<Typography.Link onClick={(): void => handleOnClick(text)}>
{text}
</Typography.Link>
</Tooltip>
),
});
const columns: ColumnsType<TopOperationList> = [
{
title: 'Name',
dataIndex: 'name',
key: 'name',
width: 100,
render: (text: string): JSX.Element => (
<Tooltip placement="topLeft" title={text}>
<Typography.Link onClick={(): void => handleOnClick(text)}>
{text}
</Typography.Link>
</Tooltip>
),
...getSearchOption(),
},
{
title: 'P50 (in ms)',
@ -92,15 +124,27 @@ function TopOperationsTable({ data }: TopOperationsTableProps): JSX.Element {
},
];
const downloadableData = convertedTracesToDownloadData(data);
return (
<ResizeTable
columns={columns}
showHeader
title={(): string => 'Key Operations'}
tableLayout="fixed"
dataSource={data}
rowKey="name"
/>
<div className="top-operation">
<div className="top-operation--download">
<Download
data={downloadableData}
isLoading={isLoading}
fileName={`top-operations-${servicename}`}
/>
</div>
<ResizeTable
columns={columns}
loading={isLoading}
showHeader
title={(): string => 'Key Operations'}
tableLayout="fixed"
dataSource={data}
rowKey="name"
/>
</div>
);
}
@ -115,6 +159,7 @@ export interface TopOperationList {
interface TopOperationsTableProps {
data: TopOperationList[];
isLoading: boolean;
}
export default TopOperationsTable;

View File

@ -1,3 +1,5 @@
import { DownloadOptions } from 'container/Download/Download.types';
export const legend = {
address: '{{address}}',
};
@ -67,3 +69,8 @@ export enum WidgetKeys {
SignozExternalCallLatencySum = 'signoz_external_call_latency_sum',
Signoz_latency_bucket = 'signoz_latency_bucket',
}
export const topOperationMetricsDownloadOptions: DownloadOptions = {
isDownloadEnabled: true,
fileName: 'top-operation',
} as const;

View File

@ -35,3 +35,19 @@ export const getNearestHighestBucketValue = (
export const convertMilSecToNanoSec = (value: number): number =>
value * 1000000000;
export const convertedTracesToDownloadData = (
originalData: TopOperationList[],
): Record<string, string>[] =>
originalData.map((item) => {
const newObj: Record<string, string> = {
Name: item.name,
'P50 (in ms)': (item.p50 / 1000000).toFixed(2),
'P95 (in ms)': (item.p95 / 1000000).toFixed(2),
'P99 (in ms)': (item.p99 / 1000000).toFixed(2),
'Number of calls': item.numCalls.toString(),
'Error Rate (%)': getErrorRate(item).toFixed(2),
};
return newObj;
});

View File

@ -3,6 +3,7 @@ import {
CloseCircleFilled,
ExclamationCircleFilled,
LoadingOutlined,
MinusCircleFilled,
} from '@ant-design/icons';
import { Spin } from 'antd';
@ -16,6 +17,8 @@ export function getDeploymentStage(value: string): string {
return 'Dirty';
case 'FAILED':
return 'Failed';
case 'UNKNOWN':
return 'Unknown';
default:
return '';
}
@ -33,6 +36,8 @@ export function getDeploymentStageIcon(value: string): JSX.Element {
return <ExclamationCircleFilled />;
case 'FAILED':
return <CloseCircleFilled />;
case 'UNKNOWN':
return <MinusCircleFilled />;
default:
return <span />;
}

View File

@ -1,5 +1,6 @@
import { Input } from 'antd';
import React, { Dispatch, SetStateAction, useCallback } from 'react';
import { debounce } from 'lodash-es';
import { BaseSyntheticEvent, Dispatch, SetStateAction } from 'react';
import { useTranslation } from 'react-i18next';
function PipelinesSearchSection({
@ -7,18 +8,18 @@ function PipelinesSearchSection({
}: PipelinesSearchSectionProps): JSX.Element {
const { t } = useTranslation(['pipeline']);
const onSeachHandler = useCallback(
(event: React.SetStateAction<string>) => {
setPipelineSearchValue(event);
},
[setPipelineSearchValue],
);
const handleSearch = (searchEv: BaseSyntheticEvent): void => {
setPipelineSearchValue(searchEv?.target?.value || '');
};
const debouncedHandleSearch = debounce(handleSearch, 300);
return (
<Input.Search
<Input
type="text"
allowClear
placeholder={t('search_pipeline_placeholder')}
onSearch={onSeachHandler}
onChange={debouncedHandleSearch}
/>
);
}

View File

@ -16,11 +16,7 @@ function DescriptionTextArea({
label={<FormLabelStyle>{fieldData.fieldName}</FormLabelStyle>}
key={fieldData.id}
>
<Input.TextArea
rows={3}
name={fieldData.name}
placeholder={t(fieldData.placeholder)}
/>
<Input.TextArea rows={3} placeholder={t(fieldData.placeholder)} />
</Form.Item>
);
}

View File

@ -16,7 +16,7 @@ function NameInput({ fieldData }: NameInputProps): JSX.Element {
rules={formValidationRules}
name={fieldData.name}
>
<Input name={fieldData.name} placeholder={t(fieldData.placeholder)} />
<Input placeholder={t(fieldData.placeholder)} />
</Form.Item>
);
}

View File

@ -22,8 +22,9 @@ function NameInput({ fieldData }: NameInputProps): JSX.Element {
name={fieldData.name}
initialValue={fieldData.initialValue}
rules={fieldData.rules ? fieldData.rules : formValidationRules}
dependencies={fieldData.dependencies || []}
>
<Input placeholder={t(fieldData.placeholder)} name={fieldData.name} />
<Input placeholder={t(fieldData.placeholder)} />
</Form.Item>
</FormWrapper>
</Container>

View File

@ -20,11 +20,7 @@ function ParsingRulesTextArea({
name={fieldData.name}
label={<ModalFooterTitle>{fieldData.fieldName}</ModalFooterTitle>}
>
<Input.TextArea
rows={4}
name={fieldData.name}
placeholder={t(fieldData.placeholder)}
/>
<Input.TextArea rows={4} placeholder={t(fieldData.placeholder)} />
</Form.Item>
</FormWrapper>
</Container>

View File

@ -1,3 +1,6 @@
import { Rule, RuleRender } from 'antd/es/form';
import { NamePath } from 'antd/es/form/interface';
type ProcessorType = {
key: string;
value: string;
@ -8,11 +11,11 @@ type ProcessorType = {
export const processorTypes: Array<ProcessorType> = [
{ key: 'grok_parser', value: 'grok_parser', label: 'Grok' },
{ key: 'json_parser', value: 'json_parser', label: 'Json Parser' },
{ key: 'regex_parser', value: 'regex_parser', label: 'Regex' },
{ key: 'json_parser', value: 'json_parser', label: 'Json Parser' },
{ key: 'trace_parser', value: 'trace_parser', label: 'Trace Parser' },
{ key: 'add', value: 'add', label: 'Add' },
{ key: 'remove', value: 'remove', label: 'Remove' },
{ key: 'trace_parser', value: 'trace_parser', label: 'Trace Parser' },
// { key: 'retain', value: 'retain', label: 'Retain' }, @Chintan - Commented as per Nitya's suggestion
{ key: 'move', value: 'move', label: 'Move' },
{ key: 'copy', value: 'copy', label: 'Copy' },
@ -24,11 +27,30 @@ export type ProcessorFormField = {
id: number;
fieldName: string;
placeholder: string;
name: string;
rules?: Array<{ [key: string]: boolean }>;
name: string | NamePath;
rules?: Array<Rule>;
initialValue?: string;
dependencies?: Array<string | NamePath>;
};
const traceParserFieldValidator: RuleRender = (form) => ({
validator: (): Promise<void> => {
const parseFromValues = [
['trace_id', 'parse_from'],
['span_id', 'parse_from'],
['trace_flags', 'parse_from'],
].map((np) => form.getFieldValue(np));
if (!parseFromValues.some((v) => v?.length > 0)) {
return Promise.reject(
new Error('At least one of the trace parser fields must be specified.'),
);
}
return Promise.resolve();
},
});
const commonFields = [
{
id: 3,
@ -152,21 +174,36 @@ export const processorFields: { [key: string]: Array<ProcessorFormField> } = {
},
{
id: 2,
fieldName: 'Trace Id Parce From',
fieldName: 'Parse Trace Id From',
placeholder: 'processor_trace_id_placeholder',
name: 'traceId',
name: ['trace_id', 'parse_from'],
rules: [traceParserFieldValidator],
dependencies: [
['span_id', 'parse_from'],
['trace_flags', 'parse_from'],
],
},
{
id: 3,
fieldName: 'Span id Parse From',
fieldName: 'Parse Span Id From',
placeholder: 'processor_span_id_placeholder',
name: 'spanId',
name: ['span_id', 'parse_from'],
rules: [traceParserFieldValidator],
dependencies: [
['trace_id', 'parse_from'],
['trace_flags', 'parse_from'],
],
},
{
id: 4,
fieldName: 'Trace flags parse from',
fieldName: 'Parse Trace flags From',
placeholder: 'processor_trace_flags_placeholder',
name: 'traceFlags',
name: ['trace_flags', 'parse_from'],
rules: [traceParserFieldValidator],
dependencies: [
['trace_id', 'parse_from'],
['span_id', 'parse_from'],
],
},
],
retain: [

View File

@ -4,6 +4,6 @@ import NameInput from './FormFields/NameInput';
export const renderProcessorForm = (
processorType: string,
): Array<JSX.Element> =>
processorFields[processorType]?.map((fieldName: ProcessorFormField) => (
<NameInput key={fieldName.id} fieldData={fieldName} />
processorFields[processorType]?.map((fieldData: ProcessorFormField) => (
<NameInput key={fieldData.id} fieldData={fieldData} />
));

View File

@ -14,8 +14,8 @@ import {
import { tableComponents } from '../config';
import { ModalFooterTitle } from '../styles';
import { AlertMessage } from '.';
import { processorColumns } from './config';
import { AlertMessage } from './PipelineListsView';
import { FooterButton, StyledTable } from './styles';
import DragAction from './TableComponents/DragAction';
import ProcessorActions from './TableComponents/ProcessorActions';

View File

@ -0,0 +1,489 @@
import { ExclamationCircleOutlined, PlusOutlined } from '@ant-design/icons';
import { Modal, Table } from 'antd';
import { ExpandableConfig } from 'antd/es/table/interface';
import savePipeline from 'api/pipeline/post';
import { useNotifications } from 'hooks/useNotifications';
import cloneDeep from 'lodash-es/cloneDeep';
import React, { useCallback, useMemo, useState } from 'react';
import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
import { useTranslation } from 'react-i18next';
import {
ActionMode,
ActionType,
Pipeline,
PipelineData,
ProcessorData,
} from 'types/api/pipeline/def';
import { v4 } from 'uuid';
import { tableComponents } from '../config';
import AddNewPipeline from './AddNewPipeline';
import AddNewProcessor from './AddNewProcessor';
import { pipelineColumns } from './config';
import ModeAndConfiguration from './ModeAndConfiguration';
import PipelineExpanView from './PipelineExpandView';
import SaveConfigButton from './SaveConfigButton';
import {
AlertContentWrapper,
AlertModalTitle,
Container,
FooterButton,
} from './styles';
import DragAction from './TableComponents/DragAction';
import PipelineActions from './TableComponents/PipelineActions';
import PreviewAction from './TableComponents/PipelineActions/components/PreviewAction';
import TableExpandIcon from './TableComponents/TableExpandIcon';
import {
getDataOnSearch,
getEditedDataSource,
getElementFromArray,
getRecordIndex,
getTableColumn,
getUpdatedRow,
} from './utils';
function PipelineListsView({
isActionType,
setActionType,
isActionMode,
setActionMode,
pipelineData,
refetchPipelineLists,
pipelineSearchValue,
}: PipelineListsViewProps): JSX.Element {
const { t } = useTranslation(['pipeline', 'common']);
const [modal, contextHolder] = Modal.useModal();
const { notifications } = useNotifications();
const [prevPipelineData, setPrevPipelineData] = useState<Array<PipelineData>>(
cloneDeep(pipelineData?.pipelines || []),
);
const [currPipelineData, setCurrPipelineData] = useState<Array<PipelineData>>(
cloneDeep(pipelineData?.pipelines || []),
);
const [expandedPipelineId, setExpandedPipelineId] = useState<
string | undefined
>(undefined);
const expandedPipelineData = useCallback(
() => currPipelineData?.find((p) => p.id === expandedPipelineId),
[currPipelineData, expandedPipelineId],
);
const setExpandedPipelineData = useCallback(
(newData: PipelineData): void => {
if (expandedPipelineId) {
const pipelineIdx = currPipelineData?.findIndex(
(p) => p.id === expandedPipelineId,
);
if (pipelineIdx >= 0) {
const newPipelineData = [...currPipelineData];
newPipelineData[pipelineIdx] = newData;
setCurrPipelineData(newPipelineData);
}
}
},
[expandedPipelineId, currPipelineData],
);
const [
selectedProcessorData,
setSelectedProcessorData,
] = useState<ProcessorData>();
const [
selectedPipelineData,
setSelectedPipelineData,
] = useState<PipelineData>();
const [expandedRowKeys, setExpandedRowKeys] = useState<Array<string>>();
const [showSaveButton, setShowSaveButton] = useState<string>();
const isEditingActionMode = isActionMode === ActionMode.Editing;
const visibleCurrPipelines = useMemo((): Array<PipelineData> => {
if (pipelineSearchValue === '') {
return currPipelineData;
}
return currPipelineData.filter((data) =>
getDataOnSearch(data as never, pipelineSearchValue),
);
}, [currPipelineData, pipelineSearchValue]);
const handleAlert = useCallback(
({ title, descrition, buttontext, onCancel, onOk }: AlertMessage) => {
modal.confirm({
title: <AlertModalTitle>{title}</AlertModalTitle>,
icon: <ExclamationCircleOutlined />,
content: <AlertContentWrapper>{descrition}</AlertContentWrapper>,
okText: <span>{buttontext}</span>,
cancelText: <span>{t('cancel')}</span>,
onOk,
onCancel,
});
},
[modal, t],
);
const pipelineEditAction = useCallback(
(record: PipelineData) => (): void => {
setActionType(ActionType.EditPipeline);
setSelectedPipelineData(record);
},
[setActionType],
);
const pipelineDeleteHandler = useCallback(
(record: PipelineData) => (): void => {
setShowSaveButton(ActionMode.Editing);
const filteredData = getElementFromArray(currPipelineData, record, 'id');
filteredData.forEach((item, index) => {
const obj = item;
obj.orderId = index + 1;
});
setCurrPipelineData(filteredData);
},
[currPipelineData],
);
const pipelineDeleteAction = useCallback(
(record: PipelineData) => (): void => {
handleAlert({
title: `${t('delete_pipeline')} : ${record.name}?`,
descrition: t('delete_pipeline_description'),
buttontext: t('delete'),
onOk: pipelineDeleteHandler(record),
});
},
[handleAlert, pipelineDeleteHandler, t],
);
const processorEditAction = useCallback(
(record: ProcessorData) => (): void => {
setActionType(ActionType.EditProcessor);
setSelectedProcessorData(record);
},
[setActionType],
);
const onSwitchPipelineChange = useCallback(
(checked: boolean, record: PipelineData): void => {
setShowSaveButton(ActionMode.Editing);
const findRecordIndex = getRecordIndex(currPipelineData, record, 'id');
const updateSwitch = {
...currPipelineData[findRecordIndex],
enabled: checked,
};
const editedPipelineData = getEditedDataSource(
currPipelineData,
record,
'id',
updateSwitch,
);
setCurrPipelineData(editedPipelineData);
},
[currPipelineData],
);
const columns = useMemo(() => {
const fieldColumns = getTableColumn(pipelineColumns);
if (isEditingActionMode) {
fieldColumns.push(
{
title: 'Actions',
dataIndex: 'smartAction',
key: 'smartAction',
align: 'center',
render: (_value, record): JSX.Element => (
<PipelineActions
pipeline={record}
editAction={pipelineEditAction(record)}
deleteAction={pipelineDeleteAction(record)}
/>
),
},
{
title: '',
dataIndex: 'enabled',
key: 'enabled',
render: (value, record) => (
<DragAction
isEnabled={value}
onChange={(checked: boolean): void =>
onSwitchPipelineChange(checked, record)
}
/>
),
},
);
} else {
fieldColumns.push({
title: 'Actions',
dataIndex: 'smartAction',
key: 'smartAction',
align: 'center',
render: (_value, record): JSX.Element => (
<PreviewAction pipeline={record} />
),
});
}
return fieldColumns;
}, [
isEditingActionMode,
pipelineEditAction,
pipelineDeleteAction,
onSwitchPipelineChange,
]);
const updatePipelineSequence = useCallback(
(updatedRow: PipelineData[]) => (): void => {
setShowSaveButton(ActionMode.Editing);
setCurrPipelineData(updatedRow);
},
[],
);
const onCancelPipelineSequence = useCallback(
(rawData: PipelineData[]) => (): void => {
setCurrPipelineData(rawData);
},
[],
);
const movePipelineRow = useCallback(
(dragIndex: number, hoverIndex: number) => {
if (currPipelineData && isEditingActionMode) {
const rawData = currPipelineData;
const updatedRows = getUpdatedRow(
currPipelineData,
visibleCurrPipelines[dragIndex].orderId - 1,
visibleCurrPipelines[hoverIndex].orderId - 1,
);
updatedRows.forEach((item, index) => {
const obj = item;
obj.orderId = index + 1;
});
handleAlert({
title: t('reorder_pipeline'),
descrition: t('reorder_pipeline_description'),
buttontext: t('reorder'),
onOk: updatePipelineSequence(updatedRows),
onCancel: onCancelPipelineSequence(rawData),
});
}
},
[
currPipelineData,
isEditingActionMode,
visibleCurrPipelines,
handleAlert,
t,
updatePipelineSequence,
onCancelPipelineSequence,
],
);
const expandedRowView = useCallback(
(): JSX.Element => (
<PipelineExpanView
handleAlert={handleAlert}
isActionMode={isActionMode}
setActionType={setActionType}
processorEditAction={processorEditAction}
setShowSaveButton={setShowSaveButton}
expandedPipelineData={expandedPipelineData()}
setExpandedPipelineData={setExpandedPipelineData}
prevPipelineData={prevPipelineData}
/>
),
[
handleAlert,
processorEditAction,
isActionMode,
expandedPipelineData,
setActionType,
prevPipelineData,
setExpandedPipelineData,
],
);
const onExpand = useCallback(
(expanded: boolean, record: PipelineData): void => {
const keys = [];
if (expanded && record.id) {
keys.push(record?.id);
}
setExpandedRowKeys(keys);
setExpandedPipelineId(record.id);
},
[],
);
const getExpandIcon = (
expanded: boolean,
onExpand: (record: PipelineData, e: React.MouseEvent<HTMLElement>) => void,
record: PipelineData,
): JSX.Element => (
<TableExpandIcon expanded={expanded} onExpand={onExpand} record={record} />
);
const addNewPipelineHandler = useCallback((): void => {
setActionType(ActionType.AddPipeline);
}, [setActionType]);
const footer = useCallback((): JSX.Element | undefined => {
if (isEditingActionMode) {
return (
<FooterButton
type="link"
onClick={addNewPipelineHandler}
icon={<PlusOutlined />}
>
{t('add_new_pipeline')}
</FooterButton>
);
}
return undefined;
}, [isEditingActionMode, addNewPipelineHandler, t]);
const onSaveConfigurationHandler = useCallback(async () => {
const modifiedPipelineData = currPipelineData.map((item: PipelineData) => {
const pipelineData = { ...item };
delete pipelineData?.id;
return pipelineData;
});
const response = await savePipeline({
data: { pipelines: modifiedPipelineData },
});
if (response.statusCode === 200) {
refetchPipelineLists();
setActionMode(ActionMode.Viewing);
setShowSaveButton(undefined);
setCurrPipelineData(response.payload?.pipelines || []);
setPrevPipelineData(response.payload?.pipelines || []);
} else {
modifiedPipelineData.forEach((item: PipelineData) => {
const pipelineData = item;
pipelineData.id = v4();
return pipelineData;
});
setActionMode(ActionMode.Editing);
setShowSaveButton(ActionMode.Editing);
notifications.error({
message: 'Error',
description: response.error || t('something_went_wrong'),
});
setCurrPipelineData(modifiedPipelineData);
setPrevPipelineData(modifiedPipelineData);
}
}, [currPipelineData, notifications, refetchPipelineLists, setActionMode, t]);
const onCancelConfigurationHandler = useCallback((): void => {
setActionMode(ActionMode.Viewing);
setShowSaveButton(undefined);
prevPipelineData.forEach((item, index) => {
const obj = item;
obj.orderId = index + 1;
if (obj.config) {
obj.config?.forEach((configItem, index) => {
const config = configItem;
config.orderId = index + 1;
});
for (let i = 0; i < obj.config.length - 1; i += 1) {
obj.config[i].output = obj.config[i + 1].id;
}
}
});
setCurrPipelineData(prevPipelineData);
setExpandedRowKeys([]);
}, [prevPipelineData, setActionMode]);
const onRowHandler = (
_data: PipelineData,
index?: number,
): React.HTMLAttributes<unknown> =>
({
index,
moveRow: movePipelineRow,
} as React.HTMLAttributes<unknown>);
const expandableConfig: ExpandableConfig<PipelineData> = {
expandedRowKeys,
onExpand,
expandIcon: ({ expanded, onExpand, record }: ExpandRowConfig) =>
getExpandIcon(expanded, onExpand, record),
};
return (
<>
{contextHolder}
<AddNewPipeline
isActionType={isActionType}
setActionType={setActionType}
selectedPipelineData={selectedPipelineData}
setShowSaveButton={setShowSaveButton}
setCurrPipelineData={setCurrPipelineData}
currPipelineData={currPipelineData}
/>
<AddNewProcessor
isActionType={isActionType}
setActionType={setActionType}
selectedProcessorData={selectedProcessorData}
setShowSaveButton={setShowSaveButton}
expandedPipelineData={expandedPipelineData()}
setExpandedPipelineData={setExpandedPipelineData}
/>
<Container>
<ModeAndConfiguration
isActionMode={isActionMode}
version={pipelineData?.version}
/>
<DndProvider backend={HTML5Backend}>
<Table
rowKey="id"
columns={columns}
expandedRowRender={expandedRowView}
expandable={expandableConfig}
components={tableComponents}
dataSource={visibleCurrPipelines}
onRow={onRowHandler}
footer={footer}
pagination={false}
/>
</DndProvider>
{showSaveButton && (
<SaveConfigButton
onSaveConfigurationHandler={onSaveConfigurationHandler}
onCancelConfigurationHandler={onCancelConfigurationHandler}
/>
)}
</Container>
</>
);
}
interface PipelineListsViewProps {
isActionType: string;
setActionType: (actionType?: ActionType) => void;
isActionMode: string;
setActionMode: (actionMode: ActionMode) => void;
pipelineData: Pipeline;
refetchPipelineLists: VoidFunction;
pipelineSearchValue: string;
}
interface ExpandRowConfig {
expanded: boolean;
onExpand: (record: PipelineData, e: React.MouseEvent<HTMLElement>) => void;
record: PipelineData;
}
export interface AlertMessage {
title: string;
descrition: string;
buttontext: string;
onOk: VoidFunction;
onCancel?: VoidFunction;
}
export default PipelineListsView;

View File

@ -3,7 +3,6 @@
height: 100%;
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: stretch;
box-sizing: border-box;
@ -11,8 +10,9 @@
}
.logs-preview-list-item {
width: 100%;
position: relative;
width: 100%;
max-height: 2rem;
display: flex;
justify-content: space-between;

View File

@ -1,489 +1,3 @@
import { ExclamationCircleOutlined, PlusOutlined } from '@ant-design/icons';
import { Modal, Table } from 'antd';
import { ExpandableConfig } from 'antd/es/table/interface';
import savePipeline from 'api/pipeline/post';
import { useNotifications } from 'hooks/useNotifications';
import cloneDeep from 'lodash-es/cloneDeep';
import React, { useCallback, useMemo, useState } from 'react';
import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
import { useTranslation } from 'react-i18next';
import {
ActionMode,
ActionType,
Pipeline,
PipelineData,
ProcessorData,
} from 'types/api/pipeline/def';
import { v4 } from 'uuid';
import { tableComponents } from '../config';
import AddNewPipeline from './AddNewPipeline';
import AddNewProcessor from './AddNewProcessor';
import { pipelineColumns } from './config';
import ModeAndConfiguration from './ModeAndConfiguration';
import PipelineExpanView from './PipelineExpandView';
import SaveConfigButton from './SaveConfigButton';
import {
AlertContentWrapper,
AlertModalTitle,
Container,
FooterButton,
} from './styles';
import DragAction from './TableComponents/DragAction';
import PipelineActions from './TableComponents/PipelineActions';
import PreviewAction from './TableComponents/PipelineActions/components/PreviewAction';
import TableExpandIcon from './TableComponents/TableExpandIcon';
import {
getDataOnSearch,
getEditedDataSource,
getElementFromArray,
getRecordIndex,
getTableColumn,
getUpdatedRow,
} from './utils';
function PipelineListsView({
isActionType,
setActionType,
isActionMode,
setActionMode,
pipelineData,
refetchPipelineLists,
pipelineSearchValue,
}: PipelineListsViewProps): JSX.Element {
const { t } = useTranslation(['pipeline', 'common']);
const [modal, contextHolder] = Modal.useModal();
const { notifications } = useNotifications();
const [prevPipelineData, setPrevPipelineData] = useState<Array<PipelineData>>(
cloneDeep(pipelineData?.pipelines),
);
const [currPipelineData, setCurrPipelineData] = useState<Array<PipelineData>>(
cloneDeep(pipelineData?.pipelines),
);
const [expandedPipelineId, setExpandedPipelineId] = useState<
string | undefined
>(undefined);
const expandedPipelineData = useCallback(
() => currPipelineData.find((p) => p.id === expandedPipelineId),
[currPipelineData, expandedPipelineId],
);
const setExpandedPipelineData = useCallback(
(newData: PipelineData): void => {
if (expandedPipelineId) {
const pipelineIdx = currPipelineData.findIndex(
(p) => p.id === expandedPipelineId,
);
if (pipelineIdx >= 0) {
const newPipelineData = [...currPipelineData];
newPipelineData[pipelineIdx] = newData;
setCurrPipelineData(newPipelineData);
}
}
},
[expandedPipelineId, currPipelineData],
);
const [
selectedProcessorData,
setSelectedProcessorData,
] = useState<ProcessorData>();
const [
selectedPipelineData,
setSelectedPipelineData,
] = useState<PipelineData>();
const [expandedRowKeys, setExpandedRowKeys] = useState<Array<string>>();
const [showSaveButton, setShowSaveButton] = useState<string>();
const isEditingActionMode = isActionMode === ActionMode.Editing;
const visibleCurrPipelines = useMemo((): Array<PipelineData> => {
if (pipelineSearchValue === '') {
return currPipelineData;
}
return currPipelineData.filter((data) =>
getDataOnSearch(data as never, pipelineSearchValue),
);
}, [currPipelineData, pipelineSearchValue]);
const handleAlert = useCallback(
({ title, descrition, buttontext, onCancel, onOk }: AlertMessage) => {
modal.confirm({
title: <AlertModalTitle>{title}</AlertModalTitle>,
icon: <ExclamationCircleOutlined />,
content: <AlertContentWrapper>{descrition}</AlertContentWrapper>,
okText: <span>{buttontext}</span>,
cancelText: <span>{t('cancel')}</span>,
onOk,
onCancel,
});
},
[modal, t],
);
const pipelineEditAction = useCallback(
(record: PipelineData) => (): void => {
setActionType(ActionType.EditPipeline);
setSelectedPipelineData(record);
},
[setActionType],
);
const pipelineDeleteHandler = useCallback(
(record: PipelineData) => (): void => {
setShowSaveButton(ActionMode.Editing);
const filteredData = getElementFromArray(currPipelineData, record, 'id');
filteredData.forEach((item, index) => {
const obj = item;
obj.orderId = index + 1;
});
setCurrPipelineData(filteredData);
},
[currPipelineData],
);
const pipelineDeleteAction = useCallback(
(record: PipelineData) => (): void => {
handleAlert({
title: `${t('delete_pipeline')} : ${record.name}?`,
descrition: t('delete_pipeline_description'),
buttontext: t('delete'),
onOk: pipelineDeleteHandler(record),
});
},
[handleAlert, pipelineDeleteHandler, t],
);
const processorEditAction = useCallback(
(record: ProcessorData) => (): void => {
setActionType(ActionType.EditProcessor);
setSelectedProcessorData(record);
},
[setActionType],
);
const onSwitchPipelineChange = useCallback(
(checked: boolean, record: PipelineData): void => {
setShowSaveButton(ActionMode.Editing);
const findRecordIndex = getRecordIndex(currPipelineData, record, 'id');
const updateSwitch = {
...currPipelineData[findRecordIndex],
enabled: checked,
};
const editedPipelineData = getEditedDataSource(
currPipelineData,
record,
'id',
updateSwitch,
);
setCurrPipelineData(editedPipelineData);
},
[currPipelineData],
);
const columns = useMemo(() => {
const fieldColumns = getTableColumn(pipelineColumns);
if (isEditingActionMode) {
fieldColumns.push(
{
title: 'Actions',
dataIndex: 'smartAction',
key: 'smartAction',
align: 'center',
render: (_value, record): JSX.Element => (
<PipelineActions
pipeline={record}
editAction={pipelineEditAction(record)}
deleteAction={pipelineDeleteAction(record)}
/>
),
},
{
title: '',
dataIndex: 'enabled',
key: 'enabled',
render: (value, record) => (
<DragAction
isEnabled={value}
onChange={(checked: boolean): void =>
onSwitchPipelineChange(checked, record)
}
/>
),
},
);
} else {
fieldColumns.push({
title: 'Actions',
dataIndex: 'smartAction',
key: 'smartAction',
align: 'center',
render: (_value, record): JSX.Element => (
<PreviewAction pipeline={record} />
),
});
}
return fieldColumns;
}, [
isEditingActionMode,
pipelineEditAction,
pipelineDeleteAction,
onSwitchPipelineChange,
]);
const updatePipelineSequence = useCallback(
(updatedRow: PipelineData[]) => (): void => {
setShowSaveButton(ActionMode.Editing);
setCurrPipelineData(updatedRow);
},
[],
);
const onCancelPipelineSequence = useCallback(
(rawData: PipelineData[]) => (): void => {
setCurrPipelineData(rawData);
},
[],
);
const movePipelineRow = useCallback(
(dragIndex: number, hoverIndex: number) => {
if (currPipelineData && isEditingActionMode) {
const rawData = currPipelineData;
const updatedRows = getUpdatedRow(
currPipelineData,
visibleCurrPipelines[dragIndex].orderId - 1,
visibleCurrPipelines[hoverIndex].orderId - 1,
);
updatedRows.forEach((item, index) => {
const obj = item;
obj.orderId = index + 1;
});
handleAlert({
title: t('reorder_pipeline'),
descrition: t('reorder_pipeline_description'),
buttontext: t('reorder'),
onOk: updatePipelineSequence(updatedRows),
onCancel: onCancelPipelineSequence(rawData),
});
}
},
[
currPipelineData,
isEditingActionMode,
visibleCurrPipelines,
handleAlert,
t,
updatePipelineSequence,
onCancelPipelineSequence,
],
);
const expandedRowView = useCallback(
(): JSX.Element => (
<PipelineExpanView
handleAlert={handleAlert}
isActionMode={isActionMode}
setActionType={setActionType}
processorEditAction={processorEditAction}
setShowSaveButton={setShowSaveButton}
expandedPipelineData={expandedPipelineData()}
setExpandedPipelineData={setExpandedPipelineData}
prevPipelineData={prevPipelineData}
/>
),
[
handleAlert,
processorEditAction,
isActionMode,
expandedPipelineData,
setActionType,
prevPipelineData,
setExpandedPipelineData,
],
);
const onExpand = useCallback(
(expanded: boolean, record: PipelineData): void => {
const keys = [];
if (expanded && record.id) {
keys.push(record?.id);
}
setExpandedRowKeys(keys);
setExpandedPipelineId(record.id);
},
[],
);
const getExpandIcon = (
expanded: boolean,
onExpand: (record: PipelineData, e: React.MouseEvent<HTMLElement>) => void,
record: PipelineData,
): JSX.Element => (
<TableExpandIcon expanded={expanded} onExpand={onExpand} record={record} />
);
const addNewPipelineHandler = useCallback((): void => {
setActionType(ActionType.AddPipeline);
}, [setActionType]);
const footer = useCallback((): JSX.Element | undefined => {
if (isEditingActionMode) {
return (
<FooterButton
type="link"
onClick={addNewPipelineHandler}
icon={<PlusOutlined />}
>
{t('add_new_pipeline')}
</FooterButton>
);
}
return undefined;
}, [isEditingActionMode, addNewPipelineHandler, t]);
const onSaveConfigurationHandler = useCallback(async () => {
const modifiedPipelineData = currPipelineData.map((item: PipelineData) => {
const pipelineData = { ...item };
delete pipelineData?.id;
return pipelineData;
});
const response = await savePipeline({
data: { pipelines: modifiedPipelineData },
});
if (response.statusCode === 200) {
refetchPipelineLists();
setActionMode(ActionMode.Viewing);
setShowSaveButton(undefined);
setCurrPipelineData(response.payload?.pipelines);
setPrevPipelineData(response.payload?.pipelines);
} else {
modifiedPipelineData.forEach((item: PipelineData) => {
const pipelineData = item;
pipelineData.id = v4();
return pipelineData;
});
setActionMode(ActionMode.Editing);
setShowSaveButton(ActionMode.Editing);
notifications.error({
message: 'Error',
description: response.error || t('something_went_wrong'),
});
setCurrPipelineData(modifiedPipelineData);
setPrevPipelineData(modifiedPipelineData);
}
}, [currPipelineData, notifications, refetchPipelineLists, setActionMode, t]);
const onCancelConfigurationHandler = useCallback((): void => {
setActionMode(ActionMode.Viewing);
setShowSaveButton(undefined);
prevPipelineData.forEach((item, index) => {
const obj = item;
obj.orderId = index + 1;
if (obj.config) {
obj.config?.forEach((configItem, index) => {
const config = configItem;
config.orderId = index + 1;
});
for (let i = 0; i < obj.config.length - 1; i += 1) {
obj.config[i].output = obj.config[i + 1].id;
}
}
});
setCurrPipelineData(prevPipelineData);
setExpandedRowKeys([]);
}, [prevPipelineData, setActionMode]);
const onRowHandler = (
_data: PipelineData,
index?: number,
): React.HTMLAttributes<unknown> =>
({
index,
moveRow: movePipelineRow,
} as React.HTMLAttributes<unknown>);
const expandableConfig: ExpandableConfig<PipelineData> = {
expandedRowKeys,
onExpand,
expandIcon: ({ expanded, onExpand, record }: ExpandRowConfig) =>
getExpandIcon(expanded, onExpand, record),
};
return (
<>
{contextHolder}
<AddNewPipeline
isActionType={isActionType}
setActionType={setActionType}
selectedPipelineData={selectedPipelineData}
setShowSaveButton={setShowSaveButton}
setCurrPipelineData={setCurrPipelineData}
currPipelineData={currPipelineData}
/>
<AddNewProcessor
isActionType={isActionType}
setActionType={setActionType}
selectedProcessorData={selectedProcessorData}
setShowSaveButton={setShowSaveButton}
expandedPipelineData={expandedPipelineData()}
setExpandedPipelineData={setExpandedPipelineData}
/>
<Container>
<ModeAndConfiguration
isActionMode={isActionMode}
version={pipelineData?.version}
/>
<DndProvider backend={HTML5Backend}>
<Table
rowKey="id"
columns={columns}
expandedRowRender={expandedRowView}
expandable={expandableConfig}
components={tableComponents}
dataSource={visibleCurrPipelines}
onRow={onRowHandler}
footer={footer}
pagination={false}
/>
</DndProvider>
{showSaveButton && (
<SaveConfigButton
onSaveConfigurationHandler={onSaveConfigurationHandler}
onCancelConfigurationHandler={onCancelConfigurationHandler}
/>
)}
</Container>
</>
);
}
interface PipelineListsViewProps {
isActionType: string;
setActionType: (actionType?: ActionType) => void;
isActionMode: string;
setActionMode: (actionMode: ActionMode) => void;
pipelineData: Pipeline;
refetchPipelineLists: VoidFunction;
pipelineSearchValue: string;
}
interface ExpandRowConfig {
expanded: boolean;
onExpand: (record: PipelineData, e: React.MouseEvent<HTMLElement>) => void;
record: PipelineData;
}
export interface AlertMessage {
title: string;
descrition: string;
buttontext: string;
onOk: VoidFunction;
onCancel?: VoidFunction;
}
import PipelineListsView from './PipelineListsView';
export default PipelineListsView;

View File

@ -82,78 +82,42 @@ exports[`PipelinePage container test should render PipelinePageLayout section 1`
</button>
</div>
<span
class="ant-input-group-wrapper ant-input-search css-dev-only-do-not-override-1i536d8"
class="ant-input-affix-wrapper css-dev-only-do-not-override-1i536d8"
>
<input
class="ant-input css-dev-only-do-not-override-1i536d8"
placeholder="search_pipeline_placeholder"
type="text"
value=""
/>
<span
class="ant-input-wrapper ant-input-group css-dev-only-do-not-override-1i536d8"
class="ant-input-suffix"
>
<span
class="ant-input-affix-wrapper css-dev-only-do-not-override-1i536d8"
class="ant-input-clear-icon ant-input-clear-icon-hidden"
role="button"
tabindex="-1"
>
<input
class="ant-input css-dev-only-do-not-override-1i536d8"
placeholder="search_pipeline_placeholder"
type="text"
value=""
/>
<span
class="ant-input-suffix"
aria-label="close-circle"
class="anticon anticon-close-circle"
role="img"
>
<span
class="ant-input-clear-icon ant-input-clear-icon-hidden"
role="button"
tabindex="-1"
<svg
aria-hidden="true"
data-icon="close-circle"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<span
aria-label="close-circle"
class="anticon anticon-close-circle"
role="img"
>
<svg
aria-hidden="true"
data-icon="close-circle"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm165.4 618.2l-66-.3L512 563.4l-99.3 118.4-66.1.3c-4.4 0-8-3.5-8-8 0-1.9.7-3.7 1.9-5.2l130.1-155L340.5 359a8.32 8.32 0 01-1.9-5.2c0-4.4 3.6-8 8-8l66.1.3L512 464.6l99.3-118.4 66-.3c4.4 0 8 3.5 8 8 0 1.9-.7 3.7-1.9 5.2L553.5 514l130 155c1.2 1.5 1.9 3.3 1.9 5.2 0 4.4-3.6 8-8 8z"
/>
</svg>
</span>
</span>
<path
d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm165.4 618.2l-66-.3L512 563.4l-99.3 118.4-66.1.3c-4.4 0-8-3.5-8-8 0-1.9.7-3.7 1.9-5.2l130.1-155L340.5 359a8.32 8.32 0 01-1.9-5.2c0-4.4 3.6-8 8-8l66.1.3L512 464.6l99.3-118.4 66-.3c4.4 0 8 3.5 8 8 0 1.9-.7 3.7-1.9 5.2L553.5 514l130 155c1.2 1.5 1.9 3.3 1.9 5.2 0 4.4-3.6 8-8 8z"
/>
</svg>
</span>
</span>
<span
class="ant-input-group-addon"
>
<button
class="ant-btn css-dev-only-do-not-override-1i536d8 ant-btn-default ant-btn-icon-only ant-input-search-button"
type="button"
>
<span
aria-label="search"
class="anticon anticon-search"
role="img"
>
<svg
aria-hidden="true"
data-icon="search"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M909.6 854.5L649.9 594.8C690.2 542.7 712 479 712 412c0-80.2-31.3-155.4-87.9-212.1-56.6-56.7-132-87.9-212.1-87.9s-155.5 31.3-212.1 87.9C143.2 256.5 112 331.8 112 412c0 80.1 31.3 155.5 87.9 212.1C256.5 680.8 331.8 712 412 712c67 0 130.6-21.8 182.7-62l259.7 259.6a8.2 8.2 0 0011.6 0l43.6-43.5a8.2 8.2 0 000-11.6zM570.4 570.4C528 612.7 471.8 636 412 636s-116-23.3-158.4-65.6C211.3 528 188 471.8 188 412s23.3-116.1 65.6-158.4C296 211.3 352.2 188 412 188s116.1 23.2 158.4 65.6S636 352.2 636 412s-23.3 116.1-65.6 158.4z"
/>
</svg>
</span>
</button>
</span>
</span>
</span>
.c0 {

View File

@ -3,78 +3,42 @@
exports[`PipelinePage container test should render PipelinesSearchSection section 1`] = `
<DocumentFragment>
<span
class="ant-input-group-wrapper ant-input-search css-dev-only-do-not-override-1i536d8"
class="ant-input-affix-wrapper css-dev-only-do-not-override-1i536d8"
>
<input
class="ant-input css-dev-only-do-not-override-1i536d8"
placeholder="search_pipeline_placeholder"
type="text"
value=""
/>
<span
class="ant-input-wrapper ant-input-group css-dev-only-do-not-override-1i536d8"
class="ant-input-suffix"
>
<span
class="ant-input-affix-wrapper css-dev-only-do-not-override-1i536d8"
class="ant-input-clear-icon ant-input-clear-icon-hidden"
role="button"
tabindex="-1"
>
<input
class="ant-input css-dev-only-do-not-override-1i536d8"
placeholder="search_pipeline_placeholder"
type="text"
value=""
/>
<span
class="ant-input-suffix"
aria-label="close-circle"
class="anticon anticon-close-circle"
role="img"
>
<span
class="ant-input-clear-icon ant-input-clear-icon-hidden"
role="button"
tabindex="-1"
<svg
aria-hidden="true"
data-icon="close-circle"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<span
aria-label="close-circle"
class="anticon anticon-close-circle"
role="img"
>
<svg
aria-hidden="true"
data-icon="close-circle"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm165.4 618.2l-66-.3L512 563.4l-99.3 118.4-66.1.3c-4.4 0-8-3.5-8-8 0-1.9.7-3.7 1.9-5.2l130.1-155L340.5 359a8.32 8.32 0 01-1.9-5.2c0-4.4 3.6-8 8-8l66.1.3L512 464.6l99.3-118.4 66-.3c4.4 0 8 3.5 8 8 0 1.9-.7 3.7-1.9 5.2L553.5 514l130 155c1.2 1.5 1.9 3.3 1.9 5.2 0 4.4-3.6 8-8 8z"
/>
</svg>
</span>
</span>
<path
d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm165.4 618.2l-66-.3L512 563.4l-99.3 118.4-66.1.3c-4.4 0-8-3.5-8-8 0-1.9.7-3.7 1.9-5.2l130.1-155L340.5 359a8.32 8.32 0 01-1.9-5.2c0-4.4 3.6-8 8-8l66.1.3L512 464.6l99.3-118.4 66-.3c4.4 0 8 3.5 8 8 0 1.9-.7 3.7-1.9 5.2L553.5 514l130 155c1.2 1.5 1.9 3.3 1.9 5.2 0 4.4-3.6 8-8 8z"
/>
</svg>
</span>
</span>
<span
class="ant-input-group-addon"
>
<button
class="ant-btn css-dev-only-do-not-override-1i536d8 ant-btn-default ant-btn-icon-only ant-input-search-button"
type="button"
>
<span
aria-label="search"
class="anticon anticon-search"
role="img"
>
<svg
aria-hidden="true"
data-icon="search"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M909.6 854.5L649.9 594.8C690.2 542.7 712 479 712 412c0-80.2-31.3-155.4-87.9-212.1-56.6-56.7-132-87.9-212.1-87.9s-155.5 31.3-212.1 87.9C143.2 256.5 112 331.8 112 412c0 80.1 31.3 155.5 87.9 212.1C256.5 680.8 331.8 712 412 712c67 0 130.6-21.8 182.7-62l259.7 259.6a8.2 8.2 0 0011.6 0l43.6-43.5a8.2 8.2 0 000-11.6zM570.4 570.4C528 612.7 471.8 636 412 636s-116-23.3-158.4-65.6C211.3 528 188 471.8 188 412s23.3-116.1 65.6-158.4C296 211.3 352.2 188 412 188s116.1 23.2 158.4 65.6S636 352.2 636 412s-23.3 116.1-65.6 158.4z"
/>
</svg>
</span>
</button>
</span>
</span>
</span>
</DocumentFragment>

View File

@ -1,6 +1,7 @@
import { Select, SelectProps, Space } from 'antd';
import { getCategorySelectOptionByName } from 'container/NewWidget/RightContainer/alertFomatCategories';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { popupContainer } from 'utils/selectPopupContainer';
import { categoryToSupport } from './config';
import { DefaultLabel, selectStyles } from './styles';
@ -31,6 +32,7 @@ function BuilderUnitsFilter({
<Space>
<DefaultLabel>Y-axis unit</DefaultLabel>
<Select
getPopupContainer={popupContainer}
style={selectStyles}
onChange={onChangeHandler}
value={selectedValue}

View File

@ -1,5 +1,6 @@
import { TableProps } from 'antd';
import { ColumnsType } from 'antd/es/table';
import { DownloadOptions } from 'container/Download/Download.types';
import { RowData } from 'lib/query/createTableColumnsFromQuery';
import { ReactNode } from 'react';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
@ -14,4 +15,5 @@ export type QueryTableProps = Omit<
renderActionCell?: (record: RowData) => ReactNode;
modifyColumns?: (columns: ColumnsType<RowData>) => ColumnsType<RowData>;
renderColumnCell?: Record<string, (record: RowData) => ReactNode>;
downloadOption?: DownloadOptions;
};

View File

@ -0,0 +1,9 @@
.query-table {
position: relative;
.query-table--download {
position: absolute;
top: 15px;
right: 0px;
z-index: 1;
}
}

View File

@ -1,8 +1,14 @@
import './QueryTable.styles.scss';
import { ResizeTable } from 'components/ResizeTable';
import Download from 'container/Download/Download';
import { IServiceName } from 'container/MetricsApplication/Tabs/types';
import { createTableColumnsFromQuery } from 'lib/query/createTableColumnsFromQuery';
import { useMemo } from 'react';
import { useParams } from 'react-router-dom';
import { QueryTableProps } from './QueryTable.intefaces';
import { createDownloadableData } from './utils';
export function QueryTable({
queryTableData,
@ -10,8 +16,12 @@ export function QueryTable({
renderActionCell,
modifyColumns,
renderColumnCell,
downloadOption,
...props
}: QueryTableProps): JSX.Element {
const { isDownloadEnabled = false, fileName = '' } = downloadOption || {};
const { servicename } = useParams<IServiceName>();
const { loading } = props;
const { columns, dataSource } = useMemo(
() =>
createTableColumnsFromQuery({
@ -23,16 +33,29 @@ export function QueryTable({
[query, queryTableData, renderActionCell, renderColumnCell],
);
const downloadableData = createDownloadableData(dataSource);
const tableColumns = modifyColumns ? modifyColumns(columns) : columns;
return (
<ResizeTable
columns={tableColumns}
tableLayout="fixed"
dataSource={dataSource}
scroll={{ x: true }}
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
/>
<div className="query-table">
{isDownloadEnabled && (
<div className="query-table--download">
<Download
data={downloadableData}
fileName={`${fileName}-${servicename}`}
isLoading={loading as boolean}
/>
</div>
)}
<ResizeTable
columns={tableColumns}
tableLayout="fixed"
dataSource={dataSource}
scroll={{ x: true }}
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
/>
</div>
);
}

View File

@ -0,0 +1,14 @@
import { RowData } from 'lib/query/createTableColumnsFromQuery';
export function createDownloadableData(
inputData: RowData[],
): Record<string, string>[] {
return inputData.map((row) => ({
Name: String(row.operation || ''),
'P50 (in ns)': String(row.A || ''),
'P90 (in ns)': String(row.B || ''),
'P99 (in ns)': String(row.C || ''),
'Number Of Calls': String(row.F || ''),
'Error Rate (%)': String(row.F1 && row.F1 !== 'N/A' ? row.F1 : '0'),
}));
}

View File

@ -7,13 +7,20 @@ import ROUTES from 'constants/routes';
import useLicense, { LICENSE_PLAN_KEY } from 'hooks/useLicense';
import history from 'lib/history';
import { LifeBuoy } from 'lucide-react';
import { useCallback, useLayoutEffect, useMemo, useState } from 'react';
import {
useCallback,
useEffect,
useLayoutEffect,
useMemo,
useState,
} from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux';
import { useLocation } from 'react-router-dom';
import { sideBarCollapse } from 'store/actions/app';
import { AppState } from 'store/reducers';
import AppReducer from 'types/reducer/app';
import { USER_ROLES } from 'types/roles';
import { checkVersionState, isCloudUser, isEECloudUser } from 'utils/app';
import { routeConfig, styles } from './config';
@ -33,6 +40,7 @@ import {
function SideNav(): JSX.Element {
const dispatch = useDispatch();
const [menuItems, setMenuItems] = useState(defaultMenuItems);
const [collapsed, setCollapsed] = useState<boolean>(
getLocalStorageKey(IS_SIDEBAR_COLLAPSED) === 'true',
);
@ -44,36 +52,45 @@ function SideNav(): JSX.Element {
featureResponse,
} = useSelector<AppState, AppReducer>((state) => state.app);
const { data } = useLicense();
const { data, isFetching } = useLicense();
let secondaryMenuItems: MenuItem[] = [];
const isOnBasicPlan =
data?.payload?.licenses?.some(
(license) =>
license.isCurrent && license.planKey === LICENSE_PLAN_KEY.BASIC_PLAN,
) || data?.payload?.licenses === null;
useEffect((): void => {
const isOnboardingEnabled =
featureResponse.data?.find(
(feature) => feature.name === FeatureKeys.ONBOARDING,
)?.active || false;
const menuItems = useMemo(
() =>
defaultMenuItems.filter((item) => {
const isOnboardingEnabled =
featureResponse.data?.find(
(feature) => feature.name === FeatureKeys.ONBOARDING,
)?.active || false;
if (!isOnboardingEnabled || !isCloudUser()) {
let items = [...menuItems];
if (role !== 'ADMIN' || isOnBasicPlan) {
return item.key !== ROUTES.BILLING;
}
items = items.filter((item) => item.key !== ROUTES.GET_STARTED);
if (!isOnboardingEnabled || !isCloudUser()) {
return item.key !== ROUTES.GET_STARTED;
}
setMenuItems(items);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [featureResponse.data]);
return true;
}),
[featureResponse.data, isOnBasicPlan, role],
);
// using a separate useEffect as the license fetching call takes few milliseconds
useEffect(() => {
if (!isFetching) {
let items = [...menuItems];
const isOnBasicPlan =
data?.payload?.licenses?.some(
(license) =>
license.isCurrent && license.planKey === LICENSE_PLAN_KEY.BASIC_PLAN,
) || data?.payload?.licenses === null;
if (role !== USER_ROLES.ADMIN || isOnBasicPlan) {
items = items.filter((item) => item.key !== ROUTES.BILLING);
}
setMenuItems(items);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [data?.payload?.licenses, isFetching, role]);
const { pathname, search } = useLocation();
@ -121,6 +138,7 @@ function SideNav(): JSX.Element {
key: SecondaryMenuItemKey.Support,
label: 'Support',
icon: <LifeBuoy />,
onClick: onClickMenuHandler,
},
];
} else {
@ -172,7 +190,6 @@ function SideNav(): JSX.Element {
mode="vertical"
style={styles}
items={secondaryMenuItems}
onClick={onClickMenuHandler}
/>
</Sider>
);

View File

@ -45,6 +45,6 @@ export const routeConfig: Record<string, QueryParams[]> = {
[ROUTES.USAGE_EXPLORER]: [QueryParams.resourceAttributes],
[ROUTES.VERSION]: [QueryParams.resourceAttributes],
[ROUTES.TRACE_EXPLORER]: [QueryParams.resourceAttributes],
[ROUTES.PIPELINES]: [QueryParams.resourceAttributes],
[ROUTES.LOGS_PIPELINES]: [QueryParams.resourceAttributes],
[ROUTES.WORKSPACE_LOCKED]: [QueryParams.resourceAttributes],
};

View File

@ -9,6 +9,7 @@ import {
LineChartOutlined,
MenuOutlined,
RocketOutlined,
SearchOutlined,
SettingOutlined,
} from '@ant-design/icons';
import ROUTES from 'constants/routes';
@ -35,6 +36,18 @@ const menuItems: SidebarMenu[] = [
key: ROUTES.LOGS_EXPLORER,
label: 'Logs',
icon: <AlignLeftOutlined />,
children: [
{
key: ROUTES.LOGS_EXPLORER,
icon: <SearchOutlined />,
label: 'Logs Explorer',
},
{
key: ROUTES.LOGS_PIPELINES,
icon: <DeploymentUnitOutlined />,
label: 'Logs Pipelines',
},
],
},
{
key: ROUTES.ALL_DASHBOARD,

View File

@ -23,7 +23,7 @@ const breadcrumbNameMap = {
[ROUTES.LOGS]: 'Logs',
[ROUTES.LOGS_EXPLORER]: 'Logs Explorer',
[ROUTES.LIVE_LOGS]: 'Live View',
[ROUTES.PIPELINES]: 'Pipelines',
[ROUTES.LOGS_PIPELINES]: 'Logs Pipelines',
[ROUTES.BILLING]: 'Billing',
[ROUTES.SUPPORT]: 'Support',
[ROUTES.WORKSPACE_LOCKED]: 'Workspace Locked',

View File

@ -80,10 +80,7 @@ export const routesToSkip = [
ROUTES.ORG_SETTINGS,
ROUTES.INGESTION_SETTINGS,
ROUTES.ERROR_DETAIL,
ROUTES.ALERTS_NEW,
ROUTES.EDIT_ALERTS,
ROUTES.LIST_ALL_ALERT,
ROUTES.PIPELINES,
ROUTES.LOGS_PIPELINES,
ROUTES.BILLING,
ROUTES.SUPPORT,
ROUTES.WORKSPACE_LOCKED,

View File

@ -34,8 +34,8 @@ function Version(): JSX.Element {
);
return (
<Card>
<Typography.Title ellipsis level={4}>
<Card style={{ margin: '16px 0' }}>
<Typography.Title ellipsis level={4} style={{ marginTop: 0 }}>
{t('version')}
</Typography.Title>

View File

@ -1,6 +1,6 @@
export const LICENSE_PLAN_KEY = {
ENTERPRISE_PLAN: 'ENTERPRISE_PLAN',
BASIC_PLAN: 'BASIC_PLAN ',
BASIC_PLAN: 'BASIC_PLAN',
};
export const LICENSE_PLAN_STATUS = {

View File

@ -7,6 +7,40 @@ export const licensesSuccessResponse = {
workSpaceBlock: false,
trialConvertedToSubscription: false,
gracePeriodEnd: -1,
licenses: [
{
key: 'testKeyId1',
activationId: 'testActivationId1',
ValidationMessage: '',
isCurrent: false,
planKey: 'ENTERPRISE_PLAN',
ValidFrom: '2022-10-13T13:58:51Z',
ValidUntil: '2023-10-13T19:57:37Z',
status: 'VALID',
},
{
key: 'testKeyId2',
activationId: 'testActivationId2',
ValidationMessage: '',
isCurrent: true,
planKey: 'ENTERPRISE_PLAN',
ValidFrom: '2023-09-12T11:55:43Z',
ValidUntil: '2024-09-11T17:34:29Z',
status: 'VALID',
},
],
},
};
export const licensesSuccessWorkspaceLockedResponse = {
status: 'success',
data: {
trialStart: 1695992049,
trialEnd: 1697806449,
onTrial: false,
workSpaceBlock: true,
trialConvertedToSubscription: false,
gracePeriodEnd: -1,
licenses: [
{
key: 'testKeyId1',

View File

@ -1,46 +1,70 @@
import { licensesSuccessWorkspaceLockedResponse } from 'mocks-server/__mockdata__/licenses';
import { server } from 'mocks-server/server';
import { rest } from 'msw';
import { act, render, screen } from 'tests/test-utils';
import WorkspaceLocked from '.';
describe('WorkspaceLocked', () => {
const apiURL = 'http://localhost/api/v2/licenses';
test('Should render the component', async () => {
server.use(
rest.get(apiURL, (req, res, ctx) =>
res(ctx.status(200), ctx.json(licensesSuccessWorkspaceLockedResponse)),
),
);
act(() => {
render(<WorkspaceLocked />);
});
const workspaceLocked = screen.getByRole('heading', {
const workspaceLocked = await screen.findByRole('heading', {
name: /workspace locked/i,
});
expect(workspaceLocked).toBeInTheDocument();
const gotQuestionText = screen.getByText(/got question?/i);
const gotQuestionText = await screen.findByText(/got question?/i);
expect(gotQuestionText).toBeInTheDocument();
const contactUsLink = screen.getByRole('link', {
const contactUsLink = await screen.findByRole('link', {
name: /contact us/i,
});
expect(contactUsLink).toBeInTheDocument();
});
test('Render for Admin', async () => {
server.use(
rest.get(apiURL, (req, res, ctx) =>
res(ctx.status(200), ctx.json(licensesSuccessWorkspaceLockedResponse)),
),
);
render(<WorkspaceLocked />);
const contactAdminMessage = screen.queryByText(
const contactAdminMessage = await screen.queryByText(
/please contact your administrator for further help/i,
);
expect(contactAdminMessage).not.toBeInTheDocument();
const updateCreditCardBtn = screen.getByRole('button', {
const updateCreditCardBtn = await screen.findByRole('button', {
name: /update credit card/i,
});
expect(updateCreditCardBtn).toBeInTheDocument();
});
test('Render for non Admin', async () => {
server.use(
rest.get(apiURL, (req, res, ctx) =>
res(ctx.status(200), ctx.json(licensesSuccessWorkspaceLockedResponse)),
),
);
render(<WorkspaceLocked />, {}, 'VIEWER');
const updateCreditCardBtn = screen.queryByRole('button', {
const updateCreditCardBtn = await screen.queryByRole('button', {
name: /update credit card/i,
});
expect(updateCreditCardBtn).not.toBeInTheDocument();
const contactAdminMessage = screen.getByText(
const contactAdminMessage = await screen.findByText(
/please contact your administrator for further help/i,
);
expect(contactAdminMessage).toBeInTheDocument();

View File

@ -2,11 +2,13 @@
import './WorkspaceLocked.styles.scss';
import { CreditCardOutlined, LockOutlined } from '@ant-design/icons';
import { Button, Card, Typography } from 'antd';
import { Button, Card, Skeleton, Typography } from 'antd';
import updateCreditCardApi from 'api/billing/checkout';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import ROUTES from 'constants/routes';
import useLicense from 'hooks/useLicense';
import { useNotifications } from 'hooks/useNotifications';
import history from 'lib/history';
import { useCallback, useEffect, useState } from 'react';
import { useMutation } from 'react-query';
import { useSelector } from 'react-redux';
@ -22,16 +24,28 @@ export default function WorkspaceBlocked(): JSX.Element {
const { notifications } = useNotifications();
const { isFetching, data: licensesData } = useLicense();
const {
isFetching: isFetchingLicenseData,
isLoading: isLoadingLicenseData,
data: licensesData,
} = useLicense();
useEffect(() => {
const activeValidLicense =
licensesData?.payload?.licenses?.find(
(license) => license.isCurrent === true,
) || null;
if (!isFetchingLicenseData) {
const shouldBlockWorkspace = licensesData?.payload?.workSpaceBlock;
setActiveLicense(activeValidLicense);
}, [isFetching, licensesData]);
if (!shouldBlockWorkspace) {
history.push(ROUTES.APPLICATION);
}
const activeValidLicense =
licensesData?.payload?.licenses?.find(
(license) => license.isCurrent === true,
) || null;
setActiveLicense(activeValidLicense);
}
}, [isFetchingLicenseData, licensesData]);
const { mutate: updateCreditCard, isLoading } = useMutation(
updateCreditCardApi,
@ -62,36 +76,41 @@ export default function WorkspaceBlocked(): JSX.Element {
return (
<Card className="workspace-locked-container">
<LockOutlined style={{ fontSize: '36px', color: '#08c' }} />
<Typography.Title level={4}> Workspace Locked </Typography.Title>
<Typography.Paragraph className="workpace-locked-details">
You have been locked out of your workspace because your trial ended without
an upgrade to a paid plan. Your data will continue to be ingested till{' '}
{getFormattedDate(licensesData?.payload?.gracePeriodEnd || Date.now())} , at
which point we will drop all the ingested data and terminate the account.
{!isAdmin && 'Please contact your administrator for further help'}
</Typography.Paragraph>
{isAdmin && (
<Button
className="update-credit-card-btn"
type="primary"
icon={<CreditCardOutlined />}
size="middle"
loading={isLoading}
onClick={handleUpdateCreditCard}
>
Update Credit Card
</Button>
{isLoadingLicenseData || !licensesData?.payload?.workSpaceBlock ? (
<Skeleton />
) : (
<>
<LockOutlined style={{ fontSize: '36px', color: '#08c' }} />
<Typography.Title level={4}> Workspace Locked </Typography.Title>
<Typography.Paragraph className="workpace-locked-details">
You have been locked out of your workspace because your trial ended
without an upgrade to a paid plan. Your data will continue to be ingested
till{' '}
{getFormattedDate(licensesData?.payload?.gracePeriodEnd || Date.now())} ,
at which point we will drop all the ingested data and terminate the
account.
{!isAdmin && 'Please contact your administrator for further help'}
</Typography.Paragraph>
{isAdmin && (
<Button
className="update-credit-card-btn"
type="primary"
icon={<CreditCardOutlined />}
size="middle"
loading={isLoading}
onClick={handleUpdateCreditCard}
>
Update Credit Card
</Button>
)}
<div className="contact-us">
Got Questions?
<span>
<a href="mailto:support@signoz.io"> Contact Us </a>
</span>
</div>
</>
)}
<div className="contact-us">
Got Questions?
<span>
<a href="mailto:support@signoz.io"> Contact Us </a>
</span>
</div>
</Card>
);
}

View File

@ -9,6 +9,10 @@ export interface GettableAlert extends AlertDef {
alert: string;
state: string;
disabled: boolean;
createAt: string;
createBy: string;
updateAt: string;
updateBy: string;
}
export type PayloadProps = {

View File

@ -42,6 +42,8 @@ export interface Dashboard {
uuid: string;
created_at: string;
updated_at: string;
created_by: string;
updated_by: string;
data: DashboardData;
}

View File

@ -16,6 +16,17 @@ export interface ProcessorData {
on_error?: string;
field?: string;
value?: string;
// trace parser fields.
trace_id?: {
parse_from: string;
};
span_id?: {
parse_from: string;
};
trace_flags?: {
parse_from: string;
};
}
export interface PipelineData {

View File

@ -3,3 +3,9 @@ export type VIEWER = 'VIEWER';
export type EDITOR = 'EDITOR';
export type ROLES = ADMIN | VIEWER | EDITOR;
export const USER_ROLES = {
ADMIN: 'ADMIN',
VIEWER: 'VIEWER',
EDITOR: 'EDITOR',
};

View File

@ -75,9 +75,8 @@ export const routePermission: Record<keyof typeof ROUTES, ROLES[]> = {
LIVE_LOGS: ['ADMIN', 'EDITOR', 'VIEWER'],
LIST_LICENSES: ['ADMIN'],
LOGS_INDEX_FIELDS: ['ADMIN', 'EDITOR', 'VIEWER'],
LOGS_PIPELINE: ['ADMIN', 'EDITOR', 'VIEWER'],
LOGS_PIPELINES: ['ADMIN', 'EDITOR', 'VIEWER'],
TRACE_EXPLORER: ['ADMIN', 'EDITOR', 'VIEWER'],
PIPELINES: ['ADMIN', 'EDITOR', 'VIEWER'],
GET_STARTED: ['ADMIN', 'EDITOR', 'VIEWER'],
WORKSPACE_LOCKED: ['ADMIN', 'EDITOR', 'VIEWER'],
BILLING: ['ADMIN', 'EDITOR', 'VIEWER'],

View File

@ -218,6 +218,16 @@
"@jridgewell/trace-mapping" "^0.3.17"
jsesc "^2.5.1"
"@babel/generator@^7.23.0":
version "7.23.0"
resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.23.0.tgz#df5c386e2218be505b34837acbcb874d7a983420"
integrity sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==
dependencies:
"@babel/types" "^7.23.0"
"@jridgewell/gen-mapping" "^0.3.2"
"@jridgewell/trace-mapping" "^0.3.17"
jsesc "^2.5.1"
"@babel/helper-annotate-as-pure@^7.15.4", "@babel/helper-annotate-as-pure@^7.16.0", "@babel/helper-annotate-as-pure@^7.18.6":
version "7.18.6"
resolved "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.18.6.tgz"
@ -354,6 +364,11 @@
resolved "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz"
integrity sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==
"@babel/helper-environment-visitor@^7.22.20":
version "7.22.20"
resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz#96159db61d34a29dba454c959f5ae4a649ba9167"
integrity sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==
"@babel/helper-environment-visitor@^7.22.5":
version "7.22.5"
resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.5.tgz#f06dd41b7c1f44e1f8da6c4055b41ab3a09a7e98"
@ -382,6 +397,14 @@
"@babel/template" "^7.22.5"
"@babel/types" "^7.22.5"
"@babel/helper-function-name@^7.23.0":
version "7.23.0"
resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz#1f9a3cdbd5b2698a670c30d2735f9af95ed52759"
integrity sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==
dependencies:
"@babel/template" "^7.22.15"
"@babel/types" "^7.23.0"
"@babel/helper-hoist-variables@^7.18.6":
version "7.18.6"
resolved "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz"
@ -593,6 +616,11 @@
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.15.tgz#601fa28e4cc06786c18912dca138cec73b882044"
integrity sha512-4E/F9IIEi8WR94324mbDUMo074YTheJmd7eZF5vITTeYchqAi6sYXRLHUVsmkdmY4QjfKTcB2jB7dVP3NaBElQ==
"@babel/helper-validator-identifier@^7.22.20":
version "7.22.20"
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz#c4ae002c61d2879e724581d96665583dbc1dc0e0"
integrity sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==
"@babel/helper-validator-identifier@^7.22.5":
version "7.22.5"
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.5.tgz#9544ef6a33999343c8740fa51350f30eeaaaf193"
@ -692,6 +720,11 @@
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.22.15.tgz#d34592bfe288a32e741aa0663dbc4829fcd55160"
integrity sha512-RWmQ/sklUN9BvGGpCDgSubhHWfAx24XDTDObup4ffvxaYsptOg2P3KG0j+1eWKLxpkX0j0uHxmpq2Z1SP/VhxA==
"@babel/parser@^7.23.0":
version "7.23.0"
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.23.0.tgz#da950e622420bf96ca0d0f2909cdddac3acd8719"
integrity sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==
"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.18.6":
version "7.18.6"
resolved "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.18.6.tgz"
@ -2047,51 +2080,19 @@
"@babel/parser" "^7.22.5"
"@babel/types" "^7.22.5"
"@babel/traverse@^7.20.5", "@babel/traverse@^7.20.7", "@babel/traverse@^7.21.0", "@babel/traverse@^7.21.2", "@babel/traverse@^7.21.4", "@babel/traverse@^7.4.5", "@babel/traverse@^7.7.0", "@babel/traverse@^7.7.2":
version "7.21.4"
resolved "https://registry.npmjs.org/@babel/traverse/-/traverse-7.21.4.tgz"
integrity sha512-eyKrRHKdyZxqDm+fV1iqL9UAHMoIg0nDaGqfIOd8rKH17m5snv7Gn4qgjBoFfLz9APvjFU/ICT00NVCv1Epp8Q==
dependencies:
"@babel/code-frame" "^7.21.4"
"@babel/generator" "^7.21.4"
"@babel/helper-environment-visitor" "^7.18.9"
"@babel/helper-function-name" "^7.21.0"
"@babel/helper-hoist-variables" "^7.18.6"
"@babel/helper-split-export-declaration" "^7.18.6"
"@babel/parser" "^7.21.4"
"@babel/types" "^7.21.4"
debug "^4.1.0"
globals "^11.1.0"
"@babel/traverse@^7.22.11":
version "7.22.11"
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.22.11.tgz#71ebb3af7a05ff97280b83f05f8865ac94b2027c"
integrity sha512-mzAenteTfomcB7mfPtyi+4oe5BZ6MXxWcn4CX+h4IRJ+OOGXBrWU6jDQavkQI9Vuc5P+donFabBfFCcmWka9lQ==
dependencies:
"@babel/code-frame" "^7.22.10"
"@babel/generator" "^7.22.10"
"@babel/helper-environment-visitor" "^7.22.5"
"@babel/helper-function-name" "^7.22.5"
"@babel/helper-hoist-variables" "^7.22.5"
"@babel/helper-split-export-declaration" "^7.22.6"
"@babel/parser" "^7.22.11"
"@babel/types" "^7.22.11"
debug "^4.1.0"
globals "^11.1.0"
"@babel/traverse@^7.22.15":
version "7.22.15"
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.22.15.tgz#75be4d2d6e216e880e93017f4e2389aeb77ef2d9"
integrity sha512-DdHPwvJY0sEeN4xJU5uRLmZjgMMDIvMPniLuYzUVXj/GGzysPl0/fwt44JBkyUIzGJPV8QgHMcQdQ34XFuKTYQ==
"@babel/traverse@^7.20.5", "@babel/traverse@^7.20.7", "@babel/traverse@^7.21.0", "@babel/traverse@^7.21.2", "@babel/traverse@^7.21.4", "@babel/traverse@^7.22.11", "@babel/traverse@^7.22.15", "@babel/traverse@^7.4.5", "@babel/traverse@^7.7.0", "@babel/traverse@^7.7.2":
version "7.23.2"
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.23.2.tgz#329c7a06735e144a506bdb2cad0268b7f46f4ad8"
integrity sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==
dependencies:
"@babel/code-frame" "^7.22.13"
"@babel/generator" "^7.22.15"
"@babel/helper-environment-visitor" "^7.22.5"
"@babel/helper-function-name" "^7.22.5"
"@babel/generator" "^7.23.0"
"@babel/helper-environment-visitor" "^7.22.20"
"@babel/helper-function-name" "^7.23.0"
"@babel/helper-hoist-variables" "^7.22.5"
"@babel/helper-split-export-declaration" "^7.22.6"
"@babel/parser" "^7.22.15"
"@babel/types" "^7.22.15"
"@babel/parser" "^7.23.0"
"@babel/types" "^7.23.0"
debug "^4.1.0"
globals "^11.1.0"
@ -2122,6 +2123,15 @@
"@babel/helper-validator-identifier" "^7.22.15"
to-fast-properties "^2.0.0"
"@babel/types@^7.23.0":
version "7.23.0"
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.23.0.tgz#8c1f020c9df0e737e4e247c0619f58c68458aaeb"
integrity sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==
dependencies:
"@babel/helper-string-parser" "^7.22.5"
"@babel/helper-validator-identifier" "^7.22.20"
to-fast-properties "^2.0.0"
"@bcoe/v8-coverage@^0.2.3":
version "0.2.3"
resolved "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz"

14
go.mod
View File

@ -5,6 +5,7 @@ go 1.21
require (
github.com/ClickHouse/clickhouse-go/v2 v2.14.0
github.com/SigNoz/govaluate v0.0.0-20220522085550-d19c08c206cb
github.com/SigNoz/signoz-otel-collector v0.79.12
github.com/SigNoz/zap_otlp/zap_otlp_encoder v0.0.0-20230822164844-1b861a431974
github.com/SigNoz/zap_otlp/zap_otlp_sync v0.0.0-20230822164844-1b861a431974
github.com/antonmedv/expr v1.12.5
@ -64,7 +65,7 @@ require (
golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1
golang.org/x/net v0.17.0
golang.org/x/oauth2 v0.10.0
google.golang.org/grpc v1.57.0
google.golang.org/grpc v1.57.1
google.golang.org/protobuf v1.31.0
gopkg.in/segmentio/analytics-go.v3 v3.1.0
gopkg.in/yaml.v2 v2.4.0
@ -93,7 +94,7 @@ require (
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/edsrzf/mmap-go v1.1.0 // indirect
github.com/felixge/httpsnoop v1.0.3 // indirect
github.com/form3tech-oss/jwt-go v3.2.2+incompatible // indirect
github.com/form3tech-oss/jwt-go v3.2.5+incompatible // indirect
github.com/go-faster/city v1.0.1 // indirect
github.com/go-faster/errors v0.6.1 // indirect
github.com/go-logfmt/logfmt v0.6.0 // indirect
@ -147,17 +148,18 @@ require (
github.com/robfig/cron/v3 v3.0.1 // indirect
github.com/segmentio/asm v1.2.0 // indirect
github.com/segmentio/backo-go v1.0.1 // indirect
github.com/shirou/gopsutil/v3 v3.23.4 // indirect
github.com/shoenig/go-m1cpu v0.1.5 // indirect
github.com/shirou/gopsutil/v3 v3.23.5 // indirect
github.com/shoenig/go-m1cpu v0.1.6 // indirect
github.com/shopspring/decimal v1.3.1 // indirect
github.com/sirupsen/logrus v1.9.0 // indirect
github.com/sirupsen/logrus v1.9.2 // indirect
github.com/smarty/assertions v1.15.0 // indirect
github.com/spf13/cobra v1.7.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/tklauser/go-sysconf v0.3.11 // indirect
github.com/tklauser/numcpus v0.6.0 // indirect
github.com/vjeantet/grok v1.0.1 // indirect
github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c // indirect
github.com/yusufpapurcu/wmi v1.2.2 // indirect
github.com/yusufpapurcu/wmi v1.2.3 // indirect
go.opencensus.io v0.24.0 // indirect
go.opentelemetry.io/collector/featuregate v1.0.0-rcv0012 // indirect
go.opentelemetry.io/collector/semconv v0.81.0 // indirect

72
go.sum
View File

@ -99,6 +99,8 @@ github.com/SigNoz/govaluate v0.0.0-20220522085550-d19c08c206cb h1:bneLSKPf9YUSFm
github.com/SigNoz/govaluate v0.0.0-20220522085550-d19c08c206cb/go.mod h1:JznGDNg9x1cujDKa22RaQOimOvvEfy3nxzDGd8XDgmA=
github.com/SigNoz/prometheus v1.9.78 h1:bB3yuDrRzi/Mv00kWayR9DZbyjTuGfendSqISyDcXiY=
github.com/SigNoz/prometheus v1.9.78/go.mod h1:MffmFu2qFILQrOHehx3D0XjYtaZMVfI+Ppeiv98x4Ww=
github.com/SigNoz/signoz-otel-collector v0.79.12 h1:0yDMhcN7Taa8WrFv8YrHRaDvRxHqLfp5c6w1TSEWk+I=
github.com/SigNoz/signoz-otel-collector v0.79.12/go.mod h1:MXjHt3atjTAF2Wrqu0W7Xx+oJ1yb8UfpsNu+A8Ssjtg=
github.com/SigNoz/zap_otlp v0.1.0 h1:T7rRcFN87GavY8lDGZj0Z3Xv6OhJA6Pj3I9dNPmqvRc=
github.com/SigNoz/zap_otlp v0.1.0/go.mod h1:lcHvbDbRgvDnPxo9lDlaL1JK2PyOyouP/C3ynnYIvyo=
github.com/SigNoz/zap_otlp/zap_otlp_encoder v0.0.0-20230822164844-1b861a431974 h1:PKVgdf83Yw+lZJbFtNGBgqXiXNf3+kOXW2qZ7Ms7OaY=
@ -196,8 +198,8 @@ github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m3
github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/docker v24.0.5+incompatible h1:WmgcE4fxyI6EEXxBRxsHnZXrO1pQ3smi0k/jho4HLeY=
github.com/docker/docker v24.0.5+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
github.com/docker/go-connections v0.4.1-0.20210727194412-58542c764a11 h1:IPrmumsT9t5BS7XcPhgsCTlkWbYg80SEXUzDpReaU6Y=
github.com/docker/go-connections v0.4.1-0.20210727194412-58542c764a11/go.mod h1:a6bNUGTbQBsY6VRHTr4h/rkOXjl244DyRD0tx3fgq4Q=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
@ -205,8 +207,8 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/edsrzf/mmap-go v1.1.0 h1:6EUwBLQ/Mcr1EYLE4Tn1VdW1A4ckqCQWZBw8Hr0kjpQ=
github.com/edsrzf/mmap-go v1.1.0/go.mod h1:19H/e8pUPLicwkyNgOykDXkJ9F0MHE+Z52B8EIth78Q=
github.com/emicklei/go-restful/v3 v3.9.0 h1:XwGDlfxEnQZzuopoqxwSEllNcCOM9DhhFyhFIIGKwxE=
github.com/emicklei/go-restful/v3 v3.9.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
github.com/emicklei/go-restful/v3 v3.10.1 h1:rc42Y5YTp7Am7CS630D7JmhRjq4UlEUuEKfrDac4bSQ=
github.com/emicklei/go-restful/v3 v3.10.1/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
@ -229,8 +231,9 @@ github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga
github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk=
github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/form3tech-oss/jwt-go v3.2.2+incompatible h1:TcekIExNqud5crz4xD2pavyTgWiPvpYe4Xau31I0PRk=
github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k=
github.com/form3tech-oss/jwt-go v3.2.5+incompatible h1:/l4kBbb4/vGSsdtB5nUe8L7B9mImVMaBPw9L/0TBHU8=
github.com/form3tech-oss/jwt-go v3.2.5+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
@ -270,8 +273,8 @@ github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE=
github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs=
github.com/go-openapi/jsonreference v0.20.1 h1:FBLnyygC4/IZZr893oiomc9XaghoveYTrLC1F86HID8=
github.com/go-openapi/jsonreference v0.20.1/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k=
github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE=
github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k=
github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g=
github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=
@ -280,8 +283,9 @@ github.com/go-redis/redismock/v8 v8.11.5 h1:RJFIiua58hrBrSpXhnGX3on79AU3S271H4Zh
github.com/go-redis/redismock/v8 v8.11.5/go.mod h1:UaAU9dEe1C+eGr+FHV5prCWIt0hafyPWbGMEWE0UWdA=
github.com/go-resty/resty/v2 v2.7.0 h1:me+K9p3uhSmXtrBZ4k9jcEAfJmuC8IivWHwaLZwPrFY=
github.com/go-resty/resty/v2 v2.7.0/go.mod h1:9PWDzw47qPphMRFfhsyk0NnSgvluHcljSMVIq3w7q0I=
github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI=
github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
github.com/go-test/deep v1.0.2-0.20181118220953-042da051cf31/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
@ -337,8 +341,8 @@ github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/gnostic v0.5.7-v3refs h1:FhTMOKj2VhjpouxvWJAV1TL304uMlb9zcDqkl6cEI54=
github.com/google/gnostic v0.5.7-v3refs/go.mod h1:73MKFl6jIHelAJNaBGFzt3SPtZULs9dYrGFt8OiIsHQ=
github.com/google/gnostic v0.6.9 h1:ZK/5VhkoX835RikCHpSUJV9a+S3e1zLh59YnyWeBW+0=
github.com/google/gnostic v0.6.9/go.mod h1:Nm8234We1lq6iB9OmlgNv3nH91XLLVZHCDayfA3xq+E=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
@ -358,8 +362,8 @@ github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeN
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gofuzz v1.1.0 h1:Hsa8mG0dQ46ij8Sl2AYJDUv1oA9/d6Vk+3LG99Oe02g=
github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
@ -443,8 +447,8 @@ github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+l
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/hashicorp/go-plugin v1.0.1/go.mod h1:++UyYGoz3o5w9ZzAdZxtQKrWWP+iqPBn3cQptSMzBuY=
github.com/hashicorp/go-retryablehttp v0.5.4/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs=
github.com/hashicorp/go-retryablehttp v0.7.1 h1:sUiuQAnLlbvmExtFQs72iFW/HXeUn8Z1aJLQ4LJJbTQ=
github.com/hashicorp/go-retryablehttp v0.7.1/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY=
github.com/hashicorp/go-retryablehttp v0.7.2 h1:AcYqCvkpalPnPF2pn0KamgwamS42TqUDDYFRKq/RAd0=
github.com/hashicorp/go-retryablehttp v0.7.2/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8=
github.com/hashicorp/go-rootcerts v1.0.1/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc=
github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
@ -472,6 +476,7 @@ github.com/hashicorp/vault/api v1.0.4/go.mod h1:gDcqh3WGcR1cpF5AJz/B1UFheUEneMoI
github.com/hashicorp/vault/sdk v0.1.13/go.mod h1:B+hVj7TpuQY1Y/GPbCpffmgd+tSEwvhkWnjtSYCaS2M=
github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM=
github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM=
github.com/hetznercloud/hcloud-go v1.41.0 h1:KJGFRRc68QiVu4PrEP5BmCQVveCP2CM26UGQUKGpIUs=
github.com/hetznercloud/hcloud-go/v2 v2.0.0 h1:Sg1DJ+MAKvbYAqaBaq9tPbwXBS2ckPIaMtVdUjKu+4g=
github.com/hetznercloud/hcloud-go/v2 v2.0.0/go.mod h1:4iUG2NG8b61IAwNx6UsMWQ6IfIf/i1RsG0BbsKAyR5Q=
github.com/hjson/hjson-go/v4 v4.0.0 h1:wlm6IYYqHjOdXH1gHev4VoXCaW20HdQAGCxdOEEg2cs=
@ -479,8 +484,8 @@ github.com/hjson/hjson-go/v4 v4.0.0/go.mod h1:KaYt3bTw3zhBjYqnXkYywcYctk0A2nxeEF
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU=
github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk=
github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/ionos-cloud/sdk-go/v6 v6.1.8 h1:493wE/BkZxJf7x79UCE0cYGPZoqQcPiEBALvt7uVGY0=
@ -537,8 +542,9 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0=
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/linode/linodego v1.19.0 h1:n4WJrcr9+30e9JGZ6DI0nZbm5SdAj1kSwvvt/998YUw=
github.com/linode/linodego v1.19.0/go.mod h1:XZFR+yJ9mm2kwf6itZ6SCpu+6w3KnIevV0Uu5HNWJgQ=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
@ -750,12 +756,12 @@ github.com/segmentio/backo-go v1.0.1 h1:68RQccglxZeyURy93ASB/2kc9QudzgIDexJ927N+
github.com/segmentio/backo-go v1.0.1/go.mod h1:9/Rh6yILuLysoQnZ2oNooD2g7aBnvM7r/fNVxRNWfBc=
github.com/sethvargo/go-password v0.2.0 h1:BTDl4CC/gjf/axHMaDQtw507ogrXLci6XRiLc7i/UHI=
github.com/sethvargo/go-password v0.2.0/go.mod h1:Ym4Mr9JXLBycr02MFuVQ/0JHidNetSgbzutTr3zsYXE=
github.com/shirou/gopsutil/v3 v3.23.4 h1:hZwmDxZs7Ewt75DV81r4pFMqbq+di2cbt9FsQBqLD2o=
github.com/shirou/gopsutil/v3 v3.23.4/go.mod h1:ZcGxyfzAMRevhUR2+cfhXDH6gQdFYE/t8j1nsU4mPI8=
github.com/shoenig/go-m1cpu v0.1.5 h1:LF57Z/Fpb/WdGLjt2HZilNnmZOxg/q2bSKTQhgbrLrQ=
github.com/shoenig/go-m1cpu v0.1.5/go.mod h1:Wwvst4LR89UxjeFtLRMrpgRiyY4xPsejnVZym39dbAQ=
github.com/shoenig/test v0.6.3 h1:GVXWJFk9PiOjN0KoJ7VrJGH6uLPnqxR7/fe3HUPfE0c=
github.com/shoenig/test v0.6.3/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k=
github.com/shirou/gopsutil/v3 v3.23.5 h1:5SgDCeQ0KW0S4N0znjeM/eFHXXOKyv2dVNgRq/c9P6Y=
github.com/shirou/gopsutil/v3 v3.23.5/go.mod h1:Ng3Maa27Q2KARVJ0SPZF5NdrQSC3XHKP8IIWrHgMeLY=
github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ=
github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU=
github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k=
github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8=
github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
@ -763,8 +769,8 @@ github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPx
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.5.0/go.mod h1:+F7Ogzej0PZc/94MaYx/nvG9jOFMD2osvC3s+Squfpo=
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/sirupsen/logrus v1.9.2 h1:oxx1eChJGI6Uks2ZC4W1zpLlVgqB8ner4EuQwV4Ik1Y=
github.com/sirupsen/logrus v1.9.2/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/smarty/assertions v1.15.0 h1:cR//PqUBUiQRakZWqBiFFQ9wb8emQGDb0HeGdqGByCY=
github.com/smarty/assertions v1.15.0/go.mod h1:yABtdzeQs6l1brC900WlRNwj6ZR55d7B+E8C6HtKdec=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
@ -777,8 +783,9 @@ github.com/smartystreets/goconvey v1.8.1 h1:qGjIddxOk4grTu9JPOU31tVfq3cNdBlNa5sS
github.com/smartystreets/goconvey v1.8.1/go.mod h1:+/u4qLyY6x1jReYOp7GOM2FSt8aP9CzCZL03bI28W60=
github.com/soheilhy/cmux v0.1.5 h1:jjzc5WVemNEDTLwv9tlmemhC73tI08BNOIGwBOo10Js=
github.com/soheilhy/cmux v0.1.5/go.mod h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE1GqG0=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72 h1:qLC7fQah7D6K1B0ujays3HV9gkFtllcxhzImRR7ArPQ=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
@ -800,6 +807,7 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stvp/go-udp-testing v0.0.0-20201019212854-469649b16807/go.mod h1:7jxmlfBCDBXRzr0eAQJ48XC1hBu1np4CS5+cHEYfwpc=
@ -811,6 +819,8 @@ github.com/tklauser/numcpus v0.6.0/go.mod h1:FEZLMke0lhOUG6w2JadTzp0a+Nl8PF/GFkQ
github.com/urfave/cli v1.22.5/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/urfave/negroni v1.0.0 h1:kIimOitoypq34K7TG7DUaJ9kq/N4Ofuwi1sjz0KipXc=
github.com/urfave/negroni v1.0.0/go.mod h1:Meg73S6kFm/4PpbYdq35yYWoCZ9mS/YSx+lKnmiohz4=
github.com/vjeantet/grok v1.0.1 h1:2rhIR7J4gThTgcZ1m2JY4TrJZNgjn985U28kT2wQrJ4=
github.com/vjeantet/grok v1.0.1/go.mod h1:ax1aAchzC6/QMXMcyzHQGZWaW1l195+uMYIkCWPCNIo=
github.com/vultr/govultr/v2 v2.17.2 h1:gej/rwr91Puc/tgh+j33p/BLR16UrIPnSr+AIwYWZQs=
github.com/vultr/govultr/v2 v2.17.2/go.mod h1:ZFOKGWmgjytfyjeyAdhQlSWwTjh2ig+X49cAp50dzXI=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
@ -825,8 +835,8 @@ github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yusufpapurcu/wmi v1.2.2 h1:KBNDSne4vP5mbSWnJbO+51IMOXJB67QiYCSBrubbPRg=
github.com/yusufpapurcu/wmi v1.2.2/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw=
github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
go.etcd.io/etcd/api/v3 v3.5.4/go.mod h1:5GB2vv4A4AOn3yk7MftYGHkUfGtDHnEraIjym4dYz5A=
go.etcd.io/etcd/client/pkg/v3 v3.5.4/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g=
go.etcd.io/etcd/client/v3 v3.5.4/go.mod h1:ZaRkVgBZC+L+dLCjTcF1hRXpgZXQPOvnA/Ak/gq3kiY=
@ -1136,7 +1146,7 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
@ -1395,8 +1405,8 @@ google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11
google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
google.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
google.golang.org/grpc v1.47.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
google.golang.org/grpc v1.57.0 h1:kfzNeI/klCGD2YPMUlaGNT3pxvYfga7smW3Vth8Zsiw=
google.golang.org/grpc v1.57.0/go.mod h1:Sd+9RMTACXwmub0zcNY2c4arhtrbBYD1AUHI/dt16Mo=
google.golang.org/grpc v1.57.1 h1:upNTNqv0ES+2ZOOqACwVtS3Il8M12/+Hz41RCPzAjQg=
google.golang.org/grpc v1.57.1/go.mod h1:Sd+9RMTACXwmub0zcNY2c4arhtrbBYD1AUHI/dt16Mo=
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=

View File

@ -1,5 +1,5 @@
# use a minimal alpine image
FROM alpine:3.17
FROM alpine:3.18.3
# Add Maintainer Info
LABEL maintainer="signoz"

View File

@ -12,6 +12,7 @@ import (
"go.signoz.io/signoz/pkg/query-service/agentConf/sqlite"
"go.signoz.io/signoz/pkg/query-service/model"
"go.uber.org/zap"
"golang.org/x/exp/slices"
)
func init() {
@ -61,6 +62,13 @@ func (r *Repo) GetConfigHistory(
return nil, model.InternalError(err)
}
incompleteStatuses := []DeployStatus{DeployInitiated, Deploying}
for idx := 1; idx < len(c); idx++ {
if slices.Contains(incompleteStatuses, c[idx].DeployStatus) {
c[idx].DeployStatus = DeployStatusUnknown
}
}
return c, nil
}

View File

@ -18,11 +18,12 @@ const (
type DeployStatus string
const (
PendingDeploy DeployStatus = "DIRTY"
Deploying DeployStatus = "DEPLOYING"
Deployed DeployStatus = "DEPLOYED"
DeployInitiated DeployStatus = "IN_PROGRESS"
DeployFailed DeployStatus = "FAILED"
PendingDeploy DeployStatus = "DIRTY"
Deploying DeployStatus = "DEPLOYING"
Deployed DeployStatus = "DEPLOYED"
DeployInitiated DeployStatus = "IN_PROGRESS"
DeployFailed DeployStatus = "FAILED"
DeployStatusUnknown DeployStatus = "UNKNOWN"
)
type ConfigVersion struct {

View File

@ -3493,10 +3493,16 @@ func (r *ClickHouseReader) UpdateLogField(ctx context.Context, field *model.Upda
return &model.ApiError{Err: err, Typ: model.ErrorInternal}
}
query = fmt.Sprintf("ALTER TABLE %s.%s ON CLUSTER %s ADD COLUMN IF NOT EXISTS %s %s MATERIALIZED -1",
defaultValueDistributed := "-1"
if strings.ToLower(field.DataType) == "bool" {
defaultValueDistributed = "false"
field.IndexType = "set(2)"
}
query = fmt.Sprintf("ALTER TABLE %s.%s ON CLUSTER %s ADD COLUMN IF NOT EXISTS %s %s MATERIALIZED %s",
r.logsDB, r.logsTable,
r.cluster,
colname, field.DataType,
defaultValueDistributed,
)
err = r.db.Exec(ctx, query)
if err != nil {

View File

@ -140,7 +140,7 @@ func (ic *LogParsingPipelineController) PreviewLogsPipelines(
ctx context.Context,
request *PipelinesPreviewRequest,
) (*PipelinesPreviewResponse, *model.ApiError) {
result, err := SimulatePipelinesProcessing(
result, _, err := SimulatePipelinesProcessing(
ctx, request.Pipelines, request.Logs,
)

View File

@ -41,6 +41,7 @@ type PipelineOperator struct {
ID string `json:"id,omitempty" yaml:"id,omitempty"`
Output string `json:"output,omitempty" yaml:"output,omitempty"`
OnError string `json:"on_error,omitempty" yaml:"on_error,omitempty"`
If string `json:"if,omitempty" yaml:"if,omitempty"`
// don't need the following in the final config
OrderId int `json:"orderId" yaml:"-"`
@ -48,20 +49,20 @@ type PipelineOperator struct {
Name string `json:"name,omitempty" yaml:"-"`
// optional keys depending on the type
ParseTo string `json:"parse_to,omitempty" yaml:"parse_to,omitempty"`
Pattern string `json:"pattern,omitempty" yaml:"pattern,omitempty"`
Regex string `json:"regex,omitempty" yaml:"regex,omitempty"`
ParseFrom string `json:"parse_from,omitempty" yaml:"parse_from,omitempty"`
Timestamp *TimestampParser `json:"timestamp,omitempty" yaml:"timestamp,omitempty"`
TraceParser *TraceParser `json:"trace_parser,omitempty" yaml:"trace_parser,omitempty"`
Field string `json:"field,omitempty" yaml:"field,omitempty"`
Value string `json:"value,omitempty" yaml:"value,omitempty"`
From string `json:"from,omitempty" yaml:"from,omitempty"`
To string `json:"to,omitempty" yaml:"to,omitempty"`
Expr string `json:"expr,omitempty" yaml:"expr,omitempty"`
Routes *[]Route `json:"routes,omitempty" yaml:"routes,omitempty"`
Fields []string `json:"fields,omitempty" yaml:"fields,omitempty"`
Default string `json:"default,omitempty" yaml:"default,omitempty"`
ParseTo string `json:"parse_to,omitempty" yaml:"parse_to,omitempty"`
Pattern string `json:"pattern,omitempty" yaml:"pattern,omitempty"`
Regex string `json:"regex,omitempty" yaml:"regex,omitempty"`
ParseFrom string `json:"parse_from,omitempty" yaml:"parse_from,omitempty"`
Timestamp *TimestampParser `json:"timestamp,omitempty" yaml:"timestamp,omitempty"`
*TraceParser `yaml:",inline,omitempty"`
Field string `json:"field,omitempty" yaml:"field,omitempty"`
Value string `json:"value,omitempty" yaml:"value,omitempty"`
From string `json:"from,omitempty" yaml:"from,omitempty"`
To string `json:"to,omitempty" yaml:"to,omitempty"`
Expr string `json:"expr,omitempty" yaml:"expr,omitempty"`
Routes *[]Route `json:"routes,omitempty" yaml:"routes,omitempty"`
Fields []string `json:"fields,omitempty" yaml:"fields,omitempty"`
Default string `json:"default,omitempty" yaml:"default,omitempty"`
}
type TimestampParser struct {

View File

@ -1,6 +1,9 @@
package logparsingpipeline
import (
"fmt"
"strings"
"github.com/pkg/errors"
"go.signoz.io/signoz/pkg/query-service/constants"
"go.signoz.io/signoz/pkg/query-service/queryBuilderToExpr"
@ -72,6 +75,39 @@ func getOperators(ops []PipelineOperator) []PipelineOperator {
if len(filteredOp) > 0 {
filteredOp[len(filteredOp)-1].Output = operator.ID
}
if operator.Type == "regex_parser" {
parseFromParts := strings.Split(operator.ParseFrom, ".")
parseFromPath := strings.Join(parseFromParts, "?.")
operator.If = fmt.Sprintf(
`%s != nil && %s matches "%s"`,
parseFromPath,
parseFromPath,
strings.ReplaceAll(
strings.ReplaceAll(operator.Regex, `\`, `\\`),
`"`, `\"`,
),
)
} else if operator.Type == "json_parser" {
parseFromParts := strings.Split(operator.ParseFrom, ".")
parseFromPath := strings.Join(parseFromParts, "?.")
operator.If = fmt.Sprintf(`%s != nil && %s matches "^\\s*{.*}\\s*$"`, parseFromPath, parseFromPath)
} else if operator.Type == "move" || operator.Type == "copy" {
fromParts := strings.Split(operator.From, ".")
fromPath := strings.Join(fromParts, "?.")
operator.If = fmt.Sprintf(`%s != nil`, fromPath)
} else if operator.Type == "remove" {
fieldParts := strings.Split(operator.Field, ".")
fieldPath := strings.Join(fieldParts, "?.")
operator.If = fmt.Sprintf(`%s != nil`, fieldPath)
} else if operator.Type == "trace_parser" {
cleanTraceParser(&operator)
}
filteredOp = append(filteredOp, operator)
} else if i == len(ops)-1 && len(filteredOp) != 0 {
filteredOp[len(filteredOp)-1].Output = ""
@ -79,3 +115,15 @@ func getOperators(ops []PipelineOperator) []PipelineOperator {
}
return filteredOp
}
func cleanTraceParser(operator *PipelineOperator) {
if operator.TraceId != nil && len(operator.TraceId.ParseFrom) < 1 {
operator.TraceId = nil
}
if operator.SpanId != nil && len(operator.SpanId.ParseFrom) < 1 {
operator.SpanId = nil
}
if operator.TraceFlags != nil && len(operator.TraceFlags.ParseFrom) < 1 {
operator.TraceFlags = nil
}
}

View File

@ -1,9 +1,17 @@
package logparsingpipeline
import (
"context"
"strings"
"testing"
"time"
"github.com/open-telemetry/opentelemetry-collector-contrib/pkg/stanza/entry"
. "github.com/smartystreets/goconvey/convey"
"github.com/stretchr/testify/require"
"go.signoz.io/signoz/pkg/query-service/model"
v3 "go.signoz.io/signoz/pkg/query-service/model/v3"
"go.signoz.io/signoz/pkg/query-service/utils"
)
var prepareProcessorTestData = []struct {
@ -195,3 +203,161 @@ func TestPreparePipelineProcessor(t *testing.T) {
})
}
}
func TestNoCollectorErrorsFromProcessorsForMismatchedLogs(t *testing.T) {
require := require.New(t)
testPipelineFilter := &v3.FilterSet{
Operator: "AND",
Items: []v3.FilterItem{
{
Key: v3.AttributeKey{
Key: "method",
DataType: v3.AttributeKeyDataTypeString,
Type: v3.AttributeKeyTypeTag,
},
Operator: "=",
Value: "GET",
},
},
}
makeTestPipeline := func(config []PipelineOperator) Pipeline {
return Pipeline{
OrderId: 1,
Name: "pipeline1",
Alias: "pipeline1",
Enabled: true,
Filter: testPipelineFilter,
Config: config,
}
}
makeTestLog := func(
body string,
attributes map[string]string,
) model.SignozLog {
attributes["method"] = "GET"
testTraceId, err := utils.RandomHex(16)
require.Nil(err)
testSpanId, err := utils.RandomHex(8)
require.Nil(err)
return model.SignozLog{
Timestamp: uint64(time.Now().UnixNano()),
Body: body,
Attributes_string: attributes,
Resources_string: attributes,
SeverityText: entry.Info.String(),
SeverityNumber: uint8(entry.Info),
SpanID: testSpanId,
TraceID: testTraceId,
}
}
testCases := []struct {
Name string
Operator PipelineOperator
NonMatchingLog model.SignozLog
}{
{
"regex processor should ignore log with missing field",
PipelineOperator{
ID: "regex",
Type: "regex_parser",
Enabled: true,
Name: "regex parser",
ParseFrom: "attributes.test_regex_target",
ParseTo: "attributes",
Regex: `^\s*(?P<json_data>{.*})\s*$`,
},
makeTestLog("mismatching log", map[string]string{}),
}, {
"regex processor should ignore non-matching log",
PipelineOperator{
ID: "regex",
Type: "regex_parser",
Enabled: true,
Name: "regex parser",
ParseFrom: "body",
ParseTo: "attributes",
Regex: `^\s*(?P<body_json>{.*})\s*$`,
},
makeTestLog("mismatching log", map[string]string{}),
}, {
"json parser should ignore logs with missing field.",
PipelineOperator{
ID: "json",
Type: "json_parser",
Enabled: true,
Name: "json parser",
ParseFrom: "attributes.test_json",
ParseTo: "attributes",
},
makeTestLog("mismatching log", map[string]string{}),
},
{
"json parser should ignore log with non JSON target field value",
PipelineOperator{
ID: "json",
Type: "json_parser",
Enabled: true,
Name: "json parser",
ParseFrom: "attributes.test_json",
ParseTo: "attributes",
},
makeTestLog("mismatching log", map[string]string{
"test_json": "bad json",
}),
}, {
"move parser should ignore non matching logs",
PipelineOperator{
ID: "move",
Type: "move",
Enabled: true,
Name: "move",
From: "attributes.test1",
To: "attributes.test2",
},
makeTestLog("mismatching log", map[string]string{}),
}, {
"copy parser should ignore non matching logs",
PipelineOperator{
ID: "copy",
Type: "copy",
Enabled: true,
Name: "copy",
From: "attributes.test1",
To: "attributes.test2",
},
makeTestLog("mismatching log", map[string]string{}),
}, {
"remove parser should ignore non matching logs",
PipelineOperator{
ID: "remove",
Type: "remove",
Enabled: true,
Name: "remove",
Field: "attributes.test",
},
makeTestLog("mismatching log", map[string]string{}),
},
// TODO(Raj): see if there is an error scenario for grok parser.
// TODO(Raj): see if there is an error scenario for trace parser.
// TODO(Raj): see if there is an error scenario for Add operator.
}
for _, testCase := range testCases {
testPipelines := []Pipeline{makeTestPipeline([]PipelineOperator{testCase.Operator})}
result, collectorErrorLogs, err := SimulatePipelinesProcessing(
context.Background(),
testPipelines,
[]model.SignozLog{testCase.NonMatchingLog},
)
require.Nil(err)
require.Equal(0, len(collectorErrorLogs), strings.Join(collectorErrorLogs, "\n"))
require.Equal(1, len(result))
}
}

View File

@ -137,20 +137,35 @@ func isValidOperator(op PipelineOperator) error {
if op.Field == "" {
return fmt.Errorf(fmt.Sprintf("field of %s remove operator cannot be empty", op.ID))
}
case "traceParser":
case "trace_parser":
if op.TraceParser == nil {
return fmt.Errorf(fmt.Sprintf("field of %s remove operator cannot be empty", op.ID))
}
if op.TraceParser.SpanId.ParseFrom == "" && op.TraceParser.TraceId.ParseFrom == "" && op.TraceParser.TraceFlags.ParseFrom == "" {
return fmt.Errorf(fmt.Sprintf("one of trace_id,span_id,parse_from of %s traceParser operator must be present", op.ID))
hasTraceIdParseFrom := (op.TraceParser.TraceId != nil && op.TraceParser.TraceId.ParseFrom != "")
hasSpanIdParseFrom := (op.TraceParser.SpanId != nil && op.TraceParser.SpanId.ParseFrom != "")
hasTraceFlagsParseFrom := (op.TraceParser.TraceFlags != nil && op.TraceParser.TraceFlags.ParseFrom != "")
if !(hasTraceIdParseFrom || hasSpanIdParseFrom || hasTraceFlagsParseFrom) {
return fmt.Errorf(fmt.Sprintf("one of trace_id, span_id, trace_flags of %s trace_parser operator must be present", op.ID))
}
if hasTraceIdParseFrom && !isValidOtelValue(op.TraceParser.TraceId.ParseFrom) {
return fmt.Errorf("trace id can't be parsed from %s", op.TraceParser.TraceId.ParseFrom)
}
if hasSpanIdParseFrom && !isValidOtelValue(op.TraceParser.SpanId.ParseFrom) {
return fmt.Errorf("span id can't be parsed from %s", op.TraceParser.SpanId.ParseFrom)
}
if hasTraceFlagsParseFrom && !isValidOtelValue(op.TraceParser.TraceFlags.ParseFrom) {
return fmt.Errorf("trace flags can't be parsed from %s", op.TraceParser.TraceFlags.ParseFrom)
}
case "retain":
if len(op.Fields) == 0 {
return fmt.Errorf(fmt.Sprintf("fields of %s retain operator cannot be empty", op.ID))
}
default:
return fmt.Errorf(fmt.Sprintf("operator type %s not supported for %s, use one of (grok_parser, regex_parser, copy, move, add, remove, traceParser, retain)", op.Type, op.ID))
return fmt.Errorf(fmt.Sprintf("operator type %s not supported for %s, use one of (grok_parser, regex_parser, copy, move, add, remove, trace_parser, retain)", op.Type, op.ID))
}
if !isValidOtelValue(op.ParseFrom) ||

View File

@ -250,6 +250,31 @@ var operatorTest = []struct {
ParseTo: "attributes",
},
IsValid: false,
}, {
Name: "Trace Parser - invalid - no trace_parser spec",
Operator: PipelineOperator{
ID: "trace",
Type: "trace_parser",
},
IsValid: false,
}, {
Name: "Trace Parser - invalid - no ParseFrom specified",
Operator: PipelineOperator{
ID: "trace",
Type: "trace_parser",
TraceParser: &TraceParser{},
},
IsValid: false,
}, {
Name: "Trace Parser - invalid - bad parsefrom attribute",
Operator: PipelineOperator{
ID: "trace",
Type: "trace_parser",
TraceParser: &TraceParser{
TraceId: &ParseFrom{ParseFrom: "trace_id"},
},
},
IsValid: false,
},
}

View File

@ -4,9 +4,9 @@ import (
"context"
"fmt"
"sort"
"strings"
"time"
_ "github.com/SigNoz/signoz-otel-collector/pkg/parser/grok"
"github.com/open-telemetry/opentelemetry-collector-contrib/processor/logstransformprocessor"
"github.com/pkg/errors"
"go.opentelemetry.io/collector/pdata/pcommon"
@ -22,11 +22,11 @@ func SimulatePipelinesProcessing(
pipelines []Pipeline,
logs []model.SignozLog,
) (
[]model.SignozLog, *model.ApiError,
output []model.SignozLog, collectorErrorLogs []string, apiErr *model.ApiError,
) {
if len(pipelines) < 1 {
return logs, nil
return logs, nil, nil
}
// Collector simulation does not guarantee that logs will come
@ -45,7 +45,7 @@ func SimulatePipelinesProcessing(
// Simulate processing of logs through an otel collector
processorConfigs, err := collectorProcessorsForPipelines(pipelines)
if err != nil {
return nil, model.BadRequest(errors.Wrap(
return nil, nil, model.BadRequest(errors.Wrap(
err, "could not prepare otel processors for pipelines",
))
}
@ -54,7 +54,7 @@ func SimulatePipelinesProcessing(
logstransformprocessor.NewFactory(),
)
if err != nil {
return nil, model.InternalError(errors.Wrap(
return nil, nil, model.InternalError(errors.Wrap(
err, "could not construct processor factory map",
))
}
@ -74,10 +74,9 @@ func SimulatePipelinesProcessing(
simulatorInputPLogs,
timeout,
)
collectorErrsText := strings.Join(collectorErrs, "\n")
if apiErr != nil {
return nil, model.WrapApiError(apiErr, fmt.Sprintf(
"could not simulate log pipelines processing.\nCollector errors: %s\n", collectorErrsText,
return nil, collectorErrs, model.WrapApiError(apiErr, fmt.Sprintf(
"could not simulate log pipelines processing.\nCollector errors",
))
}
@ -93,7 +92,7 @@ func SimulatePipelinesProcessing(
delete(sigLog.Attributes_int64, inputOrderAttribute)
}
return outputSignozLogs, nil
return outputSignozLogs, collectorErrs, nil
}
func collectorProcessorsForPipelines(pipelines []Pipeline) (

View File

@ -2,6 +2,8 @@ package logparsingpipeline
import (
"context"
"encoding/json"
"strconv"
"testing"
"time"
@ -10,6 +12,7 @@ import (
"github.com/stretchr/testify/require"
"go.signoz.io/signoz/pkg/query-service/model"
v3 "go.signoz.io/signoz/pkg/query-service/model/v3"
"go.signoz.io/signoz/pkg/query-service/utils"
)
func TestPipelinePreview(t *testing.T) {
@ -101,7 +104,7 @@ func TestPipelinePreview(t *testing.T) {
},
)
result, err := SimulatePipelinesProcessing(
result, collectorErrorLogs, err := SimulatePipelinesProcessing(
context.Background(),
testPipelines,
[]model.SignozLog{
@ -111,6 +114,7 @@ func TestPipelinePreview(t *testing.T) {
)
require.Nil(err)
require.Equal(0, len(collectorErrorLogs))
require.Equal(2, len(result))
// matching log should have been modified as expected.
@ -141,6 +145,175 @@ func TestPipelinePreview(t *testing.T) {
}
func TestGrokParsingPreview(t *testing.T) {
require := require.New(t)
testPipelines := []Pipeline{
{
OrderId: 1,
Name: "pipeline1",
Alias: "pipeline1",
Enabled: true,
Filter: &v3.FilterSet{
Operator: "AND",
Items: []v3.FilterItem{
{
Key: v3.AttributeKey{
Key: "method",
DataType: v3.AttributeKeyDataTypeString,
Type: v3.AttributeKeyTypeTag,
},
Operator: "=",
Value: "GET",
},
},
},
Config: []PipelineOperator{
{
OrderId: 1,
ID: "grok",
Type: "grok_parser",
Enabled: true,
Name: "test grok parser",
OnError: "send",
ParseFrom: "body",
ParseTo: "attributes",
Pattern: "%{TIMESTAMP_ISO8601:timestamp}%{SPACE}%{WORD:log_level}%{SPACE}%{NOTSPACE:location}%{SPACE}%{GREEDYDATA:message}",
},
},
},
}
testLog := makeTestLogEntry(
"2023-10-26T04:38:00.602Z INFO route/server.go:71 HTTP request received",
map[string]string{
"method": "GET",
},
)
result, collectorErrorLogs, err := SimulatePipelinesProcessing(
context.Background(),
testPipelines,
[]model.SignozLog{
testLog,
},
)
require.Nil(err)
require.Equal(0, len(collectorErrorLogs))
require.Equal(1, len(result))
processed := result[0]
require.Equal("INFO", processed.Attributes_string["log_level"])
require.Equal("route/server.go:71", processed.Attributes_string["location"])
}
func TestTraceParsingPreview(t *testing.T) {
require := require.New(t)
testPipelines := []Pipeline{
{
OrderId: 1,
Name: "pipeline1",
Alias: "pipeline1",
Enabled: true,
Filter: &v3.FilterSet{
Operator: "AND",
Items: []v3.FilterItem{
{
Key: v3.AttributeKey{
Key: "method",
DataType: v3.AttributeKeyDataTypeString,
Type: v3.AttributeKeyTypeTag,
},
Operator: "=",
Value: "GET",
},
},
},
Config: []PipelineOperator{},
},
}
// Start with JSON serialized trace parser to validate deserialization too
var traceParserOp PipelineOperator
err := json.Unmarshal([]byte(`
{
"orderId": 1,
"enabled": true,
"type": "trace_parser",
"name": "Test trace parser",
"id": "test-trace-parser",
"trace_id": {
"parse_from": "attributes.test_trace_id"
},
"span_id": {
"parse_from": "attributes.test_span_id"
},
"trace_flags": {
"parse_from": "attributes.test_trace_flags"
}
}
`), &traceParserOp)
require.Nil(err)
testPipelines[0].Config = append(testPipelines[0].Config, traceParserOp)
testTraceId, err := utils.RandomHex(16)
require.Nil(err)
testSpanId, err := utils.RandomHex(8)
require.Nil(err)
testTraceFlags, err := utils.RandomHex(1)
require.Nil(err)
testLog := model.SignozLog{
Timestamp: uint64(time.Now().UnixNano()),
Body: "test log",
Attributes_string: map[string]string{
"method": "GET",
"test_trace_id": testTraceId,
"test_span_id": testSpanId,
"test_trace_flags": testTraceFlags,
},
SpanID: "",
TraceID: "",
TraceFlags: 0,
}
result, collectorErrorLogs, err := SimulatePipelinesProcessing(
context.Background(),
testPipelines,
[]model.SignozLog{
testLog,
},
)
require.Nil(err)
require.Equal(1, len(result))
require.Equal(0, len(collectorErrorLogs))
processed := result[0]
require.Equal(testTraceId, processed.TraceID)
require.Equal(testSpanId, processed.SpanID)
expectedTraceFlags, err := strconv.ParseUint(testTraceFlags, 16, 16)
require.Nil(err)
require.Equal(uint32(expectedTraceFlags), processed.TraceFlags)
// trace parser should work even if parse_from value is empty
testPipelines[0].Config[0].SpanId.ParseFrom = ""
result, collectorErrorLogs, err = SimulatePipelinesProcessing(
context.Background(),
testPipelines,
[]model.SignozLog{
testLog,
},
)
require.Nil(err)
require.Equal(1, len(result))
require.Equal(0, len(collectorErrorLogs))
require.Equal("", result[0].SpanID)
}
func makeTestLogEntry(
body string,
attributes map[string]string,

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