Compare commits

..

1 Commits

Author SHA1 Message Date
SagarRajput-7
ad8f3328e0 fix(mcp-page): added acitve host url instead of current url on mcp page (#11141)
Some checks are pending
build-staging / prepare (push) Waiting to run
build-staging / js-build (push) Blocked by required conditions
build-staging / go-build (push) Blocked by required conditions
build-staging / staging (push) Blocked by required conditions
Release Drafter / update_release_draft (push) Waiting to run
* fix(mcp-page): added acitve host url instead of current url on mcp page

* fix(mcp-page): configure access and role control

* chore: move get hosts api access to viewers (#11145)

* chore: move get hosts api access to viewers

* chore: update openapi spec

---------

Co-authored-by: SagarRajput-7 <162284829+SagarRajput-7@users.noreply.github.com>

* fix: allowed hosts api to run on all the cloud users

* fix: updated test cases

---------

Co-authored-by: Karan Balani <29383381+balanikaran@users.noreply.github.com>
2026-04-30 13:31:43 +00:00
222 changed files with 9198 additions and 21 deletions

View File

@@ -17273,9 +17273,9 @@ paths:
description: Internal Server Error
security:
- api_key:
- ADMIN
- VIEWER
- tokenizer:
- ADMIN
- VIEWER
summary: Get host info from Zeus.
tags:
- zeus

View File

@@ -1,5 +1,5 @@
{
"$schema": "https://unpkg.com/knip@5/schema.json",
"project": ["src/**/*.ts", "src/**/*.tsx"],
"ignore": ["src/api/generated/**/*.ts", "src/typings/*.ts"]
"ignore": ["src/api/generated/**/*.ts"]
}

View File

@@ -0,0 +1,33 @@
.error-state-container {
height: 240px;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
border-radius: 3px;
.error-state-container-content {
display: flex;
flex-direction: column;
gap: 8px;
.error-state-text {
font-size: 14px;
font-weight: 500;
}
.error-state-additional-messages {
margin-top: 8px;
display: flex;
flex-direction: column;
gap: 4px;
.error-state-additional-text {
font-size: 12px;
font-weight: 400;
margin-left: 8px;
}
}
}
}

View File

@@ -0,0 +1,59 @@
import { Typography } from 'antd';
import APIError from '../../types/api/error';
import './Common.styles.scss';
interface ErrorStateComponentProps {
message?: string;
error?: APIError;
}
const defaultProps: Partial<ErrorStateComponentProps> = {
message: undefined,
error: undefined,
};
function ErrorStateComponent({
message,
error,
}: ErrorStateComponentProps): JSX.Element {
// Handle API Error object
if (error) {
const mainMessage = error.getErrorMessage();
const additionalErrors = error.getErrorDetails().error.errors || [];
return (
<div className="error-state-container">
<div className="error-state-container-content">
<Typography className="error-state-text">{mainMessage}</Typography>
{additionalErrors.length > 0 && (
<div className="error-state-additional-messages">
{additionalErrors.map((additionalError) => (
<Typography
key={`error-${additionalError.message}`}
className="error-state-additional-text"
>
{additionalError.message}
</Typography>
))}
</div>
)}
</div>
</div>
);
}
// Handle simple string message (backwards compatibility)
return (
<div className="error-state-container">
<div className="error-state-container-content">
<Typography className="error-state-text">{message}</Typography>
</div>
</div>
);
}
ErrorStateComponent.defaultProps = defaultProps;
export default ErrorStateComponent;

View File

@@ -0,0 +1,4 @@
.custom-date-picker {
display: flex;
flex-direction: column;
}

View File

@@ -0,0 +1,105 @@
import { Dispatch, SetStateAction, useMemo } from 'react';
// eslint-disable-next-line no-restricted-imports
import { useSelector } from 'react-redux';
import { DatePicker } from 'antd';
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
import { DateTimeRangeType } from 'container/TopNav/CustomDateTimeModal';
import {
CustomTimeType,
LexicalContext,
Time,
} from 'container/TopNav/DateTimeSelectionV2/types';
import dayjs, { Dayjs } from 'dayjs';
import { useTimezone } from 'providers/Timezone';
import { AppState } from 'store/reducers';
import { GlobalReducer } from 'types/reducer/globalTime';
import './RangePickerModal.styles.scss';
interface RangePickerModalProps {
setCustomDTPickerVisible: Dispatch<SetStateAction<boolean>>;
setIsOpen: Dispatch<SetStateAction<boolean>>;
onCustomDateHandler: (
dateTimeRange: DateTimeRangeType,
lexicalContext?: LexicalContext | undefined,
) => void;
selectedTime: string;
onTimeChange?: (
interval: Time | CustomTimeType,
dateTimeRange?: [number, number],
) => void;
}
function RangePickerModal(props: RangePickerModalProps): JSX.Element {
const {
setCustomDTPickerVisible,
setIsOpen,
onCustomDateHandler,
selectedTime,
onTimeChange,
} = props;
const { RangePicker } = DatePicker;
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
// Using any type here because antd's DatePicker expects its own internal Dayjs type
// which conflicts with our project's Dayjs type that has additional plugins (tz, utc etc).
const disabledDate = (current: any): boolean => {
const currentDay = dayjs(current);
return currentDay.isAfter(dayjs());
};
const onPopoverClose = (visible: boolean): void => {
if (!visible) {
setCustomDTPickerVisible(false);
}
setIsOpen(visible);
};
const onModalOkHandler = (date_time: any): void => {
if (date_time?.[1]) {
onPopoverClose(false);
}
onCustomDateHandler(date_time, LexicalContext.CUSTOM_DATE_PICKER);
};
const { timezone } = useTimezone();
const rangeValue: [Dayjs, Dayjs] = useMemo(
() => [
dayjs(minTime / 1000_000).tz(timezone.value),
dayjs(maxTime / 1000_000).tz(timezone.value),
],
[maxTime, minTime, timezone.value],
);
return (
<div className="custom-date-picker">
<RangePicker
disabledDate={disabledDate}
allowClear
showTime
format={(date: Dayjs): string =>
date.tz(timezone.value).format(DATE_TIME_FORMATS.ISO_DATETIME)
}
onOk={onModalOkHandler}
data-1p-ignore
{...(selectedTime === 'custom' &&
!onTimeChange && {
value: rangeValue,
})}
// use default value if onTimeChange is provided
{...(selectedTime === 'custom' &&
onTimeChange && {
defaultValue: rangeValue,
})}
/>
</div>
);
}
RangePickerModal.defaultProps = {
onTimeChange: undefined,
};
export default RangePickerModal;

View File

@@ -0,0 +1,93 @@
.details-drawer {
.ant-drawer-wrapper-body {
border-left: 1px solid var(--l1-border);
}
.ant-drawer-header {
background: var(--l2-background);
border-bottom: 1px solid var(--l1-border);
.ant-drawer-header-title {
display: flex;
align-items: center;
.ant-drawer-close {
margin-inline-end: 0px;
padding: 0px;
padding-right: 16px;
border-right: 1px solid var(--l1-border);
}
.ant-drawer-title {
padding-left: 16px;
color: var(--l2-foreground);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
}
}
.ant-drawer-body {
padding: 16px;
background: var(--l2-background);
&::-webkit-scrollbar {
width: 0.1rem;
}
}
.details-drawer-tabs {
margin-top: 32px;
.ant-tabs-tab {
display: flex;
align-items: center;
justify-content: center;
width: 114px;
height: 32px;
flex-shrink: 0;
padding: 7px 20px;
border-radius: 2px 0px 0px 2px;
border: 1px solid var(--l1-border);
background: var(--l2-background);
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
color: var(--l1-foreground);
font-family: Inter;
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 18px; /* 150% */
letter-spacing: -0.06px;
.ant-btn {
display: flex;
align-items: center;
justify-content: center;
padding: 0px;
}
.ant-btn:hover {
background: unset;
}
}
.ant-tabs-tab-active {
background: var(--l3-background);
}
.ant-tabs-tab + .ant-tabs-tab {
margin-left: 0px;
}
.ant-tabs-nav::before {
border-bottom: 0px;
}
.ant-tabs-ink-bar {
background: none;
}
}
}

View File

@@ -0,0 +1,57 @@
import { Dispatch, SetStateAction } from 'react';
import { Drawer, Tabs, TabsProps } from 'antd';
import cx from 'classnames';
import './DetailsDrawer.styles.scss';
interface IDetailsDrawerProps {
open: boolean;
setOpen: Dispatch<SetStateAction<boolean>>;
title: string;
descriptiveContent: JSX.Element;
defaultActiveKey: string;
items: TabsProps['items'];
detailsDrawerClassName?: string;
tabBarExtraContent?: JSX.Element;
}
function DetailsDrawer(props: IDetailsDrawerProps): JSX.Element {
const {
open,
setOpen,
title,
descriptiveContent,
defaultActiveKey,
detailsDrawerClassName,
items,
tabBarExtraContent,
} = props;
return (
<Drawer
width="60%"
open={open}
afterOpenChange={setOpen}
mask={false}
title={title}
onClose={(): void => setOpen(false)}
className="details-drawer"
>
<div>{descriptiveContent}</div>
<Tabs
items={items}
addIcon
defaultActiveKey={defaultActiveKey}
animated
className={cx('details-drawer-tabs', detailsDrawerClassName)}
tabBarExtraContent={tabBarExtraContent}
/>
</Drawer>
);
}
DetailsDrawer.defaultProps = {
detailsDrawerClassName: '',
tabBarExtraContent: null,
};
export default DetailsDrawer;

View File

@@ -0,0 +1,143 @@
import { useState } from 'react';
import { Button } from 'antd';
import { withErrorBoundary } from './index';
/**
* Example component that can throw errors
*/
function ProblematicComponent(): JSX.Element {
const [shouldThrow, setShouldThrow] = useState(false);
if (shouldThrow) {
throw new Error('This is a test error from ProblematicComponent!');
}
return (
<div style={{ padding: '20px' }}>
<h3>Problematic Component</h3>
<p>This component can throw errors when the button is clicked.</p>
<Button type="primary" onClick={(): void => setShouldThrow(true)} danger>
Trigger Error
</Button>
</div>
);
}
/**
* Basic usage - wraps component with default error boundary
*/
export const SafeProblematicComponent = withErrorBoundary(ProblematicComponent);
/**
* Usage with custom fallback component
*/
function CustomErrorFallback(): JSX.Element {
return (
<div
style={{ padding: '20px', border: '1px solid red', borderRadius: '4px' }}
>
<h4 style={{ color: 'red' }}>Custom Error Fallback</h4>
<p>Something went wrong in this specific component!</p>
<Button onClick={(): void => window.location.reload()}>Reload Page</Button>
</div>
);
}
export const SafeProblematicComponentWithCustomFallback = withErrorBoundary(
ProblematicComponent,
{
fallback: <CustomErrorFallback />,
},
);
/**
* Usage with custom error handler
*/
export const SafeProblematicComponentWithErrorHandler = withErrorBoundary(
ProblematicComponent,
{
onError: (error, errorInfo) => {
console.error('Custom error handler:', error);
console.error('Error info:', errorInfo);
// You could also send to analytics, logging service, etc.
},
sentryOptions: {
tags: {
section: 'dashboard',
priority: 'high',
},
level: 'error',
},
},
);
/**
* Example of wrapping an existing component from the codebase
*/
function ExistingComponent({
title,
data,
}: {
title: string;
data: any[];
}): JSX.Element {
// This could be any existing component that might throw errors
return (
<div>
<h4>{title}</h4>
<ul>
{data.map((item, index) => (
// eslint-disable-next-line react/no-array-index-key
<li key={index}>{item.name}</li>
))}
</ul>
</div>
);
}
export const SafeExistingComponent = withErrorBoundary(ExistingComponent, {
sentryOptions: {
tags: {
component: 'ExistingComponent',
feature: 'data-display',
},
},
});
/**
* Usage examples in a container component
*/
export function ErrorBoundaryExamples(): JSX.Element {
const sampleData = [
{ name: 'Item 1' },
{ name: 'Item 2' },
{ name: 'Item 3' },
];
return (
<div style={{ padding: '20px' }}>
<h2>Error Boundary HOC Examples</h2>
<div style={{ marginBottom: '20px' }}>
<h3>1. Basic Usage</h3>
<SafeProblematicComponent />
</div>
<div style={{ marginBottom: '20px' }}>
<h3>2. With Custom Fallback</h3>
<SafeProblematicComponentWithCustomFallback />
</div>
<div style={{ marginBottom: '20px' }}>
<h3>3. With Custom Error Handler</h3>
<SafeProblematicComponentWithErrorHandler />
</div>
<div style={{ marginBottom: '20px' }}>
<h3>4. Wrapped Existing Component</h3>
<SafeExistingComponent title="Sample Data" data={sampleData} />
</div>
</div>
);
}

View File

@@ -0,0 +1,13 @@
.query-builder-search-wrapper {
margin-top: 10px;
border: 1px solid var(--l1-border);
border-bottom: none;
.ant-select-selector {
border: none !important;
input {
font-size: 12px;
}
}
}

View File

@@ -0,0 +1,79 @@
import { Dispatch, SetStateAction, useEffect } from 'react';
import useInitialQuery from 'container/LogsExplorerContext/useInitialQuery';
import QueryBuilderSearch from 'container/QueryBuilder/filters/QueryBuilderSearch';
import { ILog } from 'types/api/logs/log';
import { Query, TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import './QueryBuilderSearchWrapper.styles.scss';
function QueryBuilderSearchWrapper({
log,
filters,
contextQuery,
isEdit,
suffixIcon,
setFilters,
setContextQuery,
}: QueryBuilderSearchWraperProps): JSX.Element {
const initialContextQuery = useInitialQuery(log);
useEffect(() => {
setContextQuery(initialContextQuery);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const handleSearch = (tagFilters: TagFilter): void => {
const tagFiltersLength = tagFilters.items.length;
if (
(!tagFiltersLength && (!filters || !filters.items.length)) ||
tagFiltersLength === filters?.items.length ||
!contextQuery
) {
return;
}
const nextQuery: Query = {
...contextQuery,
builder: {
...contextQuery.builder,
queryData: contextQuery.builder.queryData.map((item) => ({
...item,
filters: tagFilters,
})),
},
};
setFilters({ ...tagFilters });
setContextQuery({ ...nextQuery });
};
if (!contextQuery || !isEdit) {
return <></>;
}
return (
<QueryBuilderSearch
query={contextQuery?.builder.queryData[0]}
onChange={handleSearch}
className="query-builder-search-wrapper"
suffixIcon={suffixIcon}
/>
);
}
interface QueryBuilderSearchWraperProps {
log: ILog;
isEdit: boolean;
contextQuery: Query | undefined;
setContextQuery: Dispatch<SetStateAction<Query | undefined>>;
filters: TagFilter | null;
setFilters: Dispatch<SetStateAction<TagFilter | null>>;
suffixIcon?: React.ReactNode;
}
QueryBuilderSearchWrapper.defaultProps = {
suffixIcon: undefined,
};
export default QueryBuilderSearchWrapper;

View File

@@ -0,0 +1,3 @@
import { CSSProperties } from 'react';
export const rawLineStyle: CSSProperties = {};

View File

@@ -0,0 +1,8 @@
import { Button } from 'antd';
import styled from 'styled-components';
export const ButtonContainer = styled(Button)`
&&& {
padding-left: 0;
}
`;

View File

@@ -0,0 +1,13 @@
.custom-multiselect-dropdown {
.divider {
height: 1px;
background-color: #e8e8e8;
margin: 4px 0;
}
.all-option {
font-weight: 500;
border-bottom: 1px solid #f0f0f0;
margin-bottom: 8px;
}
}

View File

@@ -0,0 +1,19 @@
.loading-panel-data {
padding: 24px 0;
height: 240px;
display: flex;
justify-content: center;
align-items: flex-start;
.loading-panel-data-content {
display: flex;
align-items: flex-start;
flex-direction: column;
.loading-gif {
height: 72px;
margin-left: -24px;
}
}
}

View File

@@ -0,0 +1,17 @@
import { Typography } from 'antd';
import loadingPlaneUrl from '@/assets/Icons/loading-plane.gif';
import './PanelDataLoading.styles.scss';
export function PanelDataLoading(): JSX.Element {
return (
<div className="loading-panel-data">
<div className="loading-panel-data-content">
<img className="loading-gif" src={loadingPlaneUrl} alt="wait-icon" />
<Typography.Text>Fetching data...</Typography.Text>
</div>
</div>
);
}

View File

@@ -0,0 +1,41 @@
import { css, FlattenSimpleInterpolation } from 'styled-components';
const cssProperty = (key: any, value: any): FlattenSimpleInterpolation =>
key &&
value &&
css`
${key}: ${value};
`;
interface IFlexProps {
flexDirection?: string; // Need to replace this with exact css props. Not able to find any :(
flex?: number | string;
}
export const Flex = ({
flexDirection,
flex,
}: IFlexProps): FlattenSimpleInterpolation => css`
${cssProperty('flex-direction', flexDirection)}
${cssProperty('flex', flex)}
`;
interface IDisplayProps {
display?: string;
}
export const Display = ({
display,
}: IDisplayProps): FlattenSimpleInterpolation => css`
${cssProperty('display', display)}
`;
interface ISpacingProps {
margin?: string;
padding?: string;
}
export const Spacing = ({
margin,
padding,
}: ISpacingProps): FlattenSimpleInterpolation => css`
${cssProperty('margin', margin)}
${cssProperty('padding', padding)}
`;

View File

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

View File

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

View File

@@ -0,0 +1,5 @@
.tab-title {
display: flex;
gap: 4px;
align-items: center;
}

View File

@@ -0,0 +1,41 @@
import { useState } from 'react';
import { Radio } from 'antd';
import type { RadioChangeEvent } from 'antd/lib';
import { History, Table } from 'lucide-react';
import { ALERT_TABS } from '../constants';
import './Tabs.styles.scss';
export function Tabs(): JSX.Element {
const [selectedTab, setSelectedTab] = useState('overview');
const handleTabChange = (e: RadioChangeEvent): void => {
setSelectedTab(e.target.value);
};
return (
<Radio.Group className="tabs" onChange={handleTabChange} value={selectedTab}>
<Radio.Button
className={
selectedTab === ALERT_TABS.OVERVIEW ? 'selected_view tab' : 'tab'
}
value={ALERT_TABS.OVERVIEW}
>
<div className="tab-title">
<Table size={14} />
Overview
</div>
</Radio.Button>
<Radio.Button
className={selectedTab === ALERT_TABS.HISTORY ? 'selected_view tab' : 'tab'}
value={ALERT_TABS.HISTORY}
>
<div className="tab-title">
<History size={14} />
History
</div>
</Radio.Button>
</Radio.Group>
);
}

View File

@@ -0,0 +1,18 @@
@mixin flex-center {
display: flex;
justify-content: space-between;
align-items: center;
}
.tabs-and-filters {
@include flex-center;
margin-top: 1rem;
margin-bottom: 1rem;
.filters {
@include flex-center;
gap: 16px;
.reset-button {
@include flex-center;
}
}
}

View File

@@ -0,0 +1,16 @@
import { Filters } from 'components/AlertDetailsFilters/Filters';
import { Tabs } from './Tabs/Tabs';
import './TabsAndFilters.styles.scss';
function TabsAndFilters(): JSX.Element {
return (
<div className="tabs-and-filters">
<Tabs />
<Filters />
</div>
);
}
export default TabsAndFilters;

View File

@@ -0,0 +1,5 @@
export const ALERT_TABS = {
OVERVIEW: 'OVERVIEW',
HISTORY: 'HISTORY',
ACTIVITY: 'ACTIVITY',
} as const;

View File

@@ -0,0 +1,73 @@
import { useMemo } from 'react';
import type { ColumnSizingState } from '@tanstack/react-table';
import { Skeleton } from 'antd';
import { TableColumnDef } from './types';
import { getColumnWidthStyle } from './utils';
import tableStyles from './TanStackTable.module.scss';
import styles from './TanStackTableSkeleton.module.scss';
type TanStackTableSkeletonProps<TData> = {
columns: TableColumnDef<TData>[];
rowCount: number;
isDarkMode: boolean;
columnSizing?: ColumnSizingState;
};
export function TanStackTableSkeleton<TData>({
columns,
rowCount,
isDarkMode,
columnSizing,
}: TanStackTableSkeletonProps<TData>): JSX.Element {
const rows = useMemo(
() => Array.from({ length: rowCount }, (_, i) => i),
[rowCount],
);
return (
<table className={tableStyles.tanStackTable}>
<colgroup>
{columns.map((column, index) => (
<col
key={column.id}
style={getColumnWidthStyle(
column,
columnSizing?.[column.id],
index === columns.length - 1,
)}
/>
))}
</colgroup>
<thead>
<tr>
{columns.map((column) => (
<th
key={column.id}
className={tableStyles.tableHeaderCell}
data-dark-mode={isDarkMode}
>
{typeof column.header === 'function' ? (
<Skeleton.Input active size="small" className={styles.headerSkeleton} />
) : (
column.header
)}
</th>
))}
</tr>
</thead>
<tbody>
{rows.map((rowIndex) => (
<tr key={rowIndex} className={tableStyles.tableRow}>
{columns.map((column) => (
<td key={column.id} className={tableStyles.tableCell}>
<Skeleton.Input active size="small" className={styles.cellSkeleton} />
</td>
))}
</tr>
))}
</tbody>
</table>
);
}

View File

@@ -0,0 +1,13 @@
import styled from 'styled-components';
interface TextContainerProps {
noButtonMargin?: boolean;
}
export const TextContainer = styled.div<TextContainerProps>`
display: flex;
> button {
margin-left: ${({ noButtonMargin }): string =>
noButtonMargin ? '0' : '0.5rem'}
`;

View File

@@ -0,0 +1,42 @@
import { ReactChild } from 'react';
import { useTranslation } from 'react-i18next';
import { Card, Space, Typography } from 'antd';
import signozBrandLogoUrl from '@/assets/Logos/signoz-brand-logo.svg';
import { Container, LeftContainer, Logo } from './styles';
const { Title } = Typography;
function WelcomeLeftContainer({
version,
children,
}: WelcomeLeftContainerProps): JSX.Element {
const { t } = useTranslation();
return (
<Container>
<LeftContainer direction="vertical">
<Space align="center">
<Logo src={signozBrandLogoUrl} alt="logo" />
<Title style={{ fontSize: '46px', margin: 0 }}>SigNoz</Title>
</Space>
<Typography>{t('monitor_signup')}</Typography>
<Card
style={{ width: 'max-content' }}
bodyStyle={{ padding: '1px 8px', width: '100%' }}
>
SigNoz {version}
</Card>
</LeftContainer>
{children}
</Container>
);
}
interface WelcomeLeftContainerProps {
version: string;
children: ReactChild;
}
export default WelcomeLeftContainer;

View File

@@ -0,0 +1,23 @@
import { Space } from 'antd';
import styled from 'styled-components';
export const LeftContainer = styled(Space)`
flex: 1;
`;
export const Logo = styled.img`
width: 60px;
`;
export const Container = styled.div`
&&& {
display: flex;
justify-content: center;
gap: 16px;
align-items: center;
min-height: 100vh;
max-width: 1024px;
margin: 0 auto;
}
`;

View File

@@ -0,0 +1,9 @@
import {
AlertRuleTimelineTableResponse,
AlertRuleTimelineTableResponsePayload,
} from 'types/api/alerts/def';
export type TimelineTableProps = {
timelineData: AlertRuleTimelineTableResponse[];
totalItems: AlertRuleTimelineTableResponsePayload['data']['total'];
};

View File

@@ -0,0 +1,2 @@
// setting to 25 hours because we want to display the horizontal graph when the user selects 'Last 1 day' from date and time selector
export const HORIZONTAL_GRAPH_HOURS_THRESHOLD = 25;

View File

@@ -0,0 +1,42 @@
import { UseQueryResult } from 'react-query';
import { SuccessResponse } from 'types/api';
import noDataUrl from '@/assets/Icons/no-data.svg';
import EndPointsDropDown from './EndPointsDropDown';
function EndPointDetailsZeroState({
setSelectedEndPointName,
endPointDropDownDataQuery,
}: {
setSelectedEndPointName: (endPointName: string) => void;
endPointDropDownDataQuery: UseQueryResult<SuccessResponse<any>>;
}): JSX.Element {
return (
<div className="end-point-details-zero-state-wrapper">
<div className="end-point-details-zero-state-content">
<img
src={noDataUrl}
alt="no-data"
width={32}
height={32}
className="end-point-details-zero-state-icon"
/>
<div className="end-point-details-zero-state-content-wrapper">
<div className="end-point-details-zero-state-text-content">
<div className="title">No endpoint selected yet</div>
<div className="description">Select an endpoint to see the details</div>
</div>
<EndPointsDropDown
setSelectedEndPointName={setSelectedEndPointName}
endPointDropDownDataQuery={endPointDropDownDataQuery}
parentContainerDiv=".end-point-details-zero-state-wrapper"
dropdownStyle={{ width: '60%' }}
/>
</div>
</div>
</div>
);
}
export default EndPointDetailsZeroState;

View File

@@ -0,0 +1,136 @@
import { useMemo } from 'react';
import { useQueries } from 'react-query';
// eslint-disable-next-line no-restricted-imports
import { useSelector } from 'react-redux';
import { LoadingOutlined } from '@ant-design/icons';
import { Spin, Table } from 'antd';
import type { ColumnType } from 'antd/lib/table';
import logEvent from 'api/common/logEvent';
import { ENTITY_VERSION_V4 } from 'constants/app';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import {
createFiltersForSelectedRowData,
EndPointsTableRowData,
formatEndPointsDataForTable,
getEndPointsColumnsConfig,
getEndPointsQueryPayload,
} from 'container/ApiMonitoring/utils';
import LoadingContainer from 'container/InfraMonitoringK8s/LoadingContainer';
import { GetMetricQueryRange } from 'lib/dashboard/getQueryResults';
import { AppState } from 'store/reducers';
import { SuccessResponse } from 'types/api';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import { OrderByPayload } from 'types/api/queryBuilder/queryBuilderData';
import { GlobalReducer } from 'types/reducer/globalTime';
import { VIEW_TYPES, VIEWS } from '../constants';
function ExpandedRow({
domainName,
selectedRowData,
setSelectedEndPointName,
setSelectedView,
orderBy,
}: {
domainName: string;
selectedRowData: EndPointsTableRowData;
setSelectedEndPointName: (name: string) => void;
setSelectedView: (view: VIEWS) => void;
orderBy: OrderByPayload | null;
}): JSX.Element {
const nestedColumns = useMemo(() => getEndPointsColumnsConfig(false, []), []);
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const groupedByRowDataQueryPayload = useMemo(() => {
if (!selectedRowData) {
return null;
}
const filters = createFiltersForSelectedRowData(selectedRowData);
const baseQueryPayload = getEndPointsQueryPayload(
[],
domainName,
Math.floor(minTime / 1e9),
Math.floor(maxTime / 1e9),
);
return baseQueryPayload.map((currentQueryPayload) => ({
...currentQueryPayload,
query: {
...currentQueryPayload.query,
builder: {
...currentQueryPayload.query.builder,
queryData: currentQueryPayload.query.builder.queryData.map(
(queryData) => ({
...queryData,
filters: {
items: [...(queryData.filters?.items || []), ...(filters?.items || [])],
op: 'AND',
},
}),
),
},
},
}));
}, [domainName, minTime, maxTime, selectedRowData]);
const groupedByRowQueries = useQueries(
groupedByRowDataQueryPayload
? groupedByRowDataQueryPayload.map((payload) => ({
queryKey: [
`${REACT_QUERY_KEY.GET_NESTED_ENDPOINTS_LIST}-${domainName}-${selectedRowData?.key}`,
payload,
ENTITY_VERSION_V4,
selectedRowData?.key,
],
queryFn: (): Promise<SuccessResponse<MetricRangePayloadProps>> =>
GetMetricQueryRange(payload, ENTITY_VERSION_V4),
enabled: !!payload && !!selectedRowData,
}))
: [],
);
const groupedByRowQuery = groupedByRowQueries[0];
return (
<div className="expanded-table-container">
{groupedByRowQuery?.isFetching || groupedByRowQuery?.isLoading ? (
<LoadingContainer />
) : (
<div className="expanded-table">
<Table
columns={nestedColumns as ColumnType<EndPointsTableRowData>[]}
dataSource={
groupedByRowQuery?.data
? formatEndPointsDataForTable(
groupedByRowQuery.data?.payload.data.result[0].table?.rows,
[],
orderBy,
)
: []
}
pagination={false}
scroll={{ x: true }}
tableLayout="fixed"
showHeader={false}
loading={{
spinning: groupedByRowQuery?.isFetching || groupedByRowQuery?.isLoading,
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
}}
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => {
setSelectedEndPointName(record.endpointName);
setSelectedView(VIEW_TYPES.ENDPOINT_STATS);
logEvent('API Monitoring: Endpoint name row clicked', {});
},
className: 'expanded-clickable-row',
})}
/>
</div>
)}
</div>
);
}
export default ExpandedRow;

View File

@@ -0,0 +1,33 @@
import { PureComponent } from 'react';
interface State {
hasError: boolean;
}
interface Props {
children: JSX.Element;
}
class ErrorLink extends PureComponent<Props, State> {
constructor(props: Props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(): State {
return { hasError: true };
}
render(): JSX.Element {
const { children } = this.props;
const { hasError } = this.state;
if (hasError) {
return <div />;
}
return children;
}
}
export default ErrorLink;

View File

@@ -0,0 +1,23 @@
import { ReactNode } from 'react';
import { Link } from 'react-router-dom';
function LinkContainer({ children, href }: LinkContainerProps): JSX.Element {
const isInternalLink = href.startsWith('/');
if (isInternalLink) {
return <Link to={href}>{children}</Link>;
}
return (
<a rel="noreferrer" target="_blank" href={href}>
{children}
</a>
);
}
interface LinkContainerProps {
children: ReactNode;
href: string;
}
export default LinkContainer;

View File

@@ -0,0 +1,49 @@
import { lazy, Suspense, useMemo } from 'react';
import { Menu, Space } from 'antd';
import Spinner from 'components/Spinner';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { ConfigProps } from 'types/api/dynamicConfigs/getDynamicConfigs';
import { lazyRetry } from 'utils/lazyWithRetries';
import ErrorLink from './ErrorLink';
import LinkContainer from './Link';
function HelpToolTip({ config }: HelpToolTipProps): JSX.Element {
const sortedConfig = useMemo(
() => config.components.sort((a, b) => a.position - b.position),
[config.components],
);
const isDarkMode = useIsDarkMode();
const items = sortedConfig.map((item) => {
const iconName = `${isDarkMode ? item.darkIcon : item.lightIcon}`;
const Component = lazy(() =>
lazyRetry(() => import(`@ant-design/icons/es/icons/${iconName}.js`)),
);
return {
key: item.text + item.href,
label: (
<ErrorLink key={item.text + item.href}>
<Suspense fallback={<Spinner height="5vh" />}>
<LinkContainer href={item.href}>
<Space size="small" align="start">
<Component />
{item.text}
</Space>
</LinkContainer>
</Suspense>
</ErrorLink>
),
};
});
return <Menu items={items} />;
}
interface HelpToolTipProps {
config: ConfigProps;
}
export default HelpToolTip;

View File

@@ -0,0 +1,76 @@
import { useMemo, useState } from 'react';
// eslint-disable-next-line no-restricted-imports
import { useSelector } from 'react-redux';
import {
CaretDownFilled,
CaretUpFilled,
QuestionCircleFilled,
QuestionCircleOutlined,
} from '@ant-design/icons';
import { Space } from 'antd';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { AppState } from 'store/reducers';
import { ConfigProps } from 'types/api/dynamicConfigs/getDynamicConfigs';
import AppReducer from 'types/reducer/app';
import HelpToolTip from './Config';
import { ConfigDropdown } from './styles';
function DynamicConfigDropdown({
frontendId,
}: DynamicConfigDropdownProps): JSX.Element {
const { configs } = useSelector<AppState, AppReducer>((state) => state.app);
const isDarkMode = useIsDarkMode();
const [isHelpDropDownOpen, setIsHelpDropDownOpen] = useState<boolean>(false);
const config = useMemo(
() =>
Object.values(configs).find(
(config) => config.frontendPositionId === frontendId,
),
[frontendId, configs],
);
const onToggleHandler = (): void => {
setIsHelpDropDownOpen(!isHelpDropDownOpen);
};
const menu = useMemo(
() => ({
items: [
{
key: '1',
label: <HelpToolTip config={config as ConfigProps} />,
},
],
}),
[config],
);
if (!config) {
return <div />;
}
const Icon = isDarkMode ? QuestionCircleOutlined : QuestionCircleFilled;
const DropDownIcon = isHelpDropDownOpen ? CaretUpFilled : CaretDownFilled;
return (
<ConfigDropdown
onOpenChange={onToggleHandler}
trigger={['click']}
menu={menu}
open={isHelpDropDownOpen}
>
<Space align="center">
<Icon style={{ fontSize: 26, color: 'white', paddingTop: 26 }} />
<DropDownIcon style={{ color: 'white' }} />
</Space>
</ConfigDropdown>
);
}
interface DynamicConfigDropdownProps {
frontendId: string;
}
export default DynamicConfigDropdown;

View File

@@ -0,0 +1,6 @@
import { Dropdown } from 'antd';
import styled from 'styled-components';
export const ConfigDropdown = styled(Dropdown)`
cursor: pointer;
`;

View File

@@ -0,0 +1,3 @@
import EvaluationSettings from './EvaluationSettings';
export default EvaluationSettings;

View File

@@ -0,0 +1,27 @@
import { ChangeEvent, Dispatch, SetStateAction, useCallback } from 'react';
import Input from 'components/Input';
function DashboardName({ setName, name }: DashboardNameProps): JSX.Element {
const onChangeHandler = useCallback(
(e: ChangeEvent<HTMLInputElement>) => {
setName(e.target.value);
},
[setName],
);
return (
<Input
size="middle"
placeholder="Title"
value={name}
onChangeHandler={onChangeHandler}
/>
);
}
interface DashboardNameProps {
name: string;
setName: Dispatch<SetStateAction<string>>;
}
export default DashboardName;

View File

@@ -0,0 +1,100 @@
import { useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useCopyToClipboard } from 'react-use';
import { CopyFilled, DownloadOutlined } from '@ant-design/icons';
import { Button, Modal } from 'antd';
import Editor from 'components/Editor';
import { useNotifications } from 'hooks/useNotifications';
import { DashboardData } from 'types/api/dashboard/getAll';
import { downloadObjectAsJson } from './utils';
function ShareModal({
isJSONModalVisible,
onToggleHandler,
selectedData,
}: ShareModalProps): JSX.Element {
const getParsedValue = (): string => JSON.stringify(selectedData, null, 2);
const [jsonValue, setJSONValue] = useState<string>(getParsedValue());
const { t } = useTranslation(['dashboard', 'common']);
const [state, setCopy] = useCopyToClipboard();
const { notifications } = useNotifications();
useEffect(() => {
if (state.error) {
notifications.error({
message: t('something_went_wrong', {
ns: 'common',
}),
});
}
if (state.value) {
notifications.success({
message: t('success', {
ns: 'common',
}),
});
}
}, [state.error, state.value, t, notifications]);
const GetFooterComponent = useMemo(() => {
return (
<>
<Button
style={{
marginTop: '16px',
}}
onClick={(): void => setCopy(jsonValue)}
type="primary"
size="small"
>
<CopyFilled /> {t('copy_to_clipboard')}
</Button>
<Button
type="primary"
size="small"
onClick={(): void => {
downloadObjectAsJson(selectedData, selectedData.title);
}}
>
<DownloadOutlined /> {t('download_json')}
</Button>
</>
);
}, [jsonValue, selectedData, setCopy, t]);
return (
<Modal
open={isJSONModalVisible}
onCancel={(): void => {
onToggleHandler();
}}
width="80vw"
centered
title={t('share', {
ns: 'common',
})}
okText={t('download_json')}
cancelText={t('cancel')}
destroyOnClose
footer={GetFooterComponent}
>
<Editor
height="70vh"
onChange={(value): void => setJSONValue(value)}
value={jsonValue}
/>
</Modal>
);
}
interface ShareModalProps {
isJSONModalVisible: boolean;
onToggleHandler: VoidFunction;
selectedData: DashboardData;
}
export default ShareModal;

View File

@@ -0,0 +1,35 @@
import { ChangeEvent, Dispatch, SetStateAction, useCallback } from 'react';
import { Input } from 'antd';
import { Container } from './styles';
const { TextArea } = Input;
function Description({
description,
setDescription,
}: DescriptionProps): JSX.Element {
const onChangeHandler = useCallback(
(e: ChangeEvent<HTMLTextAreaElement>) => {
setDescription(e.target.value);
},
[setDescription],
);
return (
<Container>
<TextArea
placeholder="Description of the dashboard"
onChange={onChangeHandler}
value={description}
/>
</Container>
);
}
interface DescriptionProps {
description: string;
setDescription: Dispatch<SetStateAction<string>>;
}
export default Description;

View File

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

View File

@@ -0,0 +1,59 @@
.download-logs-popover {
.ant-popover-inner {
border-radius: 4px;
border: 1px solid var(--l1-border);
background: linear-gradient(
139deg,
color-mix(in srgb, var(--card) 80%, transparent) 0%,
color-mix(in srgb, var(--card) 90%, transparent) 98.68%
);
box-shadow: 4px 10px 16px 2px rgba(0, 0, 0, 0.2);
backdrop-filter: blur(20px);
padding: 12px 18px 12px 14px;
.download-logs-content {
display: flex;
flex-direction: column;
gap: 8px;
align-items: flex-start;
.action-btns {
padding: 4px 0px !important;
width: 159px;
display: flex;
align-items: center;
color: var(--l2-foreground);
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: normal;
letter-spacing: 0.14px;
gap: 6px;
.ant-btn-icon {
margin-inline-end: 0px;
}
}
.action-btns:hover {
&.ant-btn-text {
background-color: color-mix(
in srgb,
var(--bg-robin-200) 4%,
transparent
) !important;
}
}
.export-heading {
color: var(--muted-foreground);
font-size: 11px;
font-style: normal;
font-weight: 600;
line-height: 18px; /* 163.636% */
letter-spacing: 0.88px;
text-transform: uppercase;
}
}
}
}

View File

@@ -0,0 +1,94 @@
import { useState } from 'react';
import { Button, Popover, Typography } from 'antd';
import { FileDigit, FileDown, Sheet } from 'lucide-react';
import { unparse } from 'papaparse';
import { DownloadProps } from './DownloadV2.types';
import './DownloadV2.styles.scss';
function Download({ data, isLoading, fileName }: DownloadProps): JSX.Element {
const [isDownloading, setIsDownloading] = useState(false);
const downloadExcelFile = async (): Promise<void> => {
setIsDownloading(true);
try {
const headers = Object.keys(Object.assign({}, ...data)).map((item) => {
const updatedTitle = item
.split('_')
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');
return {
title: updatedTitle,
dataIndex: item,
};
});
const excelLib = await import('antd-table-saveas-excel');
const excel = new excelLib.Excel();
excel
.addSheet(fileName)
.addColumns(headers)
.addDataSource(data, {
str2Percent: true,
})
.saveAs(`${fileName}.xlsx`);
} finally {
setIsDownloading(false);
}
};
const downloadCsvFile = (): void => {
const csv = unparse(data);
const csvBlob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
const csvUrl = URL.createObjectURL(csvBlob);
const downloadLink = document.createElement('a');
downloadLink.href = csvUrl;
downloadLink.download = `${fileName}.csv`;
downloadLink.click();
downloadLink.remove();
};
return (
<Popover
trigger={['click']}
placement="bottomRight"
rootClassName="download-logs-popover"
arrow={false}
content={
<div className="download-logs-content">
<Typography.Text className="export-heading">Export As</Typography.Text>
<Button
icon={<Sheet size={14} />}
type="text"
onClick={downloadExcelFile}
className="action-btns"
loading={isDownloading}
>
Excel (.xlsx)
</Button>
<Button
icon={<FileDigit size={14} />}
type="text"
onClick={downloadCsvFile}
className="action-btns"
>
CSV
</Button>
</div>
}
>
<Button
className="periscope-btn ghost"
loading={isLoading}
icon={<FileDown size={14} />}
/>
</Popover>
);
}
Download.defaultProps = {
isLoading: undefined,
};
export default Download;

View File

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

View File

@@ -0,0 +1,11 @@
import styled from 'styled-components';
export const ButtonContainer = styled.div`
&&& {
display: flex;
justify-content: flex-end;
align-items: center;
margin-top: 2rem;
}
`;

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,61 @@
import { Dispatch, SetStateAction } from 'react';
import { Form, Input, Select } from 'antd';
import { LabelFilterStatement } from 'container/CreateAlertChannels/config';
const { Option } = Select;
// LabelFilterForm supports filters or matchers on alert notifications
// presently un-used but will be introduced to the channel creation at some
// point
function LabelFilterForm({ setFilter }: LabelFilterProps): JSX.Element {
return (
<Form.Item name="label_filter" label="Notify When (Optional)">
<Input.Group compact>
<Select
defaultValue="Severity"
style={{ width: '15%' }}
onChange={(event): void => {
setFilter((value) => {
const first: LabelFilterStatement = value[0] as LabelFilterStatement;
first.name = event;
return [first];
});
}}
>
<Option value="severity">Severity</Option>
<Option value="service">Service</Option>
</Select>
<Select
defaultValue="="
onChange={(event): void => {
setFilter((value) => {
const first: LabelFilterStatement = value[0] as LabelFilterStatement;
first.comparator = event;
return [first];
});
}}
>
<Option value="=">=</Option>
<Option value="!=">!=</Option>
</Select>
<Input
style={{ width: '20%' }}
placeholder="enter a text here"
onChange={(event): void => {
setFilter((value) => {
const first: LabelFilterStatement = value[0] as LabelFilterStatement;
first.value = event.target.value;
return [first];
});
}}
/>
</Input.Group>
</Form.Item>
);
}
export interface LabelFilterProps {
setFilter: Dispatch<SetStateAction<Partial<Array<LabelFilterStatement>>>>;
}
export default LabelFilterForm;

View File

@@ -0,0 +1,164 @@
import { Trans, useTranslation } from 'react-i18next';
import { Col, Row, Typography } from 'antd';
import TextToolTip from 'components/TextToolTip';
import { EQueryType } from 'types/common/dashboard';
import {
StyledList,
StyledListItem,
StyledMainContainer,
StyledTopic,
} from './styles';
function UserGuide({ queryType }: UserGuideProps): JSX.Element {
// init namespace for translations
const { t } = useTranslation('alerts');
const renderStep1QB = (): JSX.Element => (
<>
<StyledTopic>{t('user_guide_qb_step1')}</StyledTopic>
<StyledList>
<StyledListItem>{t('user_guide_qb_step1a')}</StyledListItem>
<StyledListItem>{t('user_guide_qb_step1b')}</StyledListItem>
<StyledListItem>{t('user_guide_qb_step1c')}</StyledListItem>
<StyledListItem>{t('user_guide_qb_step1d')}</StyledListItem>
</StyledList>
</>
);
const renderStep2QB = (): JSX.Element => (
<>
<StyledTopic>{t('user_guide_qb_step2')}</StyledTopic>
<StyledList>
<StyledListItem>{t('user_guide_qb_step2a')}</StyledListItem>
<StyledListItem>{t('user_guide_qb_step2b')}</StyledListItem>
</StyledList>
</>
);
const renderStep3QB = (): JSX.Element => (
<>
<StyledTopic>{t('user_guide_qb_step3')}</StyledTopic>
<StyledList>
<StyledListItem>{t('user_guide_qb_step3a')}</StyledListItem>
<StyledListItem>{t('user_guide_qb_step3b')}</StyledListItem>
</StyledList>
</>
);
const renderGuideForQB = (): JSX.Element => (
<>
{renderStep1QB()}
{renderStep2QB()}
{renderStep3QB()}
</>
);
const renderStep1PQL = (): JSX.Element => (
<>
<StyledTopic>{t('user_guide_pql_step1')}</StyledTopic>
<StyledList>
<StyledListItem>{t('user_guide_pql_step1a')}</StyledListItem>
<StyledListItem>{t('user_guide_pql_step1b')}</StyledListItem>
</StyledList>
</>
);
const renderStep2PQL = (): JSX.Element => (
<>
<StyledTopic>{t('user_guide_pql_step2')}</StyledTopic>
<StyledList>
<StyledListItem>{t('user_guide_pql_step2a')}</StyledListItem>
<StyledListItem>{t('user_guide_pql_step2b')}</StyledListItem>
</StyledList>
</>
);
const renderStep3PQL = (): JSX.Element => (
<>
<StyledTopic>{t('user_guide_pql_step3')}</StyledTopic>
<StyledList>
<StyledListItem>{t('user_guide_pql_step3a')}</StyledListItem>
<StyledListItem>{t('user_guide_pql_step3b')}</StyledListItem>
</StyledList>
</>
);
const renderGuideForPQL = (): JSX.Element => (
<>
{renderStep1PQL()}
{renderStep2PQL()}
{renderStep3PQL()}
</>
);
const renderStep1CH = (): JSX.Element => (
<>
<StyledTopic>{t('user_guide_ch_step1')}</StyledTopic>
<StyledList>
<StyledListItem>
<Trans
i18nKey="user_guide_ch_step1a"
t={t}
components={[
<a
key={1}
target="_blank"
href=" https://signoz.io/docs/tutorial/writing-clickhouse-queries-in-dashboard/?utm_source=frontend&utm_medium=product&utm_id=alerts</>"
/>,
]}
/>
</StyledListItem>
<StyledListItem>{t('user_guide_ch_step1b')}</StyledListItem>
</StyledList>
</>
);
const renderStep2CH = (): JSX.Element => (
<>
<StyledTopic>{t('user_guide_ch_step2')}</StyledTopic>
<StyledList>
<StyledListItem>{t('user_guide_ch_step2a')}</StyledListItem>
<StyledListItem>{t('user_guide_ch_step2b')}</StyledListItem>
</StyledList>
</>
);
const renderStep3CH = (): JSX.Element => (
<>
<StyledTopic>{t('user_guide_ch_step3')}</StyledTopic>
<StyledList>
<StyledListItem>{t('user_guide_ch_step3a')}</StyledListItem>
<StyledListItem>{t('user_guide_ch_step3b')}</StyledListItem>
</StyledList>
</>
);
const renderGuideForCH = (): JSX.Element => (
<>
{renderStep1CH()}
{renderStep2CH()}
{renderStep3CH()}
</>
);
return (
<StyledMainContainer>
<Row>
<Col flex="auto">
<Typography.Paragraph> {t('user_guide_headline')} </Typography.Paragraph>
</Col>
<Col flex="none">
<TextToolTip
text={t('user_tooltip_more_help')}
url="https://signoz.io/docs/userguide/alerts-management/?utm_source=product&utm_medium=create-alert#creating-a-new-alert-in-signoz"
/>
</Col>
</Row>
{queryType === EQueryType.QUERY_BUILDER && renderGuideForQB()}
{queryType === EQueryType.PROM && renderGuideForPQL()}
{queryType === EQueryType.CLICKHOUSE && renderGuideForCH()}
</StyledMainContainer>
);
}
interface UserGuideProps {
queryType: EQueryType;
}
export default UserGuide;

View File

@@ -0,0 +1,17 @@
import { Card, Typography } from 'antd';
import styled from 'styled-components';
export const StyledMainContainer = styled(Card)``;
export const StyledTopic = styled(Typography.Paragraph)`
font-weight: 600;
`;
export const StyledList = styled.ul`
padding-left: 18px;
`;
export const StyledListItem = styled.li`
font-style: italic;
padding-bottom: 0.5rem;
`;

View File

@@ -0,0 +1,158 @@
import { memo, useCallback, useEffect, useState } from 'react';
import { Button, Input } from 'antd';
import type { CheckboxChangeEvent } from 'antd/es/checkbox';
import { ResizeTable } from 'components/ResizeTable';
import { useNotifications } from 'hooks/useNotifications';
import {
selectIsDashboardLocked,
useDashboardStore,
} from 'providers/Dashboard/store/useDashboardStore';
import { getGraphManagerTableColumns } from './TableRender/GraphManagerColumns';
import { ExtendedChartDataset, GraphManagerProps } from './types';
import {
getDefaultTableDataSet,
saveLegendEntriesToLocalStorage,
} from './utils';
import './WidgetFullView.styles.scss';
function GraphManager({
data,
name,
yAxisUnit,
onToggleModelHandler,
setGraphsVisibilityStates,
graphsVisibilityStates = [], // not trimed
lineChartRef,
parentChartRef,
options,
}: GraphManagerProps): JSX.Element {
const [tableDataSet, setTableDataSet] = useState<ExtendedChartDataset[]>(
getDefaultTableDataSet(options, data),
);
useEffect(() => {
setTableDataSet(getDefaultTableDataSet(options, data));
}, [data, options]);
const { notifications } = useNotifications();
const isDashboardLocked = useDashboardStore(selectIsDashboardLocked);
const checkBoxOnChangeHandler = useCallback(
(e: CheckboxChangeEvent, index: number): void => {
const newStates = [...graphsVisibilityStates];
newStates[index] = e.target.checked;
lineChartRef?.current?.toggleGraph(index, e.target.checked);
parentChartRef?.current?.toggleGraph(index, e.target.checked);
setGraphsVisibilityStates([...newStates]);
},
[
graphsVisibilityStates,
lineChartRef,
parentChartRef,
setGraphsVisibilityStates,
],
);
const labelClickedHandler = useCallback(
(labelIndex: number): void => {
const newGraphVisibilityStates = Array<boolean>(data.length).fill(false);
newGraphVisibilityStates[labelIndex] = true;
newGraphVisibilityStates.forEach((state, index) => {
lineChartRef?.current?.toggleGraph(index, state);
parentChartRef?.current?.toggleGraph(index, state);
});
setGraphsVisibilityStates(newGraphVisibilityStates);
},
[data.length, lineChartRef, parentChartRef, setGraphsVisibilityStates],
);
const columns = getGraphManagerTableColumns({
tableDataSet,
checkBoxOnChangeHandler,
graphVisibilityState: graphsVisibilityStates,
labelClickedHandler,
yAxisUnit,
isGraphDisabled: isDashboardLocked,
});
const filterHandler = useCallback(
(event: React.ChangeEvent<HTMLInputElement>): void => {
const value = event.target.value.toString().toLowerCase();
const updatedDataSet = tableDataSet.map((item) => {
if (item.label?.toLocaleLowerCase().includes(value)) {
return { ...item, show: true };
}
return { ...item, show: false };
});
setTableDataSet(updatedDataSet);
},
[tableDataSet],
);
const saveHandler = useCallback((): void => {
saveLegendEntriesToLocalStorage({
options,
graphVisibilityState: graphsVisibilityStates || [],
name,
});
notifications.success({
message: 'The updated graphs & legends are saved',
});
if (onToggleModelHandler) {
onToggleModelHandler();
}
}, [
graphsVisibilityStates,
name,
notifications,
onToggleModelHandler,
options,
]);
const dataSource = tableDataSet.filter(
(item, index) => index !== 0 && item.show,
);
return (
<div className="graph-manager-container">
<div className="graph-manager-header">
<Input onChange={filterHandler} placeholder="Filter Series" />
<div className="save-cancel-container">
<span className="save-cancel-button">
<Button type="default" onClick={onToggleModelHandler}>
Cancel
</Button>
</span>
<span className="save-cancel-button">
<Button type="primary" onClick={saveHandler}>
Save
</Button>
</span>
</div>
</div>
<div className="legends-list-container">
<ResizeTable
columns={columns}
dataSource={dataSource}
rowKey="index"
pagination={false}
style={{
maxHeight: 200,
overflowX: 'hidden',
overflowY: 'auto',
}}
/>
</div>
</div>
);
}
GraphManager.defaultProps = {
graphVisibilityStateHandler: undefined,
};
export default memo(GraphManager);

View File

@@ -0,0 +1,18 @@
import { TableColumnType as ColumnType } from 'antd';
import { DataSetProps } from '../types';
import Label from './Label';
export const getLabel = (
labelClickedHandler: (labelIndex: number) => void,
disabled?: boolean,
): ColumnType<DataSetProps> => ({
render: (label: string, record): JSX.Element => (
<Label
label={label}
labelIndex={record.index}
labelClickedHandler={labelClickedHandler}
disabled={disabled}
/>
),
});

View File

@@ -0,0 +1,85 @@
import { TableColumnType as ColumnType } from 'antd';
import type { CheckboxChangeEvent } from 'antd/es/checkbox';
import { ColumnsKeyAndDataIndex, ColumnsTitle } from '../contants';
import { DataSetProps, ExtendedChartDataset } from '../types';
import { getGraphManagerTableHeaderTitle } from '../utils';
import CustomCheckBox from './CustomCheckBox';
import { getLabel } from './GetLabel';
export const getGraphManagerTableColumns = ({
tableDataSet,
checkBoxOnChangeHandler,
graphVisibilityState,
labelClickedHandler,
yAxisUnit,
isGraphDisabled,
}: GetGraphManagerTableColumnsProps): ColumnType<DataSetProps>[] => [
{
title: '',
width: 50,
dataIndex: ColumnsKeyAndDataIndex.Index,
key: ColumnsKeyAndDataIndex.Index,
render: (_: string, record: DataSetProps): JSX.Element => (
<CustomCheckBox
data={tableDataSet}
index={record.index}
checkBoxOnChangeHandler={checkBoxOnChangeHandler}
graphVisibilityState={graphVisibilityState}
disabled={isGraphDisabled}
/>
),
},
{
title: ColumnsTitle[ColumnsKeyAndDataIndex.Label],
width: 300,
dataIndex: ColumnsKeyAndDataIndex.Label,
key: ColumnsKeyAndDataIndex.Label,
...getLabel(labelClickedHandler, isGraphDisabled),
},
{
title: getGraphManagerTableHeaderTitle(
ColumnsTitle[ColumnsKeyAndDataIndex.Avg],
yAxisUnit,
),
width: 90,
dataIndex: ColumnsKeyAndDataIndex.Avg,
key: ColumnsKeyAndDataIndex.Avg,
},
{
title: getGraphManagerTableHeaderTitle(
ColumnsTitle[ColumnsKeyAndDataIndex.Sum],
yAxisUnit,
),
width: 90,
dataIndex: ColumnsKeyAndDataIndex.Sum,
key: ColumnsKeyAndDataIndex.Sum,
},
{
title: getGraphManagerTableHeaderTitle(
ColumnsTitle[ColumnsKeyAndDataIndex.Max],
yAxisUnit,
),
width: 90,
dataIndex: ColumnsKeyAndDataIndex.Max,
key: ColumnsKeyAndDataIndex.Max,
},
{
title: getGraphManagerTableHeaderTitle(
ColumnsTitle[ColumnsKeyAndDataIndex.Min],
yAxisUnit,
),
width: 90,
dataIndex: ColumnsKeyAndDataIndex.Min,
key: ColumnsKeyAndDataIndex.Min,
},
];
interface GetGraphManagerTableColumnsProps {
tableDataSet: ExtendedChartDataset[];
checkBoxOnChangeHandler: (e: CheckboxChangeEvent, index: number) => void;
labelClickedHandler: (labelIndex: number) => void;
graphVisibilityState: boolean[];
yAxisUnit?: string;
isGraphDisabled?: boolean;
}

View File

@@ -0,0 +1,34 @@
import { Tooltip } from 'antd';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { LabelContainer } from '../styles';
import { LabelProps } from '../types';
import { getAbbreviatedLabel } from '../utils';
function Label({
labelClickedHandler,
labelIndex,
label,
disabled = false,
}: LabelProps): JSX.Element {
const isDarkMode = useIsDarkMode();
const onClickHandler = (): void => {
labelClickedHandler(labelIndex);
};
return (
<LabelContainer
isDarkMode={isDarkMode}
type="button"
disabled={disabled}
onClick={onClickHandler}
>
<Tooltip title={label} placement="topLeft">
{getAbbreviatedLabel(label)}
</Tooltip>
</LabelContainer>
);
}
export default Label;

View File

@@ -0,0 +1,15 @@
import { ChartData } from 'chart.js';
export const mockTestData: ChartData = {
labels: ['test1', 'test2'],
datasets: [
{
label: 'customer',
data: [481.60377358490564, 730.0000000000002],
},
{
label: 'demo-app',
data: [4471.4285714285725],
},
],
};

View File

@@ -0,0 +1,12 @@
import { LegendEntryProps } from '../FullView/types';
export const mocklegendEntryResult: LegendEntryProps[] = [
{
label: 'customer',
show: true,
},
{
label: 'demo-app',
show: false,
},
];

View File

@@ -0,0 +1,3 @@
import { GridValueComponentProps } from './types';
export const GridValueConfig: Pick<GridValueComponentProps, 'title'> = {};

View File

@@ -0,0 +1,3 @@
import Home from './Home';
export default Home;

View File

@@ -0,0 +1,19 @@
.loading-host-metrics {
padding: 24px 0;
height: 600px;
display: flex;
justify-content: center;
align-items: center;
.loading-host-metrics-content {
display: flex;
align-items: center;
flex-direction: column;
.loading-gif {
height: 72px;
margin-left: -24px;
}
}
}

View File

@@ -0,0 +1,24 @@
import { useTranslation } from 'react-i18next';
import { Typography } from 'antd';
import { DataSource } from 'types/common/queryBuilder';
import loadingPlaneUrl from '@/assets/Icons/loading-plane.gif';
import './HostMetricsLoading.styles.scss';
export function HostMetricsLoading(): JSX.Element {
const { t } = useTranslation('common');
return (
<div className="loading-host-metrics">
<div className="loading-host-metrics-content">
<img className="loading-gif" src={loadingPlaneUrl} alt="wait-icon" />
<Typography>
{t('pending_data_placeholder', {
dataSource: `host ${DataSource.METRICS}`,
})}
</Typography>
</div>
</div>
);
}

View File

@@ -0,0 +1,42 @@
.config-connection-status-popover {
.ant-popover-inner {
padding: 0;
background-color: var(--l2-background);
border-radius: 4px;
border: 1px solid var(--l3-background);
padding: 8px;
width: 240px;
.ant-popover-content {
padding: 0;
}
}
.config-connection-status-item {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
gap: 8px;
padding: 4px 8px;
border-radius: 4px;
}
.config-connection-status-icon {
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
}
.config-connection-status-category-display-name {
color: var(--l2-foreground);
font-family: Inter;
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 18px; /* 138.462% */
}
}

View File

@@ -0,0 +1,30 @@
import { Color } from '@signozhq/design-tokens';
import { IConfigConnectionStatus } from 'container/Integrations/types';
import { CheckCircle, TriangleAlert } from 'lucide-react';
import './ConfigConnectionStatus.styles.scss';
export function ConfigConnectionStatus({
status,
}: {
status: IConfigConnectionStatus[] | null;
}): JSX.Element {
return (
<div className="config-connection-status-container">
{status?.map((status) => (
<div key={status.category} className="config-connection-status-item">
<div className="config-connection-status-icon">
{status.last_received_ts_ms && status.last_received_ts_ms > 0 ? (
<CheckCircle size={16} color={Color.BG_FOREST_500} />
) : (
<TriangleAlert size={16} color={Color.BG_AMBER_500} />
)}
</div>
<div className="config-connection-status-category-display-name">
{status.category_display_name}
</div>
</div>
))}
</div>
);
}

View File

@@ -0,0 +1,39 @@
import cx from 'classnames';
import LineClampedText from 'periscope/components/LineClampedText/LineClampedText';
import { Service } from './AmazonWebServices/types';
function ServiceItem({
service,
onClick,
isActive,
}: {
service: Service;
onClick: (serviceName: string) => void;
isActive?: boolean;
}): JSX.Element {
return (
<button
className={cx('service-item', { active: isActive })}
onClick={(): void => onClick(service.id)}
type="button"
>
<div className="service-item__icon-wrapper">
<img
src={service.icon}
alt={service.title}
className="service-item__icon"
/>
</div>
<div className="service-item__title">
<LineClampedText text={service.title} />
</div>
</button>
);
}
ServiceItem.defaultProps = {
isActive: false,
};
export default ServiceItem;

View File

@@ -0,0 +1,111 @@
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { LoadingOutlined } from '@ant-design/icons';
import { Button, Input, Space, Typography } from 'antd';
import logEvent from 'api/common/logEvent';
import { IntegrationType } from 'container/Integrations/types';
import { useNotifications } from 'hooks/useNotifications';
import { Check } from 'lucide-react';
import './Integrations.styles.scss';
interface RequestIntegrationBtnProps {
type?: IntegrationType;
message?: string;
}
export function RequestIntegrationBtn({
type,
message,
}: RequestIntegrationBtnProps): JSX.Element {
const [
isSubmittingRequestForIntegration,
setIsSubmittingRequestForIntegration,
] = useState(false);
const [requestedIntegrationName, setRequestedIntegrationName] = useState('');
const { notifications } = useNotifications();
const { t } = useTranslation(['common']);
const handleRequestIntegrationSubmit = async (): Promise<void> => {
try {
setIsSubmittingRequestForIntegration(true);
const eventName =
type === IntegrationType.AWS_SERVICES
? 'AWS service integration requested'
: 'Integration requested';
const screenName =
type === IntegrationType.AWS_SERVICES
? 'AWS integration details'
: 'Integration list page';
const response = await logEvent(eventName, {
screen: screenName,
integration: requestedIntegrationName,
});
if (response.statusCode === 200) {
notifications.success({
message: 'Integration Request Submitted',
});
setIsSubmittingRequestForIntegration(false);
} else {
notifications.error({
message:
response.error ||
t('something_went_wrong', {
ns: 'common',
}),
});
setIsSubmittingRequestForIntegration(false);
}
} catch (error) {
notifications.error({
message: t('something_went_wrong', {
ns: 'common',
}),
});
setIsSubmittingRequestForIntegration(false);
}
};
return (
<div className="request-entity-container">
<Typography.Text>{message}</Typography.Text>
<div className="form-section">
<Space.Compact style={{ width: '100%' }}>
<Input
placeholder="Enter integration name..."
style={{ width: 300, marginBottom: 0 }}
value={requestedIntegrationName}
onChange={(e): void => setRequestedIntegrationName(e.target.value)}
/>
<Button
className="periscope-btn primary"
icon={
isSubmittingRequestForIntegration ? (
<LoadingOutlined />
) : (
<Check size={12} />
)
}
type="primary"
onClick={handleRequestIntegrationSubmit}
disabled={
isSubmittingRequestForIntegration ||
!requestedIntegrationName ||
requestedIntegrationName?.trim().length === 0
}
>
Submit
</Button>
</Space.Compact>
</div>
</div>
);
}

View File

@@ -0,0 +1,3 @@
import Integrations from './Integrations';
export default Integrations;

View File

@@ -0,0 +1,17 @@
import { Typography } from 'antd';
import convertDateToAmAndPm from 'lib/convertDateToAmAndPm';
import getFormattedDate from 'lib/getFormatedDate';
import { Data } from '../DashboardsList';
function Created(createdBy: Data['createdBy']): JSX.Element {
const time = new Date(createdBy);
const date = getFormattedDate(time);
const timeString = `${date} ${convertDateToAmAndPm(time)}`;
return <Typography>{`${timeString}`}</Typography>;
}
export default Created;

View File

@@ -0,0 +1,29 @@
import { LockFilled } from '@ant-design/icons';
import ROUTES from 'constants/routes';
import history from 'lib/history';
import { openInNewTab } from 'utils/navigation';
import { Data } from '../DashboardsList';
import { TableLinkText } from './styles';
function Name(name: Data['name'], data: Data): JSX.Element {
const { id: DashboardId, isLocked } = data;
const getLink = (): string => `${ROUTES.ALL_DASHBOARD}/${DashboardId}`;
const onClickHandler = (event: React.MouseEvent<HTMLElement>): void => {
if (event.metaKey || event.ctrlKey) {
openInNewTab(getLink());
} else {
history.push(getLink());
}
};
return (
<TableLinkText onClick={onClickHandler}>
{isLocked && <LockFilled />} {name}
</TableLinkText>
);
}
export default Name;

View File

@@ -0,0 +1,15 @@
import { Tag } from 'antd';
import { Data } from '../DashboardsList';
function Tags(data: Data['tags']): JSX.Element {
return (
<>
{data.map((e) => (
<Tag key={e}>{e}</Tag>
))}
</>
);
}
export default Tags;

View File

@@ -0,0 +1,24 @@
import { Dashboard } from 'types/api/dashboard/getAll';
interface IDashboardSearchData {
title: string;
description: string | undefined;
tags: string[];
id: string;
}
export const generateSearchData = (
dashboards: Dashboard[],
): IDashboardSearchData[] => {
const dashboardSearchData: IDashboardSearchData[] = [];
dashboards.forEach((dashboard) => {
dashboardSearchData.push({
id: dashboard.id,
title: dashboard.data.title,
description: dashboard.data.description,
tags: dashboard.data.tags || [],
});
});
return dashboardSearchData;
};

View File

@@ -0,0 +1,25 @@
import { Button, Row } from 'antd';
import styled from 'styled-components';
export const NewDashboardButton = styled(Button)`
&&& {
display: flex;
justify-content: center;
align-items: center;
margin-left: 1rem;
}
`;
export const TableContainer = styled(Row)`
&&& {
margin-top: 1rem;
}
`;
export const ButtonContainer = styled.div`
&&& {
display: flex;
align-items: center;
}
`;

View File

@@ -0,0 +1,55 @@
import { useCallback } from 'react';
import { useHistory } from 'react-router-dom';
import { ArrowLeftOutlined } from '@ant-design/icons';
import { Button } from 'antd';
import { QueryParams } from 'constants/query';
import {
initialQueryBuilderFormValuesMap,
PANEL_TYPES,
} from 'constants/queryBuilder';
import ROUTES from 'constants/routes';
import { useGetCompositeQueryParam } from 'hooks/queryBuilder/useGetCompositeQueryParam';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { DataSource } from 'types/common/queryBuilder';
import { constructCompositeQuery } from '../constants';
function BackButton(): JSX.Element {
const history = useHistory();
const { updateAllQueriesOperators } = useQueryBuilder();
const compositeQuery = useGetCompositeQueryParam();
const handleBack = useCallback(() => {
if (!compositeQuery) {
return;
}
const nextCompositeQuery = constructCompositeQuery({
query: compositeQuery,
initialQueryData: initialQueryBuilderFormValuesMap.logs,
customQueryData: { disabled: false },
});
const updatedQuery = updateAllQueriesOperators(
nextCompositeQuery,
PANEL_TYPES.LIST,
DataSource.LOGS,
);
const JSONCompositeQuery = encodeURIComponent(JSON.stringify(updatedQuery));
const path = `${ROUTES.LOGS_EXPLORER}?${QueryParams.compositeQuery}=${JSONCompositeQuery}`;
history.push(path);
}, [history, compositeQuery, updateAllQueriesOperators]);
return (
<Button icon={<ArrowLeftOutlined />} onClick={handleBack}>
Exit live view
</Button>
);
}
export default BackButton;

View File

@@ -0,0 +1,80 @@
import { useCallback, useMemo } from 'react';
import { Col } from 'antd';
import { initialQueriesMap } from 'constants/queryBuilder';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useEventSource } from 'providers/EventSource';
import {
IBuilderQuery,
Query,
TagFilter,
} from 'types/api/queryBuilder/queryBuilderData';
import { getQueryWithoutFilterId } from '../utils';
import {
ContainerStyled,
FilterSearchInputStyled,
SearchButtonStyled,
} from './styles';
function FiltersInput(): JSX.Element {
const {
stagedQuery,
handleSetQueryData,
redirectWithQueryBuilderData,
currentQuery,
} = useQueryBuilder();
const { initialLoading, handleSetInitialLoading } = useEventSource();
const handleChange = useCallback(
(filters: TagFilter) => {
const listQueryData = stagedQuery?.builder.queryData[0];
if (!listQueryData) {
return;
}
const queryData: IBuilderQuery = {
...listQueryData,
filters,
};
handleSetQueryData(0, queryData);
},
[stagedQuery, handleSetQueryData],
);
const query = useMemo(() => {
if (stagedQuery && stagedQuery.builder.queryData.length > 0) {
return stagedQuery?.builder.queryData[0];
}
return initialQueriesMap.logs.builder.queryData[0];
}, [stagedQuery]);
const handleSearch = useCallback(() => {
if (initialLoading) {
handleSetInitialLoading(false);
}
const preparedQuery: Query = getQueryWithoutFilterId(currentQuery);
redirectWithQueryBuilderData(preparedQuery);
}, [
initialLoading,
currentQuery,
redirectWithQueryBuilderData,
handleSetInitialLoading,
]);
return (
<ContainerStyled>
<Col flex={1}>
<FilterSearchInputStyled query={query} onChange={handleChange} />
</Col>
<SearchButtonStyled onSearch={handleSearch} />
</ContainerStyled>
);
}
export default FiltersInput;

View File

@@ -0,0 +1,24 @@
import { Input, Row } from 'antd';
import { themeColors } from 'constants/theme';
import QueryBuilderSearch from 'container/QueryBuilder/filters/QueryBuilderSearch';
import styled from 'styled-components';
export const FilterSearchInputStyled = styled(QueryBuilderSearch)`
z-index: 1;
.ant-select-selector {
width: 100%;
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
`;
export const ContainerStyled = styled(Row)`
color: ${themeColors.white};
`;
export const SearchButtonStyled = styled(Input.Search)`
width: 2rem;
.ant-input {
display: none;
}
`;

View File

@@ -0,0 +1,77 @@
import { useCallback } from 'react';
import { Button, Popover, Select } from 'antd';
import Spinner from 'components/Spinner';
import { LOCALSTORAGE } from 'constants/localStorage';
import { useOptionsMenu } from 'container/OptionsMenu';
import {
defaultSelectStyle,
logsOptions,
viewModeOptionList,
} from 'pages/Logs/config';
import PopoverContent from 'pages/Logs/PopoverContent';
import { useEventSource } from 'providers/EventSource';
import { DataSource, StringOperators } from 'types/common/queryBuilder';
import { popupContainer } from 'utils/selectPopupContainer';
import { SpinnerWrapper } from './styles';
function ListViewPanel(): JSX.Element {
const { config } = useOptionsMenu({
storageKey: LOCALSTORAGE.LOGS_LIST_OPTIONS,
dataSource: DataSource.LOGS,
aggregateOperator: StringOperators.NOOP,
});
const { isConnectionLoading } = useEventSource();
const isFormatButtonVisible = logsOptions.includes(config.format?.value);
const renderPopoverContent = useCallback(() => {
if (!config.maxLines) {
return null;
}
const linedPerRow = config.maxLines.value as number;
const handleLinesPerRowChange = config.maxLines.onChange as (
value: unknown,
) => void;
return (
<PopoverContent
linesPerRow={linedPerRow}
handleLinesPerRowChange={handleLinesPerRowChange}
/>
);
}, [config]);
return (
<div className="live-logs-settings-panel">
<Select
getPopupContainer={popupContainer}
style={defaultSelectStyle}
value={config.format?.value}
onChange={config.format?.onChange}
>
{viewModeOptionList.map((option) => (
<Select.Option key={option.value}>{option.label}</Select.Option>
))}
</Select>
{isFormatButtonVisible && (
<Popover
getPopupContainer={popupContainer}
placement="right"
content={renderPopoverContent}
>
<Button>Format</Button>
</Popover>
)}
{isConnectionLoading && (
<SpinnerWrapper>
<Spinner style={{ height: 'auto' }} />
</SpinnerWrapper>
)}
</div>
);
}
export default ListViewPanel;

View File

@@ -0,0 +1,11 @@
import styled from 'styled-components';
export const Wrapper = styled.div`
display: flex;
align-items: center;
gap: 1.5rem;
`;
export const SpinnerWrapper = styled.div`
margin-left: auto;
`;

View File

@@ -0,0 +1,20 @@
import { Row } from 'antd';
import { themeColors } from 'constants/theme';
import styled from 'styled-components';
import LiveLogsListChart from '../LiveLogsListChart';
export const LiveLogsChart = styled(LiveLogsListChart)`
margin-bottom: 0.5rem;
`;
export const ContentWrapper = styled(Row)`
color: rgba(${(themeColors.white, 0.85)});
`;
export const Wrapper = styled.div`
padding-bottom: 4rem;
padding-top: 1rem;
padding-left: 1rem;
padding-right: 1rem;
`;

View File

@@ -0,0 +1,74 @@
import { OPERATORS } from 'constants/queryBuilder';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import {
Query,
TagFilter,
TagFilterItem,
} from 'types/api/queryBuilder/queryBuilderData';
import { v4 as uuid } from 'uuid';
const getIdFilter = (filtersItems: TagFilterItem[]): TagFilterItem | null =>
filtersItems.find((item) => item.key?.key === 'id') || null;
const getFilter = (
filters: TagFilter,
tagFilter: BaseAutocompleteData,
value: string,
): TagFilter => {
let newItems = filters.items;
const isExistIdFilter = getIdFilter(newItems);
if (isExistIdFilter) {
newItems = newItems.map((item) =>
item.key?.key === 'id' ? { ...item, value } : item,
);
} else {
newItems = [
...newItems,
{ value, key: tagFilter, op: OPERATORS['>'], id: uuid() },
];
}
return { items: newItems, op: filters.op };
};
export const prepareQueryByFilter = (
query: Query,
tagFilter: BaseAutocompleteData,
value: string | null,
): Query => {
const preparedQuery: Query = {
...query,
builder: {
...query.builder,
queryData: query.builder.queryData?.map((item) => ({
...item,
filters: value
? getFilter(item.filters || { items: [], op: 'AND' }, tagFilter, value)
: item.filters,
})),
},
};
return preparedQuery;
};
export const getQueryWithoutFilterId = (query: Query): Query => {
const preparedQuery: Query = {
...query,
builder: {
...query.builder,
queryData: query.builder.queryData?.map((item) => ({
...item,
filters: {
...item.filters,
items: item.filters?.items?.filter((item) => item.key?.key !== 'id') || [],
op: item.filters?.op || 'AND',
},
})),
},
};
return preparedQuery;
};

View File

@@ -0,0 +1,71 @@
import { memo, useCallback, useMemo } from 'react';
import { PauseCircleFilled, PlayCircleFilled } from '@ant-design/icons';
import { Space } from 'antd';
import BackButton from 'container/LiveLogs/BackButton';
import { getQueryWithoutFilterId } from 'container/LiveLogs/utils';
import LocalTopNav from 'container/LocalTopNav';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useEventSource } from 'providers/EventSource';
import { LiveButtonStyled } from './styles';
function LiveLogsTopNav(): JSX.Element {
const {
isConnectionOpen,
isConnectionLoading,
initialLoading,
handleCloseConnection,
handleSetInitialLoading,
} = useEventSource();
const { redirectWithQueryBuilderData, currentQuery } = useQueryBuilder();
const isPlaying = isConnectionOpen || isConnectionLoading || initialLoading;
const onLiveButtonClick = useCallback(() => {
if (initialLoading) {
handleSetInitialLoading(false);
}
if ((!isConnectionOpen && isConnectionLoading) || isConnectionOpen) {
handleCloseConnection();
} else {
const preparedQuery = getQueryWithoutFilterId(currentQuery);
redirectWithQueryBuilderData(preparedQuery);
}
}, [
initialLoading,
isConnectionOpen,
isConnectionLoading,
currentQuery,
handleSetInitialLoading,
handleCloseConnection,
redirectWithQueryBuilderData,
]);
const liveButton = useMemo(
() => (
<Space size={16}>
<LiveButtonStyled
icon={isPlaying ? <PauseCircleFilled /> : <PlayCircleFilled />}
danger={isPlaying}
onClick={onLiveButtonClick}
type="primary"
>
{isPlaying ? 'Pause' : 'Resume'}
</LiveButtonStyled>
<BackButton />
</Space>
),
[isPlaying, onLiveButtonClick],
);
return (
<LocalTopNav
actions={liveButton}
renderPermissions={{ isDateTimeEnabled: false }}
/>
);
}
export default memo(LiveLogsTopNav);

View File

@@ -0,0 +1,19 @@
import { Button, ButtonProps } from 'antd';
import styled, { css, FlattenSimpleInterpolation } from 'styled-components';
export const LiveButtonStyled = styled(Button)<ButtonProps>`
background-color: #1eb475;
${({ danger }): FlattenSimpleInterpolation =>
!danger
? css`
&:hover {
background-color: #1eb475 !important;
}
&:active {
background-color: #1eb475 !important;
}
`
: css``}
`;

View File

@@ -0,0 +1,50 @@
import { useLocation } from 'react-use';
import { Col, Row, Space, Typography } from 'antd';
import ROUTES from 'constants/routes';
import NewExplorerCTA from 'container/NewExplorerCTA';
import { FileText } from 'lucide-react';
import DateTimeSelector from '../TopNav/DateTimeSelectionV2';
import { Container } from './styles';
import { LocalTopNavProps } from './types';
function LocalTopNav({
actions,
renderPermissions,
}: LocalTopNavProps): JSX.Element | null {
const { pathname } = useLocation();
const isLiveLogsPage = pathname === ROUTES.LIVE_LOGS;
return (
<Container>
{isLiveLogsPage && (
<Col span={16}>
<Space>
<FileText color="#fff" size={16} />
<Typography.Title level={4} style={{ marginTop: 0, marginBottom: 0 }}>
Live Logs
</Typography.Title>
</Space>
</Col>
)}
<Col span={isLiveLogsPage ? 8 : 24}>
<Row justify="end">
<Space align="start" size={30} direction="horizontal">
<NewExplorerCTA />
{actions}
{renderPermissions?.isDateTimeEnabled && (
<div>
<DateTimeSelector showAutoRefresh={false} />
</div>
)}
</Space>
</Row>
</Col>
</Container>
);
}
export default LocalTopNav;

View File

@@ -0,0 +1,9 @@
import { Row } from 'antd';
import styled from 'styled-components';
export const Container = styled(Row)`
&&& {
margin-top: 1rem;
min-height: 8vh;
}
`;

View File

@@ -0,0 +1,6 @@
import { ReactNode } from 'react';
export type LocalTopNavProps = {
actions?: ReactNode;
renderPermissions?: { isDateTimeEnabled: boolean };
};

View File

@@ -0,0 +1,3 @@
.log-context-container {
border: 1px solid var(--l1-border);
}

View File

@@ -0,0 +1,55 @@
import RawLogView from 'components/Logs/RawLogView';
import LogsContextList from 'container/LogsContextList';
import { FontSize } from 'container/OptionsMenu/types';
import { ORDERBY_FILTERS } from 'container/QueryBuilder/filters/OrderByFilter/config';
import { ILog } from 'types/api/logs/log';
import { Query, TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import './LogContext.styles.scss';
interface LogContextProps {
log: ILog;
contextQuery: Query | undefined;
filters: TagFilter | null;
isEdit: boolean;
}
function LogContext({
log,
filters,
contextQuery,
isEdit,
}: LogContextProps): JSX.Element {
if (!contextQuery) {
return <></>;
}
return (
<div className="log-context-container">
<LogsContextList
order={ORDERBY_FILTERS.ASC}
filters={filters}
isEdit={isEdit}
log={log}
query={contextQuery}
/>
<RawLogView
isActiveLog
isReadOnly
isTextOverflowEllipsisDisabled={false}
data={log}
linesPerRow={1}
fontSize={FontSize.SMALL}
/>
<LogsContextList
order={ORDERBY_FILTERS.DESC}
filters={filters}
isEdit={isEdit}
log={log}
query={contextQuery}
/>
</div>
);
}
export default LogContext;

View File

@@ -0,0 +1,32 @@
.context-logs-list {
position: relative;
.show-more-button {
position: absolute;
opacity: 1;
z-index: 1;
cursor: pointer;
&.up {
top: -1px;
left: -1px;
}
&.down {
bottom: -1px;
left: -1px;
}
&.disabled {
cursor: not-allowed;
}
}
.virtuoso-list {
&::-webkit-scrollbar {
width: 0.3rem;
height: 0.3rem;
}
}
}

View File

@@ -0,0 +1,227 @@
import { memo, useCallback, useEffect, useMemo, useState } from 'react';
import { Virtuoso } from 'react-virtuoso';
import RawLogView from 'components/Logs/RawLogView';
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
import Spinner from 'components/Spinner';
import { DEFAULT_ENTITY_VERSION } from 'constants/app';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { FontSize } from 'container/OptionsMenu/types';
import { ORDERBY_FILTERS } from 'container/QueryBuilder/filters/OrderByFilter/config';
import { useGetExplorerQueryRange } from 'hooks/queryBuilder/useGetExplorerQueryRange';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { SuccessResponse } from 'types/api';
import { ILog } from 'types/api/logs/log';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import { Query, TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import {
getOrderByTimestamp,
INITIAL_PAGE_SIZE,
LOGS_MORE_PAGE_SIZE,
} from './configs';
import ShowButton from './ShowButton';
import { EmptyText, ListContainer } from './styles';
import { getRequestData } from './utils';
import './LogsContextList.styles.scss';
interface LogsContextListProps {
className?: string;
isEdit: boolean;
query: Query;
log: ILog;
order: string;
filters: TagFilter | null;
}
function LogsContextList({
className,
isEdit,
query,
log,
order,
filters,
}: LogsContextListProps): JSX.Element {
const isDarkMode = useIsDarkMode();
const [logs, setLogs] = useState<ILog[]>([]);
const [page, setPage] = useState<number>(1);
const firstLog = useMemo(() => logs[0], [logs]);
const lastLog = useMemo(() => logs[logs.length - 1], [logs]);
const orderByTimestamp = useMemo(() => getOrderByTimestamp(order), [order]);
const logsMorePageSize = useMemo(
() => (page - 1) * LOGS_MORE_PAGE_SIZE,
[page],
);
const pageSize = useMemo(
() => (page <= 1 ? INITIAL_PAGE_SIZE : logsMorePageSize + INITIAL_PAGE_SIZE),
[page, logsMorePageSize],
);
const isDisabledFetch = useMemo(
() => logs.length < pageSize,
[logs.length, pageSize],
);
const currentStagedQueryData = useMemo(() => {
if (!query || query.builder.queryData.length !== 1) {
return null;
}
return query.builder.queryData[0];
}, [query]);
const initialLogsRequest = useMemo(
() =>
getRequestData({
stagedQueryData: currentStagedQueryData,
query,
log,
orderByTimestamp,
page,
}),
[currentStagedQueryData, page, log, query, orderByTimestamp],
);
const [requestData, setRequestData] = useState<Query | null>(
initialLogsRequest,
);
const handleSuccess = useCallback(
(data: SuccessResponse<MetricRangePayloadProps, unknown>) => {
const currentData = data?.payload?.data?.newResult?.data?.result || [];
if (currentData.length > 0 && currentData[0].list) {
const currentLogs: ILog[] = currentData[0].list.map((item) => ({
...item.data,
timestamp: item.timestamp,
}));
if (order === ORDERBY_FILTERS.ASC) {
const reversedCurrentLogs = currentLogs.reverse();
setLogs((prevLogs) => [...reversedCurrentLogs, ...prevLogs]);
} else {
setLogs((prevLogs) => [...prevLogs, ...currentLogs]);
}
}
},
[order],
);
const { isError, isFetching } = useGetExplorerQueryRange(
requestData,
PANEL_TYPES.LIST,
DEFAULT_ENTITY_VERSION,
{
keepPreviousData: true,
enabled: !!requestData,
onSuccess: handleSuccess,
},
);
const handleShowNextLines = useCallback(() => {
if (isDisabledFetch) {
return;
}
const log = order === ORDERBY_FILTERS.ASC ? firstLog : lastLog;
const newRequestData = getRequestData({
stagedQueryData: currentStagedQueryData,
query,
log,
orderByTimestamp,
page: page + 1,
pageSize: LOGS_MORE_PAGE_SIZE,
});
setPage((prevPage) => prevPage + 1);
setRequestData(newRequestData);
}, [
query,
firstLog,
lastLog,
page,
order,
currentStagedQueryData,
isDisabledFetch,
orderByTimestamp,
]);
useEffect(() => {
if (!isEdit) {
return;
}
const newRequestData = getRequestData({
stagedQueryData: currentStagedQueryData,
query,
log,
orderByTimestamp,
page: 1,
});
setPage(1);
setLogs([]);
setRequestData(newRequestData);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [filters]);
const getItemContent = useCallback(
(_: number, log: ILog): JSX.Element => (
<RawLogView
isReadOnly
isTextOverflowEllipsisDisabled
key={log.id}
data={log}
linesPerRow={1}
fontSize={FontSize.SMALL}
/>
),
[],
);
return (
<div className={`context-logs-list ${className}`}>
{order === ORDERBY_FILTERS.ASC && (
<ShowButton
isLoading={isFetching}
isDisabled={isDisabledFetch}
order={order}
onClick={handleShowNextLines}
/>
)}
<ListContainer $isDarkMode={isDarkMode}>
{((!logs.length && !isFetching) || isError) && (
<EmptyText>No Data</EmptyText>
)}
{isFetching && <Spinner size="large" height="10rem" />}
<OverlayScrollbar isVirtuoso>
<Virtuoso
className="virtuoso-list"
initialTopMostItemIndex={0}
data={logs}
itemContent={getItemContent}
followOutput={order === ORDERBY_FILTERS.DESC}
/>
</OverlayScrollbar>
</ListContainer>
{order === ORDERBY_FILTERS.DESC && (
<ShowButton
isLoading={isFetching}
isDisabled={isDisabledFetch}
order={order}
onClick={handleShowNextLines}
/>
)}
</div>
);
}
LogsContextList.defaultProps = {
className: '',
};
export default memo(LogsContextList);

View File

@@ -0,0 +1,20 @@
import { Color } from '@signozhq/design-tokens';
import { Typography } from 'antd';
import styled from 'styled-components';
export const ListContainer = styled.div<{ $isDarkMode: boolean }>`
position: relative;
height: 21rem;
overflow: hidden;
background-color: ${({ $isDarkMode }): string =>
$isDarkMode ? Color.BG_INK_400 : Color.BG_VANILLA_100};
`;
export const EmptyText = styled(Typography)`
padding: 0 1.5rem;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
`;

View File

@@ -0,0 +1,118 @@
import { memo, useCallback, useState } from 'react';
import { EditFilled } from '@ant-design/icons';
import { Modal, Typography } from 'antd';
import RawLogView from 'components/Logs/RawLogView';
import LogsContextList from 'container/LogsContextList';
import { FontSize } from 'container/OptionsMenu/types';
import { ORDERBY_FILTERS } from 'container/QueryBuilder/filters/OrderByFilter/config';
import QueryBuilderSearch from 'container/QueryBuilder/filters/QueryBuilderSearch';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { Query, TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import { EditButton, LogContainer, TitleWrapper } from './styles';
import { LogsExplorerContextProps } from './types';
import useInitialQuery from './useInitialQuery';
function LogsExplorerContext({
log,
onClose,
}: LogsExplorerContextProps): JSX.Element | null {
const initialContextQuery = useInitialQuery(log);
const [contextQuery, setContextQuery] = useState<Query>(initialContextQuery);
const [filters, setFilters] = useState<TagFilter | null>(null);
const [isEdit, setIsEdit] = useState<boolean>(false);
const isDarkMode = useIsDarkMode();
const handleClickEditButton = useCallback(
() => setIsEdit((prevValue) => !prevValue),
[],
);
const handleSearch = useCallback(
(tagFilters: TagFilter): void => {
const tagFiltersLength = tagFilters.items.length;
if (
(!tagFiltersLength && (!filters || !filters.items.length)) ||
tagFiltersLength === filters?.items.length
) {
return;
}
const nextQuery: Query = {
...contextQuery,
builder: {
...contextQuery.builder,
queryData: contextQuery.builder.queryData.map((item) => ({
...item,
filters: tagFilters,
})),
},
};
setFilters(tagFilters);
setContextQuery(nextQuery);
},
[contextQuery, filters],
);
return (
<Modal
centered
destroyOnClose
open
width={816}
onCancel={onClose}
onOk={onClose}
footer={null}
title={
<TitleWrapper block>
<Typography>Logs Context</Typography>
<EditButton
$isDarkMode={isDarkMode}
size="small"
type="text"
icon={<EditFilled />}
onClick={handleClickEditButton}
/>
</TitleWrapper>
}
>
{isEdit && (
<QueryBuilderSearch
query={contextQuery?.builder.queryData[0]}
onChange={handleSearch}
/>
)}
<LogsContextList
order={ORDERBY_FILTERS.ASC}
filters={filters}
isEdit={isEdit}
log={log}
query={contextQuery}
/>
<LogContainer>
<RawLogView
isActiveLog
isReadOnly
isTextOverflowEllipsisDisabled
data={log}
linesPerRow={1}
fontSize={FontSize.SMALL}
/>
</LogContainer>
<LogsContextList
order={ORDERBY_FILTERS.DESC}
filters={filters}
isEdit={isEdit}
log={log}
query={contextQuery}
/>
</Modal>
);
}
export default memo(LogsExplorerContext);

View File

@@ -0,0 +1,34 @@
import { Button, Space } from 'antd';
import { themeColors } from 'constants/theme';
import styled from 'styled-components';
import getAlphaColor from 'utils/getAlphaColor';
export const TitleWrapper = styled(Space.Compact)`
justify-content: space-between;
align-items: center;
`;
export const EditButton = styled(Button)<{ $isDarkMode: boolean }>`
margin-right: 0.938rem;
width: 1.375rem !important;
height: 1.375rem;
position: absolute;
top: 1rem;
right: 1.563rem;
padding: 0;
border-radius: 0.125rem;
border-start-start-radius: 0.125rem !important;
border-end-start-radius: 0.125rem !important;
color: ${({ $isDarkMode }): string =>
$isDarkMode
? getAlphaColor(themeColors.white)[45]
: getAlphaColor(themeColors.black)[45]};
`;
export const LogContainer = styled.div`
overflow-x: auto;
`;

View File

@@ -0,0 +1,7 @@
import { MouseEventHandler } from 'react';
import { ILog } from 'types/api/logs/log';
export interface LogsExplorerContextProps {
log: ILog;
onClose: MouseEventHandler<HTMLElement>;
}

View File

@@ -0,0 +1,29 @@
import ReactDragListView from 'react-drag-listview';
import { TableComponents } from 'react-virtuoso';
import Spinner from 'components/Spinner';
import { dragColumnParams } from 'hooks/useDragColumns/configs';
import { TableStyled } from './styles';
interface LogsCustomTableProps {
isLoading?: boolean;
handleDragEnd: (fromIndex: number, toIndex: number) => void;
}
export const LogsCustomTable = ({
isLoading,
handleDragEnd,
}: LogsCustomTableProps): TableComponents['Table'] =>
function CustomTable({ style, children }): JSX.Element {
if (isLoading) {
return <Spinner height="35px" tip="Getting Logs" />;
}
return (
<ReactDragListView.DragColumn
{...dragColumnParams}
onDragEnd={handleDragEnd}
>
<TableStyled style={style}>{children}</TableStyled>
</ReactDragListView.DragColumn>
);
};

View File

@@ -0,0 +1,26 @@
.logs-table-row {
cursor: pointer;
position: relative;
}
.table-row-backdrop {
&.INFO {
background-color: var(--primary-background) 10;
}
&.WARNING,
&.WARN {
background-color: var(--warning-background) 10;
}
&.ERROR {
background-color: var(--danger-background) 10;
}
&.TRACE {
background-color: var(--success-background) 10;
}
&.DEBUG {
background-color: var(--info-background) 10;
}
&.FATAL {
background-color: var(--danger-background) 10;
}
}

View File

@@ -0,0 +1,141 @@
import {
cloneElement,
MouseEventHandler,
ReactElement,
ReactNode,
useCallback,
useMemo,
} from 'react';
import type { TableColumnsType as ColumnsType } from 'antd';
import { VIEW_TYPES } from 'components/LogDetail/constants';
import LogLinesActionButtons from 'components/Logs/LogLinesActionButtons/LogLinesActionButtons';
import { ColumnTypeRender } from 'components/Logs/TableView/types';
import { FontSize } from 'container/OptionsMenu/types';
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { ILog } from 'types/api/logs/log';
import { TableCellStyled } from './styles';
import './TableRow.styles.scss';
interface TableRowProps {
tableColumns: ColumnsType<Record<string, unknown>>;
index: number;
log: Record<string, unknown>;
onShowLogDetails?: (
log: ILog,
selectedTab?: (typeof VIEW_TYPES)[keyof typeof VIEW_TYPES],
) => void;
logs: ILog[];
hasActions: boolean;
fontSize: FontSize;
isActiveLog?: boolean;
onClearActiveLog?: () => void;
}
export default function TableRow({
tableColumns,
index,
log,
onShowLogDetails,
logs,
hasActions,
fontSize,
isActiveLog,
onClearActiveLog,
}: TableRowProps): JSX.Element {
const isDarkMode = useIsDarkMode();
const currentLog = useMemo(
() => logs.find(({ id }) => id === log.id),
[logs, log.id],
);
const { onLogCopy, isLogsExplorerPage } = useCopyLogLink(currentLog?.id);
const handleShowContext: MouseEventHandler<HTMLElement> = useCallback(
(event) => {
event.preventDefault();
event.stopPropagation();
if (!currentLog) {
return;
}
onShowLogDetails?.(currentLog, VIEW_TYPES.CONTEXT);
},
[currentLog, onShowLogDetails],
);
const handleShowLogDetails = useCallback(() => {
if (!currentLog) {
return;
}
// If this log is already active, close the detail drawer
if (isActiveLog && onClearActiveLog) {
onClearActiveLog();
return;
}
// Otherwise, open the detail drawer for this log
if (onShowLogDetails) {
onShowLogDetails(currentLog);
}
}, [currentLog, onShowLogDetails, isActiveLog, onClearActiveLog]);
const hasSingleColumn =
tableColumns.filter((column) => column.key !== 'state-indicator').length ===
1;
return (
<>
{tableColumns.map((column) => {
if (!column.key) {
return null;
}
if (!column.render) {
return <td key={column.key}>Empty</td>;
}
const element: ColumnTypeRender<Record<string, unknown>> = column.render(
log[column.key as keyof Record<string, unknown>],
log,
index,
);
const elementWithChildren = element as Exclude<
ColumnTypeRender<Record<string, unknown>>,
ReactNode
>;
const children = elementWithChildren.children as ReactElement;
const props = elementWithChildren.props as Record<string, unknown>;
return (
<TableCellStyled
$isDragColumn={false}
$isLogIndicator={column.key === 'state-indicator'}
$hasSingleColumn={hasSingleColumn}
$isDarkMode={isDarkMode}
key={column.key}
fontSize={fontSize}
columnKey={column.key as string}
onClick={handleShowLogDetails}
className={column.key as string}
>
{cloneElement(children, props)}
</TableCellStyled>
);
})}
{hasActions && isLogsExplorerPage && (
<LogLinesActionButtons
handleShowContext={handleShowContext}
onLogCopy={onLogCopy}
customClassName="table-view-log-actions"
/>
)}
</>
);
}

View File

@@ -0,0 +1,14 @@
import { CSSProperties } from 'react';
import { FontSize } from 'container/OptionsMenu/types';
export const infinityDefaultStyles: CSSProperties = {
width: '100%',
overflowX: 'scroll',
};
export function getInfinityDefaultStyles(_fontSize: FontSize): CSSProperties {
return {
width: '100%',
overflowX: 'scroll',
};
}

View File

@@ -0,0 +1,194 @@
import { forwardRef, memo, useCallback, useMemo } from 'react';
import {
TableComponents,
TableVirtuoso,
TableVirtuosoHandle,
} from 'react-virtuoso';
import { VIEW_TYPES } from 'components/LogDetail/constants';
import { getLogIndicatorType } from 'components/Logs/LogStateIndicator/utils';
import { useTableView } from 'components/Logs/TableView/useTableView';
import { LOCALSTORAGE } from 'constants/localStorage';
import { useActiveLog } from 'hooks/logs/useActiveLog';
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
import { useIsDarkMode } from 'hooks/useDarkMode';
import useDragColumns from 'hooks/useDragColumns';
import { getDraggedColumns } from 'hooks/useDragColumns/utils';
import { ILog } from 'types/api/logs/log';
import { getInfinityDefaultStyles } from './config';
import { LogsCustomTable } from './LogsCustomTable';
import { TableHeaderCellStyled, TableRowStyled } from './styles';
import TableRow from './TableRow';
import { InfinityTableProps } from './types';
interface CustomTableRowProps {
activeContextLogId: string;
activeLogId: string;
}
const CustomTableRow: TableComponents<ILog>['TableRow'] = ({
children,
context,
...props
}) => {
const { isHighlighted } = useCopyLogLink(props.item.id);
const isDarkMode = useIsDarkMode();
const logType = getLogIndicatorType(props.item);
return (
<TableRowStyled
$isDarkMode={isDarkMode}
$isActiveLog={
isHighlighted ||
(context as CustomTableRowProps).activeContextLogId === props.item.id ||
(context as CustomTableRowProps).activeLogId === props.item.id
}
$logType={logType}
{...props}
>
{children}
</TableRowStyled>
);
};
const InfinityTable = forwardRef<TableVirtuosoHandle, InfinityTableProps>(
function InfinityTableView(
{
isLoading,
tableViewProps,
infitiyTableProps,
onSetActiveLog,
onClearActiveLog,
activeLog,
},
ref,
): JSX.Element | null {
const { activeLog: activeContextLog } = useActiveLog();
const onSetActiveLogExpand = useCallback(
(log: ILog) => {
onSetActiveLog?.(log);
},
[onSetActiveLog],
);
const onSetActiveLogContext = useCallback(
(log: ILog) => {
onSetActiveLog?.(log, VIEW_TYPES.CONTEXT);
},
[onSetActiveLog],
);
const onCloseActiveLog = useCallback(() => {
onClearActiveLog?.();
}, [onClearActiveLog]);
const { dataSource, columns } = useTableView({
...tableViewProps,
onClickExpand: onSetActiveLogExpand,
onOpenLogsContext: onSetActiveLogContext,
});
const { draggedColumns, onDragColumns } = useDragColumns<
Record<string, unknown>
>(LOCALSTORAGE.LOGS_LIST_COLUMNS);
const isDarkMode = useIsDarkMode();
const tableColumns = useMemo(
() => getDraggedColumns<Record<string, unknown>>(columns, draggedColumns),
[columns, draggedColumns],
);
const handleDragEnd = useCallback(
(fromIndex: number, toIndex: number) =>
onDragColumns(tableColumns, fromIndex, toIndex),
[tableColumns, onDragColumns],
);
const itemContent = useCallback(
(index: number, log: Record<string, unknown>): JSX.Element => (
<TableRow
tableColumns={tableColumns}
index={index}
log={log}
logs={tableViewProps.logs}
hasActions
fontSize={tableViewProps.fontSize}
onShowLogDetails={onSetActiveLog}
isActiveLog={activeLog?.id === log.id}
onClearActiveLog={onCloseActiveLog}
/>
),
[
tableColumns,
onSetActiveLog,
tableViewProps.logs,
tableViewProps.fontSize,
activeLog?.id,
onCloseActiveLog,
],
);
const tableHeader = useCallback(
() => (
<tr>
{tableColumns
.filter((column) => column.key)
.map((column) => {
const isDragColumn = column.key !== 'expand';
return (
<TableHeaderCellStyled
$isLogIndicator={column.key === 'state-indicator'}
$isDarkMode={isDarkMode}
$isDragColumn={isDragColumn}
key={column.key}
fontSize={tableViewProps?.fontSize}
{...(isDragColumn && { className: `dragHandler ${column.key}` })}
columnKey={column.key as string}
>
{(column.title as string).replace(/^\w/, (c) => c.toUpperCase())}
</TableHeaderCellStyled>
);
})}
</tr>
),
[tableColumns, isDarkMode, tableViewProps?.fontSize],
);
return (
<>
<TableVirtuoso
ref={ref}
initialTopMostItemIndex={
tableViewProps.activeLogIndex !== -1 ? tableViewProps.activeLogIndex : 0
}
style={getInfinityDefaultStyles(tableViewProps.fontSize)}
data={dataSource}
components={{
Table: LogsCustomTable({ isLoading, handleDragEnd }),
// TODO: fix it in the future
TableRow: (props): any =>
CustomTableRow({
...props,
context: {
activeContextLogId: activeContextLog?.id,
activeLogId: activeLog?.id,
},
} as any),
}}
itemContent={itemContent}
fixedHeaderContent={tableHeader}
totalCount={dataSource.length}
{...(infitiyTableProps?.onEndReached
? { endReached: infitiyTableProps.onEndReached }
: {})}
/>
</>
);
},
);
export default memo(InfinityTable);

View File

@@ -0,0 +1,212 @@
import { themeColors } from 'constants/theme';
import { FontSize } from 'container/OptionsMenu/types';
import styled from 'styled-components';
import { getActiveLogBackground } from 'utils/logs';
interface TableHeaderCellStyledProps {
$isDragColumn: boolean;
$isDarkMode: boolean;
$isLogIndicator?: boolean;
$hasSingleColumn?: boolean;
fontSize?: FontSize;
columnKey?: string;
}
export const TableStyled = styled.table`
width: 100%;
border-collapse: separate;
border-spacing: 0;
`;
/**
* TanStack column sizing uses table-layout:fixed + colgroup widths; without clipping,
* cell content overflows visually on top of neighbouring columns (overlap / "ghost" text).
*/
export const TanStackTableStyled = styled(TableStyled)`
table-layout: fixed;
width: 100%;
min-width: 100%;
max-width: 100%;
& td,
& th {
overflow: hidden;
min-width: 0;
box-sizing: border-box;
vertical-align: middle;
}
& td.table-actions-cell {
overflow: visible;
}
& td.body {
word-break: break-word;
overflow-wrap: anywhere;
}
/* Let nested body HTML / line-clamp shrink inside fixed columns */
& td.body > * {
min-width: 0;
max-width: 100%;
}
/* Long column titles: ellipsis when wider than the column (TanStackHeaderRow) */
& thead th .tanstack-header-title {
min-width: 0;
flex: 1 1 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
& thead th .tanstack-header-title > * {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 100%;
}
& td.logs-table-filler-cell,
& th.logs-table-filler-header {
padding: 0 !important;
min-width: 0;
border-left: none;
}
& th.logs-table-actions-header {
position: sticky;
right: 0;
z-index: 2;
width: 0 !important;
min-width: 0 !important;
max-width: 0 !important;
padding: 0 !important;
overflow: visible;
white-space: nowrap;
border-left: none;
}
`;
const getTimestampColumnWidth = (
columnKey?: string,
$hasSingleColumn?: boolean,
): string =>
columnKey === 'timestamp'
? $hasSingleColumn
? 'width: 100%;'
: 'width: 10%;'
: '';
export const TableCellStyled = styled.td<TableHeaderCellStyledProps>`
padding: 0.5rem;
${({ fontSize }): string =>
fontSize === FontSize.SMALL
? `padding:0.3rem;`
: fontSize === FontSize.MEDIUM
? `padding:0.4rem;`
: fontSize === FontSize.LARGE
? `padding:0.5rem;`
: ``}
background-color: ${(props): string =>
props.$isDarkMode ? 'inherit' : themeColors.whiteCream};
${({ $isLogIndicator }): string =>
$isLogIndicator ? 'padding: 0 0 0 8px;width: 1%;' : ''}
color: ${(props): string =>
props.$isDarkMode ? themeColors.white : themeColors.bckgGrey};
${({ columnKey, $hasSingleColumn }): string =>
getTimestampColumnWidth(columnKey, $hasSingleColumn)}
&.table-actions-cell {
position: sticky;
right: 0;
z-index: 2;
width: 0;
min-width: 0;
max-width: 0;
padding: 0 !important;
white-space: nowrap;
overflow: visible;
background-color: inherit;
}
`;
export const TableRowStyled = styled.tr<{
$isActiveLog: boolean;
$isDarkMode: boolean;
$logType: string;
}>`
td {
${({ $isActiveLog, $isDarkMode, $logType }): string =>
$isActiveLog
? getActiveLogBackground($isActiveLog, $isDarkMode, $logType)
: ''};
}
cursor: pointer;
position: relative;
.log-line-action-buttons {
display: flex;
opacity: 0;
pointer-events: none;
transition: opacity 80ms linear;
}
&:hover {
${TableCellStyled} {
${({ $isDarkMode, $logType }): string =>
getActiveLogBackground(true, $isDarkMode, $logType)}
}
.log-line-action-buttons {
opacity: 1;
pointer-events: auto;
}
}
${({ $isActiveLog }): string =>
$isActiveLog
? `
.log-line-action-buttons {
opacity: 1;
pointer-events: auto;
}
`
: ''}
`;
export const TableHeaderCellStyled = styled.th<TableHeaderCellStyledProps>`
padding: 0.5rem;
height: 36px;
text-align: left;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 18px;
letter-spacing: -0.07px;
background: ${(props): string => (props.$isDarkMode ? '#0b0c0d' : '#fdfdfd')};
${({ $isDragColumn }): string => ($isDragColumn ? 'cursor: grab;' : '')}
${({ fontSize }): string =>
fontSize === FontSize.SMALL
? `font-size:11px; line-height:16px; padding: 0.1rem;`
: fontSize === FontSize.MEDIUM
? `font-size:13px; line-height:20px; padding:0.3rem;`
: fontSize === FontSize.LARGE
? `font-size:14px; line-height:24px; padding: 0.5rem;`
: ``};
${({ $isLogIndicator }): string =>
$isLogIndicator ? 'padding: 0px; width: 1%;' : ''}
border-top: 1px solid var(--l2-border);
border-bottom: 1px solid var(--l2-border);
box-shadow: inset 0 -1px 0 var(--l2-border);
&:first-child {
border-left: 1px solid var(--l2-border);
}
color: ${(props): string =>
props.$isDarkMode ? 'var(--bg-vanilla-100, #fff)' : themeColors.bckgGrey};
${({ columnKey, $hasSingleColumn }): string =>
getTimestampColumnWidth(columnKey, $hasSingleColumn)}
`;

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