Compare commits

..

2 Commits

Author SHA1 Message Date
YounixM
3476f3032d format 2024-06-22 14:45:47 +05:30
YounixM
4b757b382f feat: resizable table 2024-06-22 14:04:55 +05:30
171 changed files with 1361 additions and 4266 deletions

View File

@@ -158,7 +158,6 @@ jobs:
echo 'SENTRY_DSN="${{ secrets.SENTRY_DSN }}"' >> frontend/.env
echo 'TUNNEL_URL="${{ secrets.TUNNEL_URL }}"' >> frontend/.env
echo 'TUNNEL_DOMAIN="${{ secrets.TUNNEL_DOMAIN }}"' >> frontend/.env
echo 'POSTHOG_KEY="${{ secrets.POSTHOG_KEY }}"' >> frontend/.env
- name: Install dependencies
working-directory: frontend
run: yarn install

View File

@@ -347,7 +347,7 @@ curl -sL https://github.com/SigNoz/signoz/raw/develop/sample-apps/hotrod/hotrod-
```bash
kubectl -n sample-application run strzal --image=djbingham/curl \
--restart='OnFailure' -i --tty --rm --command -- curl -X POST -F \
'user_count=6' -F 'spawn_rate=2' http://locust-master:8089/swarm
'locust_count=6' -F 'hatch_rate=2' http://locust-master:8089/swarm
```
**5.1.3 To stop the load generation:**

View File

@@ -188,4 +188,3 @@ test:
go test ./pkg/query-service/tests/integration/...
go test ./pkg/query-service/rules/...
go test ./pkg/query-service/collectorsimulator/...
go test ./pkg/query-service/postprocess/...

View File

@@ -198,14 +198,14 @@ Not sure how to get started? Just ping us on `#contributing` in our [slack commu
#### Frontend
- [Palash Gupta](https://github.com/palashgdev)
- [Yunus M](https://github.com/YounixM)
- [Vikrant Gupta](https://github.com/vikrantgupta25)
- [Sagar Rajput](https://github.com/SagarRajput-7)
- [Rajat Dabade](https://github.com/Rajat-Dabade)
#### DevOps
- [Prashant Shahi](https://github.com/prashant-shahi)
- [Vibhu Pandey](https://github.com/grandwizard28)
- [Dhawal Sanghvi](https://github.com/dhawal1248)
<br /><br />

View File

@@ -146,7 +146,7 @@ services:
condition: on-failure
query-service:
image: signoz/query-service:0.49.1
image: signoz/query-service:0.46.0
command:
[
"-config=/root/config/prometheus.yml",
@@ -186,7 +186,7 @@ services:
<<: *db-depend
frontend:
image: signoz/frontend:0.48.0
image: signoz/frontend:0.46.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.102.2
image: signoz/signoz-otel-collector:0.88.24
command:
[
"--config=/etc/otel-collector-config.yaml",
@@ -237,7 +237,7 @@ services:
- query-service
otel-collector-migrator:
image: signoz/signoz-schema-migrator:0.102.2
image: signoz/signoz-schema-migrator:0.88.24
deploy:
restart_policy:
condition: on-failure

View File

@@ -66,7 +66,7 @@ services:
- --storage.path=/data
otel-collector-migrator:
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.102.2}
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.88.24}
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.102.2
image: signoz/signoz-otel-collector:0.88.24
command:
[
"--config=/etc/otel-collector-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.49.1}
image: signoz/query-service:${DOCKER_TAG:-0.46.0}
container_name: signoz-query-service
command:
[
@@ -204,7 +204,7 @@ services:
<<: *db-depend
frontend:
image: signoz/frontend:${DOCKER_TAG:-0.49.1}
image: signoz/frontend:${DOCKER_TAG:-0.46.0}
container_name: signoz-frontend
restart: on-failure
depends_on:
@@ -216,7 +216,7 @@ services:
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
otel-collector-migrator:
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.102.2}
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.88.24}
container_name: otel-migrator
command:
- "--dsn=tcp://clickhouse:9000"
@@ -230,7 +230,7 @@ services:
otel-collector:
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.102.2}
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.88.24}
container_name: signoz-otel-collector
command:
[

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.49.1}
image: signoz/query-service:${DOCKER_TAG:-0.46.0}
container_name: signoz-query-service
command:
[
@@ -203,7 +203,7 @@ services:
<<: *db-depend
frontend:
image: signoz/frontend:${DOCKER_TAG:-0.49.1}
image: signoz/frontend:${DOCKER_TAG:-0.46.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.102.2}
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.88.24}
container_name: otel-migrator
command:
- "--dsn=tcp://clickhouse:9000"
@@ -229,7 +229,7 @@ services:
otel-collector:
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.102.2}
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.88.24}
container_name: signoz-otel-collector
command:
[

View File

@@ -389,7 +389,7 @@ trap bye EXIT
URL="https://api.segment.io/v1/track"
HEADER_1="Content-Type: application/json"
HEADER_2="Authorization: Basic OWtScko3b1BDR1BFSkxGNlFqTVBMdDVibGpGaFJRQnI="
HEADER_2="Authorization: Basic NEdtb2E0aXhKQVVIeDJCcEp4c2p3QTFiRWZud0VlUno6"
send_event() {
error=""

View File

@@ -24,6 +24,7 @@ import (
type APIHandlerOptions struct {
DataConnector interfaces.DataConnector
SkipConfig *basemodel.SkipConfig
PreferDelta bool
PreferSpanMetrics bool
MaxIdleConns int
MaxOpenConns int
@@ -52,6 +53,7 @@ func NewAPIHandler(opts APIHandlerOptions) (*APIHandler, error) {
baseHandler, err := baseapp.NewAPIHandler(baseapp.APIHandlerOpts{
Reader: opts.DataConnector,
SkipConfig: opts.SkipConfig,
PerferDelta: opts.PreferDelta,
PreferSpanMetrics: opts.PreferSpanMetrics,
MaxIdleConns: opts.MaxIdleConns,
MaxOpenConns: opts.MaxOpenConns,

View File

@@ -64,6 +64,7 @@ type ServerOptions struct {
// alert specific params
DisableRules bool
RuleRepoURL string
PreferDelta bool
PreferSpanMetrics bool
MaxIdleConns int
MaxOpenConns int
@@ -255,6 +256,7 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
apiOpts := api.APIHandlerOptions{
DataConnector: reader,
SkipConfig: skipConfig,
PreferDelta: serverOptions.PreferDelta,
PreferSpanMetrics: serverOptions.PreferSpanMetrics,
MaxIdleConns: serverOptions.MaxIdleConns,
MaxOpenConns: serverOptions.MaxOpenConns,

View File

@@ -89,6 +89,7 @@ func main() {
var cacheConfigPath, fluxInterval string
var enableQueryServiceLogOTLPExport bool
var preferDelta bool
var preferSpanMetrics bool
var maxIdleConns int
@@ -99,13 +100,14 @@ func main() {
flag.StringVar(&promConfigPath, "config", "./config/prometheus.yml", "(prometheus config to read metrics)")
flag.StringVar(&skipTopLvlOpsPath, "skip-top-level-ops", "", "(config file to skip top level operations)")
flag.BoolVar(&disableRules, "rules.disable", false, "(disable rule evaluation)")
flag.BoolVar(&preferDelta, "prefer-delta", false, "(prefer delta over cumulative metrics)")
flag.BoolVar(&preferSpanMetrics, "prefer-span-metrics", false, "(prefer span metrics for service level metrics)")
flag.IntVar(&maxIdleConns, "max-idle-conns", 50, "(number of connections to maintain in the pool.)")
flag.IntVar(&maxOpenConns, "max-open-conns", 100, "(max connections for use at any time.)")
flag.DurationVar(&dialTimeout, "dial-timeout", 5*time.Second, "(the maximum time to establish a connection.)")
flag.StringVar(&ruleRepoURL, "rules.repo-url", baseconst.AlertHelpPage, "(host address used to build rule link in alert messages)")
flag.StringVar(&cacheConfigPath, "experimental.cache-config", "", "(cache config to use)")
flag.StringVar(&fluxInterval, "flux-interval", "5m", "(the interval to exclude data from being cached to avoid incorrect cache for data in motion)")
flag.StringVar(&fluxInterval, "flux-interval", "5m", "(cache config to use)")
flag.BoolVar(&enableQueryServiceLogOTLPExport, "enable.query.service.log.otlp.export", false, "(enable query service log otlp export)")
flag.StringVar(&cluster, "cluster", "cluster", "(cluster name - defaults to 'cluster')")
flag.StringVar(&gatewayUrl, "gateway-url", "", "(url to the gateway)")
@@ -123,6 +125,7 @@ func main() {
HTTPHostPort: baseconst.HTTPHostPort,
PromConfigPath: promConfigPath,
SkipTopLvlOpsPath: skipTopLvlOpsPath,
PreferDelta: preferDelta,
PreferSpanMetrics: preferSpanMetrics,
PrivateHostPort: baseconst.PrivateHostPort,
DisableRules: disableRules,

View File

@@ -9,7 +9,6 @@ const config: Config.InitialOptions = {
modulePathIgnorePatterns: ['dist'],
moduleNameMapper: {
'\\.(css|less|scss)$': '<rootDir>/__mocks__/cssMock.ts',
'\\.md$': '<rootDir>/__mocks__/cssMock.ts',
},
globals: {
extensionsToTreatAsEsm: ['.ts'],

View File

@@ -34,6 +34,8 @@
"@dnd-kit/core": "6.1.0",
"@dnd-kit/modifiers": "7.0.0",
"@dnd-kit/sortable": "8.0.0",
"@dnd-kit/utilities": "3.2.2",
"@faker-js/faker": "8.4.1",
"@grafana/data": "^9.5.2",
"@mdx-js/loader": "2.3.0",
"@mdx-js/react": "2.3.0",
@@ -43,6 +45,7 @@
"@sentry/react": "7.102.1",
"@sentry/webpack-plugin": "2.16.0",
"@signozhq/design-tokens": "0.0.8",
"@tanstack/react-table": "8.17.3",
"@uiw/react-md-editor": "3.23.5",
"@visx/group": "3.3.0",
"@visx/shape": "3.5.0",
@@ -88,7 +91,6 @@
"lucide-react": "0.379.0",
"mini-css-extract-plugin": "2.4.5",
"papaparse": "5.4.1",
"posthog-js": "1.142.1",
"rc-tween-one": "3.0.6",
"react": "18.2.0",
"react-addons-update": "15.6.3",

View File

@@ -1,8 +0,0 @@
{
"invite_user": "Invite your teammates",
"invite": "Invite",
"skip": "Skip",
"invite_user_helper_text": "Not the right person to get started? No worries! Invite someone who can.",
"select_use_case": "Select a use-case to get started",
"get_started": "Get Started"
}

View File

@@ -6,6 +6,5 @@
"share": "Share",
"save": "Save",
"edit": "Edit",
"logged_in": "Logged In",
"pending_data_placeholder": "Just a bit of patience, just a little bits enough ⎯ were getting your {{dataSource}}!"
"logged_in": "Logged In"
}

View File

@@ -1,8 +0,0 @@
{
"invite_user": "Invite your teammates",
"invite": "Invite",
"skip": "Skip",
"invite_user_helper_text": "Not the right person to get started? No worries! Invite someone who can.",
"select_use_case": "Select a use-case to get started",
"get_started": "Get Started"
}

View File

@@ -17,7 +17,6 @@ import { NotificationProvider } from 'hooks/useNotifications';
import { ResourceProvider } from 'hooks/useResourceAttribute';
import history from 'lib/history';
import { identity, pick, pickBy } from 'lodash-es';
import posthog from 'posthog-js';
import { DashboardProvider } from 'providers/Dashboard/Dashboard';
import { QueryBuilderProvider } from 'providers/QueryBuilder';
import { Suspense, useEffect, useState } from 'react';
@@ -39,7 +38,7 @@ import defaultRoutes, {
function App(): JSX.Element {
const themeConfig = useThemeConfig();
const { data: licenseData } = useLicense();
const { data } = useLicense();
const [routes, setRoutes] = useState<AppRoutes[]>(defaultRoutes);
const { role, isLoggedIn: isLoggedInState, user, org } = useSelector<
AppState,
@@ -93,10 +92,10 @@ function App(): JSX.Element {
});
const isOnBasicPlan =
licenseData?.payload?.licenses?.some(
data?.payload?.licenses?.some(
(license) =>
license.isCurrent && license.planKey === LICENSE_PLAN_KEY.BASIC_PLAN,
) || licenseData?.payload?.licenses === null;
) || data?.payload?.licenses === null;
const enableAnalytics = (user: User): void => {
const orgName =
@@ -113,7 +112,9 @@ function App(): JSX.Element {
};
const sanitizedIdentifyPayload = pickBy(identifyPayload, identity);
const domain = extractDomain(email);
const hostNameParts = hostname.split('.');
const groupTraits = {
@@ -126,30 +127,10 @@ function App(): JSX.Element {
};
window.analytics.identify(email, sanitizedIdentifyPayload);
window.analytics.group(domain, groupTraits);
window.clarity('identify', email, name);
posthog?.identify(email, {
email,
name,
orgName,
tenant_id: hostNameParts[0],
data_region: hostNameParts[1],
tenant_url: hostname,
company_domain: domain,
source: 'signoz-ui',
isPaidUser: !!licenseData?.payload?.trialConvertedToSubscription,
});
posthog?.group('company', domain, {
name: orgName,
tenant_id: hostNameParts[0],
data_region: hostNameParts[1],
tenant_url: hostname,
company_domain: domain,
source: 'signoz-ui',
isPaidUser: !!licenseData?.payload?.trialConvertedToSubscription,
});
};
useEffect(() => {
@@ -163,6 +144,10 @@ function App(): JSX.Element {
!isIdentifiedUser
) {
setLocalStorageApi(LOCALSTORAGE.IS_IDENTIFIED_USER, 'true');
if (isCloudUserVal) {
enableAnalytics(user);
}
}
if (
@@ -210,11 +195,6 @@ function App(): JSX.Element {
console.error('Failed to parse local storage theme analytics event');
}
}
if (isCloudUserVal && user && user.email) {
enableAnalytics(user);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [user]);

View File

@@ -5,13 +5,7 @@ import { Button, Dropdown, MenuProps } from 'antd';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useState } from 'react';
function DropDown({
element,
onDropDownItemClick,
}: {
element: JSX.Element[];
onDropDownItemClick?: MenuProps['onClick'];
}): JSX.Element {
function DropDown({ element }: { element: JSX.Element[] }): JSX.Element {
const isDarkMode = useIsDarkMode();
const items: MenuProps['items'] = element.map(
@@ -29,7 +23,6 @@ function DropDown({
items,
onMouseEnter: (): void => setDdOpen(true),
onMouseLeave: (): void => setDdOpen(false),
onClick: (item): void => onDropDownItemClick?.(item),
}}
open={isDdOpen}
>
@@ -47,8 +40,4 @@ function DropDown({
);
}
DropDown.defaultProps = {
onDropDownItemClick: (): void => {},
};
export default DropDown;

View File

@@ -52,7 +52,7 @@
.log-body {
font-family: 'SF Mono';
font-family: 'Geist Mono';
font-family: 'Space Mono', monospace;
font-size: var(--font-size-sm);
font-weight: var(--font-weight-normal);

View File

@@ -62,6 +62,8 @@ function RawLogView({
const isDarkMode = useIsDarkMode();
const isReadOnlyLog = !isLogsExplorerPage || isReadOnly;
const severityText = data.severity_text ? `${data.severity_text} |` : '';
const logType = getLogIndicatorType(data);
const updatedSelecedFields = useMemo(
@@ -86,16 +88,17 @@ function RawLogView({
attributesText += ' | ';
}
const text = useMemo(() => {
const date =
const text = useMemo(
() =>
typeof data.timestamp === 'string'
? dayjs(data.timestamp)
: dayjs(data.timestamp / 1e6);
return `${date.format('YYYY-MM-DD HH:mm:ss.SSS')} | ${attributesText} ${
data.body
}`;
}, [data.timestamp, data.body, attributesText]);
? `${dayjs(data.timestamp).format(
'YYYY-MM-DD HH:mm:ss.SSS',
)} | ${attributesText} ${severityText} ${data.body}`
: `${dayjs(data.timestamp / 1e6).format(
'YYYY-MM-DD HH:mm:ss.SSS',
)} | ${attributesText} ${severityText} ${data.body}`,
[data.timestamp, data.body, severityText, attributesText],
);
const handleClickExpand = useCallback(() => {
if (activeContextLog || isReadOnly) return;

View File

@@ -49,7 +49,7 @@ export const ExpandIconWrapper = styled(Col)`
export const RawLogContent = styled.div<RawLogContentProps>`
margin-bottom: 0;
font-family: 'SF Mono', monospace;
font-family: 'Geist Mono';
font-family: 'Space Mono', monospace;
font-size: 13px;
font-weight: 400;
text-align: left;

View File

@@ -2,9 +2,7 @@
import './DynamicColumnTable.syles.scss';
import { Button, Dropdown, Flex, MenuProps, Switch } from 'antd';
import { ColumnGroupType, ColumnType } from 'antd/es/table';
import { ColumnsType } from 'antd/lib/table';
import logEvent from 'api/common/logEvent';
import FacingIssueBtn from 'components/facingIssueBtn/FacingIssueBtn';
import { SlidersHorizontal } from 'lucide-react';
import { memo, useEffect, useState } from 'react';
@@ -24,7 +22,6 @@ function DynamicColumnTable({
dynamicColumns,
onDragColumn,
facingIssueBtn,
shouldSendAlertsLogEvent,
...restProps
}: DynamicColumnTableProps): JSX.Element {
const [columnsData, setColumnsData] = useState<ColumnsType | undefined>(
@@ -50,18 +47,11 @@ function DynamicColumnTable({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [columns, dynamicColumns]);
const onToggleHandler = (
index: number,
column: ColumnGroupType<any> | ColumnType<any>,
) => (checked: boolean, event: React.MouseEvent<HTMLButtonElement>): void => {
const onToggleHandler = (index: number) => (
checked: boolean,
event: React.MouseEvent<HTMLButtonElement>,
): void => {
event.stopPropagation();
if (shouldSendAlertsLogEvent) {
logEvent('Alert: Column toggled', {
column: column?.title,
action: checked ? 'Enable' : 'Disable',
});
}
setVisibleColumns({
tablesource,
dynamicColumns,
@@ -85,7 +75,7 @@ function DynamicColumnTable({
<div>{column.title?.toString()}</div>
<Switch
checked={columnsData?.findIndex((c) => c.key === column.key) !== -1}
onChange={onToggleHandler(index, column)}
onChange={onToggleHandler(index)}
/>
</div>
),

View File

@@ -3,7 +3,6 @@
import { Table } from 'antd';
import { ColumnsType } from 'antd/lib/table';
import { dragColumnParams } from 'hooks/useDragColumns/configs';
import { set } from 'lodash-es';
import {
SyntheticEvent,
useCallback,
@@ -21,7 +20,6 @@ import { ResizeTableProps } from './types';
function ResizeTable({
columns,
onDragColumn,
pagination,
...restProps
}: ResizeTableProps): JSX.Element {
const [columnsData, setColumns] = useState<ColumnsType>([]);
@@ -60,21 +58,14 @@ function ResizeTable({
[columnsData, onDragColumn, handleResize],
);
const tableParams = useMemo(() => {
const props = {
const tableParams = useMemo(
() => ({
...restProps,
components: { header: { cell: ResizableHeader } },
columns: mergedColumns,
};
set(
props,
'pagination',
pagination ? { ...pagination, hideOnSinglePage: true } : false,
);
return props;
}, [mergedColumns, pagination, restProps]);
}),
[mergedColumns, restProps],
);
useEffect(() => {
if (columns) {

View File

@@ -14,7 +14,6 @@ export interface DynamicColumnTableProps extends TableProps<any> {
dynamicColumns: TableProps<any>['columns'];
onDragColumn?: (fromIndex: number, toIndex: number) => void;
facingIssueBtn?: FacingIssueBtnProps;
shouldSendAlertsLogEvent?: boolean;
}
export type GetVisibleColumnsFunction = (

View File

@@ -9,6 +9,7 @@ import { Tooltip } from 'antd';
import { themeColors } from 'constants/theme';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useMemo } from 'react';
import { popupContainer } from 'utils/selectPopupContainer';
import { style } from './constant';
@@ -63,7 +64,7 @@ function TextToolTip({
);
return (
<Tooltip overlay={overlay}>
<Tooltip getTooltipContainer={popupContainer} overlay={overlay}>
{useFilledIcon ? (
<QuestionCircleFilled style={iconStyle} />
) : (

View File

@@ -1,15 +1,13 @@
import { PlusOutlined } from '@ant-design/icons';
import { Tooltip, Typography } from 'antd';
import getAll from 'api/channels/getAll';
import logEvent from 'api/common/logEvent';
import Spinner from 'components/Spinner';
import TextToolTip from 'components/TextToolTip';
import ROUTES from 'constants/routes';
import useComponentPermission from 'hooks/useComponentPermission';
import useFetch from 'hooks/useFetch';
import history from 'lib/history';
import { isUndefined } from 'lodash-es';
import { useCallback, useEffect } from 'react';
import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
@@ -33,14 +31,6 @@ function AlertChannels(): JSX.Element {
const { loading, payload, error, errorMessage } = useFetch(getAll);
useEffect(() => {
if (!isUndefined(payload)) {
logEvent('Alert Channel: Channel list page visited', {
number: payload?.length,
});
}
}, [payload]);
if (error) {
return <Typography>{errorMessage}</Typography>;
}

View File

@@ -6,6 +6,7 @@ import './AppLayout.styles.scss';
import * as Sentry from '@sentry/react';
import { Flex } from 'antd';
import getLocalStorageKey from 'api/browser/localstorage/get';
import getDynamicConfigs from 'api/dynamicConfigs/getDynamicConfigs';
import getUserLatestVersion from 'api/user/getLatestVersion';
import getUserVersion from 'api/user/getVersion';
import cx from 'classnames';
@@ -37,6 +38,7 @@ import { sideBarCollapse } from 'store/actions';
import { AppState } from 'store/reducers';
import AppActions from 'types/actions';
import {
UPDATE_CONFIGS,
UPDATE_CURRENT_ERROR,
UPDATE_CURRENT_VERSION,
UPDATE_LATEST_VERSION,
@@ -64,7 +66,11 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
const { pathname } = useLocation();
const { t } = useTranslation(['titles']);
const [getUserVersionResponse, getUserLatestVersionResponse] = useQueries([
const [
getUserVersionResponse,
getUserLatestVersionResponse,
getDynamicConfigsResponse,
] = useQueries([
{
queryFn: getUserVersion,
queryKey: ['getUserVersion', user?.accessJwt],
@@ -75,6 +81,10 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
queryKey: ['getUserLatestVersion', user?.accessJwt],
enabled: isLoggedIn,
},
{
queryFn: getDynamicConfigs,
queryKey: ['getDynamicConfigs', user?.accessJwt],
},
]);
useEffect(() => {
@@ -85,7 +95,15 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
if (getUserVersionResponse.status === 'idle' && isLoggedIn) {
getUserVersionResponse.refetch();
}
}, [getUserLatestVersionResponse, getUserVersionResponse, isLoggedIn]);
if (getDynamicConfigsResponse.status === 'idle') {
getDynamicConfigsResponse.refetch();
}
}, [
getUserLatestVersionResponse,
getUserVersionResponse,
isLoggedIn,
getDynamicConfigsResponse,
]);
const { children } = props;
@@ -93,6 +111,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
const latestCurrentCounter = useRef(0);
const latestVersionCounter = useRef(0);
const latestConfigCounter = useRef(0);
const { notifications } = useNotifications();
@@ -170,6 +189,23 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
},
});
}
if (
getDynamicConfigsResponse.isFetched &&
getDynamicConfigsResponse.isSuccess &&
getDynamicConfigsResponse.data &&
getDynamicConfigsResponse.data.payload &&
latestConfigCounter.current === 0
) {
latestConfigCounter.current = 1;
dispatch({
type: UPDATE_CONFIGS,
payload: {
configs: getDynamicConfigsResponse.data.payload,
},
});
}
}, [
dispatch,
isLoggedIn,
@@ -184,6 +220,9 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
getUserLatestVersionResponse.isFetched,
getUserVersionResponse.isFetched,
getUserLatestVersionResponse.isSuccess,
getDynamicConfigsResponse.data,
getDynamicConfigsResponse.isFetched,
getDynamicConfigsResponse.isSuccess,
notifications,
]);

View File

@@ -11,12 +11,11 @@ import testOpsGenie from 'api/channels/testOpsgenie';
import testPagerApi from 'api/channels/testPager';
import testSlackApi from 'api/channels/testSlack';
import testWebhookApi from 'api/channels/testWebhook';
import logEvent from 'api/common/logEvent';
import ROUTES from 'constants/routes';
import FormAlertChannels from 'container/FormAlertChannels';
import { useNotifications } from 'hooks/useNotifications';
import history from 'lib/history';
import { useCallback, useEffect, useState } from 'react';
import { useCallback, useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
@@ -44,10 +43,6 @@ function CreateAlertChannels({
const [formInstance] = Form.useForm();
useEffect(() => {
logEvent('Alert Channel: Create channel page visited', {});
}, []);
const [selectedConfig, setSelectedConfig] = useState<
Partial<
SlackChannel &
@@ -144,25 +139,19 @@ function CreateAlertChannels({
description: t('channel_creation_done'),
});
history.replace(ROUTES.ALL_CHANNELS);
return { status: 'success', statusMessage: t('channel_creation_done') };
} else {
notifications.error({
message: 'Error',
description: response.error || t('channel_creation_failed'),
});
}
notifications.error({
message: 'Error',
description: response.error || t('channel_creation_failed'),
});
return {
status: 'failed',
statusMessage: response.error || t('channel_creation_failed'),
};
} catch (error) {
notifications.error({
message: 'Error',
description: t('channel_creation_failed'),
});
return { status: 'failed', statusMessage: t('channel_creation_failed') };
} finally {
setSavingState(false);
}
setSavingState(false);
}, [prepareSlackRequest, t, notifications]);
const prepareWebhookRequest = useCallback(() => {
@@ -211,25 +200,19 @@ function CreateAlertChannels({
description: t('channel_creation_done'),
});
history.replace(ROUTES.ALL_CHANNELS);
return { status: 'success', statusMessage: t('channel_creation_done') };
} else {
notifications.error({
message: 'Error',
description: response.error || t('channel_creation_failed'),
});
}
notifications.error({
message: 'Error',
description: response.error || t('channel_creation_failed'),
});
return {
status: 'failed',
statusMessage: response.error || t('channel_creation_failed'),
};
} catch (error) {
notifications.error({
message: 'Error',
description: t('channel_creation_failed'),
});
return { status: 'failed', statusMessage: t('channel_creation_failed') };
} finally {
setSavingState(false);
}
setSavingState(false);
}, [prepareWebhookRequest, t, notifications]);
const preparePagerRequest = useCallback(() => {
@@ -262,8 +245,8 @@ function CreateAlertChannels({
setSavingState(true);
const request = preparePagerRequest();
try {
if (request) {
if (request) {
try {
const response = await createPagerApi(request);
if (response.statusCode === 200) {
@@ -272,31 +255,20 @@ function CreateAlertChannels({
description: t('channel_creation_done'),
});
history.replace(ROUTES.ALL_CHANNELS);
return { status: 'success', statusMessage: t('channel_creation_done') };
} else {
notifications.error({
message: 'Error',
description: response.error || t('channel_creation_failed'),
});
}
} catch (e) {
notifications.error({
message: 'Error',
description: response.error || t('channel_creation_failed'),
description: t('channel_creation_failed'),
});
return {
status: 'failed',
statusMessage: response.error || t('channel_creation_failed'),
};
}
notifications.error({
message: 'Error',
description: t('channel_creation_failed'),
});
return { status: 'failed', statusMessage: t('channel_creation_failed') };
} catch (error) {
notifications.error({
message: 'Error',
description: t('channel_creation_failed'),
});
return { status: 'failed', statusMessage: t('channel_creation_failed') };
} finally {
setSavingState(false);
}
setSavingState(false);
}, [t, notifications, preparePagerRequest]);
const prepareOpsgenieRequest = useCallback(
@@ -323,25 +295,19 @@ function CreateAlertChannels({
description: t('channel_creation_done'),
});
history.replace(ROUTES.ALL_CHANNELS);
return { status: 'success', statusMessage: t('channel_creation_done') };
} else {
notifications.error({
message: 'Error',
description: response.error || t('channel_creation_failed'),
});
}
notifications.error({
message: 'Error',
description: response.error || t('channel_creation_failed'),
});
return {
status: 'failed',
statusMessage: response.error || t('channel_creation_failed'),
};
} catch (error) {
notifications.error({
message: 'Error',
description: t('channel_creation_failed'),
});
return { status: 'failed', statusMessage: t('channel_creation_failed') };
} finally {
setSavingState(false);
}
setSavingState(false);
}, [prepareOpsgenieRequest, t, notifications]);
const prepareEmailRequest = useCallback(
@@ -366,25 +332,19 @@ function CreateAlertChannels({
description: t('channel_creation_done'),
});
history.replace(ROUTES.ALL_CHANNELS);
return { status: 'success', statusMessage: t('channel_creation_done') };
} else {
notifications.error({
message: 'Error',
description: response.error || t('channel_creation_failed'),
});
}
notifications.error({
message: 'Error',
description: response.error || t('channel_creation_failed'),
});
return {
status: 'failed',
statusMessage: response.error || t('channel_creation_failed'),
};
} catch (error) {
notifications.error({
message: 'Error',
description: t('channel_creation_failed'),
});
return { status: 'failed', statusMessage: t('channel_creation_failed') };
} finally {
setSavingState(false);
}
setSavingState(false);
}, [prepareEmailRequest, t, notifications]);
const prepareMsTeamsRequest = useCallback(
@@ -410,25 +370,19 @@ function CreateAlertChannels({
description: t('channel_creation_done'),
});
history.replace(ROUTES.ALL_CHANNELS);
return { status: 'success', statusMessage: t('channel_creation_done') };
} else {
notifications.error({
message: 'Error',
description: response.error || t('channel_creation_failed'),
});
}
notifications.error({
message: 'Error',
description: response.error || t('channel_creation_failed'),
});
return {
status: 'failed',
statusMessage: response.error || t('channel_creation_failed'),
};
} catch (error) {
notifications.error({
message: 'Error',
description: t('channel_creation_failed'),
});
return { status: 'failed', statusMessage: t('channel_creation_failed') };
} finally {
setSavingState(false);
}
setSavingState(false);
}, [prepareMsTeamsRequest, t, notifications]);
const onSaveHandler = useCallback(
@@ -446,15 +400,7 @@ function CreateAlertChannels({
const functionToCall = functionMapper[value as keyof typeof functionMapper];
if (functionToCall) {
const result = await functionToCall();
logEvent('Alert Channel: Save channel', {
type: value,
sendResolvedAlert: selectedConfig.send_resolved,
name: selectedConfig.name,
new: 'true',
status: result?.status,
statusMessage: result?.statusMessage,
});
functionToCall();
} else {
notifications.error({
message: 'Error',
@@ -463,7 +409,6 @@ function CreateAlertChannels({
}
}
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[
onSlackHandler,
onWebhookHandler,
@@ -527,25 +472,14 @@ function CreateAlertChannels({
description: t('channel_test_failed'),
});
}
logEvent('Alert Channel: Test notification', {
type: channelType,
sendResolvedAlert: selectedConfig.send_resolved,
name: selectedConfig.name,
new: 'true',
status:
response && response.statusCode === 200 ? 'Test success' : 'Test failed',
});
} catch (error) {
notifications.error({
message: 'Error',
description: t('channel_test_unexpected'),
});
}
setTestingState(false);
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[
prepareWebhookRequest,
t,

View File

@@ -1,6 +1,4 @@
import { Row, Typography } from 'antd';
import logEvent from 'api/common/logEvent';
import { ALERTS_DATA_SOURCE_MAP } from 'constants/alerts';
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { AlertTypes } from 'types/api/alerts/alertTypes';
@@ -36,13 +34,6 @@ function SelectAlertType({ onSelect }: SelectAlertTypeProps): JSX.Element {
default:
break;
}
logEvent('Alert: Sample alert link clicked', {
dataSource: ALERTS_DATA_SOURCE_MAP[option],
link: url,
page: 'New alert data source selection page',
});
window.open(url, '_blank');
}
const renderOptions = useMemo(

View File

@@ -1,5 +1,4 @@
import { Form, Row } from 'antd';
import logEvent from 'api/common/logEvent';
import { ENTITY_VERSION_V4 } from 'constants/app';
import { QueryParams } from 'constants/query';
import FormAlertRules from 'container/FormAlertRules';
@@ -69,8 +68,6 @@ function CreateRules(): JSX.Element {
useEffect(() => {
if (alertType) {
onSelectType(alertType);
} else {
logEvent('Alert: New alert data source selection page visited', {});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [alertType]);

View File

@@ -11,7 +11,6 @@ import testOpsgenie from 'api/channels/testOpsgenie';
import testPagerApi from 'api/channels/testPager';
import testSlackApi from 'api/channels/testSlack';
import testWebhookApi from 'api/channels/testWebhook';
import logEvent from 'api/common/logEvent';
import ROUTES from 'constants/routes';
import {
ChannelType,
@@ -90,7 +89,7 @@ function EditAlertChannels({
description: t('webhook_url_required'),
});
setSavingState(false);
return { status: 'failed', statusMessage: t('webhook_url_required') };
return;
}
const response = await editSlackApi(prepareSlackRequest());
@@ -102,17 +101,13 @@ function EditAlertChannels({
});
history.replace(ROUTES.ALL_CHANNELS);
return { status: 'success', statusMessage: t('channel_edit_done') };
} else {
notifications.error({
message: 'Error',
description: response.error || t('channel_edit_failed'),
});
}
notifications.error({
message: 'Error',
description: response.error || t('channel_edit_failed'),
});
setSavingState(false);
return {
status: 'failed',
statusMessage: response.error || t('channel_edit_failed'),
};
}, [prepareSlackRequest, t, notifications, selectedConfig]);
const prepareWebhookRequest = useCallback(() => {
@@ -141,13 +136,13 @@ function EditAlertChannels({
if (selectedConfig?.api_url === '') {
showError(t('webhook_url_required'));
setSavingState(false);
return { status: 'failed', statusMessage: t('webhook_url_required') };
return;
}
if (username && (!password || password === '')) {
showError(t('username_no_password'));
setSavingState(false);
return { status: 'failed', statusMessage: t('username_no_password') };
return;
}
const response = await editWebhookApi(prepareWebhookRequest());
@@ -159,15 +154,10 @@ function EditAlertChannels({
});
history.replace(ROUTES.ALL_CHANNELS);
return { status: 'success', statusMessage: t('channel_edit_done') };
} else {
showError(response.error || t('channel_edit_failed'));
}
showError(response.error || t('channel_edit_failed'));
setSavingState(false);
return {
status: 'failed',
statusMessage: response.error || t('channel_edit_failed'),
};
}, [prepareWebhookRequest, t, notifications, selectedConfig]);
const prepareEmailRequest = useCallback(
@@ -191,18 +181,13 @@ function EditAlertChannels({
description: t('channel_edit_done'),
});
history.replace(ROUTES.ALL_CHANNELS);
return { status: 'success', statusMessage: t('channel_edit_done') };
} else {
notifications.error({
message: 'Error',
description: response.error || t('channel_edit_failed'),
});
}
notifications.error({
message: 'Error',
description: response.error || t('channel_edit_failed'),
});
setSavingState(false);
return {
status: 'failed',
statusMessage: response.error || t('channel_edit_failed'),
};
}, [prepareEmailRequest, t, notifications]);
const preparePagerRequest = useCallback(
@@ -233,7 +218,7 @@ function EditAlertChannels({
description: validationError,
});
setSavingState(false);
return { status: 'failed', statusMessage: validationError };
return;
}
const response = await editPagerApi(preparePagerRequest());
@@ -244,18 +229,13 @@ function EditAlertChannels({
});
history.replace(ROUTES.ALL_CHANNELS);
return { status: 'success', statusMessage: t('channel_edit_done') };
} else {
notifications.error({
message: 'Error',
description: response.error || t('channel_edit_failed'),
});
}
notifications.error({
message: 'Error',
description: response.error || t('channel_edit_failed'),
});
setSavingState(false);
return {
status: 'failed',
statusMessage: response.error || t('channel_edit_failed'),
};
}, [preparePagerRequest, notifications, selectedConfig, t]);
const prepareOpsgenieRequest = useCallback(
@@ -279,7 +259,7 @@ function EditAlertChannels({
description: t('api_key_required'),
});
setSavingState(false);
return { status: 'failed', statusMessage: t('api_key_required') };
return;
}
const response = await editOpsgenie(prepareOpsgenieRequest());
@@ -291,18 +271,13 @@ function EditAlertChannels({
});
history.replace(ROUTES.ALL_CHANNELS);
return { status: 'success', statusMessage: t('channel_edit_done') };
} else {
notifications.error({
message: 'Error',
description: response.error || t('channel_edit_failed'),
});
}
notifications.error({
message: 'Error',
description: response.error || t('channel_edit_failed'),
});
setSavingState(false);
return {
status: 'failed',
statusMessage: response.error || t('channel_edit_failed'),
};
}, [prepareOpsgenieRequest, t, notifications, selectedConfig]);
const prepareMsTeamsRequest = useCallback(
@@ -326,7 +301,7 @@ function EditAlertChannels({
description: t('webhook_url_required'),
});
setSavingState(false);
return { status: 'failed', statusMessage: t('webhook_url_required') };
return;
}
const response = await editMsTeamsApi(prepareMsTeamsRequest());
@@ -338,46 +313,31 @@ function EditAlertChannels({
});
history.replace(ROUTES.ALL_CHANNELS);
return { status: 'success', statusMessage: t('channel_edit_done') };
} else {
notifications.error({
message: 'Error',
description: response.error || t('channel_edit_failed'),
});
}
notifications.error({
message: 'Error',
description: response.error || t('channel_edit_failed'),
});
setSavingState(false);
return {
status: 'failed',
statusMessage: response.error || t('channel_edit_failed'),
};
}, [prepareMsTeamsRequest, t, notifications, selectedConfig]);
const onSaveHandler = useCallback(
async (value: ChannelType) => {
let result;
(value: ChannelType) => {
if (value === ChannelType.Slack) {
result = await onSlackEditHandler();
onSlackEditHandler();
} else if (value === ChannelType.Webhook) {
result = await onWebhookEditHandler();
onWebhookEditHandler();
} else if (value === ChannelType.Pagerduty) {
result = await onPagerEditHandler();
onPagerEditHandler();
} else if (value === ChannelType.MsTeams) {
result = await onMsTeamsEditHandler();
onMsTeamsEditHandler();
} else if (value === ChannelType.Opsgenie) {
result = await onOpsgenieEditHandler();
onOpsgenieEditHandler();
} else if (value === ChannelType.Email) {
result = await onEmailEditHandler();
onEmailEditHandler();
}
logEvent('Alert Channel: Save channel', {
type: value,
sendResolvedAlert: selectedConfig.send_resolved,
name: selectedConfig.name,
new: 'false',
status: result?.status,
statusMessage: result?.statusMessage,
});
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[
onSlackEditHandler,
onWebhookEditHandler,
@@ -439,14 +399,6 @@ function EditAlertChannels({
description: t('channel_test_failed'),
});
}
logEvent('Alert Channel: Test notification', {
type: channelType,
sendResolvedAlert: selectedConfig.send_resolved,
name: selectedConfig.name,
new: 'false',
status:
response && response.statusCode === 200 ? 'Test success' : 'Test failed',
});
} catch (error) {
notifications.error({
message: 'Error',
@@ -455,7 +407,6 @@ function EditAlertChannels({
}
setTestingState(false);
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[
t,
prepareWebhookRequest,

View File

@@ -91,7 +91,8 @@
box-shadow: none !important;
&.ant-btn-round {
padding: 8px 12px 8px 10px;
padding-inline-start: 10px;
padding-inline-end: 8px;
font-weight: 500;
}

View File

@@ -1,6 +1,5 @@
import { Button, Typography } from 'antd';
import createDashboard from 'api/dashboard/create';
import { ENTITY_VERSION_V4 } from 'constants/app';
import { useGetAllDashboard } from 'hooks/dashboard/useGetAllDashboard';
import useAxiosError from 'hooks/useAxiosError';
import { useCallback, useMemo, useState } from 'react';
@@ -71,7 +70,6 @@ function ExportPanelContainer({
ns: 'dashboard',
}),
uploadedGrafana: false,
version: ENTITY_VERSION_V4,
});
}, [t, createNewDashboard]);

View File

@@ -3,8 +3,6 @@ import './FormAlertRules.styles.scss';
import { PlusOutlined } from '@ant-design/icons';
import { Button, Form, Select, Switch, Tooltip } from 'antd';
import getChannels from 'api/channels/getAll';
import logEvent from 'api/common/logEvent';
import { ALERTS_DATA_SOURCE_MAP } from 'constants/alerts';
import ROUTES from 'constants/routes';
import useComponentPermission from 'hooks/useComponentPermission';
import useFetch from 'hooks/useFetch';
@@ -12,7 +10,6 @@ import { useCallback, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { AlertTypes } from 'types/api/alerts/alertTypes';
import { AlertDef, Labels } from 'types/api/alerts/def';
import AppReducer from 'types/reducer/app';
import { requireErrorMessage } from 'utils/form/requireErrorMessage';
@@ -76,24 +73,9 @@ function BasicInfo({
const noChannels = channels.payload?.length === 0;
const handleCreateNewChannels = useCallback(() => {
logEvent('Alert: Create notification channel button clicked', {
dataSource: ALERTS_DATA_SOURCE_MAP[alertDef?.alertType as AlertTypes],
ruleId: isNewRule ? 0 : alertDef?.id,
});
window.open(ROUTES.CHANNELS_NEW, '_blank');
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
if (!channels.loading && isNewRule) {
logEvent('Alert: New alert creation page visited', {
dataSource: ALERTS_DATA_SOURCE_MAP[alertDef?.alertType as AlertTypes],
numberOfChannels: channels.payload?.length,
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [channels.payload, channels.loading]);
return (
<>
<StepHeading> {t('alert_form_step3')} </StepHeading>

View File

@@ -246,19 +246,17 @@ function ChartPreview({
return (
<ChartContainer>
{headline}
<div ref={graphRef} style={{ height: '100%' }}>
{queryResponse.isLoading && (
<Spinner size="large" tip="Loading..." height="100%" />
)}
{(queryResponse?.isError || queryResponse?.error) && (
<FailedMessageContainer color="red" title="Failed to refresh the chart">
<InfoCircleOutlined />{' '}
{queryResponse.error.message || t('preview_chart_unexpected_error')}
</FailedMessageContainer>
)}
{chartData && !queryResponse.isError && (
{(queryResponse?.isError || queryResponse?.error) && (
<FailedMessageContainer color="red" title="Failed to refresh the chart">
<InfoCircleOutlined />{' '}
{queryResponse.error.message || t('preview_chart_unexpected_error')}
</FailedMessageContainer>
)}
{chartData && !queryResponse.isError && (
<div ref={graphRef} style={{ height: '100%' }}>
{queryResponse.isLoading && (
<Spinner size="large" tip="Loading..." height="100%" />
)}
<GridPanelSwitch
options={options}
panelType={graphType}
@@ -270,8 +268,8 @@ function ChartPreview({
query={query || initialQueriesMap.metrics}
yAxisUnit={yAxisUnit}
/>
)}
</div>
</div>
)}
</ChartContainer>
);
}

View File

@@ -2,7 +2,6 @@ import './QuerySection.styles.scss';
import { Color } from '@signozhq/design-tokens';
import { Button, Tabs, Tooltip } from 'antd';
import logEvent from 'api/common/logEvent';
import PromQLIcon from 'assets/Dashboard/PromQl';
import { ALERTS_DATA_SOURCE_MAP } from 'constants/alerts';
import { ENTITY_VERSION_V4 } from 'constants/app';
@@ -32,7 +31,6 @@ function QuerySection({
runQuery,
alertDef,
panelType,
ruleId,
}: QuerySectionProps): JSX.Element {
// init namespace for translations
const { t } = useTranslation('alerts');
@@ -160,15 +158,7 @@ function QuerySection({
<span style={{ display: 'flex', gap: '1rem', alignItems: 'center' }}>
<Button
type="primary"
onClick={(): void => {
runQuery();
logEvent('Alert: Stage and run query', {
dataSource: ALERTS_DATA_SOURCE_MAP[alertType],
isNewRule: !ruleId || ruleId === 0,
ruleId,
queryType: queryCategory,
});
}}
onClick={runQuery}
className="stage-run-query"
icon={<Play size={14} />}
>
@@ -238,7 +228,6 @@ interface QuerySectionProps {
runQuery: VoidFunction;
alertDef: AlertDef;
panelType: PANEL_TYPES;
ruleId: number;
}
export default QuerySection;

View File

@@ -12,10 +12,8 @@ import {
} from 'antd';
import saveAlertApi from 'api/alerts/save';
import testAlertApi from 'api/alerts/testAlert';
import logEvent from 'api/common/logEvent';
import FacingIssueBtn from 'components/facingIssueBtn/FacingIssueBtn';
import { alertHelpMessage } from 'components/facingIssueBtn/util';
import { ALERTS_DATA_SOURCE_MAP } from 'constants/alerts';
import { FeatureKeys } from 'constants/features';
import { QueryParams } from 'constants/query';
import { PANEL_TYPES } from 'constants/queryBuilder';
@@ -340,13 +338,8 @@ function FormAlertRules({
return;
}
const postableAlert = memoizedPreparePostData();
setLoading(true);
let logData = {
status: 'error',
statusMessage: t('unexpected_error'),
};
try {
const apiReq =
ruleId && ruleId > 0
@@ -356,15 +349,10 @@ function FormAlertRules({
const response = await saveAlertApi(apiReq);
if (response.statusCode === 200) {
logData = {
status: 'success',
statusMessage:
!ruleId || ruleId === 0 ? t('rule_created') : t('rule_edited'),
};
notifications.success({
message: 'Success',
description: logData.statusMessage,
description:
!ruleId || ruleId === 0 ? t('rule_created') : t('rule_edited'),
});
// invalidate rule in cache
@@ -379,42 +367,18 @@ function FormAlertRules({
history.replace(`${ROUTES.LIST_ALL_ALERT}?${urlQuery.toString()}`);
}, 2000);
} else {
logData = {
status: 'error',
statusMessage: response.error || t('unexpected_error'),
};
notifications.error({
message: 'Error',
description: logData.statusMessage,
description: response.error || t('unexpected_error'),
});
}
} catch (e) {
logData = {
status: 'error',
statusMessage: t('unexpected_error'),
};
notifications.error({
message: 'Error',
description: logData.statusMessage,
description: t('unexpected_error'),
});
}
setLoading(false);
logEvent('Alert: Save alert', {
...logData,
dataSource: ALERTS_DATA_SOURCE_MAP[postableAlert?.alertType as AlertTypes],
channelNames: postableAlert?.preferredChannels,
broadcastToAll: postableAlert?.broadcastToAll,
isNewRule: !ruleId || ruleId === 0,
ruleId,
queryType: currentQuery.queryType,
alertId: postableAlert?.id,
alertName: postableAlert?.alert,
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
isFormValid,
memoizedPreparePostData,
@@ -450,7 +414,6 @@ function FormAlertRules({
}
const postableAlert = memoizedPreparePostData();
let statusResponse = { status: 'failed', message: '' };
setLoading(true);
try {
const response = await testAlertApi({ data: postableAlert });
@@ -462,43 +425,25 @@ function FormAlertRules({
message: 'Error',
description: t('no_alerts_found'),
});
statusResponse = { status: 'failed', message: t('no_alerts_found') };
} else {
notifications.success({
message: 'Success',
description: t('rule_test_fired'),
});
statusResponse = { status: 'success', message: t('rule_test_fired') };
}
} else {
notifications.error({
message: 'Error',
description: response.error || t('unexpected_error'),
});
statusResponse = {
status: 'failed',
message: response.error || t('unexpected_error'),
};
}
} catch (e) {
notifications.error({
message: 'Error',
description: t('unexpected_error'),
});
statusResponse = { status: 'failed', message: t('unexpected_error') };
}
setLoading(false);
logEvent('Alert: Test notification', {
dataSource: ALERTS_DATA_SOURCE_MAP[alertDef?.alertType as AlertTypes],
channelNames: postableAlert?.preferredChannels,
broadcastToAll: postableAlert?.broadcastToAll,
isNewRule: !ruleId || ruleId === 0,
ruleId,
queryType: currentQuery.queryType,
status: statusResponse.status,
statusMessage: statusResponse.message,
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [t, isFormValid, memoizedPreparePostData, notifications]);
const renderBasicInfo = (): JSX.Element => (
@@ -568,16 +513,6 @@ function FormAlertRules({
const isRuleCreated = !ruleId || ruleId === 0;
useEffect(() => {
if (!isRuleCreated) {
logEvent('Alert: Edit page visited', {
ruleId,
dataSource: ALERTS_DATA_SOURCE_MAP[alertType as AlertTypes],
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
function handleRedirection(option: AlertTypes): void {
let url = '';
switch (option) {
@@ -600,13 +535,6 @@ function FormAlertRules({
default:
break;
}
logEvent('Alert: Check example alert clicked', {
dataSource: ALERTS_DATA_SOURCE_MAP[alertDef?.alertType as AlertTypes],
isNewRule: !ruleId || ruleId === 0,
ruleId,
queryType: currentQuery.queryType,
link: url,
});
window.open(url, '_blank');
}
@@ -644,7 +572,6 @@ function FormAlertRules({
alertDef={alertDef}
panelType={panelType || PANEL_TYPES.TIME_SERIES}
key={currentQuery.queryType}
ruleId={ruleId}
/>
<RuleOptions

View File

@@ -80,8 +80,6 @@ function FullView({
query: updatedQuery,
globalSelectedInterval: globalSelectedTime,
variables: getDashboardVariables(selectedDashboard?.data.variables),
fillGaps: widget.fillSpans,
formatForWeb: widget.panelTypes === PANEL_TYPES.TABLE,
};
}
updatedQuery.builder.queryData[0].pageSize = 10;

View File

@@ -109,7 +109,6 @@ function GridCardGraph({
globalSelectedInterval,
variables: getDashboardVariables(variables),
fillGaps: widget.fillSpans,
formatForWeb: widget.panelTypes === PANEL_TYPES.TABLE,
};
}
updatedQuery.builder.queryData[0].pageSize = 10;

View File

@@ -1,215 +0,0 @@
export const tableDataMultipleQueriesSuccessResponse = {
columns: [
{
name: 'service_name',
queryName: '',
isValueColumn: false,
},
{
name: 'A',
queryName: 'A',
isValueColumn: true,
},
{
name: 'B',
queryName: 'B',
isValueColumn: true,
},
],
rows: [
{
data: {
A: 4196.71,
B: 'n/a',
service_name: 'demo-app',
},
},
{
data: {
A: 500.83,
B: 'n/a',
service_name: 'customer',
},
},
{
data: {
A: 499.5,
B: 'n/a',
service_name: 'mysql',
},
},
{
data: {
A: 293.22,
B: 'n/a',
service_name: 'frontend',
},
},
{
data: {
A: 230.03,
B: 'n/a',
service_name: 'driver',
},
},
{
data: {
A: 67.09,
B: 'n/a',
service_name: 'route',
},
},
{
data: {
A: 30.96,
B: 'n/a',
service_name: 'redis',
},
},
{
data: {
A: 'n/a',
B: 112.27,
service_name: 'n/a',
},
},
],
};
export const widgetQueryWithLegend = {
clickhouse_sql: [
{
name: 'A',
legend: '',
disabled: false,
query: '',
},
],
promql: [
{
name: 'A',
query: '',
legend: '',
disabled: false,
},
],
builder: {
queryData: [
{
dataSource: 'metrics',
queryName: 'A',
aggregateOperator: 'count',
aggregateAttribute: {
dataType: 'float64',
id: 'signoz_latency--float64--ExponentialHistogram--true',
isColumn: true,
isJSON: false,
key: 'signoz_latency',
type: 'ExponentialHistogram',
},
timeAggregation: '',
spaceAggregation: 'p90',
functions: [],
filters: {
items: [],
op: 'AND',
},
expression: 'A',
disabled: false,
stepInterval: 60,
having: [],
limit: null,
orderBy: [],
groupBy: [
{
dataType: 'string',
isColumn: false,
isJSON: false,
key: 'service_name',
type: 'tag',
id: 'service_name--string--tag--false',
},
],
legend: 'p99',
reduceTo: 'avg',
},
{
dataSource: 'metrics',
queryName: 'B',
aggregateOperator: 'rate',
aggregateAttribute: {
dataType: 'float64',
id: 'system_disk_operations--float64--Sum--true',
isColumn: true,
isJSON: false,
key: 'system_disk_operations',
type: 'Sum',
},
timeAggregation: 'rate',
spaceAggregation: 'sum',
functions: [],
filters: {
items: [],
op: 'AND',
},
expression: 'B',
disabled: false,
stepInterval: 60,
having: [],
limit: null,
orderBy: [],
groupBy: [],
legend: '',
reduceTo: 'avg',
},
],
queryFormulas: [],
},
id: '48ad5a67-9a3c-49d4-a886-d7a34f8b875d',
queryType: 'builder',
};
export const expectedOutputWithLegends = {
dataSource: [
{
A: 4196.71,
B: 'n/a',
service_name: 'demo-app',
},
{
A: 500.83,
B: 'n/a',
service_name: 'customer',
},
{
A: 499.5,
B: 'n/a',
service_name: 'mysql',
},
{
A: 293.22,
B: 'n/a',
service_name: 'frontend',
},
{
A: 230.03,
B: 'n/a',
service_name: 'driver',
},
{
A: 67.09,
B: 'n/a',
service_name: 'route',
},
{
A: 30.96,
B: 'n/a',
service_name: 'redis',
},
{
A: 'n/a',
B: 112.27,
service_name: 'n/a',
},
],
};

View File

@@ -1,42 +0,0 @@
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { createColumnsAndDataSource, getQueryLegend } from '../utils';
import {
expectedOutputWithLegends,
tableDataMultipleQueriesSuccessResponse,
widgetQueryWithLegend,
} from './response';
describe('Table Panel utils', () => {
it('createColumnsAndDataSource function', () => {
const data = tableDataMultipleQueriesSuccessResponse;
const query = widgetQueryWithLegend as Query;
const { columns, dataSource } = createColumnsAndDataSource(data, query);
expect(dataSource).toStrictEqual(expectedOutputWithLegends.dataSource);
// this makes sure that the columns are rendered in the same order as response
expect(columns[0].title).toBe('service_name');
// the next specifically makes sure that the legends are properly applied in multiple queries
expect(columns[1].title).toBe('p99');
// this makes sure that the query without a legend takes the title from the query response
expect(columns[2].title).toBe('B');
// this is to ensure that the rows properly map to the column data indexes as the dataIndex should be equal to name of the columns
// returned in the response as the rows will be mapped with them
expect((columns[0] as any).dataIndex).toBe('service_name');
expect((columns[1] as any).dataIndex).toBe('A');
expect((columns[2] as any).dataIndex).toBe('B');
});
it('getQueryLegend function', () => {
const query = widgetQueryWithLegend as Query;
// query A has a legend of p99
expect(getQueryLegend(query, 'A')).toBe('p99');
// should return undefined when legend not present
expect(getQueryLegend(query, 'B')).toBe(undefined);
});
});

View File

@@ -3,7 +3,10 @@ import { Space, Tooltip } from 'antd';
import { getYAxisFormattedValue } from 'components/Graph/yAxisConfig';
import { Events } from 'constants/events';
import { QueryTable } from 'container/QueryTable';
import { RowData } from 'lib/query/createTableColumnsFromQuery';
import {
createTableColumnsFromQuery,
RowData,
} from 'lib/query/createTableColumnsFromQuery';
import { cloneDeep, get, isEmpty, set } from 'lodash-es';
import { memo, ReactNode, useCallback, useEffect, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
@@ -11,11 +14,7 @@ import { eventEmitter } from 'utils/getEventEmitter';
import { WrapperStyled } from './styles';
import { GridTableComponentProps } from './types';
import {
createColumnsAndDataSource,
findMatchingThreshold,
TableData,
} from './utils';
import { findMatchingThreshold } from './utils';
function GridTableComponent({
data,
@@ -26,26 +25,28 @@ function GridTableComponent({
...props
}: GridTableComponentProps): JSX.Element {
const { t } = useTranslation(['valueGraph']);
// create columns and dataSource in the ui friendly structure
// use the query from the widget here to extract the legend information
const { columns, dataSource: originalDataSource } = useMemo(
() => createColumnsAndDataSource((data as unknown) as TableData, query),
[query, data],
() =>
createTableColumnsFromQuery({
query,
queryTableData: data,
}),
[data, query],
);
const createDataInCorrectFormat = useCallback(
(dataSource: RowData[]): RowData[] =>
dataSource.map((d) => {
const finalObject = {};
// we use the order of the columns here to have similar download as the user view
columns.forEach((k) => {
set(
finalObject,
get(k, 'title', '') as string,
get(d, get(k, 'dataIndex', ''), 'n/a'),
const keys = Object.keys(d);
keys.forEach((k) => {
const label = get(
columns.find((c) => get(c, 'dataIndex', '') === k) || {},
'title',
'',
);
if (label) {
set(finalObject, label as string, d[k]);
}
});
return finalObject as RowData;
}),
@@ -64,11 +65,7 @@ function GridTableComponent({
const newValue = { ...val };
Object.keys(val).forEach((k) => {
if (columnUnits[k]) {
// the check below takes care of not adding units for rows that have n/a values
newValue[k] =
val[k] !== 'n/a'
? getYAxisFormattedValue(String(val[k]), columnUnits[k])
: val[k];
newValue[k] = getYAxisFormattedValue(String(val[k]), columnUnits[k]);
newValue[`${k}_without_unit`] = val[k];
}
});

View File

@@ -1,11 +1,4 @@
import { ColumnsType, ColumnType } from 'antd/es/table';
import { ThresholdProps } from 'container/NewWidget/RightContainer/Threshold/types';
import { QUERY_TABLE_CONFIG } from 'container/QueryTable/config';
import { QueryTableProps } from 'container/QueryTable/QueryTable.intefaces';
import { RowData } from 'lib/query/createTableColumnsFromQuery';
import { isEmpty, isNaN } from 'lodash-es';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { EQueryType } from 'types/common/dashboard';
// Helper function to evaluate the condition based on the operator
function evaluateCondition(
@@ -63,85 +56,3 @@ export function findMatchingThreshold(
hasMultipleMatches,
};
}
export interface TableData {
columns: { name: string; queryName: string; isValueColumn: boolean }[];
rows: { data: any }[];
}
export function getQueryLegend(
currentQuery: Query,
queryName: string,
): string | undefined {
let legend: string | undefined;
switch (currentQuery.queryType) {
case EQueryType.QUERY_BUILDER:
// check if the value is present in the queries
legend = currentQuery.builder.queryData.find(
(query) => query.queryName === queryName,
)?.legend;
if (!legend) {
// check if the value is present in the formula
legend = currentQuery.builder.queryFormulas.find(
(query) => query.queryName === queryName,
)?.legend;
}
break;
case EQueryType.CLICKHOUSE:
legend = currentQuery.clickhouse_sql.find(
(query) => query.name === queryName,
)?.legend;
break;
case EQueryType.PROM:
legend = currentQuery.promql.find((query) => query.name === queryName)
?.legend;
break;
default:
legend = undefined;
break;
}
return legend;
}
export function createColumnsAndDataSource(
data: TableData,
currentQuery: Query,
renderColumnCell?: QueryTableProps['renderColumnCell'],
): { columns: ColumnsType<RowData>; dataSource: RowData[] } {
const columns: ColumnsType<RowData> =
data.columns?.reduce<ColumnsType<RowData>>((acc, item) => {
// is the column is the value column then we need to check for the available legend
const legend = item.isValueColumn
? getQueryLegend(currentQuery, item.queryName)
: undefined;
const column: ColumnType<RowData> = {
dataIndex: item.name,
// if no legend present then rely on the column name value
title: !isEmpty(legend) ? legend : item.name,
width: QUERY_TABLE_CONFIG.width,
render: renderColumnCell && renderColumnCell[item.name],
sorter: (a: RowData, b: RowData): number => {
const valueA = Number(a[`${item.name}_without_unit`] ?? a[item.name]);
const valueB = Number(b[`${item.name}_without_unit`] ?? b[item.name]);
if (!isNaN(valueA) && !isNaN(valueB)) {
return valueA - valueB;
}
return ((a[item.name] as string) || '').localeCompare(
(b[item.name] as string) || '',
);
},
};
return [...acc, column];
}, []) || [];
// the rows returned have data encapsulation hence removing the same here
const dataSource = data.rows?.map((d) => d.data) || [];
return { columns, dataSource };
}

View File

@@ -27,7 +27,6 @@ import {
import { useSelector } from 'react-redux';
import { NavLink } from 'react-router-dom';
import { AppState } from 'store/reducers';
import { License } from 'types/api/licenses/def';
import AppReducer from 'types/reducer/app';
import { getFormattedDate, getRemainingDays } from 'utils/timeUtils';
@@ -110,13 +109,9 @@ function HeaderContainer(): JSX.Element {
const { data: licenseData, isFetching, status: licenseStatus } = useLicense();
const licensesStatus: string =
licenseData?.payload?.licenses?.find((e: License) => e.isCurrent)?.status ||
'';
const isLicenseActive =
licensesStatus?.toLocaleLowerCase() ===
LICENSE_PLAN_STATUS.VALID.toLocaleLowerCase();
licenseData?.payload?.licenses?.find((e) => e.isCurrent)?.status ===
LICENSE_PLAN_STATUS.VALID;
useEffect(() => {
if (

View File

@@ -7,20 +7,17 @@ interface AlertInfoCardProps {
header: string;
subheader: string;
link: string;
onClick: () => void;
}
function AlertInfoCard({
header,
subheader,
link,
onClick,
}: AlertInfoCardProps): JSX.Element {
return (
<div
className="alert-info-card"
onClick={(): void => {
onClick();
window.open(link, '_blank');
}}
>

View File

@@ -2,7 +2,6 @@ import './AlertsEmptyState.styles.scss';
import { PlusOutlined } from '@ant-design/icons';
import { Button, Divider, Typography } from 'antd';
import logEvent from 'api/common/logEvent';
import ROUTES from 'constants/routes';
import useComponentPermission from 'hooks/useComponentPermission';
import { useNotifications } from 'hooks/useNotifications';
@@ -11,26 +10,12 @@ import { useCallback, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { DataSource } from 'types/common/queryBuilder';
import AppReducer from 'types/reducer/app';
import AlertInfoCard from './AlertInfoCard';
import { ALERT_CARDS, ALERT_INFO_LINKS } from './alertLinks';
import InfoLinkText from './InfoLinkText';
const alertLogEvents = (
title: string,
link: string,
dataSource?: DataSource,
): void => {
const attributes = {
link,
page: 'Alert empty state page',
};
logEvent(title, dataSource ? { ...attributes, dataSource } : attributes);
};
export function AlertsEmptyState(): JSX.Element {
const { t } = useTranslation('common');
const { role, featureResponse } = useSelector<AppState, AppReducer>(
@@ -106,33 +91,18 @@ export function AlertsEmptyState(): JSX.Element {
link="https://youtu.be/xjxNIqiv4_M"
leftIconVisible
rightIconVisible
onClick={(): void =>
alertLogEvents(
'Alert: Video tutorial link clicked',
'https://youtu.be/xjxNIqiv4_M',
)
}
/>
</div>
{ALERT_INFO_LINKS.map((info) => {
const logEventTriggered = (): void =>
alertLogEvents(
'Alert: Tutorial doc link clicked',
info.link,
info.dataSource,
);
return (
<InfoLinkText
key={info.link}
infoText={info.infoText}
link={info.link}
leftIconVisible={info.leftIconVisible}
rightIconVisible={info.rightIconVisible}
onClick={logEventTriggered}
/>
);
})}
{ALERT_INFO_LINKS.map((info) => (
<InfoLinkText
key={info.link}
infoText={info.infoText}
link={info.link}
leftIconVisible={info.leftIconVisible}
rightIconVisible={info.rightIconVisible}
/>
))}
</div>
</section>
<div className="get-started-text">
@@ -143,23 +113,14 @@ export function AlertsEmptyState(): JSX.Element {
</Divider>
</div>
{ALERT_CARDS.map((card) => {
const logEventTriggered = (): void =>
alertLogEvents(
'Alert: Sample alert link clicked',
card.link,
card.dataSource,
);
return (
<AlertInfoCard
key={card.link}
header={card.header}
subheader={card.subheader}
link={card.link}
onClick={logEventTriggered}
/>
);
})}
{ALERT_CARDS.map((card) => (
<AlertInfoCard
key={card.link}
header={card.header}
subheader={card.subheader}
link={card.link}
/>
))}
</div>
</div>
);

View File

@@ -6,7 +6,6 @@ interface InfoLinkTextProps {
link: string;
leftIconVisible: boolean;
rightIconVisible: boolean;
onClick: () => void;
}
function InfoLinkText({
@@ -14,12 +13,10 @@ function InfoLinkText({
link,
leftIconVisible,
rightIconVisible,
onClick,
}: InfoLinkTextProps): JSX.Element {
return (
<Flex
onClick={(): void => {
onClick();
window.open(link, '_blank');
}}
className="info-link-container"

View File

@@ -1,5 +1,3 @@
import { DataSource } from 'types/common/queryBuilder';
export const ALERT_INFO_LINKS = [
{
infoText: 'How to create Metrics-based alerts',
@@ -7,7 +5,6 @@ export const ALERT_INFO_LINKS = [
'https://signoz.io/docs/alerts-management/metrics-based-alerts/?utm_source=product&utm_medium=alert-empty-page',
leftIconVisible: false,
rightIconVisible: true,
dataSource: DataSource.METRICS,
},
{
infoText: 'How to create Log-based alerts',
@@ -15,7 +12,6 @@ export const ALERT_INFO_LINKS = [
'https://signoz.io/docs/alerts-management/log-based-alerts/?utm_source=product&utm_medium=alert-empty-page',
leftIconVisible: false,
rightIconVisible: true,
dataSource: DataSource.LOGS,
},
{
infoText: 'How to create Trace-based alerts',
@@ -23,7 +19,6 @@ export const ALERT_INFO_LINKS = [
'https://signoz.io/docs/alerts-management/trace-based-alerts/?utm_source=product&utm_medium=alert-empty-page',
leftIconVisible: false,
rightIconVisible: true,
dataSource: DataSource.TRACES,
},
];
@@ -31,28 +26,24 @@ export const ALERT_CARDS = [
{
header: 'Alert on high memory usage',
subheader: "Monitor your host's memory usage",
dataSource: DataSource.METRICS,
link:
'https://signoz.io/docs/alerts-management/metrics-based-alerts/?utm_source=product&utm_medium=alert-empty-page#1-alert-when-memory-usage-for-host-goes-above-400-mb-or-any-fixed-memory',
},
{
header: 'Alert on slow external API calls',
subheader: 'Monitor your external API calls',
dataSource: DataSource.TRACES,
link:
'https://signoz.io/docs/alerts-management/trace-based-alerts/?utm_source=product&utm_medium=alert-empty-page#1-alert-when-external-api-latency-p90-is-over-1-second-for-last-5-mins',
},
{
header: 'Alert on high percentage of timeout errors in logs',
subheader: 'Monitor your logs for errors',
dataSource: DataSource.LOGS,
link:
'https://signoz.io/docs/alerts-management/log-based-alerts/?utm_source=product&utm_medium=alert-empty-page#1-alert-when-percentage-of-redis-timeout-error-logs-greater-than-7-in-last-5-mins',
},
{
header: 'Alert on high error percentage of an endpoint',
subheader: 'Monitor your API endpoint',
dataSource: DataSource.METRICS,
link:
'https://signoz.io/docs/alerts-management/metrics-based-alerts/?utm_source=product&utm_medium=alert-empty-page#3-alert-when-the-error-percentage-for-an-endpoint-exceeds-5',
},

View File

@@ -3,7 +3,6 @@ import { PlusOutlined } from '@ant-design/icons';
import { Input, Typography } from 'antd';
import type { ColumnsType } from 'antd/es/table/interface';
import saveAlertApi from 'api/alerts/save';
import logEvent from 'api/common/logEvent';
import DropDown from 'components/DropDown/DropDown';
import { listAlertMessage } from 'components/facingIssueBtn/util';
import {
@@ -42,7 +41,7 @@ import {
} from './styles';
import Status from './TableComponents/Status';
import ToggleAlertState from './ToggleAlertState';
import { alertActionLogEvent, filterAlerts } from './utils';
import { filterAlerts } from './utils';
const { Search } = Input;
@@ -108,16 +107,12 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
}, [notificationsApi, t]);
const onClickNewAlertHandler = useCallback(() => {
logEvent('Alert: New alert button clicked', {
number: allAlertRules?.length,
});
featureResponse
.refetch()
.then(() => {
history.push(ROUTES.ALERTS_NEW);
})
.catch(handleError);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [featureResponse, handleError]);
const onEditHandler = (record: GettableAlert) => (): void => {
@@ -326,7 +321,6 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
width: 10,
render: (id: GettableAlert['id'], record): JSX.Element => (
<DropDown
onDropDownItemClick={(item): void => alertActionLogEvent(item.key, record)}
element={[
<ToggleAlertState
key="1"
@@ -362,9 +356,6 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
});
}
const paginationConfig = {
defaultCurrent: Number(paginationParam) || 1,
};
return (
<>
<SearchContainer>
@@ -394,10 +385,11 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
columns={columns}
rowKey="id"
dataSource={data}
shouldSendAlertsLogEvent
dynamicColumns={dynamicColumns}
onChange={handleChange}
pagination={paginationConfig}
pagination={{
defaultCurrent: Number(paginationParam) || 1,
}}
facingIssueBtn={{
attributes: {
screen: 'Alert list page',

View File

@@ -1,11 +1,9 @@
import { Space } from 'antd';
import getAll from 'api/alerts/getAll';
import logEvent from 'api/common/logEvent';
import ReleaseNote from 'components/ReleaseNote';
import Spinner from 'components/Spinner';
import { useNotifications } from 'hooks/useNotifications';
import { isUndefined } from 'lodash-es';
import { useEffect, useRef } from 'react';
import { useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { useQuery } from 'react-query';
import { useLocation } from 'react-router-dom';
@@ -21,19 +19,8 @@ function ListAlertRules(): JSX.Element {
cacheTime: 0,
});
const logEventCalledRef = useRef(false);
const { notifications } = useNotifications();
useEffect(() => {
if (!logEventCalledRef.current && !isUndefined(data?.payload)) {
logEvent('Alert: List page visited', {
number: data?.payload?.length,
});
logEventCalledRef.current = true;
}
}, [data?.payload]);
useEffect(() => {
if (status === 'error' || (status === 'success' && data.statusCode >= 400)) {
notifications.error({

View File

@@ -1,6 +1,3 @@
import logEvent from 'api/common/logEvent';
import { ALERTS_DATA_SOURCE_MAP } from 'constants/alerts';
import { AlertTypes } from 'types/api/alerts/alertTypes';
import { GettableAlert } from 'types/api/alerts/get';
export const filterAlerts = (
@@ -26,32 +23,3 @@ export const filterAlerts = (
);
});
};
export const alertActionLogEvent = (
action: string,
record: GettableAlert,
): void => {
let actionValue = '';
switch (action) {
case '0':
actionValue = 'Enable/Disable';
break;
case '1':
actionValue = 'Edit';
break;
case '2':
actionValue = 'Clone';
break;
case '3':
actionValue = 'Delete';
break;
default:
break;
}
logEvent('Alert: Action', {
ruleId: record.id,
dataSource: ALERTS_DATA_SOURCE_MAP[record.alertType as AlertTypes],
name: record.alert,
action: actionValue,
});
};

View File

@@ -609,16 +609,6 @@ function DashboardsList(): JSX.Element {
</>
);
const paginationConfig = data.length > 20 && {
pageSize: 20,
showTotal: showPaginationItem,
showSizeChanger: false,
onChange: (page: any): void => handlePageSizeUpdate(page),
current: Number(sortOrder.pagination),
defaultCurrent: Number(sortOrder.pagination) || 1,
hideOnSinglePage: true,
};
return (
<div className="dashboards-list-container">
<div className="dashboards-list-view-content">
@@ -832,7 +822,16 @@ function DashboardsList(): JSX.Element {
showSorterTooltip
loading={isDashboardListLoading || isFilteringDashboards}
showHeader={false}
pagination={paginationConfig}
pagination={
data.length > 20 && {
pageSize: 20,
showTotal: showPaginationItem,
showSizeChanger: false,
onChange: (page): void => handlePageSizeUpdate(page),
current: Number(sortOrder.pagination),
defaultCurrent: Number(sortOrder.pagination) || 1,
}
}
/>
</>
)}

View File

@@ -141,6 +141,11 @@ function ImportJSON({
colors: {
'editor.background': Color.BG_INK_300,
},
fontFamily: 'Space Mono',
fontSize: 20,
fontWeight: 'normal',
lineHeight: 18,
letterSpacing: -0.06,
});
}
@@ -228,11 +233,6 @@ function ImportJSON({
fontFamily: 'Space Mono',
}}
theme={isDarkMode ? 'my-theme' : 'light'}
onMount={(_, monaco): void => {
document.fonts.ready.then(() => {
monaco.editor.remeasureFonts();
});
}}
// eslint-disable-next-line react/jsx-no-bind
beforeMount={setEditorTheme}
/>

View File

@@ -8,7 +8,7 @@
.label {
color: var(--text-robin-400);
font-family: SF Mono;
font-family: 'Geist Mono';
font-family: 'Space Mono', monospace;
font-size: 13px;
font-weight: var(--font-weight-normal);
line-height: 18px;

View File

@@ -28,7 +28,7 @@ function JSONView({ logData }: JSONViewProps): JSX.Element {
},
fontWeight: 400,
// fontFamily: 'SF Mono',
fontFamily: 'Geist Mono',
fontFamily: 'Space Mono',
fontSize: 13,
lineHeight: '18px',
colorDecorators: true,

View File

@@ -53,7 +53,8 @@ function Overview({
enabled: false,
},
fontWeight: 400,
fontFamily: 'Geist Mono',
// fontFamily: 'SF Mono',
fontFamily: 'Space Mono',
fontSize: 13,
lineHeight: '18px',
colorDecorators: true,
@@ -79,6 +80,12 @@ function Overview({
colors: {
'editor.background': Color.BG_INK_400,
},
// fontFamily: 'SF Mono',
fontFamily: 'Space Mono',
fontSize: 12,
fontWeight: 'normal',
lineHeight: 18,
letterSpacing: -0.06,
});
}
@@ -117,11 +124,6 @@ function Overview({
onChange={(): void => {}}
height="20vh"
theme={isDarkMode ? 'my-theme' : 'light'}
onMount={(_, monaco): void => {
document.fonts.ready.then(() => {
monaco.editor.remeasureFonts();
});
}}
// eslint-disable-next-line react/jsx-no-bind
beforeMount={setEditorTheme}
/>

View File

@@ -57,8 +57,6 @@
background: rgba(22, 25, 34, 0.4);
.value-field {
font-family: 'Geist Mono';
position: relative;
}

View File

@@ -289,13 +289,7 @@ function TableView({
return (
<div className="value-field">
<CopyClipboardHOC textToCopy={textToCopy}>
<span
style={{
color: Color.BG_SIENNA_400,
whiteSpace: 'pre-wrap',
tabSize: 4,
}}
>
<span style={{ color: Color.BG_SIENNA_400 }}>
{removeEscapeCharacters(fieldData.value)}
</span>
</CopyClipboardHOC>

View File

@@ -9,6 +9,6 @@ export const CardStyled = styled(Card)`
height: 200px;
min-height: 200px;
padding: 0 16px 16px 16px;
font-family: 'Geist Mono';
font-family: 'Space Mono', monospace;
}
`;

View File

@@ -16,6 +16,7 @@ import { useOptionsMenu } from 'container/OptionsMenu';
import { useActiveLog } from 'hooks/logs/useActiveLog';
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import PeriscopeTable from 'periscope/components/Table/Table';
import { memo, useCallback, useMemo, useRef } from 'react';
import { Virtuoso, VirtuosoHandle } from 'react-virtuoso';
// interfaces
@@ -157,6 +158,7 @@ function LogsExplorerList({
return (
<div className="logs-list-view-container">
<PeriscopeTable />
{(isLoading || (isFetching && logs.length === 0)) && <LogsLoading />}
{!isLoading &&

View File

@@ -63,10 +63,10 @@ import { v4 } from 'uuid';
function LogsExplorerViews({
selectedView,
showFrequencyChart,
showHistogram,
}: {
selectedView: SELECTED_VIEWS;
showFrequencyChart: boolean;
showHistogram: boolean;
}): JSX.Element {
const { notifications } = useNotifications();
const history = useHistory();
@@ -561,7 +561,7 @@ function LogsExplorerViews({
return (
<div className="logs-explorer-views-container">
{showFrequencyChart && (
{showHistogram && (
<LogsExplorerChart
className="logs-histogram"
isLoading={isFetchingListChartData || isLoadingListChartData}

View File

@@ -76,10 +76,7 @@ const renderer = (): RenderResult =>
<VirtuosoMockContext.Provider
value={{ viewportHeight: 300, itemHeight: 100 }}
>
<LogsExplorerViews
selectedView={SELECTED_VIEWS.SEARCH}
showFrequencyChart
/>
<LogsExplorerViews selectedView={SELECTED_VIEWS.SEARCH} showHistogram />
</VirtuosoMockContext.Provider>
</QueryBuilderProvider>
</MockQueryClientProvider>
@@ -123,7 +120,11 @@ describe('LogsExplorerViews -', () => {
// switch to table view
await userEvent.click(queryByTestId('table-view') as HTMLElement);
expect(queryByText('pending_data_placeholder')).toBeInTheDocument();
expect(
queryByText(
'Just a bit of patience, just a little bits enough ⎯ were getting your logs!',
),
).toBeInTheDocument();
});
it('check error state', async () => {

View File

@@ -1,11 +1,8 @@
import './LogsLoading.styles.scss';
import { Typography } from 'antd';
import { useTranslation } from 'react-i18next';
import { DataSource } from 'types/common/queryBuilder';
export function LogsLoading(): JSX.Element {
const { t } = useTranslation('common');
return (
<div className="loading-logs">
<div className="loading-logs-content">
@@ -16,7 +13,8 @@ export function LogsLoading(): JSX.Element {
/>
<Typography>
{t('pending_data_placeholder', { dataSource: DataSource.LOGS })}
Just a bit of patience, just a little bits enough were getting your
logs!
</Typography>
</div>
</div>

View File

@@ -15,7 +15,6 @@ import {
} from 'hooks/useResourceAttribute/utils';
import { useMemo, useState } from 'react';
import { useParams } from 'react-router-dom';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { EQueryType } from 'types/common/dashboard';
import { v4 as uuid } from 'uuid';
@@ -94,26 +93,6 @@ function External(): JSX.Element {
[servicename, tagFilterItems],
);
const errorApmToTraceQuery = useGetAPMToTracesQueries({
servicename,
isExternalCall: true,
filters: [
{
id: uuid().slice(0, 8),
key: {
key: 'hasError',
dataType: DataTypes.bool,
type: 'tag',
isColumn: true,
isJSON: false,
id: 'hasError--bool--tag--true',
},
op: 'in',
value: ['true'],
},
],
});
const externalCallRPSWidget = useMemo(
() =>
getWidgetQueryBuilder({
@@ -177,7 +156,7 @@ function External(): JSX.Element {
servicename,
selectedTraceTags,
timestamp: selectedTimeStamp,
apmToTraceQuery: errorApmToTraceQuery,
apmToTraceQuery,
})}
>
View Traces

View File

@@ -58,7 +58,6 @@
display: flex;
justify-content: space-between;
align-items: center;
margin-right: 16px;
.dashboard-breadcrumbs {
height: 48px;

View File

@@ -87,6 +87,9 @@ function ClickHouseQueryBuilder({
'editor.background': Color.BG_INK_300,
},
});
document.fonts.ready.then(() => {
monaco.editor.remeasureFonts();
});
}
return (
@@ -102,11 +105,6 @@ function ClickHouseQueryBuilder({
height="200px"
onChange={handleUpdateEditor}
value={queryData.query}
onMount={(_, monaco): void => {
document.fonts.ready.then(() => {
monaco.editor.remeasureFonts();
});
}}
options={{
scrollbar: {
alwaysConsumeMouseWheel: false,

View File

@@ -2,6 +2,8 @@ import { Card, Typography } from 'antd';
import Spinner from 'components/Spinner';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { WidgetGraphContainerProps } from 'container/NewWidget/types';
// import useUrlQuery from 'hooks/useUrlQuery';
// import { useDashboard } from 'providers/Dashboard/Dashboard';
import { getSortedSeriesData } from 'utils/getSortedSeriesData';
import { NotFoundContainer } from './styles';
@@ -12,7 +14,6 @@ function WidgetGraphContainer({
queryResponse,
setRequestData,
selectedWidget,
isLoadingPanelData,
}: WidgetGraphContainerProps): JSX.Element {
if (queryResponse.data && selectedGraph === PANEL_TYPES.BAR) {
const sortedSeriesData = getSortedSeriesData(
@@ -37,10 +38,6 @@ function WidgetGraphContainer({
return <Spinner size="large" tip="Loading..." />;
}
if (isLoadingPanelData) {
return <Spinner size="large" tip="Loading..." />;
}
if (
selectedGraph !== PANEL_TYPES.LIST &&
queryResponse.data?.payload.data?.result?.length === 0
@@ -62,14 +59,6 @@ function WidgetGraphContainer({
);
}
if (queryResponse.isIdle) {
return (
<NotFoundContainer>
<Typography>No Data</Typography>
</NotFoundContainer>
);
}
return (
<WidgetGraph
selectedWidget={selectedWidget}

View File

@@ -17,7 +17,6 @@ function WidgetGraph({
queryResponse,
setRequestData,
selectedWidget,
isLoadingPanelData,
}: WidgetGraphContainerProps): JSX.Element {
const { currentQuery } = useQueryBuilder();
@@ -44,7 +43,6 @@ function WidgetGraph({
)}
<WidgetGraphComponent
isLoadingPanelData={isLoadingPanelData}
selectedGraph={selectedGraph}
queryResponse={queryResponse}
setRequestData={setRequestData}

View File

@@ -1,15 +1,18 @@
import './LeftContainer.styles.scss';
import { DEFAULT_ENTITY_VERSION } from 'constants/app';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables';
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import { memo } from 'react';
import { memo, useEffect, useState } from 'react';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { GlobalReducer } from 'types/reducer/globalTime';
import { getGraphType } from 'utils/getGraphType';
import { WidgetGraphProps } from '../types';
import ExplorerColumnsRenderer from './ExplorerColumnsRenderer';
@@ -24,17 +27,62 @@ function LeftContainer({
selectedTracesFields,
setSelectedTracesFields,
selectedWidget,
requestData,
setRequestData,
isLoadingPanelData,
selectedTime,
}: WidgetGraphProps): JSX.Element {
const { stagedQuery } = useQueryBuilder();
const { stagedQuery, redirectWithQueryBuilderData } = useQueryBuilder();
const { selectedDashboard } = useDashboard();
const { selectedTime: globalSelectedInterval } = useSelector<
AppState,
GlobalReducer
>((state) => state.globalTime);
const [requestData, setRequestData] = useState<GetQueryResultsProps>(() => {
if (selectedWidget && selectedGraph !== PANEL_TYPES.LIST) {
return {
selectedTime: selectedWidget?.timePreferance,
graphType: getGraphType(selectedGraph || selectedWidget.panelTypes),
query: stagedQuery || initialQueriesMap.metrics,
globalSelectedInterval,
variables: getDashboardVariables(selectedDashboard?.data.variables),
};
}
const updatedQuery = { ...(stagedQuery || initialQueriesMap.metrics) };
updatedQuery.builder.queryData[0].pageSize = 10;
redirectWithQueryBuilderData(updatedQuery);
return {
query: updatedQuery,
graphType: PANEL_TYPES.LIST,
selectedTime: selectedTime.enum || 'GLOBAL_TIME',
globalSelectedInterval,
tableParams: {
pagination: {
offset: 0,
limit: updatedQuery.builder.queryData[0].limit || 0,
},
},
};
});
useEffect(() => {
if (stagedQuery) {
setRequestData((prev) => ({
...prev,
selectedTime: selectedTime.enum || prev.selectedTime,
globalSelectedInterval,
graphType: getGraphType(selectedGraph || selectedWidget.panelTypes),
query: stagedQuery,
fillGaps: selectedWidget.fillSpans || false,
}));
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
stagedQuery,
selectedTime,
selectedWidget.fillSpans,
globalSelectedInterval,
]);
const queryResponse = useGetQueryRange(
requestData,
selectedDashboard?.data?.version || DEFAULT_ENTITY_VERSION,
@@ -56,7 +104,6 @@ function LeftContainer({
queryResponse={queryResponse}
setRequestData={setRequestData}
selectedWidget={selectedWidget}
isLoadingPanelData={isLoadingPanelData}
/>
<QueryContainer className="query-section-left-container">
<QuerySection selectedGraph={selectedGraph} queryResponse={queryResponse} />

View File

@@ -7,7 +7,7 @@ import FacingIssueBtn from 'components/facingIssueBtn/FacingIssueBtn';
import { chartHelpMessage } from 'components/facingIssueBtn/util';
import { FeatureKeys } from 'constants/features';
import { QueryParams } from 'constants/query';
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
import { PANEL_TYPES } from 'constants/queryBuilder';
import ROUTES from 'constants/routes';
import { DashboardShortcuts } from 'constants/shortcuts/DashboardShortcuts';
import { DEFAULT_BUCKET_COUNT } from 'container/PanelWrapper/constants';
@@ -18,8 +18,6 @@ import useAxiosError from 'hooks/useAxiosError';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { MESSAGE, useIsFeatureDisabled } from 'hooks/useFeatureFlag';
import useUrlQuery from 'hooks/useUrlQuery';
import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables';
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
import history from 'lib/history';
import { defaultTo, isUndefined } from 'lodash-es';
import { Check, X } from 'lucide-react';
@@ -40,8 +38,6 @@ import { IField } from 'types/api/logs/fields';
import { EQueryType } from 'types/common/dashboard';
import { DataSource } from 'types/common/queryBuilder';
import AppReducer from 'types/reducer/app';
import { GlobalReducer } from 'types/reducer/globalTime';
import { getGraphType, getGraphTypeForFormat } from 'utils/getGraphType';
import LeftContainer from './LeftContainer';
import QueryTypeTag from './LeftContainer/QueryTypeTag';
@@ -87,10 +83,6 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
const { featureResponse } = useSelector<AppState, AppReducer>(
(state) => state.app,
);
const { selectedTime: globalSelectedInterval } = useSelector<
AppState,
GlobalReducer
>((state) => state.globalTime);
const { widgets = [] } = selectedDashboard?.data || {};
@@ -286,65 +278,6 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
const handleError = useAxiosError();
// this loading state is to take care of mismatch in the responses for table and other panels
// hence while changing the query contains the older value and the processing logic fails
const [isLoadingPanelData, setIsLoadingPanelData] = useState<boolean>(false);
// request data should be handled by the parent and the child components should consume the same
// this has been moved here from the left container
const [requestData, setRequestData] = useState<GetQueryResultsProps>(() => {
if (selectedWidget && selectedGraph !== PANEL_TYPES.LIST) {
return {
selectedTime: selectedWidget?.timePreferance,
graphType: getGraphType(selectedGraph || selectedWidget.panelTypes),
query: stagedQuery || initialQueriesMap.metrics,
globalSelectedInterval,
formatForWeb:
getGraphTypeForFormat(selectedGraph || selectedWidget.panelTypes) ===
PANEL_TYPES.TABLE,
variables: getDashboardVariables(selectedDashboard?.data.variables),
};
}
const updatedQuery = { ...(stagedQuery || initialQueriesMap.metrics) };
updatedQuery.builder.queryData[0].pageSize = 10;
redirectWithQueryBuilderData(updatedQuery);
return {
query: updatedQuery,
graphType: PANEL_TYPES.LIST,
selectedTime: selectedTime.enum || 'GLOBAL_TIME',
globalSelectedInterval,
tableParams: {
pagination: {
offset: 0,
limit: updatedQuery.builder.queryData[0].limit || 0,
},
},
};
});
useEffect(() => {
if (stagedQuery) {
setIsLoadingPanelData(false);
setRequestData((prev) => ({
...prev,
selectedTime: selectedTime.enum || prev.selectedTime,
globalSelectedInterval,
graphType: getGraphType(selectedGraph || selectedWidget.panelTypes),
query: stagedQuery,
fillGaps: selectedWidget.fillSpans || false,
formatForWeb:
getGraphTypeForFormat(selectedGraph || selectedWidget.panelTypes) ===
PANEL_TYPES.TABLE,
}));
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
stagedQuery,
selectedTime,
selectedWidget.fillSpans,
globalSelectedInterval,
]);
const onClickSaveHandler = useCallback(() => {
if (!selectedDashboard) {
return;
@@ -469,7 +402,6 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
}, [dashboardId]);
const setGraphHandler = (type: PANEL_TYPES): void => {
setIsLoadingPanelData(true);
const updatedQuery = handleQueryChange(type as any, supersetQuery);
setGraphType(type);
redirectWithQueryBuilderData(
@@ -595,9 +527,6 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
setSelectedTracesFields={setSelectedTracesFields}
selectedWidget={selectedWidget}
selectedTime={selectedTime}
requestData={requestData}
setRequestData={setRequestData}
isLoadingPanelData={isLoadingPanelData}
/>
)}
</LeftContainerWrapper>

View File

@@ -24,9 +24,6 @@ export interface WidgetGraphProps {
selectedWidget: Widgets;
selectedGraph: PANEL_TYPES;
selectedTime: timePreferance;
requestData: GetQueryResultsProps;
setRequestData: Dispatch<SetStateAction<GetQueryResultsProps>>;
isLoadingPanelData: boolean;
}
export type WidgetGraphContainerProps = {
@@ -37,5 +34,4 @@ export type WidgetGraphContainerProps = {
setRequestData: Dispatch<SetStateAction<GetQueryResultsProps>>;
selectedGraph: PANEL_TYPES;
selectedWidget: Widgets;
isLoadingPanelData: boolean;
};

View File

@@ -372,12 +372,8 @@ export function handleQueryChange(
builder: {
...supersetQuery.builder,
queryData: supersetQuery.builder.queryData.map((query, index) => {
const { dataSource, expression, queryName } = query;
const tempQuery = {
...initialQueryBuilderFormValuesMap[dataSource],
expression,
queryName,
};
const { dataSource } = query;
const tempQuery = { ...initialQueryBuilderFormValuesMap[dataSource] };
const fieldsToSelect =
panelTypeDataSourceFormValuesMap[newPanelType][dataSource].builder

View File

@@ -39,7 +39,6 @@
font-weight: 500;
line-height: 18px; /* 128.571% */
letter-spacing: -0.07px;
cursor: pointer;
}
}
}

View File

@@ -6,7 +6,6 @@ import history from 'lib/history';
import { ArrowUpRight } from 'lucide-react';
import { DataSource } from 'types/common/queryBuilder';
import { isCloudUser } from 'utils/app';
import DOCLINKS from 'utils/docLinks';
export default function NoLogs({
dataSource,
@@ -26,10 +25,8 @@ export default function NoLogs({
? ROUTES.GET_STARTED_APPLICATION_MONITORING
: ROUTES.GET_STARTED_LOGS_MANAGEMENT,
);
} else if (dataSource === 'traces') {
window.open(DOCLINKS.TRACES_EXPLORER_EMPTY_STATE, '_blank');
} else {
window.open(`${DOCLINKS.USER_GUIDE}${dataSource}/`, '_blank');
window.open(`https://signoz.io/docs/userguide/${dataSource}/`, '_blank');
}
};
return (

View File

@@ -4,45 +4,50 @@
Prior to installation, you must ensure your Kubernetes cluster is ready and that you have the necessary permissions to deploy applications. Follow these steps to use Helm for setting up the Collector:
&nbsp;
1. **Add the OpenTelemetry Helm repository:**
```bash
helm repo add open-telemetry https://open-telemetry.github.io/opentelemetry-helm-charts
```
&nbsp;
2. **Prepare the `otel-collector-values.yaml` Configuration**
&nbsp;
#### Azure Event Hub Receiver Configuration
If you haven't created the logs Event Hub, you can create one by following the steps in the [Azure Event Hubs documentation](../../bootstrapping/data-ingestion).
#### Azure Event Hub Receiver Configuration
and replace the placeholders `<Primary Connection String>` with the primary connection string for your Event Hub, it should look something like this:
Replace the placeholders `<Primary Connection String>` with the primary connection string for your Event Hub, it should look something like this:
```yaml
connection: Endpoint=sb://namespace.servicebus.windows.net/;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=superSecret1234=;EntityPath=hubName
```
The Event Hub docs have a step to create a SAS policy for the event hub and copy the connection string.
```yaml
connection: Endpoint=sb://namespace.servicebus.windows.net/;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=superSecret1234=;EntityPath=hubName
```
The Event Hub setup have a step to create a SAS policy for the event hub and copy the connection string.
#### Azure Monitor Receiver Configuration
&nbsp;
You will need to set up a [service principal](https://learn.microsoft.com/en-us/entra/identity-platform/howto-create-service-principal-portal) with Read permissions to receive data from Azure Monitor.
#### Azure Monitor Receiver Configuration
1. Follow the steps in the [Create a service principal Azure Doc](https://learn.microsoft.com/en-us/entra/identity-platform/howto-create-service-principal-portal#register-an-application-with-microsoft-entra-id-and-create-a-service-principal) documentation to create a service principal.
You can name it `signoz-central-collector-app` the redirect URI can be empty.
2. To add read permissions to Azure Monitor, Follow the [Assign Role](https://learn.microsoft.com/en-us/entra/identity-platform/howto-create-service-principal-portal#assign-a-role-to-the-application) documentation. The read acess can be given to the full subscription.
3. There are multiple ways to authenticate the service principal, we will use the client secret option, follow [Creating a client secret](https://learn.microsoft.com/en-us/entra/identity-platform/howto-create-service-principal-portal#option-3-create-a-new-client-secret) and don't forget to copy the client secret. The secret is used in the configuration file as `client_secret`.
You will need to set up a [service principal](https://learn.microsoft.com/en-us/entra/identity-platform/howto-create-service-principal-portal) with Read permissions to receive data from Azure Monitor.
4. To find `client_id` and `tenant_id`, go to the [Azure Portal](https://portal.azure.com/) and search for the `Application` you created. You would see the `Application (client) ID` and `Directory (tenant) ID` in the Overview section.
1. Follow the steps in the [Create a service principal Azure Doc](https://learn.microsoft.com/en-us/entra/identity-platform/howto-create-service-principal-portal#register-an-application-with-microsoft-entra-id-and-create-a-service-principal) documentation to create a service principal.
You can name it `signoz-central-collector-app` the redirect URI can be empty.
2. To add read permissions to Azure Monitor, Follow the [Assign Role](https://learn.microsoft.com/en-us/entra/identity-platform/howto-create-service-principal-portal#assign-a-role-to-the-application) documentation. The read acess can be given to the full subscription.
3. There are multiple ways to authenticate the service principal, we will use the client secret option, follow [Creating a client secret](https://learn.microsoft.com/en-us/entra/identity-platform/howto-create-service-principal-portal#option-3-create-a-new-client-secret) and don't forget to copy the client secret. The secret is used in the configuration file as `client_secret`.
<figure data-zoomable align="center">
<img
src="/img/docs/azure-monitoring/service-principal-app-overview.webp"
alt="Application Overview"
/>
<figcaption>
<i>
Application Overview
</i>
</figcaption>
</figure>
4. To find `client_id` and `tenant_id`, go to the [Azure Portal](https://portal.azure.com/) and search for the `Application` you created. You would see the `Application (client) ID` and `Directory (tenant) ID` in the Overview section.
5. To find `subscription_id`, follow steps in [Find Your Subscription](https://learn.microsoft.com/en-us/azure/azure-portal/get-subscription-tenant-id#find-your-azure-subscription) and populate them in the configuration file.
6. Ensure you replace the placeholders `<region>` and `<ingestion-key>` with the appropriate values for your signoz cloud instance.
5. To find `subscription_id`, follow steps in [Find Your Subscription](https://learn.microsoft.com/en-us/azure/azure-portal/get-subscription-tenant-id#find-your-azure-subscription) and populate them in the configuration file.
6. Ensure you replace the placeholders `<region>` and `<ingestion-key>` with the appropriate values for your signoz cloud instance.
@@ -87,15 +92,13 @@ processors:
batch: {}
exporters:
otlp:
endpoint: "ingest.{{REGION}}.signoz.cloud:443"
endpoint: "ingest.<region>.signoz.cloud:443"
tls:
insecure: false
headers:
"signoz-access-token": "{{SIGNOZ_INGESTION_KEY}}"
"signoz-access-token": "<ingestion-key>"
```
&nbsp;
3. **Deploy the OpenTelemetry Collector to your Kubernetes cluster:**
You'll need to prepare a custom configuration file, say `otel-collector-values.yaml`, that matches your environment's specific needs. Replace `<namespace>` with the Kubernetes namespace where you wish to install the Collector.

View File

@@ -1,6 +1,16 @@
.container {
width: 100%;
// max-width: 1440px;
margin: 0 auto;
&.darkMode {
}
&.lightMode {
.onboardingHeader {
color: #1d1d1d;
}
}
}
.moduleSelectContainer {
@@ -51,8 +61,6 @@
width: 300px;
transition: 0.3s;
background-color: #000;
.ant-card-body {
padding: 0px;
}
@@ -72,9 +80,6 @@
overflow: hidden;
text-overflow: ellipsis;
text-align: center;
border-bottom: 1px solid #303030;
background-color: var(--bg-ink-400);
}
.moduleStyles.selected {
@@ -152,107 +157,3 @@
padding: 12px;
margin: 24px 0;
}
.invite-member-wrapper {
display: flex;
justify-content: center;
align-items: center;
margin: 32px 0;
flex-direction: column;
gap: 12px;
.invite-member {
display: flex;
width: 480px;
height: 64px;
padding: 16px;
justify-content: space-between;
align-items: center;
flex-shrink: 0;
border-radius: 4px;
border: 1px solid var(--bg-slate-500);
background: var(--bg-ink-400);
.ant-typography {
color: var(--bg-vanilla-400);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 500;
line-height: 20px;
}
> button {
display: flex;
align-items: center;
border-radius: 2px;
}
}
}
.onboarding-page {
display: flex;
flex-direction: column;
height: 100%;
align-items: center;
justify-content: space-between;
}
.skip-to-console {
color: var(--bg-vanilla-400);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 500;
line-height: 20px;
position: absolute;
top: 40px;
right: 40px;
cursor: pointer;
&:hover {
color: var(--bg-vanilla-200);
}
}
.lightMode {
.invite-member-wrapper {
.invite-member {
border: 1px solid var(--bg-vanilla-200);
background: var(--bg-vanilla-100);
.ant-typography {
color: var(--bg-slate-200);
}
}
}
.skip-to-console {
color: var(--bg-slate-200);
&:hover {
color: var(--bg-slate-200);
}
}
}
.lightMode {
.container {
.onboardingHeader {
color: var(--bg-slate-200);
}
}
.moduleStyles {
background-color: var(--bg-vanilla-100);
}
.moduleTitleStyle {
border-bottom: 1px solid var(--bg-vanilla-300);
background-color: var(--bg-vanilla-100);
}
.moduleDesc {
background-color: var(--bg-vanilla-100);
}
}

View File

@@ -3,19 +3,15 @@
import './Onboarding.styles.scss';
import { ArrowRightOutlined } from '@ant-design/icons';
import { Button, Card, Form, Typography } from 'antd';
import logEvent from 'api/common/logEvent';
import { Button, Card, Typography } from 'antd';
import getIngestionData from 'api/settings/getIngestionData';
import cx from 'classnames';
import ROUTES from 'constants/routes';
import FullScreenHeader from 'container/FullScreenHeader/FullScreenHeader';
import InviteUserModal from 'container/OrganizationSettings/InviteUserModal/InviteUserModal';
import { InviteMemberFormValues } from 'container/OrganizationSettings/PendingInvitesContainer';
import useAnalytics from 'hooks/analytics/useAnalytics';
import { useIsDarkMode } from 'hooks/useDarkMode';
import history from 'lib/history';
import { UserPlus } from 'lucide-react';
import { useCallback, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useEffect, useState } from 'react';
import { useQuery } from 'react-query';
import { useEffectOnce } from 'react-use';
@@ -104,9 +100,9 @@ export default function Onboarding(): JSX.Element {
const [selectedModuleSteps, setSelectedModuleSteps] = useState(APM_STEPS);
const [activeStep, setActiveStep] = useState(1);
const [current, setCurrent] = useState(0);
const isDarkMode = useIsDarkMode();
const { trackEvent } = useAnalytics();
const { location } = history;
const { t } = useTranslation(['onboarding']);
const {
selectedDataSource,
@@ -283,38 +279,13 @@ export default function Onboarding(): JSX.Element {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const [form] = Form.useForm<InviteMemberFormValues>();
const [
isInviteTeamMemberModalOpen,
setIsInviteTeamMemberModalOpen,
] = useState<boolean>(false);
const toggleModal = useCallback(
(value: boolean): void => {
setIsInviteTeamMemberModalOpen(value);
if (!value) {
form.resetFields();
}
},
[form],
);
return (
<div className="container">
<div className={cx('container', isDarkMode ? 'darkMode' : 'lightMode')}>
{activeStep === 1 && (
<div className="onboarding-page">
<div
onClick={(): void => {
logEvent('Onboarding V2: Skip Button Clicked', {});
history.push('/');
}}
className="skip-to-console"
>
{t('skip')}
</div>
<>
<FullScreenHeader />
<div className="onboardingHeader">
<h1>{t('select_use_case')}</h1>
<h1> Select a use-case to get started</h1>
</div>
<div className="modulesContainer">
<div className="moduleContainerRowStyles">
@@ -327,13 +298,26 @@ export default function Onboarding(): JSX.Element {
'moduleStyles',
selectedModule.id === selectedUseCase.id ? 'selected' : '',
)}
style={{
backgroundColor: isDarkMode ? '#000' : '#FFF',
}}
key={selectedUseCase.id}
onClick={(): void => handleModuleSelect(selectedUseCase)}
>
<Typography.Title className="moduleTitleStyle" level={4}>
<Typography.Title
className="moduleTitleStyle"
level={4}
style={{
borderBottom: isDarkMode ? '1px solid #303030' : '1px solid #ddd',
backgroundColor: isDarkMode ? '#141414' : '#FFF',
}}
>
{selectedUseCase.title}
</Typography.Title>
<Typography.Paragraph className="moduleDesc">
<Typography.Paragraph
className="moduleDesc"
style={{ backgroundColor: isDarkMode ? '#000' : '#FFF' }}
>
{selectedUseCase.desc}
</Typography.Paragraph>
</Card>
@@ -343,31 +327,10 @@ export default function Onboarding(): JSX.Element {
</div>
<div className="continue-to-next-step">
<Button type="primary" icon={<ArrowRightOutlined />} onClick={handleNext}>
{t('get_started')}
Get Started
</Button>
</div>
<div className="invite-member-wrapper">
<Typography.Text className="helper-text">
{t('invite_user_helper_text')}
</Typography.Text>
<div className="invite-member">
<Typography.Text>{t('invite_user')}</Typography.Text>
<Button
onClick={(): void => {
logEvent('Onboarding V2: Invite Member', {
module: selectedModule?.id,
page: 'homepage',
});
setIsInviteTeamMemberModalOpen(true);
}}
icon={<UserPlus size={16} />}
type="primary"
>
{t('invite')}
</Button>
</div>
</div>
</div>
</>
)}
{activeStep > 1 && (
@@ -382,15 +345,9 @@ export default function Onboarding(): JSX.Element {
}}
selectedModule={selectedModule}
selectedModuleSteps={selectedModuleSteps}
setIsInviteTeamMemberModalOpen={setIsInviteTeamMemberModalOpen}
/>
</div>
)}
<InviteUserModal
form={form}
isInviteTeamMemberModalOpen={isInviteTeamMemberModalOpen}
toggleModal={toggleModal}
/>
</div>
);
}

View File

@@ -1,127 +0,0 @@
/* eslint-disable sonarjs/no-identical-functions */
import { queryByAttribute, waitFor } from '@testing-library/react';
import { fireEvent, render, screen, within } from 'tests/test-utils';
import OnboardingContainer from '..';
import { OnboardingContextProvider } from '../context/OnboardingContext';
jest.mock('react-markdown', () => jest.fn());
jest.mock('rehype-raw', () => jest.fn());
const successNotification = jest.fn();
jest.mock('hooks/useNotifications', () => ({
__esModule: true,
useNotifications: jest.fn(() => ({
notifications: {
success: successNotification,
error: jest.fn(),
},
})),
}));
window.analytics = {
track: jest.fn(),
};
describe('Onboarding invite team member flow', () => {
it('initial render and get started page', async () => {
const { findByText } = render(
<OnboardingContextProvider>
<OnboardingContainer />
</OnboardingContextProvider>,
);
await expect(findByText('SigNoz')).resolves.toBeInTheDocument();
// Check all the option present
const monitoringTexts = [
{
title: 'Application Monitoring',
description:
'Monitor application metrics like p99 latency, error rates, external API calls, and db calls.',
},
{
title: 'Logs Management',
description:
'Easily filter and query logs, build dashboards and alerts based on attributes in logs',
},
{
title: 'Infrastructure Monitoring',
description:
'Monitor Kubernetes infrastructure metrics, hostmetrics, or metrics of any third-party integration',
},
{
title: 'AWS Monitoring',
description:
'Monitor your traces, logs and metrics for AWS services like EC2, ECS, EKS etc.',
},
{
title: 'Azure Monitoring',
description:
'Monitor your traces, logs and metrics for Azure services like AKS, Container Apps, App Service etc.',
},
];
monitoringTexts.forEach(async ({ title, description }) => {
await expect(findByText(title)).resolves.toBeInTheDocument();
await expect(findByText(description)).resolves.toBeInTheDocument();
});
// Invite team member button
await expect(findByText('invite')).resolves.toBeInTheDocument();
});
it('invite team member', async () => {
const { findByText } = render(
<OnboardingContextProvider>
<OnboardingContainer />
</OnboardingContextProvider>,
);
// Invite team member button
const inviteBtn = await findByText('invite');
expect(inviteBtn).toBeInTheDocument();
fireEvent.click(inviteBtn);
const inviteModal = await screen.findByTestId('invite-team-members-modal');
expect(inviteModal).toBeInTheDocument();
const inviteModalTitle = await within(inviteModal).findAllByText(
/invite_team_members/i,
);
expect(inviteModalTitle[0]).toBeInTheDocument();
// Verify that the invite modal contains an input field for entering the email address
const emailInput = within(inviteModal).getByText('email_address');
expect(emailInput).toBeInTheDocument();
// Verify that the invite modal contains a dropdown for selecting the role
const role = within(inviteModal).getByText('role');
expect(role).toBeInTheDocument();
// Verify that the invite modal contains a button for sending the invitation
const sendButton = within(inviteModal).getByTestId(
'invite-team-members-button',
);
expect(sendButton).toBeInTheDocument();
// Verify that the invite modal sends the invitation
fireEvent.input(queryByAttribute('id', inviteModal, 'members_0_email')!, {
target: { value: 'test@example.com' },
});
expect(
queryByAttribute('value', inviteModal, 'test@example.com'),
).toBeInTheDocument();
const roleDropdown = within(inviteModal).getByTestId('role-select');
expect(roleDropdown).toBeInTheDocument();
fireEvent.click(sendButton);
await waitFor(() =>
expect(successNotification).toHaveBeenCalledWith({
message: 'Invite sent successfully',
}),
);
});
});

View File

@@ -39,9 +39,6 @@
.steps-container {
width: 20%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: space-between;
.steps-container-header {
display: flex;
@@ -72,30 +69,6 @@
}
}
}
.invite-user-btn {
display: flex;
width: 170px;
height: 32px;
padding: 6px;
justify-content: center;
align-items: center;
border-radius: 2px;
margin-bottom: 31px;
border: 1px solid var(--bg-slate-400);
background: var(--bg-ink-300);
box-shadow: none;
.ant-typography {
color: var(--bg-vanilla-400);
font-family: Inter;
font-size: 12px;
font-style: normal;
font-weight: 500;
line-height: 10px;
letter-spacing: 0.12px;
}
}
}
.selected-step-content {
@@ -223,14 +196,5 @@
}
}
}
.invite-user-btn {
border: 1px solid var(--bg-vanilla-300);
background: var(--bg-vanilla-100);
.ant-typography {
color: var(--bg-slate-200);
}
}
}
}

View File

@@ -18,8 +18,8 @@ import { hasFrameworks } from 'container/OnboardingContainer/utils/dataSourceUti
import useAnalytics from 'hooks/analytics/useAnalytics';
import history from 'lib/history';
import { isEmpty, isNull } from 'lodash-es';
import { HelpCircle, UserPlus } from 'lucide-react';
import { SetStateAction, useState } from 'react';
import { HelpCircle } from 'lucide-react';
import { useState } from 'react';
import { useOnboardingContext } from '../../context/OnboardingContext';
import {
@@ -33,7 +33,6 @@ interface ModuleStepsContainerProps {
onReselectModule: any;
selectedModule: ModuleProps;
selectedModuleSteps: SelectedModuleStepProps[];
setIsInviteTeamMemberModalOpen: (value: SetStateAction<boolean>) => void;
}
interface MetaDataProps {
@@ -64,7 +63,6 @@ export default function ModuleStepsContainer({
onReselectModule,
selectedModule,
selectedModuleSteps,
setIsInviteTeamMemberModalOpen,
}: ModuleStepsContainerProps): JSX.Element {
const {
activeStep,
@@ -411,47 +409,32 @@ Thanks
return (
<div className="onboarding-module-steps">
<div className="steps-container">
<div>
<div className="steps-container-header">
<div className="brand-logo" onClick={handleLogoClick}>
<img src="/Logos/signoz-brand-logo.svg" alt="SigNoz" />
<div className="steps-container-header">
<div className="brand-logo" onClick={handleLogoClick}>
<img src="/Logos/signoz-brand-logo.svg" alt="SigNoz" />
<div className="brand-logo-name">SigNoz</div>
</div>
<div className="brand-logo-name">SigNoz</div>
</div>
<Space style={{ marginBottom: '24px' }}>
<Button
style={{ display: 'flex', alignItems: 'center' }}
type="default"
icon={<LeftCircleOutlined />}
onClick={onReselectModule}
>
{selectedModule.title}
</Button>
</Space>
<Steps
direction="vertical"
size="small"
status="finish"
current={current}
items={selectedModuleSteps}
/>
</div>
<Button
onClick={(): void => {
logEvent('Onboarding V2: Invite Member', {
module: selectedModule?.id,
page: 'sidebar',
});
setIsInviteTeamMemberModalOpen(true);
}}
icon={<UserPlus size={16} />}
className="invite-user-btn"
>
Invite teammates
</Button>
<Space style={{ marginBottom: '24px' }}>
<Button
style={{ display: 'flex', alignItems: 'center' }}
type="default"
icon={<LeftCircleOutlined />}
onClick={onReselectModule}
>
{selectedModule.title}
</Button>
</Space>
<Steps
direction="vertical"
size="small"
status="finish"
current={current}
items={selectedModuleSteps}
/>
</div>
<div className="selected-step-content">

View File

@@ -50,7 +50,7 @@ function InviteTeamMembers({ form, onFinish }: Props): JSX.Element {
<Input placeholder={t('name_placeholder')} />
</Form.Item>
<Form.Item name={[name, 'role']} initialValue="VIEWER">
<SelectDrawer data-testid="role-select">
<SelectDrawer>
<Select.Option value="ADMIN">ADMIN</Select.Option>
<Select.Option value="VIEWER">VIEWER</Select.Option>
<Select.Option value="EDITOR">EDITOR</Select.Option>

View File

@@ -1,182 +0,0 @@
import { Button, Form, Modal } from 'antd';
import { FormInstance } from 'antd/lib';
import getPendingInvites from 'api/user/getPendingInvites';
import sendInvite from 'api/user/sendInvite';
import ROUTES from 'constants/routes';
import { useNotifications } from 'hooks/useNotifications';
import {
Dispatch,
SetStateAction,
useCallback,
useEffect,
useState,
} from 'react';
import { useTranslation } from 'react-i18next';
import { useQuery } from 'react-query';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { PayloadProps } from 'types/api/user/getPendingInvites';
import AppReducer from 'types/reducer/app';
import { ROLES } from 'types/roles';
import InviteTeamMembers from '../InviteTeamMembers';
import { InviteMemberFormValues } from '../PendingInvitesContainer';
export interface InviteUserModalProps {
isInviteTeamMemberModalOpen: boolean;
toggleModal: (value: boolean) => void;
form: FormInstance<InviteMemberFormValues>;
setDataSource?: Dispatch<SetStateAction<DataProps[]>>;
shouldCallApi?: boolean;
}
interface DataProps {
key: number;
name: string;
email: string;
accessLevel: ROLES;
inviteLink: string;
}
function InviteUserModal(props: InviteUserModalProps): JSX.Element {
const {
isInviteTeamMemberModalOpen,
toggleModal,
form,
setDataSource,
shouldCallApi = false,
} = props;
const { notifications } = useNotifications();
const { t } = useTranslation(['organizationsettings', 'common']);
const { user } = useSelector<AppState, AppReducer>((state) => state.app);
const [isInvitingMembers, setIsInvitingMembers] = useState<boolean>(false);
const [modalForm] = Form.useForm<InviteMemberFormValues>(form);
const getPendingInvitesResponse = useQuery({
queryFn: getPendingInvites,
queryKey: ['getPendingInvites', user?.accessJwt],
enabled: shouldCallApi,
});
const getParsedInviteData = useCallback(
(payload: PayloadProps = []) =>
payload?.map((data) => ({
key: data.createdAt,
name: data?.name,
email: data.email,
accessLevel: data.role,
inviteLink: `${window.location.origin}${ROUTES.SIGN_UP}?token=${data.token}`,
})),
[],
);
useEffect(() => {
if (
getPendingInvitesResponse.status === 'success' &&
getPendingInvitesResponse?.data?.payload
) {
const data = getParsedInviteData(
getPendingInvitesResponse?.data?.payload || [],
);
setDataSource?.(data);
}
}, [
getParsedInviteData,
getPendingInvitesResponse?.data?.payload,
getPendingInvitesResponse.status,
setDataSource,
]);
const onInviteClickHandler = useCallback(
async (values: InviteMemberFormValues): Promise<void> => {
try {
setIsInvitingMembers?.(true);
values?.members?.forEach(
async (member): Promise<void> => {
const { error, statusCode } = await sendInvite({
email: member.email,
name: member?.name,
role: member.role,
frontendBaseUrl: window.location.origin,
});
if (statusCode !== 200) {
notifications.error({
message:
error ||
t('something_went_wrong', {
ns: 'common',
}),
});
} else if (statusCode === 200) {
notifications.success({
message: 'Invite sent successfully',
});
}
},
);
setTimeout(async () => {
const { data, status } = await getPendingInvitesResponse.refetch();
if (status === 'success' && data.payload) {
setDataSource?.(getParsedInviteData(data?.payload || []));
}
setIsInvitingMembers?.(false);
toggleModal(false);
}, 2000);
} catch (error) {
notifications.error({
message: t('something_went_wrong', {
ns: 'common',
}),
});
}
},
[
getParsedInviteData,
getPendingInvitesResponse,
notifications,
setDataSource,
setIsInvitingMembers,
t,
toggleModal,
],
);
return (
<Modal
title={t('invite_team_members')}
open={isInviteTeamMemberModalOpen}
onCancel={(): void => toggleModal(false)}
centered
data-testid="invite-team-members-modal"
destroyOnClose
footer={[
<Button key="back" onClick={(): void => toggleModal(false)} type="default">
{t('cancel', {
ns: 'common',
})}
</Button>,
<Button
key={t('invite_team_members').toString()}
onClick={modalForm.submit}
data-testid="invite-team-members-button"
type="primary"
disabled={isInvitingMembers}
loading={isInvitingMembers}
>
{t('invite_team_members')}
</Button>,
]}
>
<InviteTeamMembers form={modalForm} onFinish={onInviteClickHandler} />
</Modal>
);
}
InviteUserModal.defaultProps = {
setDataSource: (): void => {},
shouldCallApi: false,
};
export default InviteUserModal;

View File

@@ -1,8 +1,9 @@
import { PlusOutlined } from '@ant-design/icons';
import { Button, Form, Space, Typography } from 'antd';
import { Button, Form, Modal, Space, Typography } from 'antd';
import { ColumnsType } from 'antd/lib/table';
import deleteInvite from 'api/user/deleteInvite';
import getPendingInvites from 'api/user/getPendingInvites';
import sendInvite from 'api/user/sendInvite';
import { ResizeTable } from 'components/ResizeTable';
import { INVITE_MEMBERS_HASH } from 'constants/app';
import ROUTES from 'constants/routes';
@@ -18,7 +19,7 @@ import { PayloadProps } from 'types/api/user/getPendingInvites';
import AppReducer from 'types/reducer/app';
import { ROLES } from 'types/roles';
import InviteUserModal from '../InviteUserModal/InviteUserModal';
import InviteTeamMembers from '../InviteTeamMembers';
import { TitleWrapper } from './styles';
function PendingInvitesContainer(): JSX.Element {
@@ -27,6 +28,7 @@ function PendingInvitesContainer(): JSX.Element {
setIsInviteTeamMemberModalOpen,
] = useState<boolean>(false);
const [form] = Form.useForm<InviteMemberFormValues>();
const [isInvitingMembers, setIsInvitingMembers] = useState<boolean>(false);
const { t } = useTranslation(['organizationsettings', 'common']);
const [state, setText] = useCopyToClipboard();
const { notifications } = useNotifications();
@@ -189,15 +191,83 @@ function PendingInvitesContainer(): JSX.Element {
},
];
const onInviteClickHandler = useCallback(
async (values: InviteMemberFormValues): Promise<void> => {
try {
setIsInvitingMembers(true);
values.members.forEach(
async (member): Promise<void> => {
const { error, statusCode } = await sendInvite({
email: member.email,
name: member.name,
role: member.role,
frontendBaseUrl: window.location.origin,
});
if (statusCode !== 200) {
notifications.error({
message:
error ||
t('something_went_wrong', {
ns: 'common',
}),
});
}
},
);
setTimeout(async () => {
const { data, status } = await getPendingInvitesResponse.refetch();
if (status === 'success' && data.payload) {
setDataSource(getParsedInviteData(data?.payload || []));
}
setIsInvitingMembers(false);
toggleModal(false);
}, 2000);
} catch (error) {
notifications.error({
message: t('something_went_wrong', {
ns: 'common',
}),
});
}
},
[
getParsedInviteData,
getPendingInvitesResponse,
notifications,
t,
toggleModal,
],
);
return (
<div>
<InviteUserModal
form={form}
isInviteTeamMemberModalOpen={isInviteTeamMemberModalOpen}
setDataSource={setDataSource}
toggleModal={toggleModal}
shouldCallApi
/>
<Modal
title={t('invite_team_members')}
open={isInviteTeamMemberModalOpen}
onCancel={(): void => toggleModal(false)}
centered
destroyOnClose
footer={[
<Button key="back" onClick={(): void => toggleModal(false)} type="default">
{t('cancel', {
ns: 'common',
})}
</Button>,
<Button
key={t('invite_team_members').toString()}
onClick={form.submit}
type="primary"
disabled={isInvitingMembers}
loading={isInvitingMembers}
>
{t('invite_team_members')}
</Button>,
]}
>
<InviteTeamMembers form={form} onFinish={onInviteClickHandler} />
</Modal>
<Space direction="vertical" size="middle">
<TitleWrapper>

View File

@@ -1,37 +0,0 @@
import { act, render, screen, waitFor } from 'tests/test-utils';
import Members from '../Members';
describe('Organization Settings Page', () => {
afterEach(() => {
jest.clearAllMocks();
});
it('render list of members', async () => {
act(() => {
render(<Members />);
});
const title = await screen.findByText(/Members/i);
expect(title).toBeInTheDocument();
await waitFor(() => {
expect(screen.getByText('firstUser@test.io')).toBeInTheDocument(); // first item
expect(screen.getByText('lastUser@test.io')).toBeInTheDocument(); // last item
});
});
// this is required as our edit/delete logic is dependent on the index and it will break with pagination enabled
it('render list of members without pagination', async () => {
render(<Members />);
await waitFor(() => {
expect(screen.getByText('firstUser@test.io')).toBeInTheDocument(); // first item
expect(screen.getByText('lastUser@test.io')).toBeInTheDocument(); // last item
expect(
document.querySelector('.ant-table-pagination'),
).not.toBeInTheDocument();
});
});
});

View File

@@ -9,7 +9,7 @@ function TablePanelWrapper({
tableProcessedDataRef,
}: PanelWrapperProps): JSX.Element {
const panelData =
(queryResponse.data?.payload?.data?.result?.[0] as any)?.table || [];
queryResponse.data?.payload?.data?.newResult?.data?.result || [];
const { thresholds } = widget;
return (
<GridTableComponent

View File

@@ -1,4 +1,3 @@
export const historyPagination = {
defaultPageSize: 5,
hideOnSinglePage: true,
};

View File

@@ -22,7 +22,7 @@ describe('PipelinePage container test', () => {
expect(asFragment()).toMatchSnapshot();
});
it.skip('should handle search', async () => {
it('should handle search', async () => {
const setPipelineValue = jest.fn();
const { getByPlaceholderText, container } = render(
<MemoryRouter>

View File

@@ -334,11 +334,6 @@ export function PlannedDowntimeList({
}
}, [downtimeSchedules.error, downtimeSchedules.isError, notifications]);
const paginationConfig = {
pageSize: 5,
showSizeChanger: false,
hideOnSinglePage: true,
};
return (
<Table<DowntimeSchedulesTableData>
columns={columns}
@@ -347,7 +342,7 @@ export function PlannedDowntimeList({
dataSource={tableData || []}
loading={downtimeSchedules.isLoading || downtimeSchedules.isFetching}
showHeader={false}
pagination={paginationConfig}
pagination={{ pageSize: 5, showSizeChanger: false }}
/>
);
}

View File

@@ -10,7 +10,7 @@ interface LeftToolbarActionsProps {
selectedView: string;
onToggleHistrogramVisibility: () => void;
onChangeSelectedView: (view: SELECTED_VIEWS) => void;
showFrequencyChart: boolean;
showHistogram: boolean;
}
const activeTab = 'active-tab';
@@ -22,7 +22,7 @@ export default function LeftToolbarActions({
selectedView,
onToggleHistrogramVisibility,
onChangeSelectedView,
showFrequencyChart,
showHistogram,
}: LeftToolbarActionsProps): JSX.Element {
const { clickhouse, search, queryBuilder: QB } = items;
@@ -71,11 +71,11 @@ export default function LeftToolbarActions({
)}
</div>
<div className="frequency-chart-view-controller">
<Typography>Frequency chart</Typography>
<div className="histogram-view-controller">
<Typography>Histogram</Typography>
<Switch
size="small"
checked={showFrequencyChart}
checked={showHistogram}
defaultChecked
onChange={onToggleHistrogramVisibility}
/>

View File

@@ -37,7 +37,7 @@
}
}
.frequency-chart-view-controller {
.histogram-view-controller {
display: flex;
align-items: center;
padding-left: 8px;

View File

@@ -8,7 +8,7 @@ import RightToolbarActions from '../RightToolbarActions';
describe('ToolbarActions', () => {
it('LeftToolbarActions - renders correctly with default props', async () => {
const handleChangeSelectedView = jest.fn();
const handleToggleShowFrequencyChart = jest.fn();
const handleToggleShowHistogram = jest.fn();
const { queryByTestId } = render(
<LeftToolbarActions
items={{
@@ -32,8 +32,8 @@ describe('ToolbarActions', () => {
}}
selectedView={SELECTED_VIEWS.SEARCH}
onChangeSelectedView={handleChangeSelectedView}
onToggleHistrogramVisibility={handleToggleShowFrequencyChart}
showFrequencyChart
onToggleHistrogramVisibility={handleToggleShowHistogram}
showHistogram
/>,
);
expect(screen.getByTestId('search-view')).toBeInTheDocument();
@@ -51,7 +51,7 @@ describe('ToolbarActions', () => {
it('renders - clickhouse view and test histogram toggle', async () => {
const handleChangeSelectedView = jest.fn();
const handleToggleShowFrequencyChart = jest.fn();
const handleToggleShowHistogram = jest.fn();
const { queryByTestId, getByRole } = render(
<LeftToolbarActions
items={{
@@ -76,8 +76,8 @@ describe('ToolbarActions', () => {
}}
selectedView={SELECTED_VIEWS.QUERY_BUILDER}
onChangeSelectedView={handleChangeSelectedView}
onToggleHistrogramVisibility={handleToggleShowFrequencyChart}
showFrequencyChart
onToggleHistrogramVisibility={handleToggleShowHistogram}
showHistogram
/>,
);
@@ -88,7 +88,7 @@ describe('ToolbarActions', () => {
expect(handleChangeSelectedView).toBeCalled();
await userEvent.click(getByRole('switch'));
expect(handleToggleShowFrequencyChart).toBeCalled();
expect(handleToggleShowHistogram).toBeCalled();
});
it('RightToolbarActions - render correctly with props', async () => {

View File

@@ -33,12 +33,10 @@ export const getColumnSearchProps = (
record: ServicesList,
): boolean => {
if (record[dataIndex]) {
return (
record[dataIndex]
?.toString()
.toLowerCase()
.includes(value.toString().toLowerCase()) || false
);
record[dataIndex]
?.toString()
.toLowerCase()
.includes(value.toString().toLowerCase());
}
return false;

View File

@@ -79,11 +79,6 @@ function ServiceMetricTable({
}
}, [services, licenseData, isFetching, isCloudUserVal]);
const paginationConfig = {
defaultPageSize: 10,
showTotal: (total: number, range: number[]): string =>
`${range[0]}-${range[1]} of ${total} items`,
};
return (
<>
{RPS > MAX_RPS_LIMIT && (
@@ -97,7 +92,11 @@ function ServiceMetricTable({
<ResourceAttributesFilter />
<ResizeTable
pagination={paginationConfig}
pagination={{
defaultPageSize: 10,
showTotal: (total: number, range: number[]): string =>
`${range[0]}-${range[1]} of ${total} items`,
}}
columns={tableColumns}
loading={isLoading}
dataSource={services}

View File

@@ -36,11 +36,6 @@ function ServiceTraceTable({
}
}, [services, licenseData, isFetching, isCloudUserVal]);
const paginationConfig = {
defaultPageSize: 10,
showTotal: (total: number, range: number[]): string =>
`${range[0]}-${range[1]} of ${total} items`,
};
return (
<>
{RPS > MAX_RPS_LIMIT && (
@@ -54,7 +49,11 @@ function ServiceTraceTable({
<ResourceAttributesFilter />
<ResizeTable
pagination={paginationConfig}
pagination={{
defaultPageSize: 10,
showTotal: (total: number, range: number[]): string =>
`${range[0]}-${range[1]} of ${total} items`,
}}
columns={tableColumns}
loading={loading}
dataSource={services}

View File

@@ -152,13 +152,9 @@ function SideNav({
const { t } = useTranslation('');
const licenseStatus: string =
licenseData?.payload?.licenses?.find((e: License) => e.isCurrent)?.status ||
'';
const isLicenseActive =
licenseStatus?.toLocaleLowerCase() ===
LICENSE_PLAN_STATUS.VALID.toLocaleLowerCase();
licenseData?.payload?.licenses?.find((e: License) => e.isCurrent)?.status ===
LICENSE_PLAN_STATUS.VALID;
const isEnterprise = licenseData?.payload?.licenses?.some(
(license: License) =>

View File

@@ -7,7 +7,6 @@ import LogsError from 'container/LogsError/LogsError';
import { LogsLoading } from 'container/LogsLoading/LogsLoading';
import NoLogs from 'container/NoLogs/NoLogs';
import { CustomTimeType } from 'container/TopNav/DateTimeSelectionV2/config';
import { TracesLoading } from 'container/TracesExplorer/TraceLoading/TraceLoading';
import { useIsDarkMode } from 'hooks/useDarkMode';
import useUrlQuery from 'hooks/useUrlQuery';
import GetMinMax from 'lib/getMinMax';
@@ -147,8 +146,7 @@ function TimeSeriesView({
style={{ height: '100%', width: '100%' }}
ref={graphRef}
>
{isLoading &&
(dataSource === DataSource.LOGS ? <LogsLoading /> : <TracesLoading />)}
{isLoading && <LogsLoading />}
{chartData &&
chartData[0] &&

View File

@@ -14,7 +14,6 @@ import { convertDataValueToMs } from './utils';
function TimeSeriesViewContainer({
dataSource = DataSource.TRACES,
isFilterApplied,
}: TimeSeriesViewProps): JSX.Element {
const { stagedQuery, currentQuery, panelType } = useQueryBuilder();
@@ -71,7 +70,8 @@ function TimeSeriesViewContainer({
return (
<TimeSeriesView
isFilterApplied={isFilterApplied}
// TODO handle this when revamping trace explorer
isFilterApplied={false}
isError={isError}
isLoading={isLoading}
data={responseData}
@@ -83,7 +83,6 @@ function TimeSeriesViewContainer({
interface TimeSeriesViewProps {
dataSource?: DataSource;
isFilterApplied: boolean;
}
TimeSeriesViewContainer.defaultProps = {

View File

@@ -1,5 +1,3 @@
import './Tags.styles.scss';
import { Tooltip } from 'antd';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { Fragment, useMemo } from 'react';
@@ -28,12 +26,7 @@ function Tag({ tags, onToggleHandler, setText }: TagProps): JSX.Element {
<Container>
<CustomSubTitle>{tags.key}</CustomSubTitle>
<SubTextContainer isDarkMode={isDarkMode}>
<Tooltip
overlayClassName="tagTooltip"
placement="left"
autoAdjustOverflow
title={value}
>
<Tooltip overlay={(): string => value}>
<CustomSubText
ellipsis={{
rows: isEllipsed ? 2 : 0,

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