Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
138 changes: 138 additions & 0 deletions apps/start/src/components/dashboards/select-dashboard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { handleError, useTRPC } from '@/integrations/trpc/react';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { ArrowLeftIcon, PlusIcon, SaveIcon } from 'lucide-react';
import { useId, useState } from 'react';

export function SelectDashboard({
value,
onChange,
projectId,
excludeDashboardId,
}: {
value: string;
onChange: (value: string) => void;
projectId: string;
excludeDashboardId?: string;
}) {
const trpc = useTRPC();
const queryClient = useQueryClient();
const [isCreatingNew, setIsCreatingNew] = useState(false);
const [newDashboardName, setNewDashboardName] = useState('');
const [previousValue, setPreviousValue] = useState('');
const newDashboardNameId = useId();

const dashboardQuery = useQuery(
trpc.dashboard.list.queryOptions({
projectId,
}),
);

const dashboardMutation = useMutation(
trpc.dashboard.create.mutationOptions({
onError: handleError,
async onSuccess(res) {
queryClient.invalidateQueries(trpc.dashboard.list.pathFilter());
await dashboardQuery.refetch();
onChange(res.id);
setIsCreatingNew(false);
setNewDashboardName('');
},
}),
);

const handleCreateDashboard = () => {
const name = newDashboardName.trim();
if (!name || dashboardMutation.isPending) {
return;
}

dashboardMutation.mutate({
name,
projectId,
});
};
Comment thread
coderabbitai[bot] marked this conversation as resolved.

const dashboards = (dashboardQuery.data ?? []).filter(
(dashboard) => dashboard.id !== excludeDashboardId,
);

return (
<div className="space-y-3">
<Label htmlFor={isCreatingNew ? newDashboardNameId : undefined}>
Dashboard
</Label>

{!isCreatingNew ? (
<div className="row gap-2 flex-wrap">
{dashboards.map((dashboard) => (
<Button
type="button"
key={dashboard.id}
variant={value === dashboard.id ? 'default' : 'outline'}
aria-pressed={value === dashboard.id}
onClick={() => onChange(dashboard.id)}
>
{dashboard.name}
</Button>
))}
<Button
type="button"
variant="outline"
onClick={() => {
setPreviousValue(value);
setIsCreatingNew(true);
onChange('');
}}
icon={PlusIcon}
>
Create new dashboard
</Button>
</div>
) : (
<div className="flex gap-2">
<Button
type="button"
variant="outline"
size="icon"
icon={ArrowLeftIcon}
aria-label="Back to dashboard selection"
onClick={() => {
setIsCreatingNew(false);
setNewDashboardName('');
onChange(previousValue);
}}
/>
<Input
id={newDashboardNameId}
placeholder="Enter dashboard name"
value={newDashboardName}
onChange={(e) => setNewDashboardName(e.target.value)}
onKeyDown={(e) => {
if (e.key !== 'Enter') {
return;
}
// Enter confirms an IME candidate, it should not submit.
if (e.nativeEvent.isComposing || e.keyCode === 229) {
return;
}
e.preventDefault();
handleCreateDashboard();
}}
/>
<Button
type="button"
onClick={handleCreateDashboard}
disabled={!newDashboardName.trim() || dashboardMutation.isPending}
variant="outline"
icon={SaveIcon}
>
{dashboardMutation.isPending ? 'Creating...' : 'Create'}
</Button>
</div>
)}
</div>
);
}
20 changes: 19 additions & 1 deletion apps/start/src/components/report/report-item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,12 @@ import {
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { cn } from '@/utils/cn';
import { CopyIcon, MoreHorizontal, Trash } from 'lucide-react';
import {
CopyIcon,
LayoutPanelTopIcon,
MoreHorizontal,
Trash,
} from 'lucide-react';

import { timeWindows } from '@openpanel/constants';

Expand Down Expand Up @@ -41,6 +46,7 @@ export function ReportItem({
interval,
onDelete,
onDuplicate,
onMove,
}: {
report: any;
organizationId: string;
Expand All @@ -51,6 +57,7 @@ export function ReportItem({
interval: any;
onDelete: (reportId: string) => void;
onDuplicate: (reportId: string) => void;
onMove?: (reportId: string) => void;
}) {
const router = useRouter();
const chartRange = report.range;
Expand Down Expand Up @@ -149,6 +156,17 @@ export function ReportItem({
<CopyIcon size={16} className="mr-2" />
Duplicate
</DropdownMenuItem>
{onMove && (
<DropdownMenuItem
onClick={(event) => {
event.stopPropagation();
onMove(report.id);
}}
>
<LayoutPanelTopIcon size={16} className="mr-2" />
Move to dashboard
</DropdownMenuItem>
)}
<DropdownMenuGroup>
<DropdownMenuItem
className="text-destructive"
Expand Down
2 changes: 2 additions & 0 deletions apps/start/src/modals/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import EditReport from './edit-report';
import EventDetails from './event-details';
import InsightDetails from './insight-details';
import Instructions from './Instructions';
import MoveReport from './move-report';
import OverviewChartDetails from './overview-chart-details';
import OverviewFilters from './overview-filters';
import TableFilters from './table-filters';
Expand Down Expand Up @@ -66,6 +67,7 @@ const modals = {
ConfirmDeleteAccount,
ConfirmDeleteOrganization,
SaveReport,
MoveReport,
AddDashboard,
EditDashboard,
EditReport,
Expand Down
99 changes: 99 additions & 0 deletions apps/start/src/modals/move-report.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { ButtonContainer } from '@/components/button-container';
import { SelectDashboard } from '@/components/dashboards/select-dashboard';
import { Button } from '@/components/ui/button';
import { useAppParams } from '@/hooks/use-app-params';
import { handleError, useTRPC } from '@/integrations/trpc/react';
import { zodResolver } from '@hookform/resolvers/zod';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { Controller, useForm } from 'react-hook-form';
import { toast } from 'sonner';
import { z } from 'zod';

import { popModal } from '.';
import { ModalContent, ModalHeader } from './Modal/Container';

type MoveReportProps = {
reportId: string;
dashboardId: string;
};

const validator = z.object({
dashboardId: z.string().min(1, 'Required'),
});

type IForm = z.infer<typeof validator>;

export default function MoveReport({
reportId,
dashboardId,
}: MoveReportProps) {
const queryClient = useQueryClient();
const { projectId } = useAppParams();

const trpc = useTRPC();
const move = useMutation(
trpc.report.move.mutationOptions({
onError: handleError,
onSuccess() {
queryClient.invalidateQueries(trpc.report.list.pathFilter());
queryClient.invalidateQueries(trpc.dashboard.list.pathFilter());
toast('Report moved');
popModal();
},
}),
);

const { handleSubmit, formState, control } = useForm<IForm>({
resolver: zodResolver(validator),
defaultValues: {
dashboardId: '',
},
});

return (
<ModalContent>
<ModalHeader title="Move report" />
<form
className="flex flex-col gap-4"
onSubmit={handleSubmit((values) => {
move.mutate({
reportId,
dashboardId: values.dashboardId,
});
})}
>
<Controller
control={control}
name="dashboardId"
render={({ field }) => {
return (
<SelectDashboard
value={field.value}
onChange={field.onChange}
projectId={projectId!}
excludeDashboardId={dashboardId}
/>
);
}}
/>
<ButtonContainer>
<Button
type="button"
variant="outline"
onClick={() => popModal()}
size="default"
>
Cancel
</Button>
<Button
type="submit"
disabled={!formState.isValid || move.isPending}
size="default"
>
Move
</Button>
</ButtonContainer>
</form>
</ModalContent>
);
}
Loading
Loading