web: ui improvements for report edit mode

This commit is contained in:
Carl-Gerhard Lindesvärd
2023-12-12 09:49:06 +01:00
parent 0db81832bf
commit c175707be4
11 changed files with 74 additions and 14 deletions

View File

@@ -40,6 +40,7 @@
"class-variance-authority": "^0.7.0", "class-variance-authority": "^0.7.0",
"clsx": "^2.0.0", "clsx": "^2.0.0",
"cmdk": "^0.2.0", "cmdk": "^0.2.0",
"lottie-react": "^2.4.0",
"lucide-react": "^0.286.0", "lucide-react": "^0.286.0",
"mitt": "^3.0.1", "mitt": "^3.0.1",
"next": "13.4", "next": "13.4",

View File

@@ -1,6 +1,5 @@
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { toast } from '@/components/ui/use-toast'; import { toast } from '@/components/ui/use-toast';
import { useOrganizationParams } from '@/hooks/useOrganizationParams';
import { pushModal } from '@/modals'; import { pushModal } from '@/modals';
import { useSelector } from '@/redux'; import { useSelector } from '@/redux';
import { api, handleError } from '@/utils/api'; import { api, handleError } from '@/utils/api';
@@ -9,7 +8,6 @@ import { SaveIcon } from 'lucide-react';
import { useReportId } from './hooks/useReportId'; import { useReportId } from './hooks/useReportId';
export function ReportSaveButton() { export function ReportSaveButton() {
const params = useOrganizationParams();
const { reportId } = useReportId(); const { reportId } = useReportId();
const update = api.report.update.useMutation({ const update = api.report.update.useMutation({
onSuccess() { onSuccess() {
@@ -25,6 +23,7 @@ export function ReportSaveButton() {
if (reportId) { if (reportId) {
return ( return (
<Button <Button
disabled={!report.dirty}
loading={update.isLoading} loading={update.isLoading}
onClick={() => { onClick={() => {
update.mutate({ update.mutate({
@@ -40,6 +39,7 @@ export function ReportSaveButton() {
} else { } else {
return ( return (
<Button <Button
disabled={!report.dirty}
onClick={() => { onClick={() => {
pushModal('SaveReport', { pushModal('SaveReport', {
report, report,

View File

@@ -0,0 +1,17 @@
import airplane from '@/lottie/airplane.json';
import ballon from '@/lottie/ballon.json';
import type { LottieComponentProps } from 'lottie-react';
import Lottie from 'lottie-react';
const animations = {
airplane,
ballon,
};
type Animations = keyof typeof animations;
export const ChartAnimation = ({
name,
...props
}: Omit<LottieComponentProps, 'animationData'> & {
name: Animations;
}) => <Lottie animationData={animations[name]} loop={true} {...props} />;

View File

@@ -47,7 +47,7 @@ export function ReportLineChart({ interval, data }: ReportLineChartProps) {
width={width} width={width}
height={Math.min(Math.max(width * 0.5, 250), 400)} height={Math.min(Math.max(width * 0.5, 250), 400)}
> >
<YAxis dataKey={'count'} width={30} fontSize={12}></YAxis> <YAxis dataKey={'count'} fontSize={12}></YAxis>
<Tooltip content={<ReportLineChartTooltip />} /> <Tooltip content={<ReportLineChartTooltip />} />
<CartesianGrid strokeDasharray="3 3" /> <CartesianGrid strokeDasharray="3 3" />
<XAxis <XAxis

View File

@@ -3,6 +3,7 @@ import { useOrganizationParams } from '@/hooks/useOrganizationParams';
import type { IChartInput } from '@/types'; import type { IChartInput } from '@/types';
import { api } from '@/utils/api'; import { api } from '@/utils/api';
import { ChartAnimation } from './ChartAnimation';
import { withChartProivder } from './ChartProvider'; import { withChartProivder } from './ChartProvider';
import { ReportBarChart } from './ReportBarChart'; import { ReportBarChart } from './ReportBarChart';
import { ReportLineChart } from './ReportLineChart'; import { ReportLineChart } from './ReportLineChart';
@@ -22,6 +23,7 @@ export const Chart = memo(
const hasEmptyFilters = events.some((event) => const hasEmptyFilters = events.some((event) =>
event.filters.some((filter) => filter.value.length === 0) event.filters.some((filter) => filter.value.length === 0)
); );
const enabled = events.length > 0 && !hasEmptyFilters;
const chart = api.chart.chart.useQuery( const chart = api.chart.chart.useQuery(
{ {
interval, interval,
@@ -35,17 +37,19 @@ export const Chart = memo(
projectSlug: params.project, projectSlug: params.project,
}, },
{ {
keepPreviousData: true, keepPreviousData: false,
enabled: events.length > 0 && !hasEmptyFilters, enabled,
} }
); );
console.log(chart.data);
const anyData = Boolean(chart.data?.series?.[0]?.data); const anyData = Boolean(chart.data?.series?.[0]?.data);
if (chart.isFetching && !anyData) { if (!enabled) {
return <p>Loading...</p>; return <p>Select events & filters to begin</p>;
}
if (chart.isFetching) {
return <ChartAnimation name="airplane" className="w-96 mx-auto" />;
} }
if (chart.isError) { if (chart.isError) {
@@ -53,11 +57,11 @@ export const Chart = memo(
} }
if (!chart.isSuccess) { if (!chart.isSuccess) {
return <p>Loading...</p>; return <ChartAnimation name="ballon" className="w-96 mx-auto" />;
} }
if (!anyData) { if (!anyData) {
return <p>No data</p>; return <ChartAnimation name="ballon" className="w-96 mx-auto" />;
} }
if (chartType === 'bar') { if (chartType === 'bar') {

View File

@@ -11,12 +11,14 @@ import { createSlice } from '@reduxjs/toolkit';
import type { PayloadAction } from '@reduxjs/toolkit'; import type { PayloadAction } from '@reduxjs/toolkit';
type InitialState = IChartInput & { type InitialState = IChartInput & {
dirty: boolean;
startDate: string | null; startDate: string | null;
endDate: string | null; endDate: string | null;
}; };
// First approach: define the initial state using that type // First approach: define the initial state using that type
const initialState: InitialState = { const initialState: InitialState = {
dirty: false,
name: 'screen_view', name: 'screen_view',
chartType: 'linear', chartType: 'linear',
interval: 'day', interval: 'day',
@@ -39,10 +41,12 @@ export const reportSlice = createSlice({
...action.payload, ...action.payload,
startDate: null, startDate: null,
endDate: null, endDate: null,
dirty: false,
}; };
}, },
// Events // Events
addEvent: (state, action: PayloadAction<Omit<IChartEvent, 'id'>>) => { addEvent: (state, action: PayloadAction<Omit<IChartEvent, 'id'>>) => {
state.dirty = true;
state.events.push({ state.events.push({
id: alphabetIds[state.events.length]!, id: alphabetIds[state.events.length]!,
...action.payload, ...action.payload,
@@ -54,11 +58,13 @@ export const reportSlice = createSlice({
id: string; id: string;
}> }>
) => { ) => {
state.dirty = true;
state.events = state.events.filter( state.events = state.events.filter(
(event) => event.id !== action.payload.id (event) => event.id !== action.payload.id
); );
}, },
changeEvent: (state, action: PayloadAction<IChartEvent>) => { changeEvent: (state, action: PayloadAction<IChartEvent>) => {
state.dirty = true;
state.events = state.events.map((event) => { state.events = state.events.map((event) => {
if (event.id === action.payload.id) { if (event.id === action.payload.id) {
return action.payload; return action.payload;
@@ -72,6 +78,7 @@ export const reportSlice = createSlice({
state, state,
action: PayloadAction<Omit<IChartBreakdown, 'id'>> action: PayloadAction<Omit<IChartBreakdown, 'id'>>
) => { ) => {
state.dirty = true;
state.breakdowns.push({ state.breakdowns.push({
id: alphabetIds[state.breakdowns.length]!, id: alphabetIds[state.breakdowns.length]!,
...action.payload, ...action.payload,
@@ -83,11 +90,13 @@ export const reportSlice = createSlice({
id: string; id: string;
}> }>
) => { ) => {
state.dirty = true;
state.breakdowns = state.breakdowns.filter( state.breakdowns = state.breakdowns.filter(
(event) => event.id !== action.payload.id (event) => event.id !== action.payload.id
); );
}, },
changeBreakdown: (state, action: PayloadAction<IChartBreakdown>) => { changeBreakdown: (state, action: PayloadAction<IChartBreakdown>) => {
state.dirty = true;
state.breakdowns = state.breakdowns.map((breakdown) => { state.breakdowns = state.breakdowns.map((breakdown) => {
if (breakdown.id === action.payload.id) { if (breakdown.id === action.payload.id) {
return action.payload; return action.payload;
@@ -98,11 +107,13 @@ export const reportSlice = createSlice({
// Interval // Interval
changeInterval: (state, action: PayloadAction<IInterval>) => { changeInterval: (state, action: PayloadAction<IInterval>) => {
state.dirty = true;
state.interval = action.payload; state.interval = action.payload;
}, },
// Chart type // Chart type
changeChartType: (state, action: PayloadAction<IChartType>) => { changeChartType: (state, action: PayloadAction<IChartType>) => {
state.dirty = true;
state.chartType = action.payload; state.chartType = action.payload;
if ( if (
@@ -115,15 +126,18 @@ export const reportSlice = createSlice({
// Date range // Date range
changeStartDate: (state, action: PayloadAction<string>) => { changeStartDate: (state, action: PayloadAction<string>) => {
state.dirty = true;
state.startDate = action.payload; state.startDate = action.payload;
}, },
// Date range // Date range
changeEndDate: (state, action: PayloadAction<string>) => { changeEndDate: (state, action: PayloadAction<string>) => {
state.dirty = true;
state.endDate = action.payload; state.endDate = action.payload;
}, },
changeDateRanges: (state, action: PayloadAction<IChartRange>) => { changeDateRanges: (state, action: PayloadAction<IChartRange>) => {
state.dirty = true;
state.range = action.payload; state.range = action.payload;
if (action.payload === 0.3 || action.payload === 0.6) { if (action.payload === 0.3 || action.payload === 0.6) {
state.interval = 'minute'; state.interval = 'minute';

View File

@@ -3,7 +3,7 @@ import { cn } from '@/utils/cn';
import { Slot } from '@radix-ui/react-slot'; import { Slot } from '@radix-ui/react-slot';
import { cva } from 'class-variance-authority'; import { cva } from 'class-variance-authority';
import type { VariantProps } from 'class-variance-authority'; import type { VariantProps } from 'class-variance-authority';
import type { LucideIcon, LucideProps } from 'lucide-react'; import type { LucideIcon } from 'lucide-react';
import { Loader2 } from 'lucide-react'; import { Loader2 } from 'lucide-react';
const buttonVariants = cva( const buttonVariants = cva(
@@ -64,7 +64,7 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
<Comp <Comp
className={cn(buttonVariants({ variant, size, className }))} className={cn(buttonVariants({ variant, size, className }))}
ref={ref} ref={ref}
disabled={loading ?? disabled} disabled={loading || disabled}
{...props} {...props}
> >
{Icon && ( {Icon && (

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -41,6 +41,10 @@ export default function Page() {
if (reportId && reportQuery.data) { if (reportId && reportQuery.data) {
dispatch(setReport(reportQuery.data)); dispatch(setReport(reportQuery.data));
} }
if (!reportId) {
dispatch(reset());
}
}, [reportId, reportQuery.data, dispatch]); }, [reportId, reportQuery.data, dispatch]);
return ( return (
@@ -57,7 +61,7 @@ export default function Page() {
</div> </div>
<div className="flex gap-4"> <div className="flex gap-4">
<SheetTrigger asChild> <SheetTrigger asChild>
<Button size="default">Select events & Filters</Button> <Button size="default">Select events & filters</Button>
</SheetTrigger> </SheetTrigger>
<ReportSaveButton /> <ReportSaveButton />
</div> </div>

18
pnpm-lock.yaml generated
View File

@@ -172,6 +172,9 @@ importers:
cmdk: cmdk:
specifier: ^0.2.0 specifier: ^0.2.0
version: 0.2.0(@types/react@18.2.34)(react-dom@18.2.0)(react@18.2.0) version: 0.2.0(@types/react@18.2.34)(react-dom@18.2.0)(react@18.2.0)
lottie-react:
specifier: ^2.4.0
version: 2.4.0(react-dom@18.2.0)(react@18.2.0)
lucide-react: lucide-react:
specifier: ^0.286.0 specifier: ^0.286.0
version: 0.286.0(react@18.2.0) version: 0.286.0(react@18.2.0)
@@ -4331,6 +4334,21 @@ packages:
js-tokens: 4.0.0 js-tokens: 4.0.0
dev: false dev: false
/lottie-react@2.4.0(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-pDJGj+AQlnlyHvOHFK7vLdsDcvbuqvwPZdMlJ360wrzGFurXeKPr8SiRCjLf3LrNYKANQtSsh5dz9UYQHuqx4w==}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
dependencies:
lottie-web: 5.12.2
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
dev: false
/lottie-web@5.12.2:
resolution: {integrity: sha512-uvhvYPC8kGPjXT3MyKMrL3JitEAmDMp30lVkuq/590Mw9ok6pWcFCwXJveo0t5uqYw1UREQHofD+jVpdjBv8wg==}
dev: false
/lowlight@1.20.0: /lowlight@1.20.0:
resolution: {integrity: sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw==} resolution: {integrity: sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw==}
dependencies: dependencies: