Skip to content
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions src/components/DiskStateProgressBar/DiskStateProgressBar.scss
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@
--progress-bar-full-height: var(--g-text-body-3-line-height);
--progress-bar-compact-height: 12px;

--stripe-width: 4px;
--stripe-step: 8px;

position: relative;
z-index: 0;

Expand Down Expand Up @@ -45,6 +48,20 @@
background-color: unset;
}

&_striped {
overflow: hidden;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does we really need this prop?

// Inset shadow = border overlay without shrinking the striped fill
border: none;
background-image: repeating-linear-gradient(
135deg,
transparent 0,
transparent var(--stripe-width),
var(--entity-state-fill-color) var(--stripe-width),
var(--entity-state-fill-color) var(--stripe-step)
);
box-shadow: 0 0 0 $border-width var(--entity-state-shadow-color) inset;
}

&__fill-bar {
position: absolute;
top: 0;
Expand Down Expand Up @@ -76,10 +93,21 @@
position: relative;
z-index: 2;

margin-right: var(--g-spacing-1);

font-size: var(--g-text-body-1-font-size);
// bar height minus borders
line-height: calc(var(--progress-bar-full-height) - #{$border-width * 2});

color: inherit;
}

&__icon {
position: relative;
z-index: 2;

margin-left: var(--g-spacing-1);

color: var(--entity-state-border-color);
}
}
50 changes: 43 additions & 7 deletions src/components/DiskStateProgressBar/DiskStateProgressBar.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import React from 'react';

import {Flex, Icon} from '@gravity-ui/uikit';

import {SETTING_KEYS} from '../../store/reducers/settings/constants';
import {cn} from '../../utils/cn';
import {getSeverityColor} from '../../utils/disks/helpers';
import {DONOR_COLOR} from '../../utils/disks/constants';
import {getSeverityColor, getVDiskStatusIcon} from '../../utils/disks/helpers';
import {useSetting} from '../../utils/hooks';

import './DiskStateProgressBar.scss';
Expand All @@ -16,8 +19,11 @@ interface DiskStateProgressBarProps {
faded?: boolean;
inactive?: boolean;
empty?: boolean;
striped?: boolean;
content?: React.ReactNode;
className?: string;
isDonor?: boolean;
withIcon?: boolean;
}

export function DiskStateProgressBar({
Expand All @@ -28,15 +34,29 @@ export function DiskStateProgressBar({
inactive,
empty,
content,
striped,
className,
isDonor,
withIcon,
}: DiskStateProgressBarProps) {
const [inverted] = useSetting<boolean | undefined>(SETTING_KEYS.INVERTED_DISKS);

const mods: Record<string, boolean | undefined> = {inverted, compact, faded, empty, inactive};
const mods: Record<string, boolean | undefined> = {
inverted,
compact,
faded,
empty,
inactive,
striped,
};

const color = severity !== undefined && getSeverityColor(severity);
if (color) {
mods[color.toLocaleLowerCase()] = true;
if (isDonor) {
mods[DONOR_COLOR.toLocaleLowerCase()] = true;
} else {
const color = severity !== undefined && getSeverityColor(severity);
if (color) {
mods[color.toLocaleLowerCase()] = true;
}
}

const renderAllocatedPercent = () => {
Expand Down Expand Up @@ -69,17 +89,33 @@ export function DiskStateProgressBar({
return null;
};

let iconElement: React.ReactNode = null;

if (withIcon) {
const icon = getVDiskStatusIcon(severity, isDonor);

if (icon) {
iconElement = <Icon className={b('icon')} data={icon} size={12} />;
}
}

const hasIcon = Boolean(iconElement);
const justifyContent = hasIcon ? 'space-between' : 'flex-end';

return (
<div
<Flex
alignItems="center"
justifyContent={justifyContent}
className={b(mods, className)}
role="meter"
aria-label="Disk allocated space"
aria-valuemin={0}
aria-valuemax={100}
aria-valuenow={diskAllocatedPercent}
>
{iconElement}
{renderAllocatedPercent()}
{renderContent()}
</div>
</Flex>
);
}
173 changes: 114 additions & 59 deletions src/components/PDiskPopup/PDiskPopup.tsx
Original file line number Diff line number Diff line change
@@ -1,103 +1,138 @@
import React from 'react';

import {Flex} from '@gravity-ui/uikit';
import {Flex, Label, Progress} from '@gravity-ui/uikit';
import {isNil} from 'lodash';

import {selectNodesMap} from '../../store/reducers/nodesList';
import {EFlag} from '../../types/api/enums';
import {valueIsDefined} from '../../utils';
import {EMPTY_DATA_PLACEHOLDER} from '../../utils/constants';
import {createPDiskDeveloperUILink} from '../../utils/developerUI/developerUI';
import {getStateSeverity} from '../../utils/disks/calculatePDiskSeverity';
import {NUMERIC_SEVERITY_TO_LABEL_VIEW} from '../../utils/disks/constants';
import type {PreparedPDisk} from '../../utils/disks/types';
import {useTypedSelector} from '../../utils/hooks';
import {useDatabaseFromQuery} from '../../utils/hooks/useDatabaseFromQuery';
import {useIsUserAllowedToMakeChanges} from '../../utils/hooks/useIsUserAllowedToMakeChanges';
import {bytesToGB, isNumeric} from '../../utils/utils';
import {InfoViewer} from '../InfoViewer';
import type {InfoViewerItem} from '../InfoViewer';
import {LinkWithIcon} from '../LinkWithIcon/LinkWithIcon';
import {pDiskInfoKeyset} from '../PDiskInfo/i18n';
import {PDiskPageLink} from '../PDiskPageLink/PDiskPageLink';
import {StatusIcon} from '../StatusIcon/StatusIcon';
import type {
YDBDefinitionListHeaderLabel,
YDBDefinitionListItem,
} from '../YDBDefinitionList/YDBDefinitionList';
import {YDBDefinitionList} from '../YDBDefinitionList/YDBDefinitionList';

import {pDiskPopupKeyset} from './i18n';

const errorColors = [EFlag.Orange, EFlag.Red, EFlag.Yellow];

export const preparePDiskData = (
data: PreparedPDisk,
nodeData?: {Host?: string; DC?: string},
withDeveloperUILink?: boolean,
) => {
const {
AvailableSize,
TotalSize,
State,
PDiskId,
NodeId,
StringifiedId,
Path,
Realtime,
Type,
Device,
} = data;

const pdiskData: InfoViewerItem[] = [
{
label: 'PDisk',
value: StringifiedId ?? EMPTY_DATA_PLACEHOLDER,
},
{label: 'State', value: State || 'not available'},
{label: 'Type', value: Type || 'unknown'},
export const preparePDiskData = (data: PreparedPDisk, nodeData?: {Host?: string; DC?: string}) => {
const {AvailableSize, TotalSize, NodeId, Path, Realtime, Type, Device} = data;

const pdiskData: YDBDefinitionListItem[] = [
{name: pDiskPopupKeyset('label_type'), content: Type || pDiskPopupKeyset('value_unknown')},
];

if (NodeId) {
pdiskData.push({label: 'Node Id', value: NodeId});
pdiskData.push({name: pDiskPopupKeyset('label_node-id'), content: NodeId});
}

if (nodeData?.Host) {
pdiskData.push({label: 'Host', value: nodeData.Host});
pdiskData.push({name: pDiskPopupKeyset('label_host'), content: nodeData.Host});
}

if (nodeData?.DC) {
pdiskData.push({label: 'DC', value: nodeData.DC});
pdiskData.push({name: pDiskPopupKeyset('label_dc'), content: <Label>{nodeData.DC}</Label>});
}

if (Path) {
pdiskData.push({label: 'Path', value: Path});
pdiskData.push({name: pDiskPopupKeyset('label_path'), content: Path});
}

if (isNumeric(TotalSize)) {
if (isNumeric(TotalSize) && isNumeric(AvailableSize)) {
pdiskData.push({
label: 'Available',
value: `${bytesToGB(AvailableSize)} of ${bytesToGB(TotalSize)}`,
name: pDiskPopupKeyset('label_available'),
content: (
<React.Fragment>
<Progress
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

discussed to remove progress here

theme="success"
size="s"
value={Math.min(Math.max((AvailableSize / TotalSize) * 100, 0), 100)}
/>
<span>{`${bytesToGB(AvailableSize)} ${pDiskPopupKeyset('value_of')} ${bytesToGB(TotalSize)}`}</span>
</React.Fragment>
),
});
}

if (Realtime && errorColors.includes(Realtime)) {
pdiskData.push({label: 'Realtime', value: Realtime});
pdiskData.push({
name: pDiskPopupKeyset('label_realtime'),
content: <StatusIcon mode="icons" status={Realtime} />,
});
}

if (Device && errorColors.includes(Device)) {
pdiskData.push({label: 'Device', value: Device});
pdiskData.push({
name: pDiskPopupKeyset('label_device'),
content: <StatusIcon mode="icons" status={Device} />,
});
}

if (withDeveloperUILink && valueIsDefined(NodeId) && valueIsDefined(PDiskId)) {
const pDiskInternalViewerPath = createPDiskDeveloperUILink({
nodeId: NodeId,
pDiskId: PDiskId,
return pdiskData;
};

export const preparePDiskHeaderLabels = (data: PreparedPDisk): YDBDefinitionListHeaderLabel[] => {
const labels: YDBDefinitionListHeaderLabel[] = [];
const {State} = data;

if (!State) {
labels.push({
id: 'state',
value: pDiskPopupKeyset('context_not-available'),
});

pdiskData.push({
label: 'Links',
value: (
<Flex gap={2} wrap="wrap">
<PDiskPageLink pDiskId={PDiskId} nodeId={NodeId} />
<LinkWithIcon
title={pDiskInfoKeyset('developer-ui')}
url={pDiskInternalViewerPath}
/>
</Flex>
),
return labels;
}

if (State) {
const severity = getStateSeverity(State);
const {theme, icon} = NUMERIC_SEVERITY_TO_LABEL_VIEW[severity];

labels.push({
id: 'state',
value: State,
theme: theme,
icon: icon,
});
}

return pdiskData;
return labels;
};

export const buildPDiskFooter = (
data: PreparedPDisk,
withDeveloperUILink?: boolean,
): React.ReactNode | null => {
const {NodeId, PDiskId} = data;

if (!withDeveloperUILink || isNil(NodeId) || isNil(PDiskId)) {
return null;
}

const pDiskInternalViewerPath = createPDiskDeveloperUILink({
nodeId: NodeId,
pDiskId: PDiskId,
});

return (
<Flex gap={2} wrap="wrap">
<PDiskPageLink pDiskId={PDiskId} nodeId={NodeId} />
<LinkWithIcon title={pDiskInfoKeyset('developer-ui')} url={pDiskInternalViewerPath} />
</Flex>
);
};

interface PDiskPopupProps {
Expand All @@ -108,11 +143,31 @@ export const PDiskPopup = ({data}: PDiskPopupProps) => {
const database = useDatabaseFromQuery();
const isUserAllowedToMakeChanges = useIsUserAllowedToMakeChanges();
const nodesMap = useTypedSelector((state) => selectNodesMap(state, database));
const nodeData = valueIsDefined(data.NodeId) ? nodesMap?.get(data.NodeId) : undefined;
const info = React.useMemo(
() => preparePDiskData(data, nodeData, isUserAllowedToMakeChanges),
[data, nodeData, isUserAllowedToMakeChanges],
const nodeData = isNil(data.NodeId) ? undefined : nodesMap?.get(data.NodeId);

const info = React.useMemo(() => preparePDiskData(data, nodeData), [data, nodeData]);

const headerLabels = React.useMemo<YDBDefinitionListHeaderLabel[]>(
() => preparePDiskHeaderLabels(data),
[data],
);

const footer = React.useMemo(
() => buildPDiskFooter(data, isUserAllowedToMakeChanges),
[data, isUserAllowedToMakeChanges],
);

return <InfoViewer title="PDisk" info={info} size="s" />;
const pdiskId = data.StringifiedId;

return (
<YDBDefinitionList
compact
title="PDisk"
titleSuffix={pdiskId ?? EMPTY_DATA_PLACEHOLDER}
items={info}
headerLabels={headerLabels}
footer={footer}
nameMaxWidth={100}
/>
);
};
13 changes: 13 additions & 0 deletions src/components/PDiskPopup/i18n/en.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"context_not-available": "Not available",
"label_type": "Type",
"label_node-id": "Node Id",
"label_host": "Host",
"label_dc": "DC",
"label_path": "Path",
"label_available": "Available",
"label_realtime": "Realtime",
"label_device": "Device",
"value_unknown": "unknown",
"value_of": "of"
}
7 changes: 7 additions & 0 deletions src/components/PDiskPopup/i18n/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import {registerKeysets} from '../../../utils/i18n';

import en from './en.json';

const COMPONENT = 'ydb-pDisk-popup';

export const pDiskPopupKeyset = registerKeysets(COMPONENT, {en});
Loading
Loading