diff --git a/frontend/src/component/filter/Filters/Filters.tsx b/frontend/src/component/filter/Filters/Filters.tsx index 554dfdf26e..71dc3935f2 100644 --- a/frontend/src/component/filter/Filters/Filters.tsx +++ b/frontend/src/component/filter/Filters/Filters.tsx @@ -42,7 +42,7 @@ type ITextFilterItem = IBaseFilterItem & { pluralOperators: [string, ...string[]]; }; -type IDateFilterItem = IBaseFilterItem & { +export type IDateFilterItem = IBaseFilterItem & { dateOperators: [string, ...string[]]; fromFilterKey?: string; toFilterKey?: string; diff --git a/frontend/src/component/insights/Insights.test.tsx b/frontend/src/component/insights/Insights.test.tsx index 09177fcedf..b5d531c26b 100644 --- a/frontend/src/component/insights/Insights.test.tsx +++ b/frontend/src/component/insights/Insights.test.tsx @@ -27,6 +27,9 @@ const setupApi = () => { const currentTime = '2024-04-25T08:05:00.000Z'; +// todo(lifecycleMetrics): this test won't be relevant anymore because the +// filters are on each section instead of the top-level component. Consider +// rewriting this for the individual section components instead. test('Filter insights by project and date', async () => { vi.setSystemTime(currentTime); setupApi(); diff --git a/frontend/src/component/insights/Insights.tsx b/frontend/src/component/insights/Insights.tsx index 4a73985d8b..76ab5b3e92 100644 --- a/frontend/src/component/insights/Insights.tsx +++ b/frontend/src/component/insights/Insights.tsx @@ -1,90 +1,32 @@ -import { useState, type FC } from 'react'; +import type { FC } from 'react'; import { styled } from '@mui/material'; -import { usePersistentTableState } from 'hooks/usePersistentTableState'; -import { allOption } from 'component/common/ProjectSelect/ProjectSelect'; -import { useInsights } from 'hooks/api/getters/useInsights/useInsights'; import { InsightsHeader } from './components/InsightsHeader/InsightsHeader.tsx'; -import { useInsightsData } from './hooks/useInsightsData.ts'; -import { InsightsCharts } from './InsightsCharts.tsx'; -import { Sticky } from 'component/common/Sticky/Sticky'; -import { InsightsFilters } from './InsightsFilters.tsx'; -import { FilterItemParam } from '../../utils/serializeQueryParams.ts'; -import { format, subMonths } from 'date-fns'; -import { withDefault } from 'use-query-params'; +import { useUiFlag } from 'hooks/useUiFlag.ts'; +import { LegacyInsights } from './LegacyInsights.tsx'; +import { StyledContainer } from './InsightsCharts.styles.ts'; +import { LifecycleInsights } from './sections/LifecycleInsights.tsx'; +import { PerformanceInsights } from './sections/PerformanceInsights.tsx'; +import { UserInsights } from './sections/UserInsights.tsx'; const StyledWrapper = styled('div')(({ theme }) => ({ paddingTop: theme.spacing(2), })); -const StickyContainer = styled(Sticky)(({ theme }) => ({ - position: 'sticky', - top: 0, - zIndex: theme.zIndex.sticky, - padding: theme.spacing(2, 0), - background: theme.palette.background.application, - transition: 'padding 0.3s ease', -})); - -interface InsightsProps { - withCharts?: boolean; -} - -export const Insights: FC = ({ withCharts = true }) => { - const [scrolled, setScrolled] = useState(false); - - const stateConfig = { - project: FilterItemParam, - from: withDefault(FilterItemParam, { - values: [format(subMonths(new Date(), 1), 'yyyy-MM-dd')], - operator: 'IS', - }), - to: withDefault(FilterItemParam, { - values: [format(new Date(), 'yyyy-MM-dd')], - operator: 'IS', - }), - }; - const [state, setState] = usePersistentTableState('insights', stateConfig, [ - 'from', - 'to', - ]); - - const { insights, loading } = useInsights( - state.from?.values[0], - state.to?.values[0], - ); - - const projects = state.project?.values ?? [allOption.id]; - - const insightsData = useInsightsData(insights, projects); - - const handleScroll = () => { - if (!scrolled && window.scrollY > 0) { - setScrolled(true); - } else if (scrolled && window.scrollY === 0) { - setScrolled(false); - } - }; - - if (typeof window !== 'undefined') { - window.addEventListener('scroll', handleScroll); - } - +const NewInsights: FC = () => { return ( - - - } - /> - - {withCharts && ( - - )} + + + + + + ); }; + +export const Insights: FC<{ withCharts?: boolean }> = (props) => { + const useNewInsights = useUiFlag('lifecycleMetrics'); + + return useNewInsights ? : ; +}; diff --git a/frontend/src/component/insights/InsightsCharts.styles.ts b/frontend/src/component/insights/InsightsCharts.styles.ts new file mode 100644 index 0000000000..a87db9ddf4 --- /dev/null +++ b/frontend/src/component/insights/InsightsCharts.styles.ts @@ -0,0 +1,46 @@ +import { Box, Paper, styled } from '@mui/material'; + +export const StyledContainer = styled(Box)(({ theme }) => ({ + display: 'flex', + flexDirection: 'column', + gap: theme.spacing(4), +})); + +export const StyledWidget = styled(Paper)(({ theme }) => ({ + borderRadius: `${theme.shape.borderRadiusLarge}px`, + boxShadow: 'none', + display: 'flex', + flexWrap: 'wrap', + [theme.breakpoints.up('md')]: { + flexDirection: 'row', + flexWrap: 'nowrap', + }, +})); + +export const StyledWidgetContent = styled(Box)(({ theme }) => ({ + padding: theme.spacing(3), + width: '100%', +})); + +export const StyledWidgetStats = styled(Box)<{ + width?: number; + padding?: number; +}>(({ theme, width = 300, padding = 3 }) => ({ + display: 'flex', + flexDirection: 'column', + gap: theme.spacing(2), + padding: theme.spacing(padding), + minWidth: '100%', + [theme.breakpoints.up('md')]: { + minWidth: `${width}px`, + borderRight: `1px solid ${theme.palette.background.application}`, + }, +})); + +export const StyledChartContainer = styled(Box)(({ theme }) => ({ + position: 'relative', + minWidth: 0, // bugfix, see: https://github.com/chartjs/Chart.js/issues/4156#issuecomment-295180128 + flexGrow: 1, + margin: 'auto 0', + padding: theme.spacing(3), +})); diff --git a/frontend/src/component/insights/InsightsCharts.tsx b/frontend/src/component/insights/InsightsCharts.tsx index a491a2aec3..1c53bb5bab 100644 --- a/frontend/src/component/insights/InsightsCharts.tsx +++ b/frontend/src/component/insights/InsightsCharts.tsx @@ -1,4 +1,4 @@ -import type { FC, PropsWithChildren } from 'react'; +import type { FC } from 'react'; import { Box, Paper, styled } from '@mui/material'; import { UserStats } from './componentsStat/UserStats/UserStats.tsx'; import { UsersChart } from './componentsChart/UsersChart/UsersChart.tsx'; @@ -8,6 +8,8 @@ import { FlagsChart } from './componentsChart/FlagsChart/FlagsChart.tsx'; import { FlagsProjectChart } from './componentsChart/FlagsProjectChart/FlagsProjectChart.tsx'; import { HealthStats } from './componentsStat/HealthStats/HealthStats.tsx'; import { ProjectHealthChart } from './componentsChart/ProjectHealthChart/ProjectHealthChart.tsx'; +import { TimeToProduction } from './componentsStat/TimeToProduction/TimeToProduction.tsx'; +import { TimeToProductionChart } from './componentsChart/TimeToProductionChart/TimeToProductionChart.tsx'; import { MetricsSummaryChart } from './componentsChart/MetricsSummaryChart/MetricsSummaryChart.tsx'; import { UpdatesPerEnvironmentTypeChart } from './componentsChart/UpdatesPerEnvironmentTypeChart/UpdatesPerEnvironmentTypeChart.tsx'; import type { InstanceInsightsSchema } from 'openapi'; @@ -15,8 +17,7 @@ import type { GroupedDataByProject } from './hooks/useGroupedProjectTrends.ts'; import { allOption } from 'component/common/ProjectSelect/ProjectSelect'; import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; import { WidgetTitle } from './components/WidgetTitle/WidgetTitle.tsx'; -import { useUiFlag } from 'hooks/useUiFlag.ts'; -import { LegacyInsightsCharts } from './LegacyInsightsCharts.tsx'; +import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; export interface IChartsProps { flagTrends: InstanceInsightsSchema['flagTrends']; @@ -48,7 +49,7 @@ export interface IChartsProps { const StyledContainer = styled(Box)(({ theme }) => ({ display: 'flex', flexDirection: 'column', - gap: theme.spacing(4), + gap: theme.spacing(2), })); const StyledWidget = styled(Paper)(({ theme }) => ({ @@ -89,23 +90,7 @@ const StyledChartContainer = styled(Box)(({ theme }) => ({ padding: theme.spacing(3), })); -const StyledSection = styled('section')(({ theme }) => ({ - display: 'flex', - flexDirection: 'column', - gap: theme.spacing(2), -})); - -const Section: FC> = ({ - title, - children, -}) => ( - -

{title}

- {children} -
-); - -const NewInsightsCharts: FC = ({ +export const InsightsCharts: FC = ({ projects, summary, userTrends, @@ -137,74 +122,161 @@ const NewInsightsCharts: FC = ({ return ( -
-
- {showAllProjects ? ( - - - - - - - - - - ) : ( - - - - - - - - - - )} - {isEnterprise() ? ( - - - - } - /> - - - - - - ) : null} - {isEnterprise() ? ( + + + + + + + + + + + + } + elseShow={ + <> + + + + + + + + + + + } + /> + + + + + } + /> + + + + + + + + + + + + + + + + } + /> + + + + + + + + + + + + } + elseShow={ + <> + + + + + + + + + + + } + /> + @@ -241,67 +313,8 @@ const NewInsightsCharts: FC = ({ - ) : null} -
- -
- {showAllProjects ? ( - - - - - - - - - - ) : ( - - - - - - - - - - )} -
+ } + /> ); }; - -export const InsightsCharts: FC = (props) => { - const useNewInsightsCharts = useUiFlag('lifecycleMetrics'); - - return useNewInsightsCharts ? ( - - ) : ( - - ); -}; diff --git a/frontend/src/component/insights/InsightsFilters.tsx b/frontend/src/component/insights/InsightsFilters.tsx index 51778606c7..b6df2b54e9 100644 --- a/frontend/src/component/insights/InsightsFilters.tsx +++ b/frontend/src/component/insights/InsightsFilters.tsx @@ -3,19 +3,32 @@ import useProjects from 'hooks/api/getters/useProjects/useProjects'; import { type FilterItemParamHolder, Filters, + type IDateFilterItem, type IFilterItem, } from 'component/filter/Filters/Filters'; +import { styled } from '@mui/material'; +import { useUiFlag } from 'hooks/useUiFlag'; interface IFeatureToggleFiltersProps { state: FilterItemParamHolder; onChange: (value: FilterItemParamHolder) => void; + className?: string; + filterNamePrefix?: string; } +const FiltersNoPadding = styled(Filters)({ + padding: 0, +}); + export const InsightsFilters: FC = ({ + filterNamePrefix, state, - onChange, + ...filterProps }) => { const { projects } = useProjects(); + const FilterComponent = useUiFlag('lifecycleMetrics') + ? FiltersNoPadding + : Filters; const [availableFilters, setAvailableFilters] = useState([]); @@ -27,49 +40,51 @@ export const InsightsFilters: FC = ({ const hasMultipleProjects = projectsOptions.length > 1; + const prefix = filterNamePrefix ?? ''; + const availableFilters: IFilterItem[] = [ { label: 'Date From', icon: 'today', options: [], - filterKey: 'from', + filterKey: `${prefix}from`, dateOperators: ['IS'], - fromFilterKey: 'from', - toFilterKey: 'to', + fromFilterKey: `${prefix}from`, + toFilterKey: `${prefix}to`, persistent: true, - }, + } as IDateFilterItem, { label: 'Date To', icon: 'today', options: [], - filterKey: 'to', + filterKey: `${prefix}to`, dateOperators: ['IS'], - fromFilterKey: 'from', - toFilterKey: 'to', + fromFilterKey: `${prefix}from`, + toFilterKey: `${prefix}to`, persistent: true, - }, + } as IDateFilterItem, ...(hasMultipleProjects ? ([ { label: 'Project', icon: 'topic', options: projectsOptions, - filterKey: 'project', + filterKey: `${prefix}project`, singularOperators: ['IS'], pluralOperators: ['IS_ANY_OF'], }, ] as IFilterItem[]) : []), - ]; + ].filter(({ filterKey }) => filterKey in state); setAvailableFilters(availableFilters); }, [JSON.stringify(projects)]); return ( - ); }; diff --git a/frontend/src/component/insights/LegacyInsights.tsx b/frontend/src/component/insights/LegacyInsights.tsx new file mode 100644 index 0000000000..6a45e50d22 --- /dev/null +++ b/frontend/src/component/insights/LegacyInsights.tsx @@ -0,0 +1,76 @@ +import type { FC } from 'react'; +import { styled } from '@mui/material'; +import { usePersistentTableState } from 'hooks/usePersistentTableState'; +import { allOption } from 'component/common/ProjectSelect/ProjectSelect'; +import { useInsights } from 'hooks/api/getters/useInsights/useInsights'; +import { InsightsHeader } from './components/InsightsHeader/InsightsHeader.tsx'; +import { useInsightsData } from './hooks/useInsightsData.ts'; +import { Sticky } from 'component/common/Sticky/Sticky'; +import { InsightsFilters } from './InsightsFilters.tsx'; +import { FilterItemParam } from '../../utils/serializeQueryParams.ts'; +import { format, subMonths } from 'date-fns'; +import { withDefault } from 'use-query-params'; +import { InsightsCharts } from './InsightsCharts.tsx'; + +const StyledWrapper = styled('div')(({ theme }) => ({ + paddingTop: theme.spacing(2), +})); + +const StickyContainer = styled(Sticky)(({ theme }) => ({ + position: 'sticky', + top: 0, + zIndex: theme.zIndex.sticky, + padding: theme.spacing(2, 0), + background: theme.palette.background.application, + transition: 'padding 0.3s ease', +})); + +interface InsightsProps { + withCharts?: boolean; +} + +export const LegacyInsights: FC = ({ withCharts = true }) => { + const stateConfig = { + project: FilterItemParam, + from: withDefault(FilterItemParam, { + values: [format(subMonths(new Date(), 1), 'yyyy-MM-dd')], + operator: 'IS', + }), + to: withDefault(FilterItemParam, { + values: [format(new Date(), 'yyyy-MM-dd')], + operator: 'IS', + }), + }; + const [state, setState] = usePersistentTableState('insights', stateConfig, [ + 'from', + 'to', + ]); + + const { insights, loading } = useInsights( + state.from?.values[0], + state.to?.values[0], + ); + + const projects = state.project?.values ?? [allOption.id]; + + const insightsData = useInsightsData(insights, projects); + + return ( + + + + } + /> + + {withCharts && ( + + )} + + ); +}; diff --git a/frontend/src/component/insights/LegacyInsightsCharts.tsx b/frontend/src/component/insights/LegacyInsightsCharts.tsx deleted file mode 100644 index 512b4fdd73..0000000000 --- a/frontend/src/component/insights/LegacyInsightsCharts.tsx +++ /dev/null @@ -1,320 +0,0 @@ -import type { FC } from 'react'; -import { Box, Paper, styled } from '@mui/material'; -import { UserStats } from './componentsStat/UserStats/UserStats.tsx'; -import { UsersChart } from './componentsChart/UsersChart/UsersChart.tsx'; -import { UsersPerProjectChart } from './componentsChart/UsersPerProjectChart/UsersPerProjectChart.tsx'; -import { FlagStats } from './componentsStat/FlagStats/FlagStats.tsx'; -import { FlagsChart } from './componentsChart/FlagsChart/FlagsChart.tsx'; -import { FlagsProjectChart } from './componentsChart/FlagsProjectChart/FlagsProjectChart.tsx'; -import { HealthStats } from './componentsStat/HealthStats/HealthStats.tsx'; -import { ProjectHealthChart } from './componentsChart/ProjectHealthChart/ProjectHealthChart.tsx'; -import { TimeToProduction } from './componentsStat/TimeToProduction/TimeToProduction.tsx'; -import { TimeToProductionChart } from './componentsChart/TimeToProductionChart/TimeToProductionChart.tsx'; -import { MetricsSummaryChart } from './componentsChart/MetricsSummaryChart/MetricsSummaryChart.tsx'; -import { UpdatesPerEnvironmentTypeChart } from './componentsChart/UpdatesPerEnvironmentTypeChart/UpdatesPerEnvironmentTypeChart.tsx'; -import type { InstanceInsightsSchema } from 'openapi'; -import type { GroupedDataByProject } from './hooks/useGroupedProjectTrends.ts'; -import { allOption } from 'component/common/ProjectSelect/ProjectSelect'; -import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; -import { WidgetTitle } from './components/WidgetTitle/WidgetTitle.tsx'; -import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; - -export interface IChartsProps { - flagTrends: InstanceInsightsSchema['flagTrends']; - projectsData: InstanceInsightsSchema['projectFlagTrends']; - groupedProjectsData: GroupedDataByProject< - InstanceInsightsSchema['projectFlagTrends'] - >; - metricsData: InstanceInsightsSchema['metricsSummaryTrends']; - groupedMetricsData: GroupedDataByProject< - InstanceInsightsSchema['metricsSummaryTrends'] - >; - userTrends: InstanceInsightsSchema['userTrends']; - environmentTypeTrends: InstanceInsightsSchema['environmentTypeTrends']; - summary: { - total: number; - active: number; - stale: number; - potentiallyStale: number; - averageUsers: number; - averageHealth?: string; - flagsPerUser?: string; - medianTimeToProduction?: number; - }; - loading: boolean; - projects: string[]; - allMetricsDatapoints: string[]; -} - -const StyledContainer = styled(Box)(({ theme }) => ({ - display: 'flex', - flexDirection: 'column', - gap: theme.spacing(2), -})); - -const StyledWidget = styled(Paper)(({ theme }) => ({ - borderRadius: `${theme.shape.borderRadiusLarge}px`, - boxShadow: 'none', - display: 'flex', - flexWrap: 'wrap', - [theme.breakpoints.up('md')]: { - flexDirection: 'row', - flexWrap: 'nowrap', - }, -})); - -const StyledWidgetContent = styled(Box)(({ theme }) => ({ - padding: theme.spacing(3), - width: '100%', -})); - -const StyledWidgetStats = styled(Box)<{ width?: number; padding?: number }>( - ({ theme, width = 300, padding = 3 }) => ({ - display: 'flex', - flexDirection: 'column', - gap: theme.spacing(2), - padding: theme.spacing(padding), - minWidth: '100%', - [theme.breakpoints.up('md')]: { - minWidth: `${width}px`, - borderRight: `1px solid ${theme.palette.background.application}`, - }, - }), -); - -const StyledChartContainer = styled(Box)(({ theme }) => ({ - position: 'relative', - minWidth: 0, // bugfix, see: https://github.com/chartjs/Chart.js/issues/4156#issuecomment-295180128 - flexGrow: 1, - margin: 'auto 0', - padding: theme.spacing(3), -})); - -export const LegacyInsightsCharts: FC = ({ - projects, - summary, - userTrends, - groupedProjectsData, - flagTrends, - groupedMetricsData, - environmentTypeTrends, - allMetricsDatapoints, - loading, -}) => { - const showAllProjects = projects[0] === allOption.id; - const isOneProjectSelected = projects.length === 1; - const { isEnterprise } = useUiConfig(); - - const lastUserTrend = userTrends[userTrends.length - 1]; - const lastFlagTrend = flagTrends[flagTrends.length - 1]; - - const usersTotal = lastUserTrend?.total ?? 0; - const usersActive = lastUserTrend?.active ?? 0; - const usersInactive = lastUserTrend?.inactive ?? 0; - const flagsTotal = lastFlagTrend?.total ?? 0; - - function getFlagsPerUser(flagsTotal: number, usersTotal: number) { - const flagsPerUserCalculation = flagsTotal / usersTotal; - return Number.isNaN(flagsPerUserCalculation) - ? 'N/A' - : flagsPerUserCalculation.toFixed(2); - } - - return ( - - - - - - - - - - - - - } - elseShow={ - <> - - - - - - - - - - - } - /> - - - - - } - /> - - - - - - - - - - - - - - - - } - /> - - - - - - - - - - - - } - elseShow={ - <> - - - - - - - - - - - } - /> - - - - - - - - - - - - - - - - - } - /> - - ); -}; diff --git a/frontend/src/component/insights/sections/InsightsSection.tsx b/frontend/src/component/insights/sections/InsightsSection.tsx new file mode 100644 index 0000000000..392144284c --- /dev/null +++ b/frontend/src/component/insights/sections/InsightsSection.tsx @@ -0,0 +1,34 @@ +import { styled } from '@mui/material'; +import type { FC, PropsWithChildren, ReactNode } from 'react'; + +const StyledSection = styled('section')(({ theme }) => ({ + display: 'flex', + flexDirection: 'column', + gap: theme.spacing(2), +})); + +const SectionTitleRow = styled('div')(({ theme }) => ({ + position: 'sticky', + top: 0, + zIndex: theme.zIndex.sticky, + paddingBlock: theme.spacing(2), + background: theme.palette.background.application, + transition: 'padding 0.3s ease', + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + flexFlow: 'row wrap', + rowGap: theme.spacing(2), +})); + +export const InsightsSection: FC< + PropsWithChildren<{ title: string; filters?: ReactNode }> +> = ({ title, children, filters: HeaderActions }) => ( + + +

{title}

+ {HeaderActions} +
+ {children} +
+); diff --git a/frontend/src/component/insights/sections/LifecycleInsights.tsx b/frontend/src/component/insights/sections/LifecycleInsights.tsx new file mode 100644 index 0000000000..483ab2eed6 --- /dev/null +++ b/frontend/src/component/insights/sections/LifecycleInsights.tsx @@ -0,0 +1,26 @@ +import { usePersistentTableState } from 'hooks/usePersistentTableState'; +import type { FC } from 'react'; +import { FilterItemParam } from 'utils/serializeQueryParams'; +import { InsightsSection } from 'component/insights/sections/InsightsSection'; +import { InsightsFilters } from 'component/insights/InsightsFilters'; + +export const LifecycleInsights: FC = () => { + const statePrefix = 'lifecycle-'; + const stateConfig = { + [`${statePrefix}project`]: FilterItemParam, + }; + const [state, setState] = usePersistentTableState('insights', stateConfig); + + return ( + + } + /> + ); +}; diff --git a/frontend/src/component/insights/sections/PerformanceInsights.tsx b/frontend/src/component/insights/sections/PerformanceInsights.tsx new file mode 100644 index 0000000000..8e5e560bb6 --- /dev/null +++ b/frontend/src/component/insights/sections/PerformanceInsights.tsx @@ -0,0 +1,186 @@ +import { allOption } from 'component/common/ProjectSelect/ProjectSelect'; +import { format, subMonths } from 'date-fns'; +import { useInsights } from 'hooks/api/getters/useInsights/useInsights'; +import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; +import { usePersistentTableState } from 'hooks/usePersistentTableState'; +import type { FC } from 'react'; +import { withDefault } from 'use-query-params'; +import { FilterItemParam } from 'utils/serializeQueryParams'; +import { WidgetTitle } from 'component/insights/components/WidgetTitle/WidgetTitle'; +import { FlagsChart } from 'component/insights/componentsChart/FlagsChart/FlagsChart'; +import { FlagsProjectChart } from 'component/insights/componentsChart/FlagsProjectChart/FlagsProjectChart'; +import { MetricsSummaryChart } from 'component/insights/componentsChart/MetricsSummaryChart/MetricsSummaryChart'; +import { ProjectHealthChart } from 'component/insights/componentsChart/ProjectHealthChart/ProjectHealthChart'; +import { UpdatesPerEnvironmentTypeChart } from 'component/insights/componentsChart/UpdatesPerEnvironmentTypeChart/UpdatesPerEnvironmentTypeChart'; +import { FlagStats } from 'component/insights/componentsStat/FlagStats/FlagStats'; +import { HealthStats } from 'component/insights/componentsStat/HealthStats/HealthStats'; +import { useInsightsData } from 'component/insights/hooks/useInsightsData'; +import { InsightsSection } from 'component/insights/sections/InsightsSection'; +import { InsightsFilters } from 'component/insights/InsightsFilters'; +import { + StyledChartContainer, + StyledWidget, + StyledWidgetContent, + StyledWidgetStats, +} from '../InsightsCharts.styles'; + +export const PerformanceInsights: FC = () => { + const statePrefix = 'performance-'; + const stateConfig = { + [`${statePrefix}project`]: FilterItemParam, + [`${statePrefix}from`]: withDefault(FilterItemParam, { + values: [format(subMonths(new Date(), 1), 'yyyy-MM-dd')], + operator: 'IS', + }), + [`${statePrefix}to`]: withDefault(FilterItemParam, { + values: [format(new Date(), 'yyyy-MM-dd')], + operator: 'IS', + }), + }; + const [state, setState] = usePersistentTableState('insights', stateConfig, [ + 'performance-from', + 'performance-to', + ]); + + const { insights, loading } = useInsights( + state[`${statePrefix}from`]?.values[0], + state[`${statePrefix}to`]?.values[0], + ); + + const projects = state[`${statePrefix}project`]?.values ?? [allOption.id]; + + const showAllProjects = projects[0] === allOption.id; + const { + flagTrends, + summary, + groupedProjectsData, + userTrends, + groupedMetricsData, + allMetricsDatapoints, + environmentTypeTrends, + } = useInsightsData(insights, projects); + + const { isEnterprise } = useUiConfig(); + const lastUserTrend = userTrends[userTrends.length - 1]; + const usersTotal = lastUserTrend?.total ?? 0; + const lastFlagTrend = flagTrends[flagTrends.length - 1]; + const flagsTotal = lastFlagTrend?.total ?? 0; + + function getFlagsPerUser(flagsTotal: number, usersTotal: number) { + const flagsPerUserCalculation = flagsTotal / usersTotal; + return Number.isNaN(flagsPerUserCalculation) + ? 'N/A' + : flagsPerUserCalculation.toFixed(2); + } + + return ( + + } + > + {showAllProjects ? ( + + + + + + + + + + ) : ( + + + + + + + + + + )} + {isEnterprise() ? ( + + + + } + /> + + + + + + ) : null} + {isEnterprise() ? ( + <> + + + + + + + + + + + + + + + + ) : null} + + ); +}; diff --git a/frontend/src/component/insights/sections/UserInsights.tsx b/frontend/src/component/insights/sections/UserInsights.tsx new file mode 100644 index 0000000000..08ef2a7237 --- /dev/null +++ b/frontend/src/component/insights/sections/UserInsights.tsx @@ -0,0 +1,119 @@ +import { allOption } from 'component/common/ProjectSelect/ProjectSelect'; +import { format, subMonths } from 'date-fns'; +import { useInsights } from 'hooks/api/getters/useInsights/useInsights'; +import { usePersistentTableState } from 'hooks/usePersistentTableState'; +import type { FC } from 'react'; +import { withDefault } from 'use-query-params'; +import { FilterItemParam } from 'utils/serializeQueryParams'; +import { WidgetTitle } from 'component/insights/components/WidgetTitle/WidgetTitle'; +import { UsersChart } from 'component/insights/componentsChart/UsersChart/UsersChart'; +import { UsersPerProjectChart } from 'component/insights/componentsChart/UsersPerProjectChart/UsersPerProjectChart'; +import { UserStats } from 'component/insights/componentsStat/UserStats/UserStats'; +import { useInsightsData } from 'component/insights/hooks/useInsightsData'; +import { + StyledChartContainer, + StyledWidget, + StyledWidgetStats, +} from 'component/insights/InsightsCharts.styles'; +import { InsightsSection } from 'component/insights/sections/InsightsSection'; +import { InsightsFilters } from 'component/insights/InsightsFilters'; + +export const UserInsights: FC = () => { + const statePrefix = 'users-'; + const stateConfig = { + [`${statePrefix}project`]: FilterItemParam, + [`${statePrefix}from`]: withDefault(FilterItemParam, { + values: [format(subMonths(new Date(), 1), 'yyyy-MM-dd')], + operator: 'IS', + }), + [`${statePrefix}to`]: withDefault(FilterItemParam, { + values: [format(new Date(), 'yyyy-MM-dd')], + operator: 'IS', + }), + }; + const [state, setState] = usePersistentTableState( + 'insights-users', + stateConfig, + ['users-from', 'users-to'], + ); + + const { insights, loading } = useInsights( + state['users-from']?.values[0], + state['users-to']?.values[0], + ); + + const projects = state['users-project']?.values ?? [allOption.id]; + + const showAllProjects = projects[0] === allOption.id; + const { summary, groupedProjectsData, userTrends } = useInsightsData( + insights, + projects, + ); + + const lastUserTrend = userTrends[userTrends.length - 1]; + const usersTotal = lastUserTrend?.total ?? 0; + const usersActive = lastUserTrend?.active ?? 0; + const usersInactive = lastUserTrend?.inactive ?? 0; + + const isOneProjectSelected = projects.length === 1; + + return ( + + } + > + {showAllProjects ? ( + + + + + + + + + + ) : ( + + + + + + + + + + )} + + ); +}; diff --git a/frontend/src/hooks/usePersistentTableState.ts b/frontend/src/hooks/usePersistentTableState.ts index b9148fb462..6087033bc8 100644 --- a/frontend/src/hooks/usePersistentTableState.ts +++ b/frontend/src/hooks/usePersistentTableState.ts @@ -34,7 +34,7 @@ const usePersistentSearchParams = ( export const usePersistentTableState = ( key: string, queryParamsDefinition: T, - excludedFromStorage: string[] = ['offset'], + excludedFromStorage: (keyof T)[] = ['offset'], ) => { const updateStoredParams = usePersistentSearchParams( key,