responsive design and bug fixes

This commit is contained in:
Carl-Gerhard Lindesvärd
2023-11-04 10:01:22 +01:00
parent 13618d1fd4
commit f5670253bc
51 changed files with 992 additions and 336 deletions

View File

@@ -3,32 +3,23 @@ import type { IChartType } from '@/types';
import { chartTypes } from '@/utils/constants';
import { Combobox } from '../ui/combobox';
import { RadioGroup, RadioGroupItem } from '../ui/radio-group';
import {
changeChartType,
changeDateRanges,
changeInterval,
} from './reportSlice';
import { changeChartType } from './reportSlice';
export function ReportChartType() {
const dispatch = useDispatch();
const type = useSelector((state) => state.report.chartType);
return (
<>
<div className="w-full max-w-[200px]">
<Combobox
placeholder="Chart type"
onChange={(value) => {
dispatch(changeChartType(value as IChartType));
}}
value={type}
items={Object.entries(chartTypes).map(([key, value]) => ({
label: value,
value: key,
}))}
/>
</div>
</>
<Combobox
placeholder="Chart type"
onChange={(value) => {
dispatch(changeChartType(value as IChartType));
}}
value={type}
items={Object.entries(chartTypes).map(([key, value]) => ({
label: value,
value: key,
}))}
/>
);
}

View File

@@ -1,49 +1,28 @@
import { useDispatch, useSelector } from '@/redux';
import type { IInterval } from '@/types';
import { intervals, timeRanges } from '@/utils/constants';
import { timeRanges } from '@/utils/constants';
import { Combobox } from '../ui/combobox';
import { RadioGroup, RadioGroupItem } from '../ui/radio-group';
import { changeDateRanges, changeInterval } from './reportSlice';
import { changeDateRanges } from './reportSlice';
export function ReportDateRange() {
const dispatch = useDispatch();
const range = useSelector((state) => state.report.range);
const interval = useSelector((state) => state.report.interval);
const chartType = useSelector((state) => state.report.chartType);
return (
<>
<RadioGroup>
{timeRanges.map((item) => {
return (
<RadioGroupItem
key={item.range}
active={item.range === range}
onClick={() => {
dispatch(changeDateRanges(item.range));
}}
>
{item.title}
</RadioGroupItem>
);
})}
</RadioGroup>
{chartType === 'linear' && (
<div className="w-full max-w-[200px]">
<Combobox
placeholder="Interval"
onChange={(value) => {
dispatch(changeInterval(value as IInterval));
<RadioGroup className="overflow-auto">
{timeRanges.map((item) => {
return (
<RadioGroupItem
key={item.range}
active={item.range === range}
onClick={() => {
dispatch(changeDateRanges(item.range));
}}
value={interval}
items={Object.entries(intervals).map(([key, value]) => ({
label: value,
value: key,
}))}
/>
</div>
)}
</>
>
{item.title}
</RadioGroupItem>
);
})}
</RadioGroup>
);
}

View File

@@ -0,0 +1,46 @@
import { useDispatch, useSelector } from '@/redux';
import type { IInterval } from '@/types';
import { isMinuteIntervalEnabledByRange } from '@/utils/constants';
import { Combobox } from '../ui/combobox';
import { changeInterval } from './reportSlice';
export function ReportInterval() {
const dispatch = useDispatch();
const interval = useSelector((state) => state.report.interval);
const range = useSelector((state) => state.report.range);
const chartType = useSelector((state) => state.report.chartType);
if (chartType !== 'linear') {
return null;
}
return (
<Combobox
placeholder="Interval"
onChange={(value) => {
dispatch(changeInterval(value as IInterval));
}}
value={interval}
items={[
{
value: 'minute',
label: 'Minute',
disabled: !isMinuteIntervalEnabledByRange(range),
},
{
value: 'hour',
label: 'Hour',
},
{
value: 'day',
label: 'Day',
},
{
value: 'month',
label: 'Month',
disabled: range < 1,
},
]}
/>
);
}

View File

@@ -1,12 +1,15 @@
import { Button } from '@/components/ui/button';
import { toast } from '@/components/ui/use-toast';
import { useOrganizationParams } from '@/hooks/useOrganizationParams';
import { pushModal } from '@/modals';
import { useSelector } from '@/redux';
import { api, handleError } from '@/utils/api';
import { SaveIcon } from 'lucide-react';
import { useReportId } from '../hooks/useReportId';
import { useReportId } from './hooks/useReportId';
export function ReportSaveButton() {
const params = useOrganizationParams();
const { reportId } = useReportId();
const update = api.report.update.useMutation({
onSuccess() {
@@ -27,10 +30,9 @@ export function ReportSaveButton() {
update.mutate({
reportId,
report,
dashboardId: '9227feb4-ad59-40f3-b887-3501685733dd',
projectId: 'f7eabf0c-e0b0-4ac0-940f-1589715b0c3d',
});
}}
icon={SaveIcon}
>
Update
</Button>
@@ -43,8 +45,9 @@ export function ReportSaveButton() {
report,
});
}}
icon={SaveIcon}
>
Create
Save
</Button>
);
}

View File

@@ -1,7 +1,6 @@
import { createContext, memo, useContext, useMemo } from 'react';
import { pick } from 'ramda';
interface ChartContextType {
export interface ChartContextType {
editMode: boolean;
}

View File

@@ -0,0 +1,30 @@
import React, { useEffect, useRef } from 'react';
import { useInViewport } from 'react-in-viewport';
import type { ReportChartProps } from '.';
import { Chart } from '.';
import type { ChartContextType } from './ChartProvider';
export function LazyChart(props: ReportChartProps & ChartContextType) {
const ref = useRef<HTMLDivElement>(null);
const once = useRef(false);
const { inViewport } = useInViewport(ref, undefined, {
disconnectOnLeave: true,
});
useEffect(() => {
if (inViewport) {
once.current = true;
}
}, [inViewport]);
return (
<div ref={ref}>
{once.current || inViewport ? (
<Chart {...props} editMode={false} />
) : (
<div className="h-64 w-full bg-gray-200 animate-pulse rounded" />
)}
</div>
);
}

View File

@@ -93,9 +93,9 @@ export function ReportBarChart({ data }: ReportBarChartProps) {
getCoreRowModel: getCoreRowModel(),
onSortingChange: setSorting,
getSortedRowModel: getSortedRowModel(),
debugTable: true,
debugHeaders: true,
debugColumns: true,
// debugTable: true,
// debugHeaders: true,
// debugColumns: true,
});
return (
<div ref={ref}>

View File

@@ -40,47 +40,52 @@ export function ReportLineChart({ interval, data }: ReportLineChartProps) {
return (
<>
<AutoSizer disableHeight>
{({ width }) => (
<LineChart width={width} height={Math.min(width * 0.5, 400)}>
<YAxis dataKey={'count'} width={30} fontSize={12}></YAxis>
<Tooltip content={<ReportLineChartTooltip />} />
<CartesianGrid strokeDasharray="3 3" />
<XAxis
fontSize={12}
dataKey="date"
tickFormatter={(m: string) => {
return formatDate(m);
}}
tickLine={false}
allowDuplicatedCategory={false}
/>
{data?.series
.filter((serie) => {
return visibleSeries.includes(serie.name);
})
.map((serie) => {
const realIndex = data?.series.findIndex(
(item) => item.name === serie.name
);
const key = serie.name;
const strokeColor = getChartColor(realIndex);
return (
<Line
type="monotone"
key={key}
isAnimationActive={false}
strokeWidth={2}
dataKey="count"
stroke={strokeColor}
data={serie.data}
name={serie.name}
/>
);
})}
</LineChart>
)}
</AutoSizer>
<div className="max-sm:-mx-3">
<AutoSizer disableHeight>
{({ width }) => (
<LineChart
width={width}
height={Math.min(Math.max(width * 0.5, 250), 400)}
>
<YAxis dataKey={'count'} width={30} fontSize={12}></YAxis>
<Tooltip content={<ReportLineChartTooltip />} />
<CartesianGrid strokeDasharray="3 3" />
<XAxis
fontSize={12}
dataKey="date"
tickFormatter={(m: string) => {
return formatDate(m);
}}
tickLine={false}
allowDuplicatedCategory={false}
/>
{data?.series
.filter((serie) => {
return visibleSeries.includes(serie.name);
})
.map((serie) => {
const realIndex = data?.series.findIndex(
(item) => item.name === serie.name
);
const key = serie.name;
const strokeColor = getChartColor(realIndex);
return (
<Line
type="monotone"
key={key}
isAnimationActive={false}
strokeWidth={2}
dataKey="count"
stroke={strokeColor}
data={serie.data}
name={serie.name}
/>
);
})}
</LineChart>
)}
</AutoSizer>
</div>
{editMode && (
<ReportTable
data={data}

View File

@@ -1,3 +1,5 @@
import { memo } from 'react';
import { useOrganizationParams } from '@/hooks/useOrganizationParams';
import type { IChartInput } from '@/types';
import { api } from '@/utils/api';
@@ -5,17 +7,18 @@ import { withChartProivder } from './ChartProvider';
import { ReportBarChart } from './ReportBarChart';
import { ReportLineChart } from './ReportLineChart';
type ReportLineChartProps = IChartInput;
export type ReportChartProps = IChartInput;
export const Chart = withChartProivder(
({
export const Chart = memo(
withChartProivder(function Chart({
interval,
events,
breakdowns,
chartType,
name,
range,
}: ReportLineChartProps) => {
}: ReportChartProps) {
const params = useOrganizationParams();
const hasEmptyFilters = events.some((event) =>
event.filters.some((filter) => filter.value.length === 0)
);
@@ -29,6 +32,7 @@ export const Chart = withChartProivder(
range,
startDate: null,
endDate: null,
projectSlug: params.project,
},
{
keepPreviousData: true,
@@ -63,5 +67,5 @@ export const Chart = withChartProivder(
}
return <p>Chart type &quot;{chartType}&quot; is not supported yet.</p>;
}
})
);

View File

@@ -6,7 +6,7 @@ import type {
IChartType,
IInterval,
} from '@/types';
import { alphabetIds } from '@/utils/constants';
import { alphabetIds, isMinuteIntervalEnabledByRange } from '@/utils/constants';
import { createSlice } from '@reduxjs/toolkit';
import type { PayloadAction } from '@reduxjs/toolkit';
@@ -104,6 +104,13 @@ export const reportSlice = createSlice({
// Chart type
changeChartType: (state, action: PayloadAction<IChartType>) => {
state.chartType = action.payload;
if (
!isMinuteIntervalEnabledByRange(state.range) &&
state.interval === 'minute'
) {
state.interval = 'hour';
}
},
// Date range

View File

@@ -1,6 +1,6 @@
import { ColorSquare } from '@/components/ColorSquare';
import { Combobox } from '@/components/ui/combobox';
import { RenderDots } from '@/components/ui/RenderDots';
import { useOrganizationParams } from '@/hooks/useOrganizationParams';
import { useDispatch, useSelector } from '@/redux';
import type { IChartBreakdown } from '@/types';
import { api } from '@/utils/api';
@@ -10,9 +10,12 @@ import { ReportBreakdownMore } from './ReportBreakdownMore';
import type { ReportEventMoreProps } from './ReportEventMore';
export function ReportBreakdowns() {
const params = useOrganizationParams();
const selectedBreakdowns = useSelector((state) => state.report.breakdowns);
const dispatch = useDispatch();
const propertiesQuery = api.chart.properties.useQuery();
const propertiesQuery = api.chart.properties.useQuery({
projectSlug: params.project,
});
const propertiesCombobox = (propertiesQuery.data ?? []).map((item) => ({
value: item,
label: item, // <RenderDots truncate>{item}</RenderDots>,

View File

@@ -14,6 +14,7 @@ import {
} from '@/components/ui/command';
import { RenderDots } from '@/components/ui/RenderDots';
import { useMappings } from '@/hooks/useMappings';
import { useOrganizationParams } from '@/hooks/useOrganizationParams';
import { useDispatch } from '@/redux';
import type {
IChartEvent,
@@ -37,10 +38,12 @@ export function ReportEventFilters({
isCreating,
setIsCreating,
}: ReportEventFiltersProps) {
const params = useOrganizationParams();
const dispatch = useDispatch();
const propertiesQuery = api.chart.properties.useQuery(
{
event: event.name,
projectSlug: params.project,
},
{
enabled: !!event.name,
@@ -99,11 +102,13 @@ interface FilterProps {
}
function Filter({ filter, event }: FilterProps) {
const params = useOrganizationParams();
const getLabel = useMappings();
const dispatch = useDispatch();
const potentialValues = api.chart.values.useQuery({
event: event.name,
property: filter.name,
projectSlug: params.project,
});
const valuesCombobox =

View File

@@ -2,6 +2,7 @@ import { useState } from 'react';
import { ColorSquare } from '@/components/ColorSquare';
import { Dropdown } from '@/components/Dropdown';
import { Combobox } from '@/components/ui/combobox';
import { useOrganizationParams } from '@/hooks/useOrganizationParams';
import { useDispatch, useSelector } from '@/redux';
import type { IChartEvent } from '@/types';
import { api } from '@/utils/api';
@@ -16,7 +17,10 @@ export function ReportEvents() {
const [isCreating, setIsCreating] = useState(false);
const selectedEvents = useSelector((state) => state.report.events);
const dispatch = useDispatch();
const eventsQuery = api.chart.events.useQuery();
const params = useOrganizationParams();
const eventsQuery = api.chart.events.useQuery({
projectSlug: params.project,
});
const eventsCombobox = (eventsQuery.data ?? []).map((item) => ({
value: item.name,
label: item.name,

View File

@@ -1,13 +1,17 @@
import { Button } from '@/components/ui/button';
import { SheetClose } from '@/components/ui/sheet';
import { ReportBreakdowns } from './ReportBreakdowns';
import { ReportEvents } from './ReportEvents';
import { ReportSaveButton } from './ReportSaveButton';
export function ReportSidebar() {
return (
<div className="flex flex-col gap-4 p-4">
<div className="flex flex-col gap-8">
<ReportEvents />
<ReportBreakdowns />
<ReportSaveButton />
<SheetClose asChild>
<Button>Done</Button>
</SheetClose>
</div>
);
}