diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx index 1120831996..3e8c21f6fd 100644 --- a/.storybook/preview.tsx +++ b/.storybook/preview.tsx @@ -20,6 +20,7 @@ import { Provider } from 'react-redux' import { store } from 'uiSrc/slices/store' import Router from 'uiSrc/Router' import { StyledContainer } from './helpers/styles' +import { GlobalStyles } from 'uiSrc/styles/globalStyles' const parameters: Parameters = { parameters: { @@ -67,6 +68,7 @@ const preview: Preview = { + diff --git a/redisinsight/ui/src/components/base/forms/buttons/EmptyButton.tsx b/redisinsight/ui/src/components/base/forms/buttons/EmptyButton.tsx index ebb72f943c..e360147f96 100644 --- a/redisinsight/ui/src/components/base/forms/buttons/EmptyButton.tsx +++ b/redisinsight/ui/src/components/base/forms/buttons/EmptyButton.tsx @@ -22,22 +22,26 @@ export const EmptyButton = ({ ...rest }: ButtonProps) => ( - - - {children} - - + {icon ? ( + + + {children} + + + ) : ( + children + )} ) diff --git a/redisinsight/ui/src/components/base/layout/loading-content/LoadingContent.spec.tsx b/redisinsight/ui/src/components/base/layout/loading-content/LoadingContent.spec.tsx index 4ef1dd4499..123b3cefbf 100644 --- a/redisinsight/ui/src/components/base/layout/loading-content/LoadingContent.spec.tsx +++ b/redisinsight/ui/src/components/base/layout/loading-content/LoadingContent.spec.tsx @@ -1,5 +1,5 @@ import React from 'react' -import { render } from '@testing-library/react' +import { render } from 'uiSrc/utils/test-utils' import LoadingContent from './LoadingContent' describe('LoadingContent', () => { diff --git a/redisinsight/ui/src/components/base/layout/loading-content/loading-content.styles.ts b/redisinsight/ui/src/components/base/layout/loading-content/loading-content.styles.ts index ce56380c2c..8f76d3a0bc 100644 --- a/redisinsight/ui/src/components/base/layout/loading-content/loading-content.styles.ts +++ b/redisinsight/ui/src/components/base/layout/loading-content/loading-content.styles.ts @@ -1,5 +1,6 @@ import { HTMLAttributes } from 'react' import styled, { keyframes } from 'styled-components' +import { Theme } from 'uiSrc/components/base/theme/types' export type LineRange = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 @@ -25,13 +26,13 @@ export const StyledLoadingContent = styled.span< ` export const SingleLine = styled.span< - React.HtmlHTMLAttributes + React.HtmlHTMLAttributes & { theme: Theme } >` display: block; width: 100%; - height: var(--base); - margin-bottom: var(--size-s); - border-radius: var(--size-xs); + height: ${({ theme }) => theme.core.space.space200}; + margin-bottom: ${({ theme }) => theme.core.space.space100}; + border-radius: ${({ theme }) => theme.core.space.space050}; overflow: hidden; &:last-child:not(:only-child) { @@ -39,15 +40,15 @@ export const SingleLine = styled.span< } ` -export const SingleLineBackground = styled.span` +export const SingleLineBackground = styled.span<{ theme: Theme }>` display: block; width: 220%; height: 100%; background: linear-gradient( 137deg, - var(--loadingContentColor) 45%, - var(--loadingContentLightestShade) 50%, - var(--loadingContentColor) 55% + ${({ theme }) => theme.semantic.color.background.neutral200} 45%, + ${({ theme }) => theme.semantic.color.background.neutral300} 50%, + ${({ theme }) => theme.semantic.color.background.neutral200} 55% ); animation: ${loadingAnimation} 1.5s ease-in-out infinite; ` diff --git a/redisinsight/ui/src/components/base/utils/pluginsThemeContext.tsx b/redisinsight/ui/src/components/base/utils/pluginsThemeContext.tsx index c4d1a40ed0..baf6e284bc 100644 --- a/redisinsight/ui/src/components/base/utils/pluginsThemeContext.tsx +++ b/redisinsight/ui/src/components/base/utils/pluginsThemeContext.tsx @@ -4,6 +4,7 @@ import { CommonStyles, themeLight, themeDark } from '@redis-ui/styles' import 'modern-normalize/modern-normalize.css' import '@redis-ui/styles/normalized-styles.css' import '@redis-ui/styles/fonts.css' +import { GlobalStyles } from 'uiSrc/styles/globalStyles' interface Props { children: React.ReactNode @@ -31,6 +32,7 @@ export const ThemeProvider = ({ children }: Props) => { + {children} diff --git a/redisinsight/ui/src/components/charts/bar-chart/BarChart.stories.tsx b/redisinsight/ui/src/components/charts/bar-chart/BarChart.stories.tsx index b27728f3ff..346a2e3cc6 100644 --- a/redisinsight/ui/src/components/charts/bar-chart/BarChart.stories.tsx +++ b/redisinsight/ui/src/components/charts/bar-chart/BarChart.stories.tsx @@ -1,31 +1,54 @@ +import React from 'react' import type { Meta, StoryObj } from '@storybook/react-vite' -import BarChart from './BarChart' +import BarChart, { BarChartDataType } from './BarChart' +import { formatBytes } from 'uiSrc/utils' -const barCharMeta = { +const barChartMeta: Meta = { component: BarChart, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +} + +export default barChartMeta + +type Story = StoryObj + +export const Default: Story = { args: { width: 600, - height: 200, - name: 'test', + height: 300, + name: 'default', data: [ - { x: 1, y: 0, xlabel: 'one', ylabel: 'zero' }, - { x: 5, y: 0.1, xlabel: 'five', ylabel: 'point one' }, - { x: 10, y: 20, xlabel: '', ylabel: '' }, - { x: 2, y: 30, xlabel: '', ylabel: '' }, - { x: 30, y: 40, xlabel: '', ylabel: '' }, - { x: 15, y: 500, xlabel: '', ylabel: '' }, + { x: 1, y: 100, xlabel: 'A', ylabel: '' }, + { x: 2, y: 200, xlabel: 'B', ylabel: '' }, + { x: 3, y: 150, xlabel: 'C', ylabel: '' }, + { x: 4, y: 300, xlabel: 'D', ylabel: '' }, + { x: 5, y: 250, xlabel: 'E', ylabel: '' }, ], }, -} satisfies Meta - -export default barCharMeta - -type Story = StoryObj +} -export const Default: Story = {} -export const ThinnerBars: Story = { +export const BytesDataType: Story = { args: { - barWidth: 10, + width: 700, + height: 350, + name: 'memory-usage', + dataType: BarChartDataType.Bytes, + data: [ + { x: 3600, y: 1024 * 512, xlabel: '<1 hr', ylabel: '' }, + { x: 14400, y: 1024 * 1024 * 2, xlabel: '1-4 Hrs', ylabel: '' }, + { x: 43200, y: 1024 * 1024 * 5, xlabel: '4-12 Hrs', ylabel: '' }, + { x: 86400, y: 1024 * 1024 * 10, xlabel: '12-24 Hrs', ylabel: '' }, + { x: 604800, y: 1024 * 1024 * 3, xlabel: '1-7 Days', ylabel: '' }, + { x: 2592000, y: 1024 * 1024, xlabel: '>7 Days', ylabel: '' }, + ], + tooltipValidation: (val) => formatBytes(val, 3) as string, + leftAxiosValidation: (val, i) => (i % 2 ? '' : formatBytes(val, 1)), }, } diff --git a/redisinsight/ui/src/components/charts/bar-chart/BarChart.styles.ts b/redisinsight/ui/src/components/charts/bar-chart/BarChart.styles.ts new file mode 100644 index 0000000000..d3e9e0b40c --- /dev/null +++ b/redisinsight/ui/src/components/charts/bar-chart/BarChart.styles.ts @@ -0,0 +1,77 @@ +import styled, { createGlobalStyle } from 'styled-components' +import { Theme } from 'uiSrc/components/base/theme/types' +import React from 'react' + +export const Wrapper = styled.div>` + margin: 0 auto; +` + +export const StyledSVG = styled.svg< + React.SVGProps & { + ref?: React.Ref + theme: Theme + } +>` + --bar-fill: ${({ theme }) => theme.semantic.color.background.notice500}; + --bar-stroke: ${({ theme }) => + theme.semantic.color.background.informative700}; + + width: 100%; + height: 100%; + /* D3-created bar elements */ + .bar-chart-bar { + fill: rgb(from var(--bar-fill) r g b / 0.1); + stroke: var(--bar-stroke); + stroke-width: 1.5px; + } + + /* D3-created scatter point elements */ + .bar-chart-scatter-points { + fill: var(--bar-stroke); + cursor: pointer; + } + + /* D3-created dashed line elements */ + .bar-chart-dashed-line { + stroke: ${({ theme }) => theme.semantic.color.text.neutral800}; + stroke-width: 1px; + stroke-dasharray: 5, 3; + } + + /* D3-created tick lines */ + .tick line { + stroke: ${({ theme }) => theme.semantic.color.text.neutral800}; + opacity: 0.1; + } + + /* D3-created domain */ + .domain { + opacity: 0; + } + + /* D3-created text elements */ + text { + color: ${({ theme }) => theme.semantic.color.text.neutral800}; + } +` + +// Tooltip is appended to body by D3, so needs global styles +export const TooltipGlobalStyles = createGlobalStyle<{ theme: Theme }>` + .bar-chart-tooltip { + position: fixed; + min-width: 50px; + background: ${({ theme }) => theme.semantic.color.background.neutral600}; + color: ${({ theme }) => theme.semantic.color.text.primary600} !important; + z-index: 10; + border-radius: ${({ theme }) => theme.core.space.space100}; + pointer-events: none; + font-weight: 400; + font-size: 12px !important; + box-shadow: 0 3px 15px rgba(0, 0, 0, 0.2) !important; + bottom: 0; + height: 36px; + min-height: 36px; + padding: ${({ theme }) => theme.core.space.space100}; + line-height: 16px; + } +` diff --git a/redisinsight/ui/src/components/charts/bar-chart/BarChart.tsx b/redisinsight/ui/src/components/charts/bar-chart/BarChart.tsx index 20e8d8b409..a3197d4610 100644 --- a/redisinsight/ui/src/components/charts/bar-chart/BarChart.tsx +++ b/redisinsight/ui/src/components/charts/bar-chart/BarChart.tsx @@ -4,7 +4,11 @@ import cx from 'classnames' import { curryRight, flow, toNumber } from 'lodash' import { formatBytes, toBytes } from 'uiSrc/utils' -import styles from './styles.module.scss' +import { + Wrapper, + StyledSVG, + TooltipGlobalStyles, +} from './BarChart.styles' export interface BarChartData { y: number @@ -88,7 +92,7 @@ const BarChart = (props: IProps) => { const tooltip = d3 .select('body') .append('div') - .attr('class', cx(styles.tooltip, classNames?.tooltip || '')) + .attr('class', cx('bar-chart-tooltip', classNames?.tooltip || '')) .style('opacity', 0) d3.select(svgRef.current).select('g').remove() @@ -143,7 +147,7 @@ const BarChart = (props: IProps) => { if (divideLastColumn) { svg .append('line') - .attr('class', cx(styles.dashedLine, classNames?.dashedLine)) + .attr('class', cx('bar-chart-dashed-line', classNames?.dashedLine)) .attr('x1', xAxis(cleanedData.length - 2.3)) .attr('x2', xAxis(cleanedData.length - 2.3)) .attr('y1', 0) @@ -198,7 +202,7 @@ const BarChart = (props: IProps) => { .data(cleanedData) .enter() .append('rect') - .attr('class', cx(styles.bar, classNames?.bar)) + .attr('class', cx('bar-chart-bar', classNames?.bar)) .attr('x', (d) => xAxis(d.index)) .attr('width', barWidth) // set minimal height for Bar @@ -236,12 +240,12 @@ const BarChart = (props: IProps) => { } return ( -
- -
+ <> + + + + + ) } diff --git a/redisinsight/ui/src/components/charts/bar-chart/styles.module.scss b/redisinsight/ui/src/components/charts/bar-chart/styles.module.scss deleted file mode 100644 index ff0da24e12..0000000000 --- a/redisinsight/ui/src/components/charts/bar-chart/styles.module.scss +++ /dev/null @@ -1,58 +0,0 @@ -.wrapper { - margin: 0 auto; -} - -.svg { - width: 100%; - height: 100%; -} - -.bar { - fill: rgba(var(--euiColorPrimaryRGB), 0.1); - stroke: var(--euiColorPrimary); - stroke-width: 1.5px; -} - -.tooltip { - position: fixed; - min-width: 50px; - background: var(--euiTooltipBackgroundColor); - color: var(--euiTooltipTextColor) !important; - z-index: 10; - border-radius: 8px; - pointer-events: none; - font-weight: 400; - font-size: 12px !important; - box-shadow: 0 3px 15px var(--controlsBoxShadowColor) !important; - bottom: 0; - height: 36px; - min-height: 36px; - padding: 10px; - line-height: 16px; -} - -.scatterPoints { - fill: var(--euiColorPrimary); - cursor: pointer; -} - -.dashedLine { - stroke: var(--euiTextSubduedColor); - stroke-width: 1px; - stroke-dasharray: 5, 3; -} - -:global { - .tick line { - stroke: var(--textColorShade); - opacity: 0.1; - } - - .domain { - opacity: 0; - } - - text { - color: var(--euiTextSubduedColor); - } -} diff --git a/redisinsight/ui/src/components/charts/donut-chart/DonutChart.stories.tsx b/redisinsight/ui/src/components/charts/donut-chart/DonutChart.stories.tsx index 6e756d46ea..316c3d9b3c 100644 --- a/redisinsight/ui/src/components/charts/donut-chart/DonutChart.stories.tsx +++ b/redisinsight/ui/src/components/charts/donut-chart/DonutChart.stories.tsx @@ -2,22 +2,22 @@ import type { Meta, StoryObj } from '@storybook/react-vite' import DonutChart from './index' -const donutChartMeta = { +const donutChartMeta: Meta = { component: DonutChart, args: { width: 400, height: 200, name: 'test', data: [ - { value: 1, name: 'A', color: [0, 0, 0] }, - { value: 5, name: 'B', color: [10, 10, 10] }, - { value: 10, name: 'C', color: [20, 20, 20] }, - { value: 2, name: 'D', color: [30, 30, 30] }, - { value: 30, name: 'E', color: [40, 40, 40] }, - { value: 15, name: 'F', color: [50, 50, 50] }, + { value: 1, name: 'A', color: [231, 76, 60] }, // Red + { value: 5, name: 'B', color: [52, 152, 219] }, // Blue + { value: 10, name: 'C', color: [46, 204, 113] }, // Green + { value: 2, name: 'D', color: [241, 196, 15] }, // Yellow + { value: 30, name: 'E', color: [155, 89, 182] }, // Purple + { value: 15, name: 'F', color: [230, 126, 34] }, // Orange ], }, -} satisfies Meta +} export default donutChartMeta diff --git a/redisinsight/ui/src/components/charts/donut-chart/DonutChart.styles.ts b/redisinsight/ui/src/components/charts/donut-chart/DonutChart.styles.ts new file mode 100644 index 0000000000..47631520f6 --- /dev/null +++ b/redisinsight/ui/src/components/charts/donut-chart/DonutChart.styles.ts @@ -0,0 +1,57 @@ +import React from 'react' +import styled from 'styled-components' +import { Theme } from 'uiSrc/components/base/theme/types' + +export const Wrapper = styled.div>` + position: relative; +` + +export const InnerTextContainer = styled.div< + React.HTMLAttributes +>` + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); +` + +export const Tooltip = styled.div< + React.HTMLAttributes & { + ref?: React.Ref + theme: Theme + } +>` + position: fixed; + background: ${({ theme }) => theme.semantic.color.background.neutral500}; + color: ${({ theme }) => theme.semantic.color.text.primary600}; + padding: ${({ theme }) => theme.core.space.space100}; + visibility: hidden; + border-radius: ${({ theme }) => theme.core.space.space050}; + z-index: 100; +` + +// SVG elements styled for d3 manipulation +// Using data attributes for styling since d3 dynamically creates these elements +export const StyledSVG = styled.svg< + React.SVGProps & { + ref?: React.Ref + theme: Theme + } +>` + & .donut-arc { + stroke: ${({ theme }) => theme.semantic.color.border.neutral200}; + stroke-width: 2px; + cursor: pointer; + } + + & .donut-label { + fill: ${({ theme }) => theme.semantic.color.text.primary600}; + font-size: 12px; + font-weight: bold; + letter-spacing: -0.12px !important; + + .donut-label-value { + font-weight: normal; + } + } +` diff --git a/redisinsight/ui/src/components/charts/donut-chart/DonutChart.tsx b/redisinsight/ui/src/components/charts/donut-chart/DonutChart.tsx index c1a200a297..196df0fcd7 100644 --- a/redisinsight/ui/src/components/charts/donut-chart/DonutChart.tsx +++ b/redisinsight/ui/src/components/charts/donut-chart/DonutChart.tsx @@ -7,7 +7,12 @@ import { Nullable, truncateNumberToRange } from 'uiSrc/utils' import { rgb, RGBColor } from 'uiSrc/utils/colors' import { getPercentage } from 'uiSrc/utils/numbers' -import styles from './styles.module.scss' +import { + Wrapper, + InnerTextContainer, + Tooltip, + StyledSVG, +} from './DonutChart.styles' export interface ChartData { value: number @@ -149,12 +154,15 @@ const DonutChart = (props: IProps) => { d3.select(svgRef.current).select('g').remove() + const svgElement = svgRef.current + const existingClasses = svgElement?.getAttribute('class') || '' + const svg = d3 .select(svgRef.current) .attr('width', width) .attr('height', height) .attr('data-testid', `donut-svg-${name}`) - .attr('class', cx(classNames?.chart)) + .attr('class', cx(existingClasses, classNames?.chart)) .append('g') .attr('transform', `translate(${width / 2},${height / 2})`) @@ -169,7 +177,7 @@ const DonutChart = (props: IProps) => { .attr('fill', (d) => isString(d.data.color) ? d.data.color : rgb(d.data.color), ) - .attr('class', cx(styles.arc, classNames?.arc)) + .attr('class', cx('donut-arc', classNames?.arc)) .on('mouseenter mousemove', onMouseEnterSlice) .on('mouseleave', onMouseLeaveSlice) @@ -179,7 +187,7 @@ const DonutChart = (props: IProps) => { .data(dataReady) .enter() .append('text') - .attr('class', cx(styles.chartLabel, classNames?.arcLabel)) + .attr('class', cx('donut-label', classNames?.arcLabel)) .attr('transform', getLabelPosition) .text((d) => isShowLabel(d) && !hideLabelTitle ? `${d.data.name}: ` : '', @@ -206,7 +214,7 @@ const DonutChart = (props: IProps) => { return truncateNumberToRange(d.value) }) - .attr('class', cx(styles.chartLabelValue, classNames?.arcLabelValue)) + .attr('class', cx('donut-label-value', classNames?.arcLabelValue)) }, [data, hideLabelTitle]) if (!data.length || sum === 0) { @@ -214,19 +222,19 @@ const DonutChart = (props: IProps) => { } return ( -
- -
+ + {renderTooltip && hoveredData ? renderTooltip(hoveredData) : hoveredData?.value || ''} -
- {title &&
{title}
} -
+ + {title && {title}} + ) } diff --git a/redisinsight/ui/src/components/charts/donut-chart/styles.module.scss b/redisinsight/ui/src/components/charts/donut-chart/styles.module.scss deleted file mode 100644 index ca201fc111..0000000000 --- a/redisinsight/ui/src/components/charts/donut-chart/styles.module.scss +++ /dev/null @@ -1,37 +0,0 @@ -.wrapper { - position: relative; -} - -.innerTextContainer { - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); -} - -.tooltip { - position: fixed; - background: var(--euiTooltipBackgroundColor); - color: var(--htmlColor); - padding: 10px; - visibility: hidden; - border-radius: 4px; - z-index: 100; -} - -.chartLabel { - fill: var(--euiTextSubduedColor); - font-size: 12px; - font-weight: bold; - letter-spacing: -0.12px !important; - - .chartLabelValue { - font-weight: normal; - } -} - -.arc { - stroke: var(--euiColorLightestShade); - stroke-width: 2px; - cursor: pointer; -} diff --git a/redisinsight/ui/src/components/group-badge/GroupBadge.stories.tsx b/redisinsight/ui/src/components/group-badge/GroupBadge.stories.tsx new file mode 100644 index 0000000000..d8737f4b82 --- /dev/null +++ b/redisinsight/ui/src/components/group-badge/GroupBadge.stories.tsx @@ -0,0 +1,54 @@ +import React from 'react' +import type { Meta, StoryObj } from '@storybook/react-vite' +import { CommandGroup, KeyTypes } from 'uiSrc/constants' +import GroupBadge from './GroupBadge' + +const meta: Meta = { + component: GroupBadge, + args: { + type: KeyTypes.String, + }, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +} + +export default meta + +type Story = StoryObj + +export const KeyTypeBadges: Story = { + render: () => ( +
+ + + + + + + +
+ ), +} + +export const CommandGroupBadges: Story = { + render: () => ( +
+ + + + + + + + + + + +
+ ), +} diff --git a/redisinsight/ui/src/components/group-badge/GroupBadge.styles.ts b/redisinsight/ui/src/components/group-badge/GroupBadge.styles.ts new file mode 100644 index 0000000000..d81e22c0ce --- /dev/null +++ b/redisinsight/ui/src/components/group-badge/GroupBadge.styles.ts @@ -0,0 +1,49 @@ +import styled, { css } from 'styled-components' +import { RiBadge } from 'uiSrc/components/base/display/badge/RiBadge' +import { IconButton } from 'uiSrc/components/base/forms/buttons' + +interface StyledGroupBadgeProps { + $color: string + $withDeleteBtn?: boolean + $compressed?: boolean +} +export const DeleteButton = styled(IconButton)` + margin-left: ${({ theme }) => theme.core.space.space050}; + width: ${({ theme }) => theme.core.space.space150}; + height: ${({ theme }) => theme.core.space.space150}; + color: #ffffff; + &:hover { + // disable default hover appearance + appearance: none; + background-color: transparent; + text-decoration: none; + color: #ffffff; + } + & svg { + width: 10px; + height: 10px; + } +` + +const compressedStyle = css` + padding-left: 0; + padding-right: 0; + + ${DeleteButton} { + margin-left: 0; + } +` +export const StyledGroupBadge = styled(RiBadge)` + min-width: ${({ theme }) => theme.core.space.space150}; + & > p { + display: flex; + align-items: center; + } + ${({ $color }) => + ` + background-color: ${$color}; + `} + + ${({ $withDeleteBtn }) => $withDeleteBtn && 'padding-right: 0 !important;'} + ${({ $compressed }) => $compressed && compressedStyle} +` diff --git a/redisinsight/ui/src/components/group-badge/GroupBadge.tsx b/redisinsight/ui/src/components/group-badge/GroupBadge.tsx index d928267398..5cf4aaf46d 100644 --- a/redisinsight/ui/src/components/group-badge/GroupBadge.tsx +++ b/redisinsight/ui/src/components/group-badge/GroupBadge.tsx @@ -1,15 +1,12 @@ -import cx from 'classnames' import React from 'react' -import { CommandGroup, KeyTypes, GROUP_TYPES_COLORS } from 'uiSrc/constants' +import { CommandGroup, GROUP_TYPES_COLORS, KeyTypes } from 'uiSrc/constants' import { getGroupTypeDisplay } from 'uiSrc/utils' -import { IconButton } from 'uiSrc/components/base/forms/buttons' import { CancelSlimIcon } from 'uiSrc/components/base/icons' import { Text } from 'uiSrc/components/base/text' -import { RiBadge } from 'uiSrc/components/base/display/badge/RiBadge' -import styles from './styles.module.scss' +import { DeleteButton, StyledGroupBadge } from './GroupBadge.styles' export interface Props { type: KeyTypes | CommandGroup | string @@ -29,41 +26,36 @@ const GroupBadge = ({ // @ts-ignore const backgroundColor = GROUP_TYPES_COLORS[type] ?? 'var(--defaultTypeColor)' return ( - {!compressed && ( {getGroupTypeDisplay(type)} )} {onDelete && ( - onDelete(type)} - className={styles.deleteIcon} data-testid={`${type}-delete-btn`} /> )} - + ) } diff --git a/redisinsight/ui/src/components/navigation-menu/app-navigation/AppNavigation.tsx b/redisinsight/ui/src/components/navigation-menu/app-navigation/AppNavigation.tsx index 51167a3cc3..8960286ed2 100644 --- a/redisinsight/ui/src/components/navigation-menu/app-navigation/AppNavigation.tsx +++ b/redisinsight/ui/src/components/navigation-menu/app-navigation/AppNavigation.tsx @@ -109,6 +109,7 @@ const AppNavigation = ({ actions, onChange, routes }: AppNavigationProps) => { return ( { > + {children} diff --git a/redisinsight/ui/src/mocks/factories/database-analysis/DatabaseAnalysis.factory.ts b/redisinsight/ui/src/mocks/factories/database-analysis/DatabaseAnalysis.factory.ts new file mode 100644 index 0000000000..bf9b41f0fa --- /dev/null +++ b/redisinsight/ui/src/mocks/factories/database-analysis/DatabaseAnalysis.factory.ts @@ -0,0 +1,167 @@ +import { Factory } from 'fishery' +import { faker } from '@faker-js/faker' +import { DatabaseAnalysis } from 'apiSrc/modules/database-analysis/models' + +export const DatabaseAnalysisFactory = Factory.define(() => ({ + id: faker.string.uuid(), + databaseId: faker.string.uuid(), + filter: { match: '*', count: 10000 } as any, + delimiter: ':', + progress: { total: 100000, scanned: 50000, processed: 10000 } as any, + createdAt: faker.date.recent(), + totalKeys: { total: 10000, types: [] } as any, + totalMemory: { total: 1000000, types: [] } as any, + topKeysNsp: [], + topMemoryNsp: [], + topKeysLength: [], + topKeysMemory: [], + expirationGroups: [], + recommendations: [], +})) + +export const buildDatabaseAnalysisWithTopKeys = () => { + const data = DatabaseAnalysisFactory.build({ + totalKeys: { + total: 7_500, + types: [ + { type: 'string', total: 3_000 }, + { type: 'hash', total: 2_500 }, + { type: 'zset', total: 2_000 }, + ], + } as any, + totalMemory: { + total: 450_000, + types: [ + { type: 'string', total: 50_000 }, + { type: 'hash', total: 250_000 }, + { type: 'zset', total: 150_000 }, + ], + } as any, + topKeysLength: [ + { + name: 'user:sessions', + type: 'hash', + memory: 120_000, + length: 5_000, + ttl: -1, + }, + { + name: 'orders:recent', + type: 'list', + memory: 80_000, + length: 2_000, + ttl: 3_600, + }, + ] as any, + topKeysMemory: [ + { + name: 'user:sessions', + type: 'hash', + memory: 120_000, + length: 5_000, + ttl: -1, + }, + { + name: 'metrics:pageviews', + type: 'zset', + memory: 200_000, + length: 1_000, + ttl: -1, + }, + ] as any, + expirationGroups: [ + { label: 'No expiry', total: 8_000, threshold: 0 }, + { label: '<1 hr', total: 1_500, threshold: 3_600 }, + { label: '1–24 hrs', total: 500, threshold: 86_400 }, + ] as any, + }) + + const reports = [ + { + id: data.id, + createdAt: data.createdAt, + db: data.db, + }, + ] + + return { data, reports } +} + +export const buildDatabaseAnalysisWithNamespaces = () => + DatabaseAnalysisFactory.build({ + topMemoryNsp: [ + { + nsp: 'users', + memory: 500000, + keys: 1200, + types: [ + { + type: 'hash', + memory: 400000, + keys: 800, + }, + { + type: 'string', + memory: 100000, + keys: 400, + }, + ], + }, + { + nsp: 'orders', + memory: 300000, + keys: 600, + types: [ + { + type: 'zset', + memory: 200000, + keys: 300, + }, + { + type: 'list', + memory: 100000, + keys: 300, + }, + ], + }, + ] as any, + topKeysNsp: [ + { + nsp: 'users', + memory: 500000, + keys: 1200, + types: [ + { + type: 'hash', + memory: 400000, + keys: 800, + }, + { + type: 'string', + memory: 100000, + keys: 400, + }, + ], + }, + { + nsp: 'orders', + memory: 300000, + keys: 600, + types: [ + { + type: 'zset', + memory: 200000, + keys: 300, + }, + { + type: 'list', + memory: 100000, + keys: 300, + }, + ], + }, + ] as any, + delimiter: ':', + }) + + diff --git a/redisinsight/ui/src/pages/analytics/AnalyticsPage.tsx b/redisinsight/ui/src/pages/analytics/AnalyticsPage.tsx index 2beffacad3..bd504f0587 100644 --- a/redisinsight/ui/src/pages/analytics/AnalyticsPage.tsx +++ b/redisinsight/ui/src/pages/analytics/AnalyticsPage.tsx @@ -11,8 +11,6 @@ import { useConnectionType } from 'uiSrc/components/hooks/useConnectionType' import AnalyticsPageRouter from './AnalyticsPageRouter' -import styles from './styles.module.scss' - export interface Props { routes: any[] } @@ -67,11 +65,7 @@ const AnalyticsPage = ({ routes = [] }: Props) => { pathname === Pages.analytics(instanceId) ? '' : pathname }, [pathname]) - return ( -
- -
- ) + return } export default AnalyticsPage diff --git a/redisinsight/ui/src/pages/browser/modules/key-details-header/components/key-details-header-formatter/KeyDetailsHeaderFormatter.styles.tsx b/redisinsight/ui/src/pages/browser/modules/key-details-header/components/key-details-header-formatter/KeyDetailsHeaderFormatter.styles.tsx index f4f6be8159..7a088c9102 100644 --- a/redisinsight/ui/src/pages/browser/modules/key-details-header/components/key-details-header-formatter/KeyDetailsHeaderFormatter.styles.tsx +++ b/redisinsight/ui/src/pages/browser/modules/key-details-header/components/key-details-header-formatter/KeyDetailsHeaderFormatter.styles.tsx @@ -3,6 +3,7 @@ import { ComponentProps } from 'react' import { ColorText } from 'uiSrc/components/base/text' import { RiSelect } from 'uiSrc/components/base/forms/select/RiSelect' import { RiIcon } from 'uiSrc/components/base/icons/RiIcon' +import { insightsOpen } from 'uiSrc/styles/mixins' type KeyDetailsSelectProps = ComponentProps & { $fullWidth?: boolean @@ -41,12 +42,10 @@ const ControlsIcon = styled(RiIcon)` margin-left: 3px; margin-top: 2px; - :global(.insightsOpen) { - @media only screen and (max-width: 1440px) { - width: 18px !important; - height: 18px !important; - } - } + ${insightsOpen(1440)` + width: 18px !important; + height: 18px !important; + `} ` const Container = styled.div<{ diff --git a/redisinsight/ui/src/pages/database-analysis/DatabaseAnalysisPage.tsx b/redisinsight/ui/src/pages/database-analysis/DatabaseAnalysisPage.tsx index b20e788efe..e951c910a3 100644 --- a/redisinsight/ui/src/pages/database-analysis/DatabaseAnalysisPage.tsx +++ b/redisinsight/ui/src/pages/database-analysis/DatabaseAnalysisPage.tsx @@ -1,11 +1,10 @@ import React, { useEffect, useState } from 'react' -import { useSelector, useDispatch } from 'react-redux' +import { useDispatch, useSelector } from 'react-redux' import { useParams } from 'react-router-dom' -import styled from 'styled-components' import { - dbAnalysisSelector, dbAnalysisReportsSelector, + dbAnalysisSelector, fetchDBAnalysisAction, fetchDBAnalysisReportsHistory, setSelectedAnalysisId, @@ -17,27 +16,15 @@ import { import { connectedInstanceSelector } from 'uiSrc/slices/instances/instances' import { AnalyticsViewTab } from 'uiSrc/slices/interfaces/analytics' import { - sendPageViewTelemetry, sendEventTelemetry, - TelemetryPageView, + sendPageViewTelemetry, TelemetryEvent, + TelemetryPageView, } from 'uiSrc/telemetry' import { formatLongName, getDbIndex, setTitle } from 'uiSrc/utils' +import { DatabaseAnalysisPageView } from './DatabaseAnalysisPageView' -import Header from './components/header' -import DatabaseAnalysisTabs from './components/data-nav-tabs' -import styles from './styles.module.scss' - -// Styled component for the main container with theme border -const MainContainer = styled.div>` - border: 1px solid ${({ theme }) => theme.semantic.color.border.neutral500}; - border-radius: ${({ theme }) => theme.components.semanticContainer.sizes.M.borderRadius}; - padding: ${({ theme }) => theme.components.semanticContainer.sizes.M.padding}; - height: 100%; - overflow: auto; -` - -const DatabaseAnalysisPage = () => { +export const DatabaseAnalysisPage = () => { const { viewTab } = useSelector(analyticsSettingsSelector) const { loading: analysisLoading, data } = useSelector(dbAnalysisSelector) const { data: reports, selectedAnalysis } = useSelector( @@ -101,20 +88,13 @@ const DatabaseAnalysisPage = () => { } return ( - -
- - + ) } diff --git a/redisinsight/ui/src/pages/database-analysis/DatabaseAnalysisPageView.stories.tsx b/redisinsight/ui/src/pages/database-analysis/DatabaseAnalysisPageView.stories.tsx new file mode 100644 index 0000000000..85c41e2a05 --- /dev/null +++ b/redisinsight/ui/src/pages/database-analysis/DatabaseAnalysisPageView.stories.tsx @@ -0,0 +1,64 @@ +import React, { useEffect, useMemo } from 'react' +import type { Meta, StoryObj } from '@storybook/react-vite' +import { buildDatabaseAnalysisWithTopKeys } from 'uiSrc/mocks/factories/database-analysis/DatabaseAnalysis.factory' + +import { DatabaseAnalysisPageView } from './DatabaseAnalysisPageView' +import { + getDBAnalysisSuccess, + loadDBAnalysisReportsSuccess, + setSelectedAnalysisId, +} from 'uiSrc/slices/analytics/dbAnalysis' +import { useDispatch } from 'react-redux' +import { fn } from 'storybook/test' + +const meta: Meta = { + component: DatabaseAnalysisPageView, + args: { + reports: [], + selectedAnalysis: null, + analysisLoading: false, + data: null, + handleSelectAnalysis: fn(), + }, +} + +export default meta + +type Story = StoryObj + +export const Loading: Story = { + args: { + reports: [], + selectedAnalysis: null, + analysisLoading: true, + data: null, + }, +} + +const WithDataRender = () => { + const dispatch = useDispatch() + const { data, reports } = useMemo( + () => buildDatabaseAnalysisWithTopKeys(), + [], + ) + + useEffect(() => { + dispatch(getDBAnalysisSuccess(data)) + dispatch(loadDBAnalysisReportsSuccess(reports)) + dispatch(setSelectedAnalysisId(data.id)) + }, [dispatch, data, reports]) + + return ( + + ) +} + +export const WithData: Story = { + render: () => , +} diff --git a/redisinsight/ui/src/pages/database-analysis/DatabaseAnalysisPageView.tsx b/redisinsight/ui/src/pages/database-analysis/DatabaseAnalysisPageView.tsx new file mode 100644 index 0000000000..ed5e98c2e2 --- /dev/null +++ b/redisinsight/ui/src/pages/database-analysis/DatabaseAnalysisPageView.tsx @@ -0,0 +1,41 @@ +import React from 'react' +import { + type DatabaseAnalysis, + type ShortDatabaseAnalysis, +} from 'apiSrc/modules/database-analysis/models' +import { Nullable } from 'uiSrc/utils' +import { MainContainer } from './components/styles' +import { Header } from './components' +import DatabaseAnalysisTabs from './components/data-nav-tabs' + +type Props = { + reports: ShortDatabaseAnalysis[] + selectedAnalysis: Nullable + analysisLoading: boolean + data: DatabaseAnalysis | null + handleSelectAnalysis: (value: string) => void +} +export const DatabaseAnalysisPageView = ({ + reports, + selectedAnalysis, + analysisLoading, + data, + handleSelectAnalysis, +}: Props) => { + return ( + +
+ + + ) +} diff --git a/redisinsight/ui/src/pages/database-analysis/components/analysis-data-view/AnalysisDataView.styles.ts b/redisinsight/ui/src/pages/database-analysis/components/analysis-data-view/AnalysisDataView.styles.ts new file mode 100644 index 0000000000..f570870c97 --- /dev/null +++ b/redisinsight/ui/src/pages/database-analysis/components/analysis-data-view/AnalysisDataView.styles.ts @@ -0,0 +1,10 @@ +import styled from 'styled-components' +import { scrollbarStyles } from 'uiSrc/styles/mixins' +import { Theme } from 'uiSrc/components/base/theme/types' + +export const ContentWrapper = styled.div` + ${scrollbarStyles()} + max-height: 100%; + padding: ${({ theme }: { theme: Theme }) => + `${theme.core.space.space300} ${theme.core.space.space500}`}; +` diff --git a/redisinsight/ui/src/pages/database-analysis/components/analysis-data-view/AnalysisDataView.tsx b/redisinsight/ui/src/pages/database-analysis/components/analysis-data-view/AnalysisDataView.tsx index e6f653c8dc..e96be30b96 100644 --- a/redisinsight/ui/src/pages/database-analysis/components/analysis-data-view/AnalysisDataView.tsx +++ b/redisinsight/ui/src/pages/database-analysis/components/analysis-data-view/AnalysisDataView.tsx @@ -1,7 +1,6 @@ import React, { useEffect, useState } from 'react' import { useSelector } from 'react-redux' -import cx from 'classnames' import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' import { dbAnalysisSelector, @@ -21,7 +20,7 @@ import { ExpirationGroupsView, } from 'uiSrc/pages/database-analysis/components' -import styles from './styles.module.scss' +import { ContentWrapper } from './AnalysisDataView.styles' const AnalysisDataView = () => { const { id: instanceId, provider } = useSelector(connectedInstanceSelector) @@ -54,29 +53,27 @@ const AnalysisDataView = () => { } return ( - <> -
- - - - -
- + + + + + + ) } diff --git a/redisinsight/ui/src/pages/database-analysis/components/analysis-ttl-view/ExpirationGroupsView.stories.tsx b/redisinsight/ui/src/pages/database-analysis/components/analysis-ttl-view/ExpirationGroupsView.stories.tsx new file mode 100644 index 0000000000..c0b726898a --- /dev/null +++ b/redisinsight/ui/src/pages/database-analysis/components/analysis-ttl-view/ExpirationGroupsView.stories.tsx @@ -0,0 +1,69 @@ +import React, { useEffect, useMemo } from 'react' +import type { Meta, StoryObj } from '@storybook/react-vite' +import { fn } from 'storybook/test' +import { useDispatch } from 'react-redux' + +import ExpirationGroupsView from './ExpirationGroupsView' +import { setShowNoExpiryGroup } from 'uiSrc/slices/analytics/dbAnalysis' + +const meta: Meta = { + component: ExpirationGroupsView, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +} + +export default meta + +type Story = StoryObj + +const sampleData = { + totalMemory: { total: 50000 }, + totalKeys: { total: 5000 }, + expirationGroups: [ + { label: 'No Expiry', total: 15000, threshold: 0 }, + { label: '<1 hr', total: 2000, threshold: 3600 }, + { label: '1-4 Hrs', total: 3000, threshold: 14400 }, + { label: '4-12 Hrs', total: 2500, threshold: 43200 }, + { label: '12-24 Hrs', total: 2000, threshold: 86400 }, + { label: '1-7 Days', total: 1500, threshold: 604800 }, + { label: '>7 Days', total: 1000, threshold: 2592000 }, + { label: '>1 Month', total: 500, threshold: 9007199254740991 }, + ], +} + +const DefaultRender = () => { + const dispatch = useDispatch() + + const data = useMemo(() => sampleData, []) + + useEffect(() => { + dispatch(setShowNoExpiryGroup(true)) + }, [dispatch]) + + return ( + + ) +} + +export const Default: Story = { + render: () => , +} + +export const Loading: Story = { + args: { + data: null, + loading: true, + extrapolation: 1, + onSwitchExtrapolation: fn(), + }, +} diff --git a/redisinsight/ui/src/pages/database-analysis/components/analysis-ttl-view/ExpirationGroupsView.styles.ts b/redisinsight/ui/src/pages/database-analysis/components/analysis-ttl-view/ExpirationGroupsView.styles.ts new file mode 100644 index 0000000000..579fd4deac --- /dev/null +++ b/redisinsight/ui/src/pages/database-analysis/components/analysis-ttl-view/ExpirationGroupsView.styles.ts @@ -0,0 +1,62 @@ +import styled from 'styled-components' +import { Theme } from 'uiSrc/components/base/theme/types' +import { + Section, + sectionContent, + SectionTitleWrapper, +} from 'uiSrc/pages/database-analysis/components/styles' +import { SwitchInput } from 'uiSrc/components/base/inputs' + +export const Container = styled(Section)` + position: relative; + padding-right: 0; + + @media screen and (max-width: 920px) { + ${SectionTitleWrapper} { + flex-direction: column; + align-items: flex-start !important; + } + } +` + +export const TitleWrapper = styled.div` + flex: 1; + display: flex; + align-items: center; + + @media screen and (max-width: 920px) { + margin-bottom: ${({ theme }: { theme: Theme }) => + theme.core.space.space150}; + } +` + +export const Content = styled.div` + width: 100%; + height: 300px; + ${sectionContent} +` + +export const LoadingWrapper = styled(Content)` + margin-top: ${({ theme }: { theme: Theme }) => theme.core.space.space400}; + background-color: ${({ theme }: { theme: Theme }) => + theme.semantic.color.background.neutral200}; + border-radius: ${({ theme }: { theme: Theme }) => theme.core.space.space050}; +` + +export const Chart = styled.div>` + height: calc( + 100% - ${({ theme }: { theme: Theme }) => theme.core.space.space400} + ); + clear: both; + width: 100%; +` + +export const Switch = styled(SwitchInput)` + float: right; + padding-right: ${({ theme }: { theme: Theme }) => theme.core.space.space200}; + + @media screen and (max-width: 920px) { + margin-left: ${({ theme }: { theme: Theme }) => + `-${theme.core.space.space100}`}; + } +` diff --git a/redisinsight/ui/src/pages/database-analysis/components/analysis-ttl-view/ExpirationGroupsView.tsx b/redisinsight/ui/src/pages/database-analysis/components/analysis-ttl-view/ExpirationGroupsView.tsx index 81337887b1..9138c6a073 100644 --- a/redisinsight/ui/src/pages/database-analysis/components/analysis-ttl-view/ExpirationGroupsView.tsx +++ b/redisinsight/ui/src/pages/database-analysis/components/analysis-ttl-view/ExpirationGroupsView.tsx @@ -1,12 +1,11 @@ import { useDispatch, useSelector } from 'react-redux' import React, { useEffect, useState } from 'react' -import cx from 'classnames' import AutoSizer from 'react-virtualized-auto-sizer' import { DEFAULT_EXTRAPOLATION, SectionName, -} from 'uiSrc/pages/database-analysis' +} from 'uiSrc/pages/database-analysis/constants' import { extrapolate, formatBytes, @@ -25,11 +24,21 @@ import { dbAnalysisReportsSelector, setShowNoExpiryGroup, } from 'uiSrc/slices/analytics/dbAnalysis' -import { SwitchInput } from 'uiSrc/components/base/inputs' import { Title } from 'uiSrc/components/base/text/Title' import { DatabaseAnalysis } from 'apiSrc/modules/database-analysis/models' -import styles from './styles.module.scss' +import { + SectionTitleWrapper, + SwitchExtrapolateResults, +} from 'uiSrc/pages/database-analysis/components/styles' +import { + Chart, + Container, + Content, + LoadingWrapper, + Switch, + TitleWrapper, +} from './ExpirationGroupsView.styles' export interface Props { data: Nullable @@ -83,12 +92,7 @@ const ExpirationGroupsView = (props: Props) => { }, [data?.expirationGroups, showNoExpiryGroup, isExtrapolated, extrapolation]) if (loading) { - return ( -
- ) + return } const onSwitchChange = (value: boolean) => { @@ -107,16 +111,14 @@ const ExpirationGroupsView = (props: Props) => { const yCountTicks = DEFAULT_Y_TICKS return ( -
-
-
+ + + MEMORY LIKELY TO BE FREED OVER TIME {extrapolation !== DEFAULT_EXTRAPOLATION && ( - { @@ -129,20 +131,18 @@ const ExpirationGroupsView = (props: Props) => { data-testid="extrapolate-results" /> )} -
- + -
-
-
+ + + - {({ width, height }) => ( + {({ width = 0, height = 0 }) => ( { /> )} -
-
-
+ + + ) } diff --git a/redisinsight/ui/src/pages/database-analysis/components/base/TableTextBtn.tsx b/redisinsight/ui/src/pages/database-analysis/components/base/TableTextBtn.tsx index 35f7dd50e5..dc00962b49 100644 --- a/redisinsight/ui/src/pages/database-analysis/components/base/TableTextBtn.tsx +++ b/redisinsight/ui/src/pages/database-analysis/components/base/TableTextBtn.tsx @@ -1,43 +1,21 @@ import styled, { css } from 'styled-components' import React from 'react' import { EmptyButton } from 'uiSrc/components/base/forms/buttons' +import { Theme } from 'uiSrc/components/base/theme/types' -const expandedStyle = css` +const expandedStyle = css<{ theme: Theme }>` padding: 0 20px 0 12px; - color: var(--euiTextSubduedColor) !important; - - &:hover, - &:focus { - color: var(--euiTextSubduedColorHover) !important; - } ` -/** - * Text button component in top namespaces table - * - * This is how we can implement custom styles - */ -export const TableTextBtn = styled(EmptyButton)< + +export const TableTextBtn = styled(EmptyButton).attrs({ + variant: 'primary-inline', +})< React.ComponentProps & { $expanded?: boolean + theme: Theme } >` - width: max-content; - - &:hover, - &:focus { - background-color: transparent !important; - text-decoration: underline; - color: var(--euiTextSubduedColorHover); - } - padding: 0; - font: - normal normal normal 13px/17px Graphik, - sans-serif; - color: var(--buttonSecondaryTextColor) !important; - ${({ $expanded }) => { - if (!$expanded) { - return '' - } - return expandedStyle - }} + max-width: calc(100% - 20px); + width: auto; + ${({ $expanded }) => $expanded && expandedStyle} ` diff --git a/redisinsight/ui/src/pages/database-analysis/components/data-nav-tabs/DatabaseAnalysisTabs.styles.ts b/redisinsight/ui/src/pages/database-analysis/components/data-nav-tabs/DatabaseAnalysisTabs.styles.ts new file mode 100644 index 0000000000..f07008935f --- /dev/null +++ b/redisinsight/ui/src/pages/database-analysis/components/data-nav-tabs/DatabaseAnalysisTabs.styles.ts @@ -0,0 +1,5 @@ +import styled from 'styled-components' + +export const EmptyMessageContainer = styled.div>` + height: calc(100% - 96px); +` diff --git a/redisinsight/ui/src/pages/database-analysis/components/data-nav-tabs/DatabaseAnalysisTabs.tsx b/redisinsight/ui/src/pages/database-analysis/components/data-nav-tabs/DatabaseAnalysisTabs.tsx index df0fcb62ac..7c0a280a4d 100644 --- a/redisinsight/ui/src/pages/database-analysis/components/data-nav-tabs/DatabaseAnalysisTabs.tsx +++ b/redisinsight/ui/src/pages/database-analysis/components/data-nav-tabs/DatabaseAnalysisTabs.tsx @@ -23,7 +23,7 @@ import { import Recommendations from '../recommendations-view' import AnalysisDataView from '../analysis-data-view' -import styles from './styles.module.scss' +import { EmptyMessageContainer } from './DatabaseAnalysisTabs.styles' export interface Props { loading: boolean @@ -64,6 +64,7 @@ const DatabaseAnalysisTabs = (props: Props) => { anchorPosition: 'downLeft', }, viewTab === DatabaseAnalysisViewTab.Recommendations, + 'analytics-recommendations-tab', ), value: DatabaseAnalysisViewTab.Recommendations, content: , @@ -102,22 +103,16 @@ const DatabaseAnalysisTabs = (props: Props) => { if (!loading && !reports?.length) { return ( -
+ -
+ ) } if (!loading && !!reports?.length && isNull(data?.totalKeys)) { return ( -
+ -
+ ) } diff --git a/redisinsight/ui/src/pages/database-analysis/components/data-nav-tabs/styles.module.scss b/redisinsight/ui/src/pages/database-analysis/components/data-nav-tabs/styles.module.scss deleted file mode 100644 index d77c45174c..0000000000 --- a/redisinsight/ui/src/pages/database-analysis/components/data-nav-tabs/styles.module.scss +++ /dev/null @@ -1,8 +0,0 @@ -.container { - flex-grow: 1; - overflow: hidden; -} - -.emptyMessageWrapper { - height: calc(100% - 96px); -} diff --git a/redisinsight/ui/src/pages/database-analysis/components/header/Header.styles.ts b/redisinsight/ui/src/pages/database-analysis/components/header/Header.styles.ts new file mode 100644 index 0000000000..e1e3a057e3 --- /dev/null +++ b/redisinsight/ui/src/pages/database-analysis/components/header/Header.styles.ts @@ -0,0 +1,20 @@ +import styled from 'styled-components' +import { Row } from 'uiSrc/components/base/layout/flex' +import { RiIcon } from 'uiSrc/components/base/icons' +import { RiSelect } from 'uiSrc/components/base/forms/select/RiSelect' + +export const Container = styled(Row).attrs({ + align: 'center', +})` + padding: 12px 0; +` +export const InfoIcon = styled(RiIcon).attrs({ + type: 'InfoIcon', + size: 'l', +})` + cursor: pointer; +` + +export const HeaderSelect = styled(RiSelect)` + border: 0 none; +` diff --git a/redisinsight/ui/src/pages/database-analysis/components/header/Header.tsx b/redisinsight/ui/src/pages/database-analysis/components/header/Header.tsx index 6699ecb31e..e4cf8e4420 100644 --- a/redisinsight/ui/src/pages/database-analysis/components/header/Header.tsx +++ b/redisinsight/ui/src/pages/database-analysis/components/header/Header.tsx @@ -1,9 +1,6 @@ import React from 'react' -import cx from 'classnames' import { useDispatch, useSelector } from 'react-redux' import { useParams } from 'react-router-dom' - -import styled from 'styled-components' import { CaretRightIcon } from 'uiSrc/components/base/icons' import { createNewAnalysis } from 'uiSrc/slices/analytics/dbAnalysis' import { numberWithSpaces } from 'uiSrc/utils/numbers' @@ -24,16 +21,12 @@ import { FlexItem, Row } from 'uiSrc/components/base/layout/flex' import { HideFor } from 'uiSrc/components/base/utils/ShowHide' import { PrimaryButton } from 'uiSrc/components/base/forms/buttons' import { Text } from 'uiSrc/components/base/text' -import { RiSelect } from 'uiSrc/components/base/forms/select/RiSelect' -import { RiIcon } from 'uiSrc/components/base/icons/RiIcon' import { ShortDatabaseAnalysis } from 'apiSrc/modules/database-analysis/models' import { AnalysisProgress } from 'apiSrc/modules/database-analysis/models/analysis-progress' import styles from './styles.module.scss' +import { Container, HeaderSelect, InfoIcon } from './Header.styles' -const HeaderSelect = styled(RiSelect)` - border: 0 none; -` export interface Props { items: ShortDatabaseAnalysis[] selectedValue: Nullable @@ -88,19 +81,13 @@ const Header = (props: Props) => { return (
- + {!!items.length && ( - - Report generated on: - + Report generated on: @@ -117,11 +104,7 @@ const Header = (props: Props) => { {!!progress && ( @@ -132,15 +115,14 @@ const Header = (props: Props) => { ? undefined : 'warning' } - className={cx(styles.progress, styles.text)} + className={styles.progress} size="s" data-testid="analysis-progress" > - {'Scanned '} - {getApproximatePercentage( + {`Scanned ${getApproximatePercentage( progress.total, progress.processed, - )} + )}`} {` (${numberWithSpaces(progress.processed)}`}/ {numberWithSpaces(progress.total)} @@ -152,43 +134,33 @@ const Header = (props: Props) => { )} - - - - New Report - - - - - - - + + + New Report + + + + - +
) } diff --git a/redisinsight/ui/src/pages/database-analysis/components/header/styles.module.scss b/redisinsight/ui/src/pages/database-analysis/components/header/styles.module.scss index 0855d970ad..f73524e1a1 100644 --- a/redisinsight/ui/src/pages/database-analysis/components/header/styles.module.scss +++ b/redisinsight/ui/src/pages/database-analysis/components/header/styles.module.scss @@ -26,3 +26,7 @@ color: var(--euiColorMediumShade); cursor: pointer; } + +.tooltipAnchor { + display: inline-flex; +} diff --git a/redisinsight/ui/src/pages/database-analysis/components/index.ts b/redisinsight/ui/src/pages/database-analysis/components/index.ts index c0a76143c7..d525fcc11e 100644 --- a/redisinsight/ui/src/pages/database-analysis/components/index.ts +++ b/redisinsight/ui/src/pages/database-analysis/components/index.ts @@ -4,8 +4,8 @@ import EmptyAnalysisMessage from './empty-analysis-message' import Header from './header' import SummaryPerData from './summary-per-data' import TableLoader from './table-loader' -import TopKeys from './top-keys' -import TopNamespace from './top-namespace' +import TopKeys from './top-keys/TopKeys' +import TopNamespace from './top-namespace/TopNamespace' export { AnalysisDataView, diff --git a/redisinsight/ui/src/pages/database-analysis/components/styles.ts b/redisinsight/ui/src/pages/database-analysis/components/styles.ts new file mode 100644 index 0000000000..65ff1bb03d --- /dev/null +++ b/redisinsight/ui/src/pages/database-analysis/components/styles.ts @@ -0,0 +1,43 @@ +import React from 'react' +import styled, { css } from 'styled-components' +import { Theme } from 'uiSrc/components/base/theme/types' +import { SwitchInput } from 'uiSrc/components/base/inputs' +import { Title } from 'uiSrc/components/base/text' +import { Col, Row } from 'uiSrc/components/base/layout/flex' + +export const Section = styled.div>` + border-radius: ${({ theme }: { theme: Theme }) => theme.core.space.space200}; + margin-top: ${({ theme }: { theme: Theme }) => theme.core.space.space250}; + overflow: hidden; + padding: ${({ theme }: { theme: Theme }) => + `${theme.core.space.space300} ${theme.core.space.space500}`}; + + @media screen and (max-width: 920px) { + padding: ${({ theme }: { theme: Theme }) => + `${theme.core.space.space200} ${theme.core.space.space250}`}; + } +` + +export const sectionContent = css` + max-width: 1720px; + margin: 0 auto; +` + +export const SectionTitleWrapper = styled(Row).attrs({ align: 'center' })` + margin-bottom: ${({ theme }: { theme: Theme }) => theme.core.space.space250}; +` + +export const SwitchExtrapolateResults = styled(SwitchInput)` + margin-left: ${({ theme }: { theme: Theme }) => theme.core.space.space300}; +` + +export const SectionTitle = styled(Title)` + display: inline-block; +` + +// Styled component for the main container with theme border +export const MainContainer = styled(Col)>` + height: 100%; + overflow: auto; + padding-inline: ${({ theme }: { theme: Theme }) => theme.core.space.space200}; +` diff --git a/redisinsight/ui/src/pages/database-analysis/components/summary-per-data/SummaryPerData.styles.ts b/redisinsight/ui/src/pages/database-analysis/components/summary-per-data/SummaryPerData.styles.ts new file mode 100644 index 0000000000..a8cad18447 --- /dev/null +++ b/redisinsight/ui/src/pages/database-analysis/components/summary-per-data/SummaryPerData.styles.ts @@ -0,0 +1,52 @@ +import styled from 'styled-components' +import { Theme } from 'uiSrc/components/base/theme/types' +import { insightsOpen } from 'uiSrc/styles/mixins' +import { sectionContent } from 'uiSrc/pages/database-analysis/components/styles' + +export const Wrapper = styled.div>`` + +export const ChartsWrapper = styled.div< + React.HTMLAttributes & { + $isSection?: boolean + $isLoading?: boolean + } +>` + display: flex; + align-items: center; + justify-content: space-around; + ${insightsOpen()` + flex-direction: column; + `} + ${({ $isSection }) => $isSection && sectionContent} + ${({ $isLoading, theme }) => + $isLoading && `margin-top: ${theme.core.space.space400};`} +` + +export const PreloaderCircle = styled.div>` + width: 180px; + height: 180px; + margin: 60px 0; + border-radius: 100%; + background-color: ${({ theme }: { theme: Theme }) => + theme.semantic.color.background.neutral200}; +` +export const LabelTooltip = styled.div>` + font-size: 12px; + font-weight: bold; +` + +export const TooltipPercentage = styled.span< + React.HTMLAttributes +>` + margin-right: ${({ theme }: { theme: Theme }) => theme.core.space.space100}; +` + +export const TitleSeparator = styled.hr>` + height: 1px; + border: 0; + background-color: ${({ theme }: { theme: Theme }) => + theme.semantic.color.border.neutral500}; + margin: ${({ theme }: { theme: Theme }) => theme.core.space.space050} 0; + min-width: 60px; + width: 100%; +` diff --git a/redisinsight/ui/src/pages/database-analysis/components/summary-per-data/SummaryPerData.tsx b/redisinsight/ui/src/pages/database-analysis/components/summary-per-data/SummaryPerData.tsx index 8f5ef62d85..55b21a1398 100644 --- a/redisinsight/ui/src/pages/database-analysis/components/summary-per-data/SummaryPerData.tsx +++ b/redisinsight/ui/src/pages/database-analysis/components/summary-per-data/SummaryPerData.tsx @@ -1,4 +1,3 @@ -import cx from 'classnames' import React, { useCallback, useEffect, useState } from 'react' import { DonutChart } from 'uiSrc/components/charts' @@ -15,16 +14,28 @@ import { Nullable, } from 'uiSrc/utils' import { getPercentage, numberWithSpaces } from 'uiSrc/utils/numbers' - -import { SwitchInput } from 'uiSrc/components/base/inputs' import { Title } from 'uiSrc/components/base/text/Title' -import { RiIcon } from 'uiSrc/components/base/icons/RiIcon' +import { RiIcon, AllIconsType } from 'uiSrc/components/base/icons/RiIcon' import { DatabaseAnalysis, SimpleTypeSummary, } from 'apiSrc/modules/database-analysis/models' -import styles from './styles.module.scss' +import { + ChartsWrapper, + LabelTooltip, + PreloaderCircle, + TitleSeparator, + TooltipPercentage, + Wrapper, +} from './SummaryPerData.styles' +import { + Section, + SectionTitleWrapper, + SwitchExtrapolateResults, +} from 'uiSrc/pages/database-analysis/components/styles' +import { Col, Row } from 'uiSrc/components/base/layout/flex' +import { Text } from 'uiSrc/components/base/text' export interface Props { data: Nullable @@ -33,9 +44,47 @@ export interface Props { onSwitchExtrapolation?: (value: boolean, section: SectionName) => void } +interface DonutChartTitleProps { + icon: AllIconsType + title: string + value: string | number + testId: string +} + +const DonutChartTitle = ({ + icon, + title, + value, + testId, +}: DonutChartTitleProps) => ( + + + + {title} + + + + {String(value)} + + +) + const widthResponsiveSize = 1024 const CHART_WITH_LABELS_WIDTH = 432 const CHART_WIDTH = 320 +const getChartData = (t: SimpleTypeSummary): ChartData => ({ + value: t.total, + name: getGroupTypeDisplay(t.type), + color: + t.type in GROUP_TYPES_COLORS + ? GROUP_TYPES_COLORS[t.type as GroupTypesColors] + : 'var(--defaultTypeColor)', + meta: { ...t }, +}) const SummaryPerData = ({ data, @@ -49,16 +98,6 @@ const SummaryPerData = ({ const [isExtrapolated, setIsExtrapolated] = useState(true) const [hideLabelTitle, setHideLabelTitle] = useState(false) - const getChartData = (t: SimpleTypeSummary) => ({ - value: t.total, - name: getGroupTypeDisplay(t.type), - color: - t.type in GROUP_TYPES_COLORS - ? GROUP_TYPES_COLORS[t.type as GroupTypesColors] - : 'var(--defaultTypeColor)', - meta: { ...t }, - }) - const updateChartSize = () => { setHideLabelTitle(globalThis.innerWidth < widthResponsiveSize) } @@ -77,84 +116,61 @@ const SummaryPerData = ({ useEffect(() => { if (data && totalMemory && totalKeys) { - setMemoryData(totalMemory.types?.map(getChartData) as ChartData[]) - setKeysData(totalKeys.types?.map(getChartData) as ChartData[]) + setMemoryData(totalMemory.types?.map(getChartData)) + setKeysData(totalKeys.types?.map(getChartData)) } }, [data]) const renderMemoryTooltip = useCallback( ({ value, name }: ChartData) => ( -
- - - {name}:{' '} - - - {getPercentage(value, totalMemory?.total)}% - - - (  - {extrapolate( - value, - { extrapolation, apply: isExtrapolated }, - (val: number) => formatBytes(val, 3, false) as string, - )} -  ) - - -
+ + {name}: + + {getPercentage(value, totalMemory?.total)}% + + + (  + {extrapolate( + value, + { extrapolation, apply: isExtrapolated }, + (val: number) => formatBytes(val, 3, false) as string, + )} +  ) + + ), [totalMemory, extrapolation, isExtrapolated], ) const renderKeysTooltip = useCallback( ({ name, value }: ChartData) => ( -
- - - {name}:{' '} - - - {getPercentage(value, totalKeys?.total)}% - - - (  - {extrapolate( - value, - { extrapolation, apply: isExtrapolated }, - (val: number) => numberWithSpaces(Math.round(val)), - )} -  ) - - -
+ + {name}: + + {getPercentage(value, totalKeys?.total)}% + + + (  + {extrapolate( + value, + { extrapolation, apply: isExtrapolated }, + (val: number) => numberWithSpaces(Math.round(val)), + )} +  ) + + ), [totalKeys, extrapolation, isExtrapolated], ) if (loading) { return ( -
-
-
-
-
-
+ + + + + + ) } @@ -166,18 +182,11 @@ const SummaryPerData = ({ } return ( -
-
- - SUMMARY PER DATA TYPE - +
+ + SUMMARY PER DATA TYPE {extrapolation !== DEFAULT_EXTRAPOLATION && ( - { @@ -187,11 +196,8 @@ const SummaryPerData = ({ data-testid="extrapolate-results" /> )} -
-
+ + -
- - Memory -
-
-
- {extrapolate( - totalMemory?.total || 0, - { extrapolation, apply: isExtrapolated }, - (val: number) => formatBytes(val || 0, 3) as string, - )} -
-
+ formatBytes(val || 0, 3) as string, + )} + /> } /> -
- - Keys -
-
-
- {extrapolate( - totalKeys?.total || 0, - { extrapolation, apply: isExtrapolated }, - (val: number) => - numberWithSpaces(Math.round(val) || 0) as string, - )} -
-
+ + numberWithSpaces(Math.round(val) || 0) as string, + )} + /> } /> -
-
+ + ) } diff --git a/redisinsight/ui/src/pages/database-analysis/components/summary-per-data/styles.module.scss b/redisinsight/ui/src/pages/database-analysis/components/summary-per-data/styles.module.scss deleted file mode 100644 index c2790f46fb..0000000000 --- a/redisinsight/ui/src/pages/database-analysis/components/summary-per-data/styles.module.scss +++ /dev/null @@ -1,63 +0,0 @@ -.wrapper { - .chartsWrapper { - display: flex; - align-items: center; - justify-content: space-around; - } - - &.loadingWrapper { - margin-top: 36px; - } - - .preloaderCircle { - width: 180px; - height: 180px; - margin: 60px 0; - border-radius: 100%; - background-color: var(--euiColorLightestShade); - } - - .chartCenter { - display: flex; - flex-direction: column; - align-items: center; - } - - .chartTitle { - display: flex; - align-items: center; - - .icon { - margin-right: 10px; - } - } - - .titleSeparator { - height: 1px; - border: 0; - background-color: var(--separatorColorLight); - margin: 6px 0; - width: 60px; - } - - .centerCount { - margin-top: 2px; - font-weight: 500; - font-size: 14px; - } - - .labelTooltip { - font-size: 12px; - .tooltipPercentage { - margin-right: 8px; - } - } -} - -@include global.insights-open { - .wrapper { - .chartsWrapper { - flex-direction: column; - } - } -} diff --git a/redisinsight/ui/src/pages/database-analysis/components/table-loader/TableLoader.styles.ts b/redisinsight/ui/src/pages/database-analysis/components/table-loader/TableLoader.styles.ts new file mode 100644 index 0000000000..b8ff9e3408 --- /dev/null +++ b/redisinsight/ui/src/pages/database-analysis/components/table-loader/TableLoader.styles.ts @@ -0,0 +1,25 @@ +import React from 'react' +import styled from 'styled-components' +import { Theme } from 'uiSrc/components/base/theme/types' +import LoadingContent from 'uiSrc/components/base/layout/loading-content/LoadingContent' +import { SingleLine } from 'uiSrc/components/base/layout/loading-content/loading-content.styles' + +export const Container = styled.div>` + margin-top: ${({ theme }: { theme: Theme }) => theme.core.space.space250}; +` +export const TableLoaderTitle = styled(LoadingContent)` + ${SingleLine} { + height: ${({ theme }) => theme.core.space.space550} !important; + margin-bottom: ${({ theme }) => theme.core.space.space150}; + } +` +export const TableLoaderTable = styled(LoadingContent)` + ${SingleLine} { + height: ${({ theme }) => theme.core.space.space400} !important; + margin-bottom: ${({ theme }) => theme.core.space.space100}; + + &:last-child:not(:only-child) { + width: 100% !important; + } + } +` diff --git a/redisinsight/ui/src/pages/database-analysis/components/table-loader/TableLoader.tsx b/redisinsight/ui/src/pages/database-analysis/components/table-loader/TableLoader.tsx index d688cf59e0..b3386fd140 100644 --- a/redisinsight/ui/src/pages/database-analysis/components/table-loader/TableLoader.tsx +++ b/redisinsight/ui/src/pages/database-analysis/components/table-loader/TableLoader.tsx @@ -1,13 +1,15 @@ import React from 'react' - -import { LoadingContent } from 'uiSrc/components/base/layout' -import styles from './styles.module.scss' +import { + Container, + TableLoaderTable, + TableLoaderTitle, +} from './TableLoader.styles' const TableLoader = () => ( -
- - -
+ + + + ) export default TableLoader diff --git a/redisinsight/ui/src/pages/database-analysis/components/table-loader/styles.module.scss b/redisinsight/ui/src/pages/database-analysis/components/table-loader/styles.module.scss deleted file mode 100644 index d2db2ee0fd..0000000000 --- a/redisinsight/ui/src/pages/database-analysis/components/table-loader/styles.module.scss +++ /dev/null @@ -1,17 +0,0 @@ -.container { - margin-top: 20px; -} - -.title :global(.euiLoadingContent__singleLine) { - height: 42px !important; - margin-bottom: 12px; -} - -.table :global(.euiLoadingContent__singleLine) { - height: 36px !important; - margin-bottom: 6px; - - &:last-child:not(:only-child) { - width: 100% !important; - } -} diff --git a/redisinsight/ui/src/pages/database-analysis/components/top-keys/TopKeys.spec.tsx b/redisinsight/ui/src/pages/database-analysis/components/top-keys/TopKeys.spec.tsx index 206ca32faf..0036e09ca4 100644 --- a/redisinsight/ui/src/pages/database-analysis/components/top-keys/TopKeys.spec.tsx +++ b/redisinsight/ui/src/pages/database-analysis/components/top-keys/TopKeys.spec.tsx @@ -1,6 +1,7 @@ import React from 'react' import { instance, mock } from 'ts-mockito' import { fireEvent, render, screen } from 'uiSrc/utils/test-utils' +import { DatabaseAnalysisFactory } from 'uiSrc/mocks/factories/database-analysis/DatabaseAnalysis.factory' import TopKeys, { Props } from './TopKeys' @@ -14,10 +15,10 @@ const mockKey = { ttl: -1, } -const mockData = { +const mockData = DatabaseAnalysisFactory.build({ topKeysLength: [mockKey], topKeysMemory: [mockKey], -} +}) describe('TopKeys', () => { it('should render', () => { @@ -64,12 +65,12 @@ describe('TopKeys', () => { }) it('should not render tables when topKeysLength and topKeysMemory are empty array', () => { - const mockData = { + const emptyMockData = DatabaseAnalysisFactory.build({ topKeysLength: [], topKeysMemory: [], - } + }) const { queryByTestId } = render( - , + , ) expect(queryByTestId('top-keys-table-memory')).not.toBeInTheDocument() @@ -94,13 +95,13 @@ describe('TopKeys', () => { }) it('should render TOP 15 KEYS title', () => { - const mockData = { + const largeMockData = DatabaseAnalysisFactory.build({ topKeysLength: Array.from({ length: 15 }, () => mockKey), topKeysMemory: Array.from({ length: 15 }, () => mockKey), - } + }) const { queryByText } = render( - , + , ) expect(queryByText('TOP KEYS')).not.toBeInTheDocument() diff --git a/redisinsight/ui/src/pages/database-analysis/components/top-keys/TopKeys.stories.tsx b/redisinsight/ui/src/pages/database-analysis/components/top-keys/TopKeys.stories.tsx new file mode 100644 index 0000000000..fb6c1fbb90 --- /dev/null +++ b/redisinsight/ui/src/pages/database-analysis/components/top-keys/TopKeys.stories.tsx @@ -0,0 +1,123 @@ +import React from 'react' +import type { Meta, StoryObj } from '@storybook/react-vite' +import { DatabaseAnalysisFactory } from 'uiSrc/mocks/factories/database-analysis/DatabaseAnalysis.factory' + +import TopKeys from './TopKeys' + +const meta: Meta = { + component: TopKeys, + args: { + data: null, + loading: false, + }, + decorators: [ + (Story) => ( +
+

Top Keys

+ +
+ ), + ], +} + +export default meta + +type Story = StoryObj + +export const Loading: Story = { + args: { + loading: true, + }, + decorators: [ + (Story) => ( +
+

Loading

+

Component shows loader while fetching keys data

+ +
+ ), + ], +} + +export const NoKeys: Story = { + args: { + loading: false, + data: DatabaseAnalysisFactory.build({ + topKeysMemory: [], + topKeysLength: [], + }), + }, + decorators: [ + (Story) => ( +
+

No Keys

+

Component returns null when no top keys data is available

+ +
+ ), + ], +} + +export const WithData: Story = { + args: { + loading: false, + data: DatabaseAnalysisFactory.build({ + topKeysMemory: [ + { + name: 'user:sessions', + type: 'hash', + memory: 1_000_000, + length: 5000, + ttl: -1, + }, + { + name: 'orders:recent', + type: 'list', + memory: 500_000, + length: 2000, + ttl: 3600, + }, + { + name: 'metrics:pageviews', + type: 'zset', + memory: 250_000, + length: 1000, + ttl: -1, + }, + ], + topKeysLength: [ + { + name: 'users:all', + type: 'set', + memory: 400_000, + length: 10000, + ttl: -1, + }, + { + name: 'logs:errors', + type: 'list', + memory: 150_000, + length: 5000, + ttl: 7200, + }, + { + name: 'cache:products', + type: 'hash', + memory: 80_000, + length: 2500, + ttl: 86400, + }, + ], + delimiter: ':', + }), + }, + decorators: [ + (Story) => ( +
+

With Data - Memory View

+

Component shows keys sorted by memory consumption (default view)

+ +
+ ), + ], +} diff --git a/redisinsight/ui/src/pages/database-analysis/components/top-keys/TopKeys.tsx b/redisinsight/ui/src/pages/database-analysis/components/top-keys/TopKeys.tsx index 7e1a5e078b..67787113f5 100644 --- a/redisinsight/ui/src/pages/database-analysis/components/top-keys/TopKeys.tsx +++ b/redisinsight/ui/src/pages/database-analysis/components/top-keys/TopKeys.tsx @@ -1,13 +1,17 @@ import React, { useState } from 'react' -import cx from 'classnames' -import { TableView } from 'uiSrc/pages/database-analysis' +import { TableView } from 'uiSrc/pages/database-analysis/constants' import { Nullable } from 'uiSrc/utils' -import { TableLoader } from 'uiSrc/pages/database-analysis/components' +import TableLoader from 'uiSrc/pages/database-analysis/components/table-loader' import { TextBtn } from 'uiSrc/pages/database-analysis/components/base/TextBtn' -import { Title } from 'uiSrc/components/base/text/Title' import { DatabaseAnalysis } from 'apiSrc/modules/database-analysis/models' +import { + Section, + SectionTitle, + SectionTitleWrapper, +} from 'uiSrc/pages/database-analysis/components/styles' -import Table from './Table' +import TopKeysTable from './TopKeysTable' +import { SectionContent } from 'uiSrc/pages/database-analysis/components/top-namespace/TopNamespace.styles' export interface Props { data: Nullable @@ -28,14 +32,14 @@ const TopKeys = ({ data, loading }: Props) => { } return ( -
-
- + <Section> + <SectionTitleWrapper> + <SectionTitle size="M" data-testid="top-keys-title"> {topKeysLength.length < MAX_TOP_KEYS && topKeysMemory?.length < MAX_TOP_KEYS ? 'TOP KEYS' : `TOP ${MAX_TOP_KEYS} KEYS`} - + { > by Length -
-
+ + {tableView === TableView.MEMORY && ( - { /> )} {tableView === TableView.KEYS && ( -
)} - - + + ) } diff --git a/redisinsight/ui/src/pages/database-analysis/components/top-keys/Table.spec.tsx b/redisinsight/ui/src/pages/database-analysis/components/top-keys/TopKeysTable.spec.tsx similarity index 67% rename from redisinsight/ui/src/pages/database-analysis/components/top-keys/Table.spec.tsx rename to redisinsight/ui/src/pages/database-analysis/components/top-keys/TopKeysTable.spec.tsx index 777d999535..35c5e3cdeb 100644 --- a/redisinsight/ui/src/pages/database-analysis/components/top-keys/Table.spec.tsx +++ b/redisinsight/ui/src/pages/database-analysis/components/top-keys/TopKeysTable.spec.tsx @@ -1,12 +1,13 @@ import React from 'react' import { instance, mock } from 'ts-mockito' import { render, screen } from 'uiSrc/utils/test-utils' +import { Key } from 'apiSrc/modules/database-analysis/models/key' -import Table, { Props } from './Table' +import TopKeysTable, { Props } from './TopKeysTable' const mockedProps = mock() -const mockData = [ +const mockData: Key[] = [ { name: 'name', type: 'hash', @@ -18,27 +19,27 @@ const mockData = [ name: 'name_1', type: 'hash', memory: 1000, - length: null, + length: null as any, ttl: -1, }, ] describe('Table', () => { it('should render', () => { - expect(render(
)).toBeTruthy() + expect(render()).toBeTruthy() }) it('should render', () => { - expect(render(
)).toBeTruthy() + expect(render()).toBeTruthy() }) it('should render table with 2 items', () => { - render(
) + render() expect(screen.getAllByTestId('top-keys-table-name')).toHaveLength(2) }) it('should render correct ttl', () => { - render(
) + render() expect(screen.getByTestId('ttl-no-limit-name_1')).toHaveTextContent( 'No limit', ) @@ -46,7 +47,7 @@ describe('Table', () => { }) it('should render correct length', () => { - render(
) + render() expect(screen.getByTestId('length-empty-name_1')).toHaveTextContent('-') expect(screen.getByTestId(/length-value-name/).textContent).toEqual( '100 000 000', @@ -54,7 +55,7 @@ describe('Table', () => { }) it('should highlight big keys', () => { - render(
) + render() expect( screen.getByTestId('nsp-usedMemory-value=10000000-highlighted'), ).toBeInTheDocument() diff --git a/redisinsight/ui/src/pages/database-analysis/components/top-keys/Table.tsx b/redisinsight/ui/src/pages/database-analysis/components/top-keys/TopKeysTable.tsx similarity index 73% rename from redisinsight/ui/src/pages/database-analysis/components/top-keys/Table.tsx rename to redisinsight/ui/src/pages/database-analysis/components/top-keys/TopKeysTable.tsx index 57597deeb7..223db2878d 100644 --- a/redisinsight/ui/src/pages/database-analysis/components/top-keys/Table.tsx +++ b/redisinsight/ui/src/pages/database-analysis/components/top-keys/TopKeysTable.tsx @@ -3,7 +3,6 @@ import React from 'react' import { useDispatch, useSelector } from 'react-redux' import { useHistory, useParams } from 'react-router-dom' -import { ColorText } from 'uiSrc/components/base/text' import { GroupBadge, RiTooltip } from 'uiSrc/components' import { Pages } from 'uiSrc/constants' import { @@ -38,8 +37,9 @@ import { } from 'uiSrc/utils' import { numberWithSpaces } from 'uiSrc/utils/numbers' import { TableTextBtn } from 'uiSrc/pages/database-analysis/components/base/TableTextBtn' -import { Table, ColumnDefinition } from 'uiSrc/components/base/layout/table' +import { Table, ColumnDef } from 'uiSrc/components/base/layout/table' import { Key } from 'apiSrc/modules/database-analysis/models/key' +import { CellText } from 'uiSrc/components/auto-discover' export interface Props { data: Key[] @@ -87,7 +87,7 @@ const TopKeysTable = ({ history.push(Pages.browser(instanceId)) } - const columns: ColumnDefinition[] = [ + const columns: ColumnDef[] = [ { header: 'Key Type', id: 'type', @@ -110,8 +110,11 @@ const TopKeysTable = ({ original: { name }, }, }) => { - const tooltipContent = formatLongName(name as string) - const cellContent = (name as string).substring(0, 200) + const maxLength = 35 + const nameAsString = name as string + const tooltipContent = formatLongName(nameAsString) + const cellContent = nameAsString.substring(0, maxLength) + const isTruncated = nameAsString.length > maxLength return (
- handleRedirect(name as string)} - > - {cellContent} + handleRedirect(nameAsString)}> + {`${cellContent}${isTruncated ? '...' : ''}`}
@@ -141,43 +141,31 @@ const TopKeysTable = ({ }, }) => { if (isNil(value)) { - return ( - - - - - ) + return - } if (value === -1) { return ( - - No limit - + No limit ) } return ( - - - {`${truncateTTLToSeconds(value)} s`} -
- {`(${truncateNumberToDuration(value)})`} - - } - > - - {truncateNumberToFirstUnit(value)} - -
-
+ + {`${truncateTTLToSeconds(value)} s`} +
+ {`(${truncateNumberToDuration(value)})`} + + } + > + + {truncateNumberToFirstUnit(value)} + +
) }, }, @@ -192,15 +180,7 @@ const TopKeysTable = ({ }, }) => { if (isNil(value)) { - return ( - - - - - ) + return - } const [number, size] = formatBytes(value, 3, true) const isHighlight = isBigKey(type, HighlightType.Memory, value) @@ -219,12 +199,11 @@ const TopKeysTable = ({ } data-testid="usedMemory-tooltip" > - {number} {size} - + ) }, @@ -240,15 +219,7 @@ const TopKeysTable = ({ }, }) => { if (isNil(value)) { - return ( - - - - - ) + return - } const isHighlight = isBigKey(type, HighlightType.Length, value) @@ -259,12 +230,11 @@ const TopKeysTable = ({ } data-testid="usedMemory-tooltip" > - {numberWithSpaces(value)} - + ) }, diff --git a/redisinsight/ui/src/pages/database-analysis/components/top-keys/index.ts b/redisinsight/ui/src/pages/database-analysis/components/top-keys/index.ts deleted file mode 100644 index a589327d0b..0000000000 --- a/redisinsight/ui/src/pages/database-analysis/components/top-keys/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -import TopKeys from './TopKeys' - -export default TopKeys diff --git a/redisinsight/ui/src/pages/database-analysis/components/top-namespace/TopNamespace.spec.tsx b/redisinsight/ui/src/pages/database-analysis/components/top-namespace/TopNamespace.spec.tsx index b14f1265a0..5e32e2bc5d 100644 --- a/redisinsight/ui/src/pages/database-analysis/components/top-namespace/TopNamespace.spec.tsx +++ b/redisinsight/ui/src/pages/database-analysis/components/top-namespace/TopNamespace.spec.tsx @@ -13,11 +13,19 @@ import { render, screen, } from 'uiSrc/utils/test-utils' +import { DatabaseAnalysisFactory } from 'uiSrc/mocks/factories/database-analysis/DatabaseAnalysis.factory' import TopNamespace, { Props } from './TopNamespace' const mockedProps = mock() +const mockNspData = { + nsp: 'nsp_name' as any, + memory: 1, + keys: 1, + types: [{ type: 'hash', memory: 1, keys: 1 }], +} + let store: typeof mockedStore beforeEach(() => { cleanup() @@ -31,24 +39,10 @@ describe('TopNamespace', () => { }) it('should render nsp-table-keys when click "btn-change-table-keys" ', () => { - const mockedData = { - topKeysNsp: [ - { - nsp: 'nsp_name', - memory: 1, - keys: 1, - types: [{ type: 'hash', memory: 1, keys: 1 }], - }, - ], - topMemoryNsp: [ - { - nsp: 'nsp_name', - memory: 1, - keys: 1, - types: [{ type: 'hash', memory: 1, keys: 1 }], - }, - ], - } + const mockedData = DatabaseAnalysisFactory.build({ + topKeysNsp: [mockNspData], + topMemoryNsp: [mockNspData], + }) const { queryByTestId } = render( , @@ -63,24 +57,10 @@ describe('TopNamespace', () => { }) it('should render nsp-table-keys when click "btn-change-table-memory" and memory button should be disabled', () => { - const mockedData = { - topKeysNsp: [ - { - nsp: 'nsp_name', - memory: 1, - keys: 1, - types: [{ type: 'hash', memory: 1, keys: 1 }], - }, - ], - topMemoryNsp: [ - { - nsp: 'nsp_name', - memory: 1, - keys: 1, - types: [{ type: 'hash', memory: 1, keys: 1 }], - }, - ], - } + const mockedData = DatabaseAnalysisFactory.build({ + topKeysNsp: [mockNspData], + topMemoryNsp: [mockNspData], + }) const { queryByTestId } = render( , @@ -97,24 +77,10 @@ describe('TopNamespace', () => { }) it('should render nsp-table-keys by default" ', () => { - const mockedData = { - topKeysNsp: [ - { - nsp: 'nsp_name', - memory: 1, - keys: 1, - types: [{ type: 'hash', memory: 1, keys: 1 }], - }, - ], - topMemoryNsp: [ - { - nsp: 'nsp_name', - memory: 1, - keys: 1, - types: [{ type: 'hash', memory: 1, keys: 1 }], - }, - ], - } + const mockedData = DatabaseAnalysisFactory.build({ + topKeysNsp: [mockNspData], + topMemoryNsp: [mockNspData], + }) const { queryByTestId } = render( , @@ -127,10 +93,10 @@ describe('TopNamespace', () => { }) it('should not render tables when topMemoryNsp and topKeysNsp are empty array', () => { - const mockedData = { + const mockedData = DatabaseAnalysisFactory.build({ topKeysNsp: [], topMemoryNsp: [], - } + }) const { queryByTestId } = render( , ) @@ -140,24 +106,10 @@ describe('TopNamespace', () => { }) it('should render loader when loading="true"', () => { - const mockedData = { - topKeysNsp: [ - { - nsp: 'nsp_name', - memory: 1, - keys: 1, - types: [{ type: 'hash', memory: 1, keys: 1 }], - }, - ], - topMemoryNsp: [ - { - nsp: 'nsp_name', - memory: 1, - keys: 1, - types: [{ type: 'hash', memory: 1, keys: 1 }], - }, - ], - } + const mockedData = DatabaseAnalysisFactory.build({ + topKeysNsp: [mockNspData], + topMemoryNsp: [mockNspData], + }) const { queryByTestId } = render( , ) @@ -168,26 +120,26 @@ describe('TopNamespace', () => { }) it('should render message when no namespaces', () => { - const mockedData = { + const mockedData = DatabaseAnalysisFactory.build({ topKeysNsp: [], topMemoryNsp: [], - } + }) render() expect(screen.queryByTestId('top-namespaces-empty')).toBeInTheDocument() }) it('should call proper actions and push history after click tree view link', async () => { - const mockedData = { + const mockedData = DatabaseAnalysisFactory.build({ topKeysNsp: [], topMemoryNsp: [], - } + }) const pushMock = jest.fn() reactRouterDom.useHistory = jest.fn().mockReturnValue({ push: pushMock }) render() - await act(() => { + await act(async () => { fireEvent.click(screen.getByTestId('tree-view-page-link')) }) diff --git a/redisinsight/ui/src/pages/database-analysis/components/top-namespace/TopNamespace.stories.tsx b/redisinsight/ui/src/pages/database-analysis/components/top-namespace/TopNamespace.stories.tsx new file mode 100644 index 0000000000..3817d1f35a --- /dev/null +++ b/redisinsight/ui/src/pages/database-analysis/components/top-namespace/TopNamespace.stories.tsx @@ -0,0 +1,42 @@ +import React from 'react' +import type { Meta, StoryObj } from '@storybook/react-vite' +import { buildDatabaseAnalysisWithNamespaces } from 'uiSrc/mocks/factories/database-analysis/DatabaseAnalysis.factory' + +import TopNamespace from './TopNamespace' +import { DEFAULT_EXTRAPOLATION } from '../../constants' + +const meta: Meta = { + component: TopNamespace, + args: { + data: null, + loading: false, + extrapolation: DEFAULT_EXTRAPOLATION, + onSwitchExtrapolation: () => undefined, + }, + decorators: [ + (Story) => ( +
+

Top Namespace

+ +
+ ), + ], +} + +export default meta + +type Story = StoryObj + +export const Loading: Story = { + args: { + loading: true, + }, +} + +export const Default: Story = { + args: { + loading: false, + data: buildDatabaseAnalysisWithNamespaces(), + extrapolation: 50, + }, +} diff --git a/redisinsight/ui/src/pages/database-analysis/components/top-namespace/TopNamespace.styles.ts b/redisinsight/ui/src/pages/database-analysis/components/top-namespace/TopNamespace.styles.ts new file mode 100644 index 0000000000..bf8e0917e5 --- /dev/null +++ b/redisinsight/ui/src/pages/database-analysis/components/top-namespace/TopNamespace.styles.ts @@ -0,0 +1,46 @@ +import styled from 'styled-components' +import { sectionContent } from 'uiSrc/pages/database-analysis/components/styles' +import { Col, Row } from 'uiSrc/components/base/layout/flex' +import { Text } from 'uiSrc/components/base/text' +import { EmptyButton } from 'uiSrc/components/base/forms/buttons' +import { truncateText } from 'uiSrc/styles/mixins' + +export const SectionContent = styled.div>` + ${sectionContent} +` + +export const NoNamespaceMsg = styled(Col).attrs({ + align: 'center', + justify: 'center', +})` + margin: 80px auto 100px; +` + +export const NoNamespaceText = styled(Text).attrs({ + size: 'M', +})` + margin-top: 10px; +` + +export const NoNamespaceBtn = styled(EmptyButton)` + text-decoration: underline; + + &:hover, + &:focus { + text-decoration: none; + } +` +export const ExpandedRowItem = styled.div>` + display: flex; + height: 42px; + & > div { + display: flex; + align-items: center; + flex: 1; + } +` +export const TruncatedContent = styled(Row).attrs({ + align: 'center', +})` + ${truncateText} +` diff --git a/redisinsight/ui/src/pages/database-analysis/components/top-namespace/TopNamespace.tsx b/redisinsight/ui/src/pages/database-analysis/components/top-namespace/TopNamespace.tsx index 1714438ac6..a4cf177d8e 100644 --- a/redisinsight/ui/src/pages/database-analysis/components/top-namespace/TopNamespace.tsx +++ b/redisinsight/ui/src/pages/database-analysis/components/top-namespace/TopNamespace.tsx @@ -7,8 +7,8 @@ import { DEFAULT_EXTRAPOLATION, SectionName, TableView, -} from 'uiSrc/pages/database-analysis' -import { TableLoader } from 'uiSrc/pages/database-analysis/components' +} from 'uiSrc/pages/database-analysis/constants' +import TableLoader from 'uiSrc/pages/database-analysis/components/table-loader' import { resetBrowserTree } from 'uiSrc/slices/app/context' import { changeKeyViewType } from 'uiSrc/slices/browser/keys' import { KeyViewType } from 'uiSrc/slices/interfaces/keys' @@ -16,10 +16,19 @@ import { Nullable } from 'uiSrc/utils' import { TextBtn } from 'uiSrc/pages/database-analysis/components/base/TextBtn' import { SwitchInput } from 'uiSrc/components/base/inputs' import { Title } from 'uiSrc/components/base/text/Title' -import { EmptyButton } from 'uiSrc/components/base/forms/buttons' import { DatabaseAnalysis } from 'apiSrc/modules/database-analysis/models' -import Table from './Table' -import styles from './styles.module.scss' +import TopNamespacesTable from './TopNamespacesTable' +import { + Section, + SectionTitle, + SectionTitleWrapper, +} from 'uiSrc/pages/database-analysis/components/styles' +import { + NoNamespaceBtn, + NoNamespaceMsg, + NoNamespaceText, + SectionContent, +} from './TopNamespace.styles' export interface Props { data: Nullable @@ -41,14 +50,6 @@ const TopNamespace = (props: Props) => { setIsExtrapolated(extrapolation !== DEFAULT_EXTRAPOLATION) }, [data, extrapolation]) - if (loading) { - return - } - - if (isNull(data)) { - return null - } - const handleTreeViewClick = (e: React.MouseEvent) => { e.preventDefault() @@ -57,44 +58,47 @@ const TopNamespace = (props: Props) => { history.push(Pages.browser(instanceId)) } + if (loading) { + return + } + + if (isNull(data)) { + return null + } + if (!data?.topMemoryNsp || data?.totalKeys?.total === 0) { return null } if (!data?.topMemoryNsp?.length && !data?.topKeysNsp?.length) { return ( -
-
- - TOP NAMESPACES - -
-
-
+
+ + TOP NAMESPACES + + + No namespaces to display -

+ {'Configure the delimiter in '} - Tree View - + {' to customize the namespaces displayed.'} -

-
-
-
+ + + + ) } return ( -
-
- - TOP NAMESPACES - +
+ + TOP NAMESPACES { data-testid="extrapolate-results" /> )} -
-
+ + {tableView === TableView.MEMORY && ( -
{ /> )} {tableView === TableView.KEYS && ( -
{ dataTestid="nsp-table-keys" /> )} - - + + ) } diff --git a/redisinsight/ui/src/pages/database-analysis/components/top-namespace/Table.spec.tsx b/redisinsight/ui/src/pages/database-analysis/components/top-namespace/TopNamespacesTable.spec.tsx similarity index 73% rename from redisinsight/ui/src/pages/database-analysis/components/top-namespace/Table.spec.tsx rename to redisinsight/ui/src/pages/database-analysis/components/top-namespace/TopNamespacesTable.spec.tsx index b93ccfb1a0..80a4c8155a 100644 --- a/redisinsight/ui/src/pages/database-analysis/components/top-namespace/Table.spec.tsx +++ b/redisinsight/ui/src/pages/database-analysis/components/top-namespace/TopNamespacesTable.spec.tsx @@ -2,11 +2,11 @@ import React from 'react' import { instance, mock } from 'ts-mockito' import { render } from 'uiSrc/utils/test-utils' -import Table, { Props } from './Table' +import Table, { Props } from './TopNamespacesTable' const mockedProps = mock() -describe('Table', () => { +describe('Top Namespaces Table', () => { it('should render', () => { expect(render(
)).toBeTruthy() }) diff --git a/redisinsight/ui/src/pages/database-analysis/components/top-namespace/Table.tsx b/redisinsight/ui/src/pages/database-analysis/components/top-namespace/TopNamespacesTable.tsx similarity index 81% rename from redisinsight/ui/src/pages/database-analysis/components/top-namespace/Table.tsx rename to redisinsight/ui/src/pages/database-analysis/components/top-namespace/TopNamespacesTable.tsx index 4b0308b02c..b6fd88e9c9 100644 --- a/redisinsight/ui/src/pages/database-analysis/components/top-namespace/Table.tsx +++ b/redisinsight/ui/src/pages/database-analysis/components/top-namespace/TopNamespacesTable.tsx @@ -31,11 +31,11 @@ import { setBrowserTreeDelimiter, } from 'uiSrc/slices/app/context' import { TableTextBtn } from 'uiSrc/pages/database-analysis/components/base/TableTextBtn' -import { Table, ColumnDefinition } from 'uiSrc/components/base/layout/table' -import { ColorText } from 'uiSrc/components/base/text' +import { Table, ColumnDef } from 'uiSrc/components/base/layout/table' +import { CellText } from 'uiSrc/components/auto-discover' import { NspSummary } from 'apiSrc/modules/database-analysis/models' -import styles from './styles.module.scss' +import { ExpandedRowItem, TruncatedContent } from './TopNamespace.styles' export interface Props { data: Nullable @@ -97,12 +97,11 @@ const NameSpacesTable = ({ const formatNumber = formatExtrapolation(number, isExtrapolated) return ( -
-
+ -
+
- + {formatNumber} {size} - +
- + {extrapolate( type.keys, { extrapolation, apply: isExtrapolated }, (val: number) => numberWithSpaces(Math.round(val)), )} - +
-
+ ) })} ) - const columns: ColumnDefinition[] = [ + const columns: ColumnDef[] = [ { header: 'Key Pattern', id: 'nsp', @@ -155,19 +154,17 @@ const NameSpacesTable = ({ const cellContent = textWithDelimiter?.substring(0, 200) const tooltipContent = formatLongName(textWithDelimiter) return ( -
- + handleRedirect(nsp as string, filterType)} > - handleRedirect(nsp as string, filterType)} - > - {cellContent} - - -
+ {cellContent} + + ) }, }, @@ -215,12 +212,9 @@ const NameSpacesTable = ({ content={`${formatValueBytes} B`} data-testid="usedMemory-tooltip" > - + {formatValue} {size} - + ) }, @@ -235,15 +229,13 @@ const NameSpacesTable = ({ original: { keys: value }, }, }) => ( - - - {extrapolate( - value, - { extrapolation, apply: isExtrapolated }, - (val: number) => numberWithSpaces(Math.round(val)), - )} - - + + {extrapolate( + value, + { extrapolation, apply: isExtrapolated }, + (val: number) => numberWithSpaces(Math.round(val)), + )} + ), }, { diff --git a/redisinsight/ui/src/pages/database-analysis/components/top-namespace/index.ts b/redisinsight/ui/src/pages/database-analysis/components/top-namespace/index.ts deleted file mode 100644 index 3735fa8fb2..0000000000 --- a/redisinsight/ui/src/pages/database-analysis/components/top-namespace/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -import TopNamespace from './TopNamespace' - -export default TopNamespace diff --git a/redisinsight/ui/src/styles/globalStyles.ts b/redisinsight/ui/src/styles/globalStyles.ts new file mode 100644 index 0000000000..5d801ebbb6 --- /dev/null +++ b/redisinsight/ui/src/styles/globalStyles.ts @@ -0,0 +1,42 @@ +import { createGlobalStyle } from 'styled-components' +import { Theme } from 'uiSrc/components/base/theme/types' + +/** + * Global styles for the application + * These styles are applied to the entire application using styled-components + */ +export const GlobalStyles = createGlobalStyle<{ theme: Theme }>` + :root { + // todo: replace with theme colors at some point + /* Type colors for Redis data types */ + --typeHashColor: #364cff; + --typeListColor: #008556; + --typeSetColor: #9c5c2b; + --typeZSetColor: #a00a6b; + --typeStringColor: #6a1dc3; + --typeReJSONColor: #3f4b5f; + --typeStreamColor: #6a741b; + --typeGraphColor: #14708d; + --typeTimeSeriesColor: #6e6e6e; + + /* Group colors for Redis command groups */ + --groupSortedSetColor: #a00a6b; + --groupBitmapColor: #3f4b5f; + --groupClusterColor: #6e6e6e; + --groupConnectionColor: #bf1046; + --groupGeoColor: #344e36; + --groupGenericColor: #4a2923; + --groupPubSubColor: #14365d; + --groupScriptingColor: #5d141c; + --groupTransactionsColor: #14708d; + --groupServerColor: #000000; + --groupHyperLolLogColor: #3f4b5f; + + /* Default type color */ + --defaultTypeColor: #aa4e4e; + } + + .text-uppercase { + text-transform: uppercase; + } +` diff --git a/redisinsight/ui/src/styles/mixins/index.ts b/redisinsight/ui/src/styles/mixins/index.ts new file mode 100644 index 0000000000..f83f95bf74 --- /dev/null +++ b/redisinsight/ui/src/styles/mixins/index.ts @@ -0,0 +1,8 @@ +export { + scrollbarStyles, + breakpoint, + breakpoints, + insightsOpen, + truncateText, +} from './styledComponents' +export type { BreakpointKey } from './styledComponents' diff --git a/redisinsight/ui/src/styles/mixins/styledComponents.ts b/redisinsight/ui/src/styles/mixins/styledComponents.ts new file mode 100644 index 0000000000..42b0fda85a --- /dev/null +++ b/redisinsight/ui/src/styles/mixins/styledComponents.ts @@ -0,0 +1,214 @@ +import { css, CSSObject, FlattenSimpleInterpolation } from 'styled-components' + +/** + * Breakpoint values matching EUI breakpoints + * Equivalent to $euiBreakpoints in SCSS + */ +export const breakpoints = { + xs: 0, + s: 575, + m: 768, + l: 992, + xl: 1200, +} as const + +export type BreakpointKey = keyof typeof breakpoints + +/** + * Media query helper for breakpoints + * Equivalent to @include eui.euiBreakpoint() in SCSS + * + * @param sizes - One or more breakpoint keys + * @returns A function that takes template literals and returns styled-components CSS + * + * @example + * ```typescript + * const Container = styled.div` + * padding: 10px; + * + * // For xs and s screens only + * ${breakpoint('xs', 's')` + * padding: 5px; + * `} + * + * // For m, l, and xl screens + * ${breakpoint('m', 'l', 'xl')` + * padding: 20px; + * `} + * ` + * ``` + */ +export const breakpoint = (...sizes: BreakpointKey[]) => { + return ( + strings: TemplateStringsArray, + ...interpolations: Array< + string | number | FlattenSimpleInterpolation | CSSObject + > + ) => { + const content = css(strings, ...interpolations) + const breakpointKeys = Object.keys(breakpoints) as BreakpointKey[] + + return sizes.map((size) => { + const index = breakpointKeys.indexOf(size) + + if (index === -1) { + console.warn( + `breakpoint(): '${size}' is not a valid breakpoint. Valid breakpoints are: ${breakpointKeys.join(', ')}`, + ) + return '' + } + + const minSize = breakpoints[size] + + // If it's the last breakpoint, don't set max-width + if (index === breakpointKeys.length - 1) { + return css` + @media only screen and (min-width: ${minSize}px) { + ${content} + } + ` + } + + // If it's the first breakpoint (xs), only set max-width + if (index === 0) { + const nextKey = breakpointKeys[index + 1] + const maxSize = breakpoints[nextKey] - 1 + return css` + @media only screen and (max-width: ${maxSize}px) { + ${content} + } + ` + } + + // Otherwise, set both min and max width + const nextKey = breakpointKeys[index + 1] + const maxSize = breakpoints[nextKey] - 1 + return css` + @media only screen and (min-width: ${minSize}px) and (max-width: ${maxSize}px) { + ${content} + } + ` + }) + } +} + +/** + * Insights panel open state responsive mixin + * Equivalent to @include global.insights-open() in SCSS + * + * @param maxWidth - Maximum width for the media query (default: 1440px) + * @returns A function that takes template literals and returns styled-components CSS + * + * @example + * ```typescript + * const ControlsIcon = styled(RiIcon)` + * margin-left: 3px; + * + * ${insightsOpen(1440)` + * width: 18px !important; + * height: 18px !important; + * `} + * ` + * + * // With custom max-width + * const Promo = styled.div` + * display: flex; + * + * ${insightsOpen(1350)` + * display: none; + * `} + * ` + * ``` + */ +export const insightsOpen = (maxWidth: number = 1440) => { + return ( + strings: TemplateStringsArray, + ...interpolations: Array< + string | number | FlattenSimpleInterpolation | CSSObject + > + ) => { + const content = css(strings, ...interpolations) + return css` + :global(.insightsOpen) { + @media only screen and (max-width: ${maxWidth}px) { + ${content} + } + } + ` + } +} + +/** + * Scrollbar styling mixin for styled-components + * Equivalent to @include eui.scrollBar() in SCSS + * + * @param width - The width of the scrollbar (default: 16px) + * @returns CSS template with scrollbar styling + * + * @example + * ```typescript + * const Container = styled.div` + * ${scrollbarStyles()} + * height: 100%; + * ` + * + * // With custom width + * const ThinContainer = styled.div` + * ${scrollbarStyles(12)} + * height: 100%; + * ` + * ``` + */ +export const scrollbarStyles = (width: number = 16) => css` + overflow-y: auto; + overflow-x: hidden; + scrollbar-width: thin; + + &::-webkit-scrollbar { + width: ${width}px; + height: ${width}px; + } + + &::-webkit-scrollbar-thumb { + background-color: rgba(105, 112, 125, 0.5); + border: 6px solid rgba(0, 0, 0, 0); + background-clip: content-box; + } + + &::-webkit-scrollbar-corner, + &::-webkit-scrollbar-track { + background-color: rgba(0, 0, 0, 0); + } +` + +/** + * Text truncation with ellipsis mixin + * Equivalent to truncate pattern found in various components + * + * @returns CSS template with text truncation styling + * + * @example + * ```typescript + * const Label = styled.span` + * ${truncateText} + * max-width: 200px; + * ` + * + * // Can also be used inline + * const Title = styled.h1` + * font-size: 24px; + * ${truncateText} + * ` + * ``` + */ +export const truncateText = css` + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + width: 100%; + & > div, + & > span, + & > p { + max-width: 100%; + } +`