web: easier to navigate around + a lot of minor ui improvements
This commit is contained in:
@@ -1,5 +1,20 @@
|
|||||||
import type { HtmlProps } from '@/types';
|
import type { HtmlProps } from '@/types';
|
||||||
|
import { cn } from '@/utils/cn';
|
||||||
|
|
||||||
export function Card({ children }: HtmlProps<HTMLDivElement>) {
|
type CardProps = HtmlProps<HTMLDivElement> & {
|
||||||
return <div className="border border-border rounded">{children}</div>;
|
hover?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function Card({ children, hover }: CardProps) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'border border-border rounded',
|
||||||
|
hover &&
|
||||||
|
'transition-all hover:-translate-y-0.5 hover:shadow hover:border-black'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,16 @@ export function Breadcrumbs() {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const dashboard = api.dashboard.get.useQuery(
|
||||||
|
{
|
||||||
|
slug: params.dashboard,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: !!params.dashboard,
|
||||||
|
staleTime: Infinity,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="border-b border-border text-xs">
|
<div className="border-b border-border text-xs">
|
||||||
<Container className="flex items-center gap-2 h-8">
|
<Container className="flex items-center gap-2 h-8">
|
||||||
@@ -52,6 +62,18 @@ export function Breadcrumbs() {
|
|||||||
</Link>
|
</Link>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{org.data && pro.data && dashboard.data && (
|
||||||
|
<>
|
||||||
|
<ChevronRight size={10} />
|
||||||
|
<Link
|
||||||
|
shallow
|
||||||
|
href={`/${org.data.slug}/${pro.data.slug}/${dashboard.data.slug}`}
|
||||||
|
>
|
||||||
|
{dashboard.data.name}
|
||||||
|
</Link>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</Container>
|
</Container>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useOrganizationParams } from '@/hooks/useOrganizationParams';
|
import { useOrganizationParams } from '@/hooks/useOrganizationParams';
|
||||||
import { cn } from '@/utils/cn';
|
import { cn } from '@/utils/cn';
|
||||||
|
import { strip } from '@/utils/object';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
|
||||||
import { NavbarUserDropdown } from './NavbarUserDropdown';
|
import { NavbarUserDropdown } from './NavbarUserDropdown';
|
||||||
@@ -29,7 +30,12 @@ export function NavbarMenu() {
|
|||||||
{params.project && (
|
{params.project && (
|
||||||
<Link
|
<Link
|
||||||
shallow
|
shallow
|
||||||
href={`/${params.organization}/${params.project}/reports`}
|
href={{
|
||||||
|
pathname: `/${params.organization}/${params.project}/reports`,
|
||||||
|
query: strip({
|
||||||
|
dashboard: params.dashboard,
|
||||||
|
}),
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
Create report
|
Create report
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -1,16 +1,19 @@
|
|||||||
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 { pushModal } from '@/modals';
|
import { pushModal } from '@/modals';
|
||||||
import { useSelector } from '@/redux';
|
import { useDispatch, useSelector } from '@/redux';
|
||||||
import { api, handleError } from '@/utils/api';
|
import { api, handleError } from '@/utils/api';
|
||||||
import { SaveIcon } from 'lucide-react';
|
import { SaveIcon } from 'lucide-react';
|
||||||
|
|
||||||
import { useReportId } from './hooks/useReportId';
|
import { useReportId } from './hooks/useReportId';
|
||||||
|
import { resetDirty } from './reportSlice';
|
||||||
|
|
||||||
export function ReportSaveButton() {
|
export function ReportSaveButton() {
|
||||||
const { reportId } = useReportId();
|
const { reportId } = useReportId();
|
||||||
|
const dispatch = useDispatch();
|
||||||
const update = api.report.update.useMutation({
|
const update = api.report.update.useMutation({
|
||||||
onSuccess() {
|
onSuccess() {
|
||||||
|
dispatch(resetDirty());
|
||||||
toast({
|
toast({
|
||||||
title: 'Success',
|
title: 'Success',
|
||||||
description: 'Report updated.',
|
description: 'Report updated.',
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
import airplane from '@/lottie/airplane.json';
|
import airplane from '@/lottie/airplane.json';
|
||||||
import ballon from '@/lottie/ballon.json';
|
import ballon from '@/lottie/ballon.json';
|
||||||
|
import noData from '@/lottie/no-data.json';
|
||||||
|
import { cn } from '@/utils/cn';
|
||||||
import type { LottieComponentProps } from 'lottie-react';
|
import type { LottieComponentProps } from 'lottie-react';
|
||||||
import Lottie from 'lottie-react';
|
import Lottie from 'lottie-react';
|
||||||
|
|
||||||
const animations = {
|
const animations = {
|
||||||
airplane,
|
airplane,
|
||||||
ballon,
|
ballon,
|
||||||
|
noData,
|
||||||
};
|
};
|
||||||
type Animations = keyof typeof animations;
|
type Animations = keyof typeof animations;
|
||||||
|
|
||||||
@@ -15,3 +18,12 @@ export const ChartAnimation = ({
|
|||||||
}: Omit<LottieComponentProps, 'animationData'> & {
|
}: Omit<LottieComponentProps, 'animationData'> & {
|
||||||
name: Animations;
|
name: Animations;
|
||||||
}) => <Lottie animationData={animations[name]} loop={true} {...props} />;
|
}) => <Lottie animationData={animations[name]} loop={true} {...props} />;
|
||||||
|
|
||||||
|
export const ChartAnimationContainer = (
|
||||||
|
props: React.ButtonHTMLAttributes<HTMLDivElement>
|
||||||
|
) => (
|
||||||
|
<div
|
||||||
|
{...props}
|
||||||
|
className={cn('border border-border rounded-md p-8', props.className)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|||||||
@@ -3,7 +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 { ChartAnimation, ChartAnimationContainer } 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';
|
||||||
@@ -45,15 +45,32 @@ export const Chart = memo(
|
|||||||
const anyData = Boolean(chart.data?.series?.[0]?.data);
|
const anyData = Boolean(chart.data?.series?.[0]?.data);
|
||||||
|
|
||||||
if (!enabled) {
|
if (!enabled) {
|
||||||
return <p>Select events & filters to begin</p>;
|
return (
|
||||||
|
<ChartAnimationContainer>
|
||||||
|
<ChartAnimation name="ballon" className="w-96 mx-auto" />
|
||||||
|
<p className="text-center font-medium">
|
||||||
|
Please select at least one event to see the chart.
|
||||||
|
</p>
|
||||||
|
</ChartAnimationContainer>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (chart.isFetching) {
|
if (chart.isFetching) {
|
||||||
return <ChartAnimation name="airplane" className="w-96 mx-auto" />;
|
return (
|
||||||
|
<ChartAnimationContainer>
|
||||||
|
<ChartAnimation name="airplane" className="w-96 mx-auto" />
|
||||||
|
<p className="text-center font-medium">Loading...</p>
|
||||||
|
</ChartAnimationContainer>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (chart.isError) {
|
if (chart.isError) {
|
||||||
return <p>Error</p>;
|
return (
|
||||||
|
<ChartAnimationContainer>
|
||||||
|
<ChartAnimation name="noData" className="w-96 mx-auto" />
|
||||||
|
<p className="text-center font-medium">Something went wrong...</p>
|
||||||
|
</ChartAnimationContainer>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!chart.isSuccess) {
|
if (!chart.isSuccess) {
|
||||||
@@ -61,7 +78,12 @@ export const Chart = memo(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!anyData) {
|
if (!anyData) {
|
||||||
return <ChartAnimation name="ballon" className="w-96 mx-auto" />;
|
return (
|
||||||
|
<ChartAnimationContainer>
|
||||||
|
<ChartAnimation name="noData" className="w-96 mx-auto" />
|
||||||
|
<p className="text-center font-medium">No data</p>
|
||||||
|
</ChartAnimationContainer>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (chartType === 'bar') {
|
if (chartType === 'bar') {
|
||||||
@@ -72,6 +94,13 @@ export const Chart = memo(
|
|||||||
return <ReportLineChart interval={interval} data={chart.data} />;
|
return <ReportLineChart interval={interval} data={chart.data} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <p>Chart type "{chartType}" is not supported yet.</p>;
|
return (
|
||||||
|
<ChartAnimationContainer>
|
||||||
|
<ChartAnimation name="ballon" className="w-96 mx-auto" />
|
||||||
|
<p className="text-center font-medium">
|
||||||
|
Chart type "{chartType}" is not supported yet.
|
||||||
|
</p>
|
||||||
|
</ChartAnimationContainer>
|
||||||
|
);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -33,6 +33,12 @@ export const reportSlice = createSlice({
|
|||||||
name: 'counter',
|
name: 'counter',
|
||||||
initialState,
|
initialState,
|
||||||
reducers: {
|
reducers: {
|
||||||
|
resetDirty(state) {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
dirty: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
reset() {
|
reset() {
|
||||||
return initialState;
|
return initialState;
|
||||||
},
|
},
|
||||||
@@ -165,6 +171,7 @@ export const {
|
|||||||
changeInterval,
|
changeInterval,
|
||||||
changeDateRanges,
|
changeDateRanges,
|
||||||
changeChartType,
|
changeChartType,
|
||||||
|
resetDirty,
|
||||||
} = reportSlice.actions;
|
} = reportSlice.actions;
|
||||||
|
|
||||||
export default reportSlice.reducer;
|
export default reportSlice.reducer;
|
||||||
|
|||||||
1
apps/web/src/lottie/no-data.json
Normal file
1
apps/web/src/lottie/no-data.json
Normal file
File diff suppressed because one or more lines are too long
78
apps/web/src/modals/AddDashboard.tsx
Normal file
78
apps/web/src/modals/AddDashboard.tsx
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
import { ButtonContainer } from '@/components/ButtonContainer';
|
||||||
|
import { InputWithLabel } from '@/components/forms/InputWithLabel';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { toast } from '@/components/ui/use-toast';
|
||||||
|
import { useRefetchActive } from '@/hooks/useRefetchActive';
|
||||||
|
import { api, handleError } from '@/utils/api';
|
||||||
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import { popModal } from '.';
|
||||||
|
import { ModalContent, ModalHeader } from './Modal/Container';
|
||||||
|
|
||||||
|
interface AddDashboardProps {
|
||||||
|
organizationSlug: string;
|
||||||
|
projectSlug: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const validator = z.object({
|
||||||
|
name: z.string().min(1, 'Required'),
|
||||||
|
});
|
||||||
|
|
||||||
|
type IForm = z.infer<typeof validator>;
|
||||||
|
|
||||||
|
export default function AddDashboard({
|
||||||
|
// organizationSlug,
|
||||||
|
projectSlug,
|
||||||
|
}: AddDashboardProps) {
|
||||||
|
const refetch = useRefetchActive();
|
||||||
|
|
||||||
|
const { register, handleSubmit, formState } = useForm<IForm>({
|
||||||
|
resolver: zodResolver(validator),
|
||||||
|
defaultValues: {
|
||||||
|
name: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const mutation = api.dashboard.create.useMutation({
|
||||||
|
onError: handleError,
|
||||||
|
onSuccess() {
|
||||||
|
refetch();
|
||||||
|
toast({
|
||||||
|
title: 'Success',
|
||||||
|
description: 'Dashboard created.',
|
||||||
|
});
|
||||||
|
popModal();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModalContent>
|
||||||
|
<ModalHeader title="Edit client" />
|
||||||
|
<form
|
||||||
|
className="flex flex-col gap-4"
|
||||||
|
onSubmit={handleSubmit(({ name }) => {
|
||||||
|
mutation.mutate({
|
||||||
|
name,
|
||||||
|
projectSlug,
|
||||||
|
});
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<InputWithLabel
|
||||||
|
label="Name"
|
||||||
|
placeholder="Name of the dashboard"
|
||||||
|
{...register('name')}
|
||||||
|
/>
|
||||||
|
<ButtonContainer>
|
||||||
|
<Button type="button" variant="outline" onClick={() => popModal()}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" disabled={!formState.isDirty}>
|
||||||
|
Create
|
||||||
|
</Button>
|
||||||
|
</ButtonContainer>
|
||||||
|
</form>
|
||||||
|
</ModalContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { useEffect } from 'react';
|
||||||
import { ButtonContainer } from '@/components/ButtonContainer';
|
import { ButtonContainer } from '@/components/ButtonContainer';
|
||||||
import { InputWithLabel } from '@/components/forms/InputWithLabel';
|
import { InputWithLabel } from '@/components/forms/InputWithLabel';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
@@ -8,6 +9,7 @@ import { useOrganizationParams } from '@/hooks/useOrganizationParams';
|
|||||||
import { useRefetchActive } from '@/hooks/useRefetchActive';
|
import { useRefetchActive } from '@/hooks/useRefetchActive';
|
||||||
import type { IChartInput } from '@/types';
|
import type { IChartInput } from '@/types';
|
||||||
import { api, handleError } from '@/utils/api';
|
import { api, handleError } from '@/utils/api';
|
||||||
|
import { strip } from '@/utils/object';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import { Controller, useForm } from 'react-hook-form';
|
import { Controller, useForm } from 'react-hook-form';
|
||||||
@@ -30,7 +32,7 @@ type IForm = z.infer<typeof validator>;
|
|||||||
|
|
||||||
export default function SaveReport({ report }: SaveReportProps) {
|
export default function SaveReport({ report }: SaveReportProps) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { organization, project } = useOrganizationParams();
|
const { organization, project, dashboard } = useOrganizationParams();
|
||||||
const refetch = useRefetchActive();
|
const refetch = useRefetchActive();
|
||||||
const save = api.report.save.useMutation({
|
const save = api.report.save.useMutation({
|
||||||
onError: handleError,
|
onError: handleError,
|
||||||
@@ -41,7 +43,10 @@ export default function SaveReport({ report }: SaveReportProps) {
|
|||||||
});
|
});
|
||||||
popModal();
|
popModal();
|
||||||
refetch();
|
refetch();
|
||||||
router.push(`/${organization}/${project}/reports/${res.id}`);
|
router.push({
|
||||||
|
pathname: `/${organization}/${project}/reports/${res.id}`,
|
||||||
|
query: strip({ dashboard }),
|
||||||
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -58,7 +63,7 @@ export default function SaveReport({ report }: SaveReportProps) {
|
|||||||
onError: handleError,
|
onError: handleError,
|
||||||
onSuccess(res) {
|
onSuccess(res) {
|
||||||
setValue('dashboardId', res.id);
|
setValue('dashboardId', res.id);
|
||||||
dashboasrdQuery.refetch();
|
dashboardQuery.refetch();
|
||||||
toast({
|
toast({
|
||||||
title: 'Success',
|
title: 'Success',
|
||||||
description: 'Dashboard created.',
|
description: 'Dashboard created.',
|
||||||
@@ -66,15 +71,24 @@ export default function SaveReport({ report }: SaveReportProps) {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const dashboasrdQuery = api.dashboard.list.useQuery({
|
const dashboardQuery = api.dashboard.list.useQuery({
|
||||||
projectSlug: project,
|
projectSlug: project,
|
||||||
});
|
});
|
||||||
|
|
||||||
const dashboards = (dashboasrdQuery.data ?? []).map((item) => ({
|
const dashboards = (dashboardQuery.data ?? []).map((item) => ({
|
||||||
value: item.id,
|
value: item.id,
|
||||||
label: item.name,
|
label: item.name,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (dashboard && dashboardQuery.data) {
|
||||||
|
const match = dashboardQuery.data.find((item) => item.slug === dashboard);
|
||||||
|
if (match) {
|
||||||
|
setValue('dashboardId', match.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [dashboard, dashboardQuery]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ModalContent>
|
<ModalContent>
|
||||||
<ModalHeader title="Edit client" />
|
<ModalHeader title="Edit client" />
|
||||||
|
|||||||
@@ -31,6 +31,9 @@ const modals = {
|
|||||||
SaveReport: dynamic(() => import('./SaveReport'), {
|
SaveReport: dynamic(() => import('./SaveReport'), {
|
||||||
loading: Loading,
|
loading: Loading,
|
||||||
}),
|
}),
|
||||||
|
AddDashboard: dynamic(() => import('./AddDashboard'), {
|
||||||
|
loading: Loading,
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
const emitter = mitt<{
|
const emitter = mitt<{
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { api } from '@/utils/api';
|
|||||||
import { cn } from '@/utils/cn';
|
import { cn } from '@/utils/cn';
|
||||||
import { timeRanges } from '@/utils/constants';
|
import { timeRanges } from '@/utils/constants';
|
||||||
import { getRangeLabel } from '@/utils/getRangeLabel';
|
import { getRangeLabel } from '@/utils/getRangeLabel';
|
||||||
|
import { ChevronRight } from 'lucide-react';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
|
||||||
export const getServerSideProps = createServerSideProps();
|
export const getServerSideProps = createServerSideProps();
|
||||||
@@ -60,19 +61,22 @@ export default function Dashboard() {
|
|||||||
key={report.id}
|
key={report.id}
|
||||||
>
|
>
|
||||||
<Link
|
<Link
|
||||||
href={`/${params.organization}/${params.project}/reports/${report.id}`}
|
href={`/${params.organization}/${params.project}/reports/${report.id}?dashboard=${params.dashboard}`}
|
||||||
className="block border-b border-border p-4 leading-none hover:underline"
|
className="flex border-b border-border p-4 leading-none [&>svg]:hover:opacity-100 items-center justify-between"
|
||||||
shallow
|
shallow
|
||||||
>
|
>
|
||||||
<div className="font-medium">{report.name}</div>
|
<div>
|
||||||
{chartRange !== null && (
|
<div className="font-medium">{report.name}</div>
|
||||||
<div className="mt-2 text-sm flex gap-2">
|
{chartRange !== null && (
|
||||||
<span className={range !== null ? 'line-through' : ''}>
|
<div className="mt-2 text-sm flex gap-2">
|
||||||
{chartRange}
|
<span className={range !== null ? 'line-through' : ''}>
|
||||||
</span>
|
{chartRange}
|
||||||
{range !== null && <span>{getRangeLabel(range)}</span>}
|
</span>
|
||||||
</div>
|
{range !== null && <span>{getRangeLabel(range)}</span>}
|
||||||
)}
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<ChevronRight className="opacity-0 transition-opacity" />
|
||||||
</Link>
|
</Link>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
|
|||||||
@@ -3,8 +3,10 @@ import { Container } from '@/components/Container';
|
|||||||
import { MainLayout } from '@/components/layouts/MainLayout';
|
import { MainLayout } from '@/components/layouts/MainLayout';
|
||||||
import { PageTitle } from '@/components/PageTitle';
|
import { PageTitle } from '@/components/PageTitle';
|
||||||
import { useOrganizationParams } from '@/hooks/useOrganizationParams';
|
import { useOrganizationParams } from '@/hooks/useOrganizationParams';
|
||||||
|
import { pushModal } from '@/modals';
|
||||||
import { createServerSideProps } from '@/server/getServerSideProps';
|
import { createServerSideProps } from '@/server/getServerSideProps';
|
||||||
import { api } from '@/utils/api';
|
import { api } from '@/utils/api';
|
||||||
|
import { Plus } from 'lucide-react';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
|
||||||
export const getServerSideProps = createServerSideProps();
|
export const getServerSideProps = createServerSideProps();
|
||||||
@@ -27,16 +29,30 @@ export default function Home() {
|
|||||||
<PageTitle>Dashboards</PageTitle>
|
<PageTitle>Dashboards</PageTitle>
|
||||||
<div className="grid sm:grid-cols-2 gap-4">
|
<div className="grid sm:grid-cols-2 gap-4">
|
||||||
{dashboards.map((item) => (
|
{dashboards.map((item) => (
|
||||||
<Card key={item.id}>
|
<Card key={item.id} hover>
|
||||||
<Link
|
<Link
|
||||||
href={`/${params.organization}/${params.project}/${item.slug}`}
|
href={`/${params.organization}/${params.project}/${item.slug}`}
|
||||||
className="block p-4 font-medium leading-none hover:underline"
|
className="block p-4 font-medium leading-none"
|
||||||
shallow
|
shallow
|
||||||
>
|
>
|
||||||
{item.name}
|
{item.name}
|
||||||
</Link>
|
</Link>
|
||||||
</Card>
|
</Card>
|
||||||
))}
|
))}
|
||||||
|
<Card hover>
|
||||||
|
<button
|
||||||
|
className="flex items-center justify-between w-full p-4 font-medium leading-none"
|
||||||
|
onClick={() => {
|
||||||
|
pushModal('AddDashboard', {
|
||||||
|
projectSlug: params.project,
|
||||||
|
organizationSlug: params.organization,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Create new dashboard
|
||||||
|
<Plus size={16} />
|
||||||
|
</button>
|
||||||
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
</Container>
|
</Container>
|
||||||
</MainLayout>
|
</MainLayout>
|
||||||
|
|||||||
@@ -29,10 +29,10 @@ export default function Home() {
|
|||||||
<PageTitle>Projects</PageTitle>
|
<PageTitle>Projects</PageTitle>
|
||||||
<div className="grid sm:grid-cols-2 gap-4">
|
<div className="grid sm:grid-cols-2 gap-4">
|
||||||
{projects.map((item) => (
|
{projects.map((item) => (
|
||||||
<Card key={item.id}>
|
<Card key={item.id} hover>
|
||||||
<Link
|
<Link
|
||||||
href={`/${params.organization}/${item.slug}`}
|
href={`/${params.organization}/${item.slug}`}
|
||||||
className="block p-4 font-medium leading-none hover:underline"
|
className="block p-4 font-medium leading-none"
|
||||||
shallow
|
shallow
|
||||||
>
|
>
|
||||||
{item.name}
|
{item.name}
|
||||||
|
|||||||
@@ -455,8 +455,6 @@ async function getChartData({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(sql);
|
|
||||||
|
|
||||||
// group by sql label
|
// group by sql label
|
||||||
const series = result.reduce(
|
const series = result.reduce(
|
||||||
(acc, item) => {
|
(acc, item) => {
|
||||||
@@ -528,8 +526,8 @@ function fillEmptySpotsInTimeline(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (interval === 'month') {
|
if (interval === 'month') {
|
||||||
clonedStartDate.setDate(1);
|
clonedStartDate.setUTCDate(1);
|
||||||
clonedEndDate.setDate(1);
|
clonedEndDate.setUTCDate(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Force if interval is month and the start date is the same month as today
|
// Force if interval is month and the start date is the same month as today
|
||||||
@@ -537,11 +535,17 @@ function fillEmptySpotsInTimeline(
|
|||||||
interval === 'month' &&
|
interval === 'month' &&
|
||||||
clonedStartDate.getUTCFullYear() === today.getUTCFullYear() &&
|
clonedStartDate.getUTCFullYear() === today.getUTCFullYear() &&
|
||||||
clonedStartDate.getUTCMonth() === today.getUTCMonth();
|
clonedStartDate.getUTCMonth() === today.getUTCMonth();
|
||||||
|
let prev = undefined;
|
||||||
while (
|
while (
|
||||||
shouldForce() ||
|
shouldForce() ||
|
||||||
clonedStartDate.getTime() <= clonedEndDate.getTime()
|
clonedStartDate.getTime() <= clonedEndDate.getTime()
|
||||||
) {
|
) {
|
||||||
|
if (prev === clonedStartDate.getTime()) {
|
||||||
|
console.log('GET OUT NOW!');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
prev = clonedStartDate.getTime();
|
||||||
|
|
||||||
const getYear = (date: Date) => date.getUTCFullYear();
|
const getYear = (date: Date) => date.getUTCFullYear();
|
||||||
const getMonth = (date: Date) => date.getUTCMonth();
|
const getMonth = (date: Date) => date.getUTCMonth();
|
||||||
const getDay = (date: Date) => date.getUTCDate();
|
const getDay = (date: Date) => date.getUTCDate();
|
||||||
@@ -598,19 +602,19 @@ function fillEmptySpotsInTimeline(
|
|||||||
|
|
||||||
switch (interval) {
|
switch (interval) {
|
||||||
case 'day': {
|
case 'day': {
|
||||||
clonedStartDate.setDate(clonedStartDate.getUTCDate() + 1);
|
clonedStartDate.setUTCDate(clonedStartDate.getUTCDate() + 1);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'hour': {
|
case 'hour': {
|
||||||
clonedStartDate.setHours(clonedStartDate.getUTCHours() + 1);
|
clonedStartDate.setUTCHours(clonedStartDate.getUTCHours() + 1);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'minute': {
|
case 'minute': {
|
||||||
clonedStartDate.setMinutes(clonedStartDate.getUTCMinutes() + 1);
|
clonedStartDate.setUTCMinutes(clonedStartDate.getUTCMinutes() + 1);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'month': {
|
case 'month': {
|
||||||
clonedStartDate.setMonth(clonedStartDate.getUTCMonth() + 1);
|
clonedStartDate.setUTCMonth(clonedStartDate.getUTCMonth() + 1);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,20 @@
|
|||||||
import { createTRPCRouter, protectedProcedure } from '@/server/api/trpc';
|
import { createTRPCRouter, protectedProcedure } from '@/server/api/trpc';
|
||||||
import { db } from '@/server/db';
|
import { db } from '@/server/db';
|
||||||
|
import { getDashboardBySlug } from '@/server/services/dashboard.service';
|
||||||
import { getProjectBySlug } from '@/server/services/project.service';
|
import { getProjectBySlug } from '@/server/services/project.service';
|
||||||
import { slug } from '@/utils/slug';
|
import { slug } from '@/utils/slug';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
export const dashboardRouter = createTRPCRouter({
|
export const dashboardRouter = createTRPCRouter({
|
||||||
|
get: protectedProcedure
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
slug: z.string(),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.query(async ({ input: { slug } }) => {
|
||||||
|
return getDashboardBySlug(slug);
|
||||||
|
}),
|
||||||
list: protectedProcedure
|
list: protectedProcedure
|
||||||
.input(
|
.input(
|
||||||
z
|
z
|
||||||
|
|||||||
@@ -12,7 +12,10 @@ import type { Client, Project } from '@prisma/client';
|
|||||||
import type { TooltipProps } from 'recharts';
|
import type { TooltipProps } from 'recharts';
|
||||||
import type { z } from 'zod';
|
import type { z } from 'zod';
|
||||||
|
|
||||||
export type HtmlProps<T> = React.DetailedHTMLProps<React.HTMLAttributes<T>, T>;
|
export type HtmlProps<T> = Omit<
|
||||||
|
React.DetailedHTMLProps<React.HTMLAttributes<T>, T>,
|
||||||
|
'ref'
|
||||||
|
>;
|
||||||
|
|
||||||
export type IChartInput = z.infer<typeof zChartInput>;
|
export type IChartInput = z.infer<typeof zChartInput>;
|
||||||
export type IChartInputWithDates = z.infer<typeof zChartInputWithDates>;
|
export type IChartInputWithDates = z.infer<typeof zChartInputWithDates>;
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { anyPass, isEmpty, isNil, reject } from 'ramda';
|
||||||
|
|
||||||
export function toDots(
|
export function toDots(
|
||||||
obj: Record<string, unknown>,
|
obj: Record<string, unknown>,
|
||||||
path = ''
|
path = ''
|
||||||
@@ -16,3 +18,5 @@ export function toDots(
|
|||||||
};
|
};
|
||||||
}, {});
|
}, {});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const strip = reject(anyPass([isEmpty, isNil]));
|
||||||
|
|||||||
Reference in New Issue
Block a user