Skip to content

Commit

Permalink
PMM-12378 Cluster view (#683)
Browse files Browse the repository at this point in the history
* PMM-12378 Extract services table

* PMM-12378 Add basic cluster view

* PMM-12378 Add edit option back in

* PMM-12310 Handle service selection correctly

* PMM-12378 Extend services table options

* PMM-12378 Expand cluster if filtered & reset on close

* PMM-12378 Add tech preview text

* PMM-12378 Revert typings change

* PMM-12378 Fix link from nodes

* PMM-12378 Clear toggle when clicking on services link
  • Loading branch information
matejkubinec authored Sep 19, 2023
1 parent af0add9 commit 4a59c23
Show file tree
Hide file tree
Showing 11 changed files with 459 additions and 215 deletions.
1 change: 1 addition & 0 deletions public/app/percona/inventory/Inventory.constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,4 @@ export const inventoryTypes = {
export const GET_SERVICES_CANCEL_TOKEN = 'getServices';
export const GET_NODES_CANCEL_TOKEN = 'getNodes';
export const GET_AGENTS_CANCEL_TOKEN = 'getAgents';
export const CLUSTERS_SWITCH_KEY = 'pmm-organize-by-clusters';
2 changes: 2 additions & 0 deletions public/app/percona/inventory/Inventory.messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ export const Messages = {
`Are you sure that you want to permanently delete ${nrItems} service${nrItems ? 's' : ''}`,
servicesDeleted: (deletedItems: number, totalItems: number) =>
`${deletedItems} of ${totalItems} services successfully deleted`,
organizeByClusters: 'Organize by Clusters',
technicalPreview: '(Technical Preview) ',
},
agents: {
goBackToServices: 'Go back to services',
Expand Down
15 changes: 10 additions & 5 deletions public/app/percona/inventory/Tabs/Nodes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import { useAppDispatch } from 'app/store/store';
import { useSelector } from 'app/types';

import { appEvents } from '../../../core/app_events';
import { GET_NODES_CANCEL_TOKEN } from '../Inventory.constants';
import { CLUSTERS_SWITCH_KEY, GET_NODES_CANCEL_TOKEN } from '../Inventory.constants';
import { Messages } from '../Inventory.messages';
import { FlattenNode, MonitoringStatus, Node } from '../Inventory.types';
import { StatusBadge } from '../components/StatusBadge/StatusBadge';
Expand Down Expand Up @@ -65,6 +65,11 @@ export const NodesTab = () => {
[styles.actionItemTxtSpan]
);

const clearClusterToggle = useCallback(() => {
// Reset toggle to false when linking from nodes
localStorage.removeItem(CLUSTERS_SWITCH_KEY);
}, []);

const columns = useMemo(
(): Array<ExtendedColumn<Node>> => [
{
Expand Down Expand Up @@ -155,7 +160,7 @@ export const NodesTab = () => {

if (value.length === 1) {
return (
<Link className={styles.link} href={getServiceLink(value[0].serviceId)}>
<Link className={styles.link} href={getServiceLink(value[0].serviceId)} onClick={clearClusterToggle}>
{value[0].serviceName}
</Link>
);
Expand All @@ -166,7 +171,7 @@ export const NodesTab = () => {
},
getExpandAndActionsCol(getActions),
],
[styles, getActions]
[styles, getActions, clearClusterToggle]
);

const loadData = useCallback(async () => {
Expand Down Expand Up @@ -206,7 +211,7 @@ export const NodesTab = () => {
<DetailsRow.Contents title={Messages.nodes.details.serviceNames}>
{row.original.services.map((service) => (
<div key={service.serviceId}>
<Link className={styles.link} href={getServiceLink(service.serviceId)}>
<Link className={styles.link} href={getServiceLink(service.serviceId)} onClick={clearClusterToggle}>
{service.serviceName}
</Link>
</div>
Expand Down Expand Up @@ -234,7 +239,7 @@ export const NodesTab = () => {
</DetailsRow>
);
},
[styles.tagList, styles.link]
[styles.tagList, styles.link, clearClusterToggle]
);

const deletionMsg = useMemo(() => {
Expand Down
253 changes: 45 additions & 208 deletions public/app/percona/inventory/Tabs/Services.tsx
Original file line number Diff line number Diff line change
@@ -1,44 +1,31 @@
/* eslint-disable @typescript-eslint/consistent-type-assertions,@typescript-eslint/no-explicit-any */
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { Row } from 'react-table';
import { useLocalStorage } from 'react-use';

import { locationService } from '@grafana/runtime';
import { Badge, Button, HorizontalGroup, Icon, Link, TagList, useStyles2 } from '@grafana/ui';
import { Button, HorizontalGroup, Icon, InlineSwitch, Tooltip, useStyles2 } from '@grafana/ui';
import { OldPage } from 'app/core/components/Page/Page';
import { stripServiceId } from 'app/percona/check/components/FailedChecksTab/FailedChecksTab.utils';
import { Action } from 'app/percona/dbaas/components/MultipleActions';
import { DetailsRow } from 'app/percona/shared/components/Elements/DetailsRow/DetailsRow';
import { FeatureLoader } from 'app/percona/shared/components/Elements/FeatureLoader';
import { ServiceIconWithText } from 'app/percona/shared/components/Elements/ServiceIconWithText/ServiceIconWithText';
import { ExtendedColumn, FilterFieldTypes, Table } from 'app/percona/shared/components/Elements/Table';
import { ReadMoreLink } from 'app/percona/shared/components/Elements/TechnicalPreview/TechnicalPreview';
import { useCancelToken } from 'app/percona/shared/components/hooks/cancelToken.hook';
import { usePerconaNavModel } from 'app/percona/shared/components/hooks/perconaNavModel';
import { fetchActiveServiceTypesAction, fetchServicesAction } from 'app/percona/shared/core/reducers/services';
import { getServices } from 'app/percona/shared/core/selectors';
import { isApiCancelError } from 'app/percona/shared/helpers/api';
import { getDashboardLinkForService } from 'app/percona/shared/helpers/getDashboardLinkForService';
import { getExpandAndActionsCol } from 'app/percona/shared/helpers/getExpandAndActionsCol';
import { logger } from 'app/percona/shared/helpers/logger';
import { ServiceStatus } from 'app/percona/shared/services/services/Services.types';
import { useAppDispatch } from 'app/store/store';
import { useSelector } from 'app/types';

import { GET_SERVICES_CANCEL_TOKEN } from '../Inventory.constants';
import { CLUSTERS_SWITCH_KEY, GET_SERVICES_CANCEL_TOKEN } from '../Inventory.constants';
import { Messages } from '../Inventory.messages';
import { FlattenService, MonitoringStatus } from '../Inventory.types';
import { FlattenService } from '../Inventory.types';
import DeleteServiceModal from '../components/DeleteServiceModal';
import DeleteServicesModal from '../components/DeleteServicesModal';
import { StatusBadge } from '../components/StatusBadge/StatusBadge';
import { StatusInfo } from '../components/StatusInfo/StatusInfo';
import { StatusLink } from '../components/StatusLink/StatusLink';

import {
getBadgeColorForServiceStatus,
getBadgeIconForServiceStatus,
getBadgeTextForServiceStatus,
getAgentsMonitoringStatus,
getNodeLink,
} from './Services.utils';
import { getAgentsMonitoringStatus } from './Services.utils';
import Clusters from './Services/Clusters';
import ServicesTable from './Services/ServicesTable';
import { getStyles } from './Tabs.styles';

export const Services = () => {
Expand All @@ -56,146 +43,13 @@ export const Services = () => {
return {
type: value.type,
...value.params,
cluster: value.params.cluster || value.params.customLabels?.cluster || '',
agentsStatus: getAgentsMonitoringStatus(value.params.agents ?? []),
};
}),
[fetchedServices]
);

const getActions = useCallback(
(row: Row<FlattenService>): Action[] => [
{
content: (
<HorizontalGroup spacing="sm">
<Icon name="trash-alt" />
<span className={styles.actionItemTxtSpan}>{Messages.delete}</span>
</HorizontalGroup>
),
action: () => {
setActionItem(row.original);
setModalVisible(true);
},
},
{
content: (
<HorizontalGroup spacing="sm">
<Icon name="pen" />
<span className={styles.actionItemTxtSpan}>{Messages.edit}</span>
</HorizontalGroup>
),
action: () => {
const serviceId = row.original.serviceId.split('/').pop();
locationService.push(`/edit-instance/${serviceId}`);
},
},
{
content: Messages.services.actions.dashboard,
action: () => {
locationService.push(getDashboardLinkForService(row.original.type, row.original.serviceName));
},
},
{
content: Messages.services.actions.qan,
action: () => {
locationService.push(`/d/pmm-qan/pmm-query-analytics?var-service_name=${row.original.serviceName}`);
},
},
],
[styles.actionItemTxtSpan]
);

const columns = useMemo(
(): Array<ExtendedColumn<FlattenService>> => [
{
Header: Messages.services.columns.serviceId,
id: 'serviceId',
accessor: 'serviceId',
hidden: true,
type: FilterFieldTypes.TEXT,
},
{
Header: Messages.services.columns.status,
accessor: 'status',
Cell: ({ value }: { value: ServiceStatus }) => (
<Badge
text={getBadgeTextForServiceStatus(value)}
color={getBadgeColorForServiceStatus(value)}
icon={getBadgeIconForServiceStatus(value)}
/>
),
tooltipInfo: <StatusInfo />,
type: FilterFieldTypes.DROPDOWN,
options: [
{
label: 'Up',
value: ServiceStatus.UP,
},
{
label: 'Down',
value: ServiceStatus.DOWN,
},
{
label: 'Unknown',
value: ServiceStatus.UNKNOWN,
},
{
label: 'N/A',
value: ServiceStatus.NA,
},
],
},
{
Header: Messages.services.columns.serviceName,
accessor: 'serviceName',
Cell: ({ value, row }: { row: Row<FlattenService>; value: string }) => (
<ServiceIconWithText text={value} dbType={row.original.type} />
),
type: FilterFieldTypes.TEXT,
},
{
Header: Messages.services.columns.nodeName,
accessor: 'nodeName',
Cell: ({ value, row }: { row: Row<FlattenService>; value: string }) => (
<Link className={styles.link} href={getNodeLink(row.original)}>
{value}
</Link>
),
type: FilterFieldTypes.TEXT,
},
{
Header: Messages.services.columns.monitoring,
accessor: 'agentsStatus',
width: '70px',
Cell: ({ value, row }) => (
<StatusLink type="services" strippedId={stripServiceId(row.original.serviceId)} agentsStatus={value} />
),
type: FilterFieldTypes.RADIO_BUTTON,
options: [
{
label: MonitoringStatus.OK,
value: MonitoringStatus.OK,
},
{
label: MonitoringStatus.FAILED,
value: MonitoringStatus.FAILED,
},
],
},
{
Header: Messages.services.columns.address,
accessor: 'address',
type: FilterFieldTypes.TEXT,
},
{
Header: Messages.services.columns.port,
accessor: 'port',
width: '100px',
type: FilterFieldTypes.TEXT,
},
getExpandAndActionsCol(getActions),
],
[styles, getActions]
);
const [showClusters, setShowClusters] = useLocalStorage(CLUSTERS_SWITCH_KEY, false);

const loadData = useCallback(async () => {
try {
Expand All @@ -216,47 +70,18 @@ export const Services = () => {

useEffect(() => {
loadData();

// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

const handleSelectionChange = useCallback((rows: Array<Row<FlattenService>>) => {
setSelectedRows(rows);
}, []);

const renderSelectedSubRow = React.useCallback(
(row: Row<FlattenService>) => {
const labels = row.original.customLabels || {};
const labelKeys = Object.keys(labels);
const agents = row.original.agents || [];

return (
<DetailsRow>
{!!agents.length && (
<DetailsRow.Contents title={Messages.services.details.agents}>
<StatusBadge
strippedId={stripServiceId(row.original.serviceId)}
type={'services'}
agents={row.original.agents || []}
/>
</DetailsRow.Contents>
)}
<DetailsRow.Contents title={Messages.services.details.serviceId}>
<span>{row.original.serviceId}</span>
</DetailsRow.Contents>
{!!labelKeys.length && (
<DetailsRow.Contents title={Messages.services.details.labels} fullRow>
<TagList
colorIndex={9}
className={styles.tagList}
tags={labelKeys.map((label) => `${label}=${labels![label]}`)}
/>
</DetailsRow.Contents>
)}
</DetailsRow>
);
},
[styles.tagList]
);
const handleDelete = useCallback((service: FlattenService) => {
setActionItem(service);
setModalVisible(true);
}, []);

const onModalClose = useCallback(() => {
setModalVisible(false);
Expand All @@ -271,9 +96,28 @@ export const Services = () => {

return (
<OldPage navModel={navModel}>
<OldPage.Contents>
<OldPage.Contents isLoading={isLoading}>
<FeatureLoader>
<HorizontalGroup height={40} justify="flex-end" align="flex-start">
<HorizontalGroup align="center">
<InlineSwitch
id="organize-by-clusters"
label={Messages.services.organizeByClusters}
className={styles.clustersSwitch}
value={showClusters}
onClick={() => setShowClusters(!showClusters)}
showLabel
transparent
/>
<Tooltip interactive placement="top" theme="info" content={<ReadMoreLink />}>
<div className={styles.technicalPreview}>
<HorizontalGroup align="center" spacing="xs">
<span>{Messages.services.technicalPreview}</span>
<Icon name="info-circle" />
</HorizontalGroup>
</div>
</Tooltip>
</HorizontalGroup>
<Button
size="md"
disabled={selected.length === 0}
Expand Down Expand Up @@ -305,23 +149,16 @@ export const Services = () => {
onDismiss={onModalClose}
/>
)}
<Table
columns={columns}
data={flattenServices}
totalItems={flattenServices.length}
rowSelection
onRowSelection={handleSelectionChange}
showPagination
pageSize={25}
allRowsSelectionMode="page"
emptyMessage={Messages.services.emptyTable}
pendingRequest={isLoading}
overlayClassName={styles.overlay}
renderExpandedRow={renderSelectedSubRow}
autoResetSelectedRows={false}
getRowId={useCallback((row: FlattenService) => row.serviceId, [])}
showFilter
/>
{showClusters ? (
<Clusters services={flattenServices} onDelete={handleDelete} onSelectionChange={handleSelectionChange} />
) : (
<ServicesTable
flattenServices={flattenServices}
onSelectionChange={handleSelectionChange}
onDelete={handleDelete}
isLoading={isLoading}
/>
)}
</FeatureLoader>
</OldPage.Contents>
</OldPage>
Expand Down
Loading

0 comments on commit 4a59c23

Please sign in to comment.