web improvements
This commit is contained in:
4
.vscode/settings.json
vendored
4
.vscode/settings.json
vendored
@@ -1,5 +1,7 @@
|
|||||||
{
|
{
|
||||||
"editor.codeActionsOnSave": { "source.fixAll.eslint": true },
|
"editor.codeActionsOnSave": {
|
||||||
|
"source.fixAll.eslint": "explicit"
|
||||||
|
},
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
"editor.formatOnSave": true,
|
"editor.formatOnSave": true,
|
||||||
"eslint.rules.customizations": [{ "rule": "*", "severity": "warn" }],
|
"eslint.rules.customizations": [{ "rule": "*", "severity": "warn" }],
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ export function ReportLineChart({ interval, data }: ReportLineChartProps) {
|
|||||||
const { editMode } = useChartContext();
|
const { editMode } = useChartContext();
|
||||||
const [visibleSeries, setVisibleSeries] = useState<string[]>([]);
|
const [visibleSeries, setVisibleSeries] = useState<string[]>([]);
|
||||||
const formatDate = useFormatDateInterval(interval);
|
const formatDate = useFormatDateInterval(interval);
|
||||||
|
console.log(JSON.stringify(data.series[0]?.data, null, 2));
|
||||||
|
|
||||||
const ref = useRef(false);
|
const ref = useRef(false);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -39,73 +39,99 @@ export function ReportTable({
|
|||||||
const total =
|
const total =
|
||||||
'bg-gray-50 text-emerald-600 font-medium border-r border-border';
|
'bg-gray-50 text-emerald-600 font-medium border-r border-border';
|
||||||
return (
|
return (
|
||||||
<div className="flex w-fit max-w-full rounded-md border border-border">
|
<>
|
||||||
{/* Labels */}
|
<div className="flex w-fit max-w-full rounded-md border border-border">
|
||||||
<div className="border-r border-border">
|
{/* Labels */}
|
||||||
<div className={cn(header, row, cell)}>Name</div>
|
<div className="border-r border-border">
|
||||||
{data.series.map((serie, index) => {
|
<div className={cn(header, row, cell)}>Name</div>
|
||||||
const checked = visibleSeries.includes(serie.name);
|
{/* <div
|
||||||
|
className={cn(
|
||||||
|
'flex max-w-[200px] w-full min-w-full items-center gap-2',
|
||||||
|
row,
|
||||||
|
cell
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="font-medium min-w-0 overflow-scroll whitespace-nowrap scrollbar-hide">
|
||||||
|
Summary
|
||||||
|
</div>
|
||||||
|
</div> */}
|
||||||
|
{data.series.map((serie, index) => {
|
||||||
|
const checked = visibleSeries.includes(serie.name);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={serie.name}
|
key={serie.name}
|
||||||
className={cn('flex max-w-[200px] items-center gap-2', row, cell)}
|
className={cn(
|
||||||
>
|
'flex max-w-[200px] w-full min-w-full items-center gap-2',
|
||||||
<Checkbox
|
row,
|
||||||
onCheckedChange={(checked) =>
|
cell
|
||||||
handleChange(serie.name, !!checked)
|
)}
|
||||||
}
|
>
|
||||||
style={
|
<Checkbox
|
||||||
checked
|
onCheckedChange={(checked) =>
|
||||||
? {
|
handleChange(serie.name, !!checked)
|
||||||
background: getChartColor(index),
|
}
|
||||||
borderColor: getChartColor(index),
|
style={
|
||||||
}
|
checked
|
||||||
: undefined
|
? {
|
||||||
}
|
background: getChartColor(index),
|
||||||
checked={checked}
|
borderColor: getChartColor(index),
|
||||||
/>
|
}
|
||||||
<div className="min-w-0 overflow-hidden text-ellipsis whitespace-nowrap">
|
: undefined
|
||||||
{getLabel(serie.name)}
|
}
|
||||||
|
checked={checked}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
title={getLabel(serie.name)}
|
||||||
|
className="min-w-0 overflow-scroll whitespace-nowrap scrollbar-hide"
|
||||||
|
>
|
||||||
|
{getLabel(serie.name)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
);
|
||||||
);
|
})}
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* ScrollView for all values */}
|
|
||||||
<div className="w-full overflow-auto">
|
|
||||||
{/* Header */}
|
|
||||||
<div className={cn('w-max', row)}>
|
|
||||||
<div className={cn(header, value, cell, total)}>Total</div>
|
|
||||||
{data.series[0]?.data.map((serie) => (
|
|
||||||
<div
|
|
||||||
key={serie.date.toString()}
|
|
||||||
className={cn(header, value, cell)}
|
|
||||||
>
|
|
||||||
{formatDate(serie.date)}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Values */}
|
{/* ScrollView for all values */}
|
||||||
{data.series.map((serie) => {
|
<div className="w-full overflow-auto">
|
||||||
return (
|
{/* Header */}
|
||||||
<div className={cn('w-max', row)} key={serie.name}>
|
<div className={cn('w-max', row)}>
|
||||||
<div className={cn(header, value, cell, total)}>
|
<div className={cn(header, value, cell, total)}>Total</div>
|
||||||
{serie.totalCount}
|
{data.series[0]?.data.map((serie) => (
|
||||||
|
<div
|
||||||
|
key={serie.date.toString()}
|
||||||
|
className={cn(header, value, cell)}
|
||||||
|
>
|
||||||
|
{formatDate(serie.date)}
|
||||||
</div>
|
</div>
|
||||||
{serie.data.map((item) => {
|
))}
|
||||||
return (
|
</div>
|
||||||
<div key={item.date} className={cn(value, cell)}>
|
|
||||||
{item.count}
|
{/* Values */}
|
||||||
</div>
|
{data.series.map((serie) => {
|
||||||
);
|
return (
|
||||||
})}
|
<div className={cn('w-max', row)} key={serie.name}>
|
||||||
</div>
|
<div className={cn(header, value, cell, total)}>
|
||||||
);
|
{serie.totalCount}
|
||||||
})}
|
</div>
|
||||||
|
{serie.data.map((item) => {
|
||||||
|
return (
|
||||||
|
<div key={item.date} className={cn(value, cell)}>
|
||||||
|
{item.count}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div className="flex gap-2">
|
||||||
|
<div>Summary</div>
|
||||||
|
<div>
|
||||||
|
{data.series.reduce((acc, serie) => serie.totalCount + acc, 0)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,6 +40,8 @@ export const Chart = memo(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
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 (chart.isFetching && !anyData) {
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import {
|
|||||||
import { RenderDots } from '@/components/ui/RenderDots';
|
import { RenderDots } from '@/components/ui/RenderDots';
|
||||||
import { useMappings } from '@/hooks/useMappings';
|
import { useMappings } from '@/hooks/useMappings';
|
||||||
import { useOrganizationParams } from '@/hooks/useOrganizationParams';
|
import { useOrganizationParams } from '@/hooks/useOrganizationParams';
|
||||||
import { useDispatch } from '@/redux';
|
import { useDispatch, useSelector } from '@/redux';
|
||||||
import type {
|
import type {
|
||||||
IChartEvent,
|
IChartEvent,
|
||||||
IChartEventFilter,
|
IChartEventFilter,
|
||||||
|
|||||||
@@ -10,14 +10,7 @@ import { ReportSaveButton } from '@/components/report/ReportSaveButton';
|
|||||||
import { reset, setReport } from '@/components/report/reportSlice';
|
import { reset, setReport } from '@/components/report/reportSlice';
|
||||||
import { ReportSidebar } from '@/components/report/sidebar/ReportSidebar';
|
import { ReportSidebar } from '@/components/report/sidebar/ReportSidebar';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import {
|
import { Sheet, SheetContent, SheetTrigger } from '@/components/ui/sheet';
|
||||||
Sheet,
|
|
||||||
SheetContent,
|
|
||||||
SheetDescription,
|
|
||||||
SheetHeader,
|
|
||||||
SheetTitle,
|
|
||||||
SheetTrigger,
|
|
||||||
} from '@/components/ui/sheet';
|
|
||||||
import { useRouterBeforeLeave } from '@/hooks/useRouterBeforeLeave';
|
import { useRouterBeforeLeave } from '@/hooks/useRouterBeforeLeave';
|
||||||
import { useDispatch, useSelector } from '@/redux';
|
import { useDispatch, useSelector } from '@/redux';
|
||||||
import { createServerSideProps } from '@/server/getServerSideProps';
|
import { createServerSideProps } from '@/server/getServerSideProps';
|
||||||
|
|||||||
@@ -2,7 +2,12 @@ import { createTRPCRouter, protectedProcedure } from '@/server/api/trpc';
|
|||||||
import * as cache from '@/server/cache';
|
import * as cache from '@/server/cache';
|
||||||
import { db } from '@/server/db';
|
import { db } from '@/server/db';
|
||||||
import { getProjectBySlug } from '@/server/services/project.service';
|
import { getProjectBySlug } from '@/server/services/project.service';
|
||||||
import type { IChartEvent, IChartInputWithDates, IChartRange } from '@/types';
|
import type {
|
||||||
|
IChartEvent,
|
||||||
|
IChartInputWithDates,
|
||||||
|
IChartRange,
|
||||||
|
IInterval,
|
||||||
|
} from '@/types';
|
||||||
import { getDaysOldDate } from '@/utils/date';
|
import { getDaysOldDate } from '@/utils/date';
|
||||||
import { toDots } from '@/utils/object';
|
import { toDots } from '@/utils/object';
|
||||||
import { zChartInputWithDates } from '@/utils/validation';
|
import { zChartInputWithDates } from '@/utils/validation';
|
||||||
@@ -81,6 +86,7 @@ export const chartRouter = createTRPCRouter({
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
.query(async ({ input: { event, property, projectSlug } }) => {
|
.query(async ({ input: { event, property, projectSlug } }) => {
|
||||||
|
const intervalInDays = 180;
|
||||||
const project = await getProjectBySlug(projectSlug);
|
const project = await getProjectBySlug(projectSlug);
|
||||||
if (isJsonPath(property)) {
|
if (isJsonPath(property)) {
|
||||||
const events = await db.$queryRawUnsafe<{ value: string }[]>(
|
const events = await db.$queryRawUnsafe<{ value: string }[]>(
|
||||||
@@ -88,14 +94,7 @@ export const chartRouter = createTRPCRouter({
|
|||||||
property
|
property
|
||||||
)} AS value from events WHERE project_id = '${
|
)} AS value from events WHERE project_id = '${
|
||||||
project.id
|
project.id
|
||||||
}' AND name = '${event}' AND "createdAt" >= NOW() - INTERVAL '30 days'`
|
}' AND name = '${event}' AND "createdAt" >= NOW() - INTERVAL '${intervalInDays} days'`
|
||||||
);
|
|
||||||
console.log(
|
|
||||||
`SELECT ${selectJsonPath(
|
|
||||||
property
|
|
||||||
)} AS value from events WHERE project_id = '${
|
|
||||||
project.id
|
|
||||||
}' AND name = '${event}' AND "createdAt" >= NOW() - INTERVAL '30 days'`
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -110,7 +109,9 @@ export const chartRouter = createTRPCRouter({
|
|||||||
not: null,
|
not: null,
|
||||||
},
|
},
|
||||||
createdAt: {
|
createdAt: {
|
||||||
gte: new Date(new Date().getTime() - 1000 * 60 * 60 * 24 * 30),
|
gte: new Date(
|
||||||
|
new Date().getTime() - 1000 * 60 * 60 * 24 * intervalInDays
|
||||||
|
),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
distinct: property as any,
|
distinct: property as any,
|
||||||
@@ -483,7 +484,7 @@ async function getChartData({
|
|||||||
|
|
||||||
function fillEmptySpotsInTimeline(
|
function fillEmptySpotsInTimeline(
|
||||||
items: ResultItem[],
|
items: ResultItem[],
|
||||||
interval: string,
|
interval: IInterval,
|
||||||
startDate: string,
|
startDate: string,
|
||||||
endDate: string
|
endDate: string
|
||||||
) {
|
) {
|
||||||
@@ -493,31 +494,36 @@ function fillEmptySpotsInTimeline(
|
|||||||
const today = new Date();
|
const today = new Date();
|
||||||
|
|
||||||
if (interval === 'minute') {
|
if (interval === 'minute') {
|
||||||
clonedStartDate.setSeconds(0, 0);
|
clonedStartDate.setUTCSeconds(0, 0);
|
||||||
clonedEndDate.setMinutes(clonedEndDate.getMinutes() + 1, 0, 0);
|
clonedEndDate.setUTCMinutes(clonedEndDate.getUTCMinutes() + 1, 0, 0);
|
||||||
} else if (interval === 'hour') {
|
} else if (interval === 'hour') {
|
||||||
clonedStartDate.setMinutes(0, 0, 0);
|
clonedStartDate.setUTCMinutes(0, 0, 0);
|
||||||
clonedEndDate.setMinutes(0, 0, 0);
|
clonedEndDate.setUTCMinutes(0, 0, 0);
|
||||||
} else {
|
} else {
|
||||||
clonedStartDate.setUTCHours(0, 0, 0, 0);
|
clonedStartDate.setUTCHours(0, 0, 0, 0);
|
||||||
clonedEndDate.setUTCHours(0, 0, 0, 0);
|
clonedEndDate.setUTCHours(0, 0, 0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (interval === 'month') {
|
||||||
|
clonedStartDate.setDate(1);
|
||||||
|
clonedEndDate.setDate(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
|
||||||
const shouldForce = () =>
|
const shouldForce = () =>
|
||||||
interval === 'month' &&
|
interval === 'month' &&
|
||||||
clonedStartDate.getFullYear() === today.getFullYear() &&
|
clonedStartDate.getUTCFullYear() === today.getUTCFullYear() &&
|
||||||
clonedStartDate.getMonth() === today.getMonth();
|
clonedStartDate.getUTCMonth() === today.getUTCMonth();
|
||||||
|
|
||||||
while (
|
while (
|
||||||
shouldForce() ||
|
shouldForce() ||
|
||||||
clonedStartDate.getTime() <= clonedEndDate.getTime()
|
clonedStartDate.getTime() <= clonedEndDate.getTime()
|
||||||
) {
|
) {
|
||||||
const getYear = (date: Date) => date.getFullYear();
|
const getYear = (date: Date) => date.getUTCFullYear();
|
||||||
const getMonth = (date: Date) => date.getMonth();
|
const getMonth = (date: Date) => date.getUTCMonth();
|
||||||
const getDay = (date: Date) => date.getDate();
|
const getDay = (date: Date) => date.getUTCDate();
|
||||||
const getHour = (date: Date) => date.getHours();
|
const getHour = (date: Date) => date.getUTCHours();
|
||||||
const getMinute = (date: Date) => date.getMinutes();
|
const getMinute = (date: Date) => date.getUTCMinutes();
|
||||||
|
|
||||||
const item = items.find((item) => {
|
const item = items.find((item) => {
|
||||||
const date = new Date(item.date);
|
const date = new Date(item.date);
|
||||||
@@ -555,7 +561,10 @@ function fillEmptySpotsInTimeline(
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (item) {
|
if (item) {
|
||||||
result.push(item);
|
result.push({
|
||||||
|
...item,
|
||||||
|
date: clonedStartDate.toISOString(),
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
result.push({
|
result.push({
|
||||||
date: clonedStartDate.toISOString(),
|
date: clonedStartDate.toISOString(),
|
||||||
@@ -566,19 +575,19 @@ function fillEmptySpotsInTimeline(
|
|||||||
|
|
||||||
switch (interval) {
|
switch (interval) {
|
||||||
case 'day': {
|
case 'day': {
|
||||||
clonedStartDate.setDate(clonedStartDate.getDate() + 1);
|
clonedStartDate.setDate(clonedStartDate.getUTCDate() + 1);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'hour': {
|
case 'hour': {
|
||||||
clonedStartDate.setHours(clonedStartDate.getHours() + 1);
|
clonedStartDate.setHours(clonedStartDate.getUTCHours() + 1);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'minute': {
|
case 'minute': {
|
||||||
clonedStartDate.setMinutes(clonedStartDate.getMinutes() + 1);
|
clonedStartDate.setMinutes(clonedStartDate.getUTCMinutes() + 1);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'month': {
|
case 'month': {
|
||||||
clonedStartDate.setMonth(clonedStartDate.getMonth() + 1);
|
clonedStartDate.setMonth(clonedStartDate.getUTCMonth() + 1);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -146,3 +146,14 @@
|
|||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* For Webkit-based browsers (Chrome, Safari and Opera) */
|
||||||
|
.scrollbar-hide::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* For IE, Edge and Firefox */
|
||||||
|
.scrollbar-hide {
|
||||||
|
-ms-overflow-style: none; /* IE and Edge */
|
||||||
|
scrollbar-width: none; /* Firefox */
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user