web: ui improvements for report edit mode
This commit is contained in:
@@ -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",
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
17
apps/web/src/components/report/chart/ChartAnimation.tsx
Normal file
17
apps/web/src/components/report/chart/ChartAnimation.tsx
Normal 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} />;
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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') {
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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 && (
|
||||||
|
|||||||
1
apps/web/src/lottie/airplane.json
Normal file
1
apps/web/src/lottie/airplane.json
Normal file
File diff suppressed because one or more lines are too long
1
apps/web/src/lottie/ballon.json
Normal file
1
apps/web/src/lottie/ballon.json
Normal file
File diff suppressed because one or more lines are too long
@@ -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
18
pnpm-lock.yaml
generated
@@ -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:
|
||||||
|
|||||||
Reference in New Issue
Block a user