feat: improve empty hosts, incorrect metrics and no filter views (#6530)

* feat: improve empty hosts, incorrect metrics and no filter views

* feat: add infra monitoring - host lists - usage events (#6536)

* feat: add usasge events

* feat: add reqrest early access events

---------

Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com>
This commit is contained in:
Yunus M 2024-11-27 16:00:08 +05:30 committed by GitHub
parent 486632b64e
commit 2bfd31841e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 445 additions and 70 deletions

View File

@ -2,6 +2,7 @@
"containers_visualization_message": "The ability to visualise containers is in active development and should be available to you soon.",
"processes_visualization_message": "The ability to visualise processes is in active development and should be available to you soon.",
"working_message": "We're working to extend infrastructure monitoring to take care of a bunch of different cases. Thank you for your patience.",
"waitlist_message": "Join the waitlist for early access or contact support.",
"waitlist_message": "Join the waitlist for early access.",
"waitlist_success_message": "We have received your request for early access. We will get back to you as soon as we launch the feature.",
"contact_support": "Contact Support"
}

View File

@ -2,6 +2,7 @@
"containers_visualization_message": "The ability to visualise containers is in active development and should be available to you soon.",
"processes_visualization_message": "The ability to visualise processes is in active development and should be available to you soon.",
"working_message": "We're working to extend infrastructure monitoring to take care of a bunch of different cases. Thank you for your patience.",
"waitlist_message": "Join the waitlist for early access or contact support.",
"waitlist_message": "Join the waitlist for early access.",
"waitlist_success_message": "We have received your request for early access. We will get back to you as soon as we launch the feature.",
"contact_support": "Contact Support"
}

View File

@ -48,6 +48,8 @@ export interface HostListResponse {
records: HostData[];
groups: null;
total: number;
sentAnyHostMetricsData: boolean;
isSendingK8SAgentMetrics: boolean;
};
}

View File

@ -1,7 +1,24 @@
.host-containers {
max-width: 600px;
margin: 150px auto;
padding: 0 16px;
gap: 24px;
height: 60vh;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
margin: 0 auto;
box-sizing: border-box;
.infra-container-card-container {
display: flex;
flex-direction: column;
gap: 24px;
}
.dev-status-container {
display: flex;
flex-direction: column;
gap: 12px;
}
.infra-container-card {
display: flex;
@ -17,6 +34,7 @@
width: 400px;
font-family: 'Inter';
margin-top: 12px;
font-weight: 300;
}
.infra-container-working-msg {

View File

@ -3,6 +3,8 @@ import './Containers.styles.scss';
import { Space, Typography } from 'antd';
import { useTranslation } from 'react-i18next';
import WaitlistFragment from '../WaitlistFragment/WaitlistFragment';
const { Text } = Typography;
function Containers(): JSX.Element {
@ -10,24 +12,30 @@ function Containers(): JSX.Element {
return (
<Space direction="vertical" className="host-containers" size={24}>
<div className="infra-container-card">
<img
src="/Icons/infraContainers.svg"
alt="infra-container"
width={32}
height={32}
/>
<div className="infra-container-card-container">
<div className="dev-status-container">
<div className="infra-container-card">
<img
src="/Icons/infraContainers.svg"
alt="infra-container"
width={32}
height={32}
/>
<Text className="infra-container-card-text">
{t('containers_visualization_message')}
</Text>
</div>
<Text className="infra-container-card-text">
{t('containers_visualization_message')}
</Text>
</div>
<div className="infra-container-working-msg">
<Space>
<img src="/Icons/broom.svg" alt="broom" width={16} height={16} />
<Text className="infra-container-card-text">{t('working_message')}</Text>
</Space>
<div className="infra-container-working-msg">
<Space>
<img src="/Icons/broom.svg" alt="broom" width={24} height={24} />
<Text className="infra-container-card-text">{t('working_message')}</Text>
</Space>
</div>
</div>
<WaitlistFragment entityType="containers" />
</div>
</Space>
);

View File

@ -11,6 +11,7 @@ import {
Typography,
} from 'antd';
import { RadioChangeEvent } from 'antd/lib';
import logEvent from 'api/common/logEvent';
import { QueryParams } from 'constants/query';
import {
initialQueryBuilderFormValuesMap,
@ -118,6 +119,13 @@ function HostMetricsDetails({
initialFilters,
);
useEffect(() => {
logEvent('Infra Monitoring: Hosts list details page visited', {
host: host?.hostName,
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
setLogFilters(initialFilters);
setTracesFilters(initialFilters);
@ -143,6 +151,7 @@ function HostMetricsDetails({
const handleTimeChange = useCallback(
(interval: Time | CustomTimeType, dateTimeRange?: [number, number]): void => {
setSelectedInterval(interval as Time);
if (interval === 'custom' && dateTimeRange) {
setModalTimeRange({
startTime: Math.floor(dateTimeRange[0] / 1000),
@ -156,7 +165,13 @@ function HostMetricsDetails({
endTime: Math.floor(maxTime / 1000000000),
});
}
logEvent('Infra Monitoring: Hosts list details time updated', {
host: host?.hostName,
interval,
});
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[],
);
@ -171,6 +186,10 @@ function HostMetricsDetails({
(item) => item.key?.key !== 'id' && item.key?.key !== 'host.name',
);
logEvent('Infra Monitoring: Hosts list details logs filters applied', {
host: host?.hostName,
});
return {
op: 'AND',
items: [
@ -181,6 +200,7 @@ function HostMetricsDetails({
};
});
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[],
);
@ -190,6 +210,11 @@ function HostMetricsDetails({
const hostNameFilter = prevFilters.items.find(
(item) => item.key?.key === 'host.name',
);
logEvent('Infra Monitoring: Hosts list details traces filters applied', {
host: host?.hostName,
});
return {
op: 'AND',
items: [
@ -199,6 +224,7 @@ function HostMetricsDetails({
};
});
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[],
);
@ -211,6 +237,11 @@ function HostMetricsDetails({
urlQuery.set(QueryParams.endTime, modalTimeRange.endTime.toString());
}
logEvent('Infra Monitoring: Hosts list details explore clicked', {
host: host?.hostName,
view: selectedView,
});
if (selectedView === VIEW_TYPES.LOGS) {
const filtersWithoutPagination = {
...logFilters,

View File

@ -1,7 +1,24 @@
.host-processes {
max-width: 600px;
margin: 150px auto;
padding: 0 16px;
gap: 24px;
height: 60vh;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
margin: 0 auto;
box-sizing: border-box;
.infra-container-card-container {
display: flex;
flex-direction: column;
gap: 24px;
}
.dev-status-container {
display: flex;
flex-direction: column;
gap: 12px;
}
.infra-container-card {
display: flex;
@ -17,6 +34,7 @@
width: 400px;
font-family: 'Inter';
margin-top: 12px;
font-weight: 300;
}
.infra-container-working-msg {

View File

@ -3,6 +3,8 @@ import './Processes.styles.scss';
import { Space, Typography } from 'antd';
import { useTranslation } from 'react-i18next';
import WaitlistFragment from '../WaitlistFragment/WaitlistFragment';
const { Text } = Typography;
function Processes(): JSX.Element {
@ -10,23 +12,29 @@ function Processes(): JSX.Element {
return (
<Space direction="vertical" className="host-processes" size={24}>
<div className="infra-container-card">
<img
src="/Icons/infraContainers.svg"
alt="infra-container"
width={32}
height={32}
/>
<Text className="infra-container-card-text">
{t('processes_visualization_message')}
</Text>
</div>
<div className="infra-container-card-container">
<div className="dev-status-container">
<div className="infra-container-card">
<img
src="/Icons/infraContainers.svg"
alt="infra-container"
width={32}
height={32}
/>
<Text className="infra-container-card-text">
{t('processes_visualization_message')}
</Text>
</div>
<div className="infra-container-working-msg">
<Space>
<img src="/Icons/broom.svg" alt="broom" width={16} height={16} />
<Text className="infra-container-card-text">{t('working_message')}</Text>
</Space>
<div className="infra-container-working-msg">
<Space>
<img src="/Icons/broom.svg" alt="broom" width={24} height={24} />
<Text className="infra-container-card-text">{t('working_message')}</Text>
</Space>
</div>
</div>
<WaitlistFragment entityType="processes" />
</div>
</Space>
);

View File

@ -0,0 +1,15 @@
.wait-list-container {
display: flex;
flex-direction: column;
gap: 8px;
.wait-list-text {
font-weight: 300;
}
.join-waitlist-btn {
width: 160px;
border-radius: 2px;
background: var(--slate-500);
}
}

View File

@ -0,0 +1,75 @@
import './WaitListFragment.styles.scss';
import { Color } from '@signozhq/design-tokens';
import { Button, Typography } from 'antd';
import logEvent from 'api/common/logEvent';
import { useNotifications } from 'hooks/useNotifications';
import { CheckCircle2, HandPlatter } from 'lucide-react';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import AppReducer from 'types/reducer/app';
export default function WaitlistFragment({
entityType,
}: {
entityType: string;
}): JSX.Element {
const { user } = useSelector<AppState, AppReducer>((state) => state.app);
const { t } = useTranslation(['infraMonitoring']);
const { notifications } = useNotifications();
const [isSubmitting, setIsSubmitting] = useState(false);
const [isSuccess, setIsSuccess] = useState(false);
const handleJoinWaitlist = (): void => {
if (!user || !user.email) return;
setIsSubmitting(true);
logEvent('Infra Monitoring: Get Early Access Clicked', {
entity_type: entityType,
userEmail: user.email,
})
.then(() => {
notifications.success({
message: t('waitlist_success_message'),
});
setIsSubmitting(false);
setIsSuccess(true);
setTimeout(() => {
setIsSuccess(false);
}, 4000);
})
.catch((error) => {
console.error('Error logging event:', error);
});
};
return (
<div className="wait-list-container">
<Typography.Text className="wait-list-text">
{t('waitlist_message')}
</Typography.Text>
<Button
className="periscope-btn join-waitlist-btn"
type="default"
loading={isSubmitting}
icon={
isSuccess ? (
<CheckCircle2 size={16} color={Color.BG_FOREST_500} />
) : (
<HandPlatter size={16} />
)
}
onClick={handleJoinWaitlist}
>
Get early access
</Button>
</div>
);
}

View File

@ -0,0 +1,52 @@
import { Typography } from 'antd';
export default function HostsEmptyOrIncorrectMetrics({
noData,
incorrectData,
}: {
noData: boolean;
incorrectData: boolean;
}): JSX.Element {
return (
<div className="hosts-empty-state-container">
<div className="hosts-empty-state-container-content">
<img className="eyes-emoji" src="/Images/eyesEmoji.svg" alt="eyes emoji" />
{noData && (
<div className="no-hosts-message">
<Typography.Title level={5} className="no-hosts-message-title">
No host metrics data received yet.
</Typography.Title>
<Typography.Text className="no-hosts-message-text">
Infrastructure monitoring requires the{' '}
<a
href="https://github.com/open-telemetry/semantic-conventions/blob/main/docs/system/system-metrics.md"
target="_blank"
rel="noreferrer"
>
OpenTelemetry system metrics
</a>
. Please refer to{' '}
<a
href="https://signoz.io/docs/userguide/hostmetrics"
target="_blank"
rel="noreferrer"
>
this
</a>{' '}
to learn how to send host metrics to SigNoz.
</Typography.Text>
</div>
)}
{incorrectData && (
<Typography.Text className="incorrect-metrics-message">
To see host metrics, upgrade to the latest version of SigNoz k8s-infra
chart. Please contact support if you need help.
</Typography.Text>
)}
</div>
</div>
);
}

View File

@ -2,6 +2,7 @@ import './InfraMonitoring.styles.scss';
import { LoadingOutlined } from '@ant-design/icons';
import {
Skeleton,
Spin,
Table,
TablePaginationConfig,
@ -9,17 +10,17 @@ import {
Typography,
} from 'antd';
import { SorterResult } from 'antd/es/table/interface';
import logEvent from 'api/common/logEvent';
import { HostListPayload } from 'api/infraMonitoring/getHostLists';
import HostMetricDetail from 'components/HostMetricsDetail';
import NoLogs from 'container/NoLogs/NoLogs';
import { useGetHostList } from 'hooks/infraMonitoring/useGetHostList';
import { useCallback, useMemo, useState } from 'react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import { GlobalReducer } from 'types/reducer/globalTime';
import HostsEmptyOrIncorrectMetrics from './HostsEmptyOrIncorrectMetrics';
import HostsListControls from './HostsListControls';
import {
formatDataForTable,
@ -28,6 +29,7 @@ import {
HostRowData,
} from './utils';
// eslint-disable-next-line sonarjs/cognitive-complexity
function HostsList(): JSX.Element {
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
@ -69,6 +71,16 @@ function HostsList(): JSX.Element {
},
);
const sentAnyHostMetricsData = useMemo(
() => data?.payload?.data?.sentAnyHostMetricsData || false,
[data],
);
const isSendingIncorrectK8SAgentMetrics = useMemo(
() => data?.payload?.data?.isSendingK8SAgentMetrics || false,
[data],
);
const hostMetricsData = useMemo(() => data?.payload?.data?.records || [], [
data,
]);
@ -81,9 +93,6 @@ function HostsList(): JSX.Element {
const columns = useMemo(() => getHostsListColumns(), []);
const isDataPresent =
!isLoading && !isFetching && !isError && hostMetricsData.length === 0;
const handleTableChange: TableProps<HostRowData>['onChange'] = useCallback(
(
pagination: TablePaginationConfig,
@ -112,11 +121,19 @@ function HostsList(): JSX.Element {
if (isNewFilterAdded) {
setFilters(value);
setCurrentPage(1);
logEvent('Infra Monitoring: Hosts list filters applied', {
filters: value,
});
}
},
[filters],
);
useEffect(() => {
logEvent('Infra Monitoring: Hosts list page visited', {});
}, []);
const selectedHostData = useMemo(() => {
if (!selectedHostName) return null;
return (
@ -126,29 +143,85 @@ function HostsList(): JSX.Element {
const handleRowClick = (record: HostRowData): void => {
setSelectedHostName(record.hostName);
logEvent('Infra Monitoring: Hosts list item clicked', {
host: record.hostName,
});
};
const handleCloseHostDetail = (): void => {
setSelectedHostName(null);
};
const showHostsTable =
!isError &&
sentAnyHostMetricsData &&
!isSendingIncorrectK8SAgentMetrics &&
!(formattedHostMetricsData.length === 0 && filters.items.length > 0);
const showNoFilteredHostsMessage =
!isFetching &&
!isLoading &&
formattedHostMetricsData.length === 0 &&
filters.items.length > 0;
const showHostsEmptyState =
!isFetching &&
!isLoading &&
(!sentAnyHostMetricsData || isSendingIncorrectK8SAgentMetrics);
return (
<div className="hosts-list">
<HostsListControls handleFiltersChange={handleFiltersChange} />
{isError && <Typography>{data?.error || 'Something went wrong'}</Typography>}
{isDataPresent && filters.items.length === 0 && (
<NoLogs dataSource={DataSource.METRICS} />
{showHostsEmptyState && (
<HostsEmptyOrIncorrectMetrics
noData={!sentAnyHostMetricsData}
incorrectData={isSendingIncorrectK8SAgentMetrics}
/>
)}
{!isFetching &&
!isLoading &&
formattedHostMetricsData.length === 0 &&
filters.items.length > 0 && (
<div className="no-hosts-message">No hosts match the applied filters.</div>
)}
{showNoFilteredHostsMessage && (
<div className="no-filtered-hosts-message-container">
<div className="no-filtered-hosts-message-content">
<img
src="/Icons/emptyState.svg"
alt="thinking-emoji"
className="empty-state-svg"
/>
{!isError && (
<Typography.Text className="no-filtered-hosts-message">
This query had no results. Edit your query and try again!
</Typography.Text>
</div>
</div>
)}
{(isFetching || isLoading) && (
<div className="hosts-list-loading-state">
<Skeleton.Input
className="hosts-list-loading-state-item"
size="large"
block
active
/>
<Skeleton.Input
className="hosts-list-loading-state-item"
size="large"
block
active
/>
<Skeleton.Input
className="hosts-list-loading-state-item"
size="large"
block
active
/>
</div>
)}
{showHostsTable && (
<Table
className="hosts-list-table"
dataSource={isFetching || isLoading ? [] : formattedHostMetricsData}

View File

@ -10,10 +10,6 @@
margin-bottom: 16px;
}
.no-hosts-message {
padding: 16px;
}
.hosts-list-controls {
padding: 8px;
@ -210,6 +206,68 @@
}
}
.hosts-list-loading-state {
padding: 8px;
display: flex;
flex-direction: column;
gap: 2px;
.hosts-list-loading-state-item {
height: 48px;
width: 100%;
}
}
.no-filtered-hosts-message-container {
height: 30vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
.no-filtered-hosts-message-content {
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: center;
width: fit-content;
padding: 24px;
}
.no-filtered-hosts-message {
margin-top: 8px;
}
}
.hosts-empty-state-container {
padding: 16px;
height: 40vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
.hosts-empty-state-container-content {
padding: 16px;
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: center;
width: fit-content;
.no-hosts-message {
margin-bottom: 16px;
.no-hosts-message-title {
margin-top: 8px;
margin-bottom: 4px;
}
}
}
}
.lightMode {
.infra-monitoring-container {
.ant-table-thead > tr > th {

View File

@ -16,7 +16,7 @@ export default function NavItem({
isActive: boolean;
onClick: (event: React.MouseEvent<HTMLDivElement, MouseEvent>) => void;
}): JSX.Element {
const { label, icon, isBeta } = item;
const { label, icon, isBeta, isNew } = item;
return (
<div
@ -36,6 +36,14 @@ export default function NavItem({
</Tag>
</div>
)}
{isNew && (
<div className="nav-item-new">
<Tag bordered={false} className="sidenav-new-tag">
New
</Tag>
</div>
)}
</div>
</div>
);

View File

@ -184,10 +184,16 @@
display: none;
}
.nav-item-beta {
.nav-item-beta,
.nav-item-new {
display: none;
}
.sidenav-new-tag {
background-color: rgba(37, 225, 146, 0.1);
color: var(--text-forest-500);
}
&:hover {
flex: 0 0 240px;
max-width: 240px;
@ -221,7 +227,8 @@
display: block;
}
.nav-item-beta {
.nav-item-beta,
.nav-item-new {
display: block;
}
}

View File

@ -3,6 +3,7 @@ import ROUTES from 'constants/routes';
import {
BarChart2,
BellDot,
Boxes,
BugIcon,
Cloudy,
DraftingCompass,
@ -11,7 +12,6 @@ import {
LayoutGrid,
ListMinus,
MessageSquare,
PackagePlus,
Receipt,
Route,
ScrollText,
@ -82,6 +82,12 @@ const menuItems: SidebarItem[] = [
label: 'Logs',
icon: <ScrollText size={16} />,
},
{
key: ROUTES.INFRASTRUCTURE_MONITORING_HOSTS,
label: 'Infra Monitoring',
icon: <Boxes size={16} />,
isNew: true,
},
{
key: ROUTES.ALL_DASHBOARD,
label: 'Dashboards',
@ -91,7 +97,6 @@ const menuItems: SidebarItem[] = [
key: ROUTES.MESSAGING_QUEUES,
label: 'Messaging Queues',
icon: <ListMinus size={16} />,
isBeta: true,
},
{
key: ROUTES.LIST_ALL_ALERT,
@ -119,12 +124,6 @@ const menuItems: SidebarItem[] = [
label: 'Billing',
icon: <Receipt size={16} />,
},
{
key: ROUTES.INFRASTRUCTURE_MONITORING_HOSTS,
label: 'Infra Monitoring',
icon: <PackagePlus size={16} />,
isBeta: true,
},
{
key: ROUTES.SETTINGS,
label: 'Settings',

View File

@ -13,6 +13,7 @@ export interface SidebarItem {
key: string | number;
label?: ReactNode;
isBeta?: boolean;
isNew?: boolean;
}
export enum SecondaryMenuItemKey {