Feature/move list to client (#50)

This commit is contained in:
Carl-Gerhard Lindesvärd
2024-09-01 15:02:12 +02:00
committed by GitHub
parent c2abdaadf2
commit 668434d246
181 changed files with 2922 additions and 1959 deletions

View File

@@ -16,7 +16,7 @@ export function CreateClientSuccess({ id, secret, cors }: Props) {
<div className="w-full">
<CopyInput label="Secret" value={secret} />
{cors && (
<p className="mt-1 text-xs text-muted-foreground">
<p className="mt-1 text-sm text-muted-foreground">
You will only need the secret if you want to send server events.
</p>
)}
@@ -25,7 +25,7 @@ export function CreateClientSuccess({ id, secret, cors }: Props) {
{cors && (
<div className="text-left">
<Label>CORS settings</Label>
<div className="flex items-center justify-between rounded border-input bg-def-200 p-2 px-3 font-mono text-sm">
<div className="font-mono flex items-center justify-between rounded border-input bg-def-200 p-2 px-3 ">
{cors}
</div>
</div>

View File

@@ -13,7 +13,7 @@ export const columns: ColumnDef<IServiceClientWithProject>[] = [
return (
<div>
<div>{row.original.name}</div>
<div className="text-sm text-muted-foreground">
<div className=" text-muted-foreground">
{row.original.project?.name ?? 'No project'}
</div>
</div>

View File

@@ -7,7 +7,7 @@ export function ColorSquare({ children, className }: ColorSquareProps) {
return (
<div
className={cn(
'flex h-5 w-5 flex-shrink-0 items-center justify-center rounded bg-blue-600 text-xs font-medium text-white [.mini_&]:h-4 [.mini_&]:w-4 [.mini_&]:text-[0.6rem]',
'flex h-5 w-5 flex-shrink-0 items-center justify-center rounded bg-blue-600 text-sm font-medium text-white [.mini_&]:h-4 [.mini_&]:w-4 [.mini_&]:text-[0.6rem]',
className
)}
>

View File

@@ -1,5 +1,6 @@
'use client';
import { cn } from '@/utils/cn';
import {
flexRender,
getCoreRowModel,
@@ -7,20 +8,27 @@ import {
} from '@tanstack/react-table';
import type { ColumnDef } from '@tanstack/react-table';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from './ui/table';
import { Grid, GridBody, GridCell, GridHeader, GridRow } from './grid-table';
interface DataTableProps<TData> {
columns: ColumnDef<TData, any>[];
data: TData[];
}
export function TableButtons({
children,
className,
}: {
children: React.ReactNode;
className?: string;
}) {
return (
<div className={cn('mb-2 flex flex-wrap items-center gap-2', className)}>
{children}
</div>
);
}
export function DataTable<TData>({ columns, data }: DataTableProps<TData>) {
const table = useReactTable({
data,
@@ -29,47 +37,45 @@ export function DataTable<TData>({ columns, data }: DataTableProps<TData>) {
});
return (
<Table>
<TableHeader>
<Grid columns={columns.length}>
<GridHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<TableHead key={header.id}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
)}
</TableHead>
);
})}
</TableRow>
<GridRow key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<GridCell key={header.id} isHeader>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
)}
</GridCell>
))}
</GridRow>
))}
</TableHeader>
<TableBody>
</GridHeader>
<GridBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow
<GridRow
key={row.id}
data-state={row.getIsSelected() && 'selected'}
>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
<GridCell key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
</GridCell>
))}
</TableRow>
</GridRow>
))
) : (
<TableRow>
<TableCell colSpan={columns.length} className="h-24 text-center">
No results.
</TableCell>
</TableRow>
<GridRow>
<GridCell colSpan={columns.length}>
<div className="h-24 text-center">No results.</div>
</GridCell>
</GridRow>
)}
</TableBody>
</Table>
</GridBody>
</Grid>
);
}

View File

@@ -0,0 +1,145 @@
import { EventIcon } from '@/app/(app)/[organizationSlug]/[projectId]/events/event-list/event-icon';
import { ProjectLink } from '@/components/links';
import { SerieIcon } from '@/components/report/chart/SerieIcon';
import { TooltipComplete } from '@/components/tooltip-complete';
import { useNumber } from '@/hooks/useNumerFormatter';
import { pushModal } from '@/modals';
import { formatDateTime, formatTime } from '@/utils/date';
import { getProfileName } from '@/utils/getters';
import type { ColumnDef } from '@tanstack/react-table';
import { isToday } from 'date-fns';
import type { IServiceEvent } from '@openpanel/db';
export function useColumns() {
const number = useNumber();
const columns: ColumnDef<IServiceEvent>[] = [
{
accessorKey: 'name',
header: 'Name',
cell({ row }) {
const { name, path, duration } = row.original;
const renderName = () => {
if (name === 'screen_view') {
if (path.includes('/')) {
return <span className="max-w-md truncate">{path}</span>;
}
return (
<>
<span className="text-muted-foreground">Screen: </span>
<span className="max-w-md truncate">{path}</span>
</>
);
}
return name.replace(/_/g, ' ');
};
const renderDuration = () => {
if (name === 'screen_view') {
return (
<span className="text-muted-foreground">
{number.shortWithUnit(duration / 1000, 'min')}
</span>
);
}
return null;
};
return (
<div className="flex items-center gap-2">
<EventIcon
size="sm"
name={row.original.name}
meta={row.original.meta}
/>
<span className="flex gap-2">
<button
onClick={() => {
pushModal('EventDetails', {
id: row.original.id,
});
}}
className="font-medium"
>
{renderName()}
</button>
{renderDuration()}
</span>
</div>
);
},
},
{
accessorKey: 'country',
header: 'Country',
cell({ row }) {
const { country, city } = row.original;
return (
<div className="inline-flex min-w-full flex-none items-center gap-2">
<SerieIcon name={country} />
<span>{city}</span>
</div>
);
},
},
{
accessorKey: 'os',
header: 'OS',
cell({ row }) {
const { os } = row.original;
return (
<div className="flex min-w-full items-center gap-2">
<SerieIcon name={os} />
<span>{os}</span>
</div>
);
},
},
{
accessorKey: 'browser',
header: 'Browser',
cell({ row }) {
const { browser } = row.original;
return (
<div className="inline-flex min-w-full flex-none items-center gap-2">
<SerieIcon name={browser} />
<span>{browser}</span>
</div>
);
},
},
{
accessorKey: 'profileId',
header: 'Profile',
cell({ row }) {
const { profile } = row.original;
if (!profile) {
return null;
}
return (
<ProjectLink
href={`/profiles/${profile?.id}`}
className="max-w-[80px] overflow-hidden text-ellipsis whitespace-nowrap font-medium hover:underline"
>
{getProfileName(profile)}
</ProjectLink>
);
},
},
{
accessorKey: 'createdAt',
header: 'Created at',
cell({ row }) {
const date = row.original.createdAt;
return (
<div>{isToday(date) ? formatTime(date) : formatDateTime(date)}</div>
);
},
},
];
return columns;
}

View File

@@ -0,0 +1,71 @@
import type { Dispatch, SetStateAction } from 'react';
import { DataTable } from '@/components/data-table';
import { FullPageEmptyState } from '@/components/full-page-empty-state';
import { Pagination } from '@/components/pagination';
import { Button } from '@/components/ui/button';
import type { UseQueryResult } from '@tanstack/react-query';
import { GanttChartIcon } from 'lucide-react';
import type { IServiceEvent } from '@openpanel/db';
import { useColumns } from './columns';
type Props =
| {
query: UseQueryResult<IServiceEvent[]>;
}
| {
query: UseQueryResult<IServiceEvent[]>;
cursor: number;
setCursor: Dispatch<SetStateAction<number>>;
};
export const EventsTable = ({ query, ...props }: Props) => {
const columns = useColumns();
const { data, isFetching, isLoading } = query;
if (isLoading) {
return (
<div className="flex flex-col gap-2">
<div className="card h-[74px] w-full animate-pulse items-center justify-between rounded-lg p-4"></div>
<div className="card h-[74px] w-full animate-pulse items-center justify-between rounded-lg p-4"></div>
<div className="card h-[74px] w-full animate-pulse items-center justify-between rounded-lg p-4"></div>
<div className="card h-[74px] w-full animate-pulse items-center justify-between rounded-lg p-4"></div>
<div className="card h-[74px] w-full animate-pulse items-center justify-between rounded-lg p-4"></div>
</div>
);
}
if (data?.length === 0) {
return (
<FullPageEmptyState title="No events here" icon={GanttChartIcon}>
<p>Could not find any events</p>
{'cursor' in props && props.cursor !== 0 && (
<Button
className="mt-8"
variant="outline"
onClick={() => props.setCursor((p) => p - 1)}
>
Go to previous page
</Button>
)}
</FullPageEmptyState>
);
}
return (
<>
<DataTable data={data ?? []} columns={columns} />
{'cursor' in props && (
<Pagination
className="mt-2"
setCursor={props.setCursor}
cursor={props.cursor}
count={Infinity}
take={50}
loading={isFetching}
/>
)}
</>
);
};

View File

@@ -24,7 +24,7 @@ export const CheckboxItem = forwardRef<HTMLButtonElement, Props>(
<div>
<label
className={cn(
'hover:bg-def-200 flex items-center gap-4 px-4 py-6 transition-colors',
'flex items-center gap-4 px-4 py-6 transition-colors hover:bg-def-200',
disabled && 'cursor-not-allowed opacity-50'
)}
htmlFor={id}
@@ -32,8 +32,8 @@ export const CheckboxItem = forwardRef<HTMLButtonElement, Props>(
{Icon && <div className="w-6 shrink-0">{<Icon />}</div>}
<div className="flex-1">
<div className="font-medium">{label}</div>
<div className="text-sm text-muted-foreground">{description}</div>
{error && <div className="text-xs text-red-600">{error}</div>}
<div className=" text-muted-foreground">{description}</div>
{error && <div className="text-sm text-red-600">{error}</div>}
</div>
<div>
<Switch

View File

@@ -17,7 +17,7 @@ const CopyInput = ({ label, value, className }: Props) => {
onClick={() => clipboard(value)}
>
{!!label && <Label>{label}</Label>}
<div className="flex items-center justify-between rounded border-input bg-def-200 p-2 px-3 font-mono text-sm">
<div className="font-mono flex items-center justify-between rounded border-input bg-def-200 p-2 px-3 ">
{value}
<CopyIcon size={16} />
</div>

View File

@@ -38,7 +38,7 @@ export const WithLabel = ({
</Label>
{error && (
<Tooltiper asChild content={error}>
<div className="flex items-center gap-1 text-sm leading-none text-destructive">
<div className="flex items-center gap-1 leading-none text-destructive">
Issues
<BanIcon size={14} />
</div>

View File

@@ -112,7 +112,7 @@ const TagInput = ({
data-tag={tag}
key={tag}
className={cn(
'inline-flex items-center gap-2 rounded bg-def-200 px-2 py-1 text-sm',
'inline-flex items-center gap-2 rounded bg-def-200 px-2 py-1 ',
isMarkedForDeletion &&
i === value.length - 1 &&
'bg-destructive-foreground ring-2 ring-destructive/50 ring-offset-1',
@@ -136,7 +136,7 @@ const TagInput = ({
<input
ref={inputRef}
placeholder={`${placeholder}`}
className="min-w-20 flex-1 py-1 text-sm focus-visible:outline-none"
className="min-w-20 flex-1 py-1 focus-visible:outline-none"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onKeyDown={handleKeyDown}

View File

@@ -7,6 +7,7 @@ import { ChevronLeftIcon, FullscreenIcon } from 'lucide-react';
import { parseAsBoolean, useQueryState } from 'nuqs';
import { useDebounce } from 'usehooks-ts';
import { Button } from './ui/button';
import { Tooltiper } from './ui/tooltip';
type Props = {
@@ -37,18 +38,21 @@ export const Fullscreen = (props: Props) => {
};
export const FullscreenOpen = () => {
const [, setIsFullscreen] = useFullscreen();
const [fullscreen, setIsFullscreen] = useFullscreen();
if (fullscreen) {
return null;
}
return (
<Tooltiper content="Toggle fullscreen" asChild>
<button
className="flex items-center gap-2"
<Button
variant="outline"
size="icon"
onClick={() => {
setIsFullscreen((p) => !p);
}}
>
<FullscreenIcon />
Realtime
</button>
<FullscreenIcon className="size-4" />
</Button>
</Tooltiper>
);
};

View File

@@ -0,0 +1,88 @@
import React from 'react';
import { cn } from '@/utils/cn';
export const Grid: React.FC<
React.HTMLAttributes<HTMLDivElement> & { columns: number }
> = ({ className, columns, children, ...props }) => (
<div className={cn('card', className)}>
<div className="relative w-full overflow-auto rounded-md">
<div
className={cn('grid w-full')}
style={{
gridTemplateColumns: `repeat(${columns}, auto)`,
width: 'max-content',
minWidth: '100%',
}}
{...props}
>
{children}
</div>
</div>
</div>
);
export const GridHeader: React.FC<React.HTMLAttributes<HTMLDivElement>> = ({
className,
children,
...props
}) => (
<div className={cn('contents', className)} {...props}>
{children}
</div>
);
export const GridBody: React.FC<React.HTMLAttributes<HTMLDivElement>> = ({
className,
children,
...props
}) => (
<div
className={cn('contents [&>*:last-child]:border-0', className)}
{...props}
>
{children}
</div>
);
export const GridCell: React.FC<
React.HTMLAttributes<HTMLDivElement> & {
as?: React.ElementType;
colSpan?: number;
isHeader?: boolean;
}
> = ({
className,
children,
as: Component = 'div',
colSpan,
isHeader,
...props
}) => (
<Component
className={cn(
'flex h-12 items-center whitespace-nowrap px-4 align-middle shadow-[0_0_0_0.5px] shadow-border',
isHeader && 'h-10 bg-def-100 font-semibold text-muted-foreground',
colSpan && `col-span-${colSpan}`,
className
)}
{...props}
>
{children}
</Component>
);
export const GridRow: React.FC<React.HTMLAttributes<HTMLDivElement>> = ({
className,
children,
...props
}) => (
<div
className={cn(
'contents transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted',
className
)}
{...props}
>
{children}
</div>
);

View File

@@ -0,0 +1,23 @@
import { useAppParams } from '@/hooks/useAppParams';
import type { LinkProps } from 'next/link';
import Link from 'next/link';
export function ProjectLink({
children,
...props
}: LinkProps & {
children: React.ReactNode;
className?: string;
title?: string;
}) {
const { organizationSlug, projectId } = useAppParams();
if (typeof props.href === 'string') {
return (
<Link {...props} href={`/${organizationSlug}/${projectId}/${props.href}`}>
{children}
</Link>
);
}
return <p>ProjectLink</p>;
}

View File

@@ -23,7 +23,7 @@ export function OverviewFiltersButtons({
const [filters, setFilter] = useEventQueryFilters(nuqsOptions);
if (filters.length === 0 && events.length === 0) return null;
return (
<div className={cn('flex flex-wrap gap-2 px-4 pb-4', className)}>
<div className={cn('flex flex-wrap gap-2', className)}>
{events.map((event) => (
<Button
key={event}
@@ -32,7 +32,7 @@ export function OverviewFiltersButtons({
icon={X}
onClick={() => setEvents((p) => p.filter((e) => e !== event))}
>
<strong>{event}</strong>
<strong className="font-semibold">{event}</strong>
</Button>
))}
{filters.map((filter) => {
@@ -49,7 +49,7 @@ export function OverviewFiltersButtons({
onClick={() => setFilter(filter.name, filter.value[0], 'is')}
>
<span className="mr-1">{getPropertyLabel(filter.name)} is</span>
<strong>{filter.value[0]}</strong>
<strong className="font-semibold">{filter.value[0]}</strong>
</Button>
);
})}

View File

@@ -7,7 +7,7 @@ import {
TooltipContent,
TooltipTrigger,
} from '@/components/ui/tooltip';
import { useDebounceVal } from '@/hooks/useDebounceVal';
import { useDebounceState } from '@/hooks/useDebounceState';
import useWS from '@/hooks/useWS';
import { cn } from '@/utils/cn';
import { useQueryClient } from '@tanstack/react-query';
@@ -28,7 +28,7 @@ const FIFTEEN_SECONDS = 1000 * 30;
export default function LiveCounter({ data = 0, projectId }: LiveCounterProps) {
const client = useQueryClient();
const counter = useDebounceVal(data, 1000, {
const counter = useDebounceState(data, 1000, {
maxWait: 5000,
});
const lastRefresh = useRef(Date.now());

View File

@@ -13,7 +13,7 @@ export function OverviewChartToggle({ chartType, setChartType }: Props) {
return (
<Button
size={'icon'}
variant={'outline'}
variant={'ghost'}
onClick={() => {
setChartType((p) => (p === 'linear' ? 'bar' : 'linear'));
}}

View File

@@ -3,22 +3,25 @@ import { ScanEyeIcon } from 'lucide-react';
import type { IChartProps } from '@openpanel/validation';
import { Button } from '../ui/button';
type Props = {
chart: IChartProps;
};
const OverviewDetailsButton = ({ chart }: Props) => {
return (
<button
className="-mb-2 mt-5 flex w-full items-center justify-center gap-2 text-sm font-semibold"
<Button
size="icon"
variant="ghost"
onClick={() => {
pushModal('OverviewChartDetails', {
chart: chart,
});
}}
>
<ScanEyeIcon size={18} /> Details
</button>
<ScanEyeIcon size={18} />
</Button>
);
};

View File

@@ -131,7 +131,7 @@ interface WrapperProps {
function Wrapper({ children, count }: WrapperProps) {
return (
<div className="flex h-full flex-col">
<div className="relative mb-1 text-xs font-medium text-muted-foreground">
<div className="relative mb-1 text-sm font-medium text-muted-foreground">
{count} unique vistors last 30 minutes
</div>
<div className="relative flex h-full w-full flex-1 items-end gap-1">

View File

@@ -192,8 +192,8 @@ export default function OverviewMetrics({ projectId }: OverviewMetricsProps) {
return (
<>
<div className="relative -top-0.5 col-span-6 -m-4 mb-0 md:m-0">
<div className="card mb-2 grid grid-cols-4">
<div className="relative -top-0.5 col-span-6 -m-4 mb-0 mt-0 md:m-0">
<div className="card mb-2 grid grid-cols-4 overflow-hidden rounded-md">
{reports.map((report, index) => (
<button
key={index}

View File

@@ -32,7 +32,7 @@ const OverviewTopBots = ({ projectId }: Props) => {
<>
<div className="-m-4">
<WidgetTable
className="max-w-full [&_td:first-child]:w-full [&_th]:text-xs [&_tr]:text-xs"
className="max-w-full [&_td:first-child]:w-full [&_th]:text-sm [&_tr]:text-sm"
data={data}
keyExtractor={(item) => item.id}
columns={[

View File

@@ -11,7 +11,7 @@ import { LazyChart } from '../report/chart/LazyChart';
import { Widget, WidgetBody } from '../widget';
import { OverviewChartToggle } from './overview-chart-toggle';
import OverviewDetailsButton from './overview-details-button';
import { WidgetButtons, WidgetHead } from './overview-widget';
import { WidgetButtons, WidgetFooter, WidgetHead } from './overview-widget';
import { useOverviewOptions } from './useOverviewOptions';
import { useOverviewWidget } from './useOverviewWidget';
@@ -271,10 +271,7 @@ export default function OverviewTopDevices({
<>
<Widget className="col-span-6 md:col-span-3">
<WidgetHead>
<div className="title">
{widget.title}
<OverviewChartToggle {...{ chartType, setChartType }} />
</div>
<div className="title">{widget.title}</div>
<WidgetButtons>
{widgets.map((w) => (
<button
@@ -312,8 +309,11 @@ export default function OverviewTopDevices({
}
}}
/>
<OverviewDetailsButton chart={widget.chart} />
</WidgetBody>
<WidgetFooter>
<OverviewDetailsButton chart={widget.chart} />
<OverviewChartToggle {...{ chartType, setChartType }} />
</WidgetFooter>
</Widget>
</>
);

View File

@@ -10,7 +10,7 @@ import type { IChartType } from '@openpanel/validation';
import { Widget, WidgetBody } from '../../widget';
import { OverviewChartToggle } from '../overview-chart-toggle';
import OverviewDetailsButton from '../overview-details-button';
import { WidgetButtons, WidgetHead } from '../overview-widget';
import { WidgetButtons, WidgetFooter, WidgetHead } from '../overview-widget';
import { useOverviewOptions } from '../useOverviewOptions';
import { useOverviewWidget } from '../useOverviewWidget';
@@ -143,10 +143,7 @@ export default function OverviewTopEvents({
<>
<Widget className="col-span-6 md:col-span-3">
<WidgetHead>
<div className="title">
{widget.title}
<OverviewChartToggle {...{ chartType, setChartType }} />
</div>
<div className="title">{widget.title}</div>
<WidgetButtons>
{widgets
.filter((item) => item.hide !== true)
@@ -163,8 +160,11 @@ export default function OverviewTopEvents({
</WidgetHead>
<WidgetBody>
<LazyChart hideID {...widget.chart} previous={false} />
<OverviewDetailsButton chart={widget.chart} />
</WidgetBody>
<WidgetFooter>
<OverviewDetailsButton chart={widget.chart} />
<OverviewChartToggle {...{ chartType, setChartType }} />
</WidgetFooter>
</Widget>
</>
);

View File

@@ -13,7 +13,7 @@ import { LazyChart } from '../report/chart/LazyChart';
import { Widget, WidgetBody } from '../widget';
import { OverviewChartToggle } from './overview-chart-toggle';
import OverviewDetailsButton from './overview-details-button';
import { WidgetButtons, WidgetHead } from './overview-widget';
import { WidgetButtons, WidgetFooter, WidgetHead } from './overview-widget';
import { useOverviewOptions } from './useOverviewOptions';
import { useOverviewWidget } from './useOverviewWidget';
@@ -143,10 +143,7 @@ export default function OverviewTopGeo({ projectId }: OverviewTopGeoProps) {
<>
<Widget className="col-span-6 md:col-span-3">
<WidgetHead>
<div className="title">
{widget.title}
<OverviewChartToggle {...{ chartType, setChartType }} />
</div>
<div className="title">{widget.title}</div>
<WidgetButtons>
{widgets.map((w) => (
<button
@@ -180,8 +177,11 @@ export default function OverviewTopGeo({ projectId }: OverviewTopGeoProps) {
}
}}
/>
<OverviewDetailsButton chart={widget.chart} />
</WidgetBody>
<WidgetFooter>
<OverviewDetailsButton chart={widget.chart} />
<OverviewChartToggle {...{ chartType, setChartType }} />
</WidgetFooter>
</Widget>
<Widget className="col-span-6 md:col-span-3">
<WidgetHead>

View File

@@ -14,7 +14,7 @@ import { Widget, WidgetBody } from '../widget';
import { OverviewChartToggle } from './overview-chart-toggle';
import OverviewDetailsButton from './overview-details-button';
import OverviewTopBots from './overview-top-bots';
import { WidgetButtons, WidgetHead } from './overview-widget';
import { WidgetButtons, WidgetFooter, WidgetHead } from './overview-widget';
import { useOverviewOptions } from './useOverviewOptions';
import { useOverviewWidget } from './useOverviewWidget';
@@ -154,10 +154,7 @@ export default function OverviewTopPages({ projectId }: OverviewTopPagesProps) {
<>
<Widget className="col-span-6 md:col-span-3">
<WidgetHead>
<div className="title">
{widget.title}
<OverviewChartToggle {...{ chartType, setChartType }} />
</div>
<div className="title">{widget.title}</div>
<WidgetButtons>
{widgets.map((w) => (
<button
@@ -196,8 +193,13 @@ export default function OverviewTopPages({ projectId }: OverviewTopPagesProps) {
]}
/>
)}
{widget.chart?.name && <OverviewDetailsButton chart={widget.chart} />}
</WidgetBody>
{widget.chart?.name && (
<WidgetFooter>
<OverviewDetailsButton chart={widget.chart} />
<OverviewChartToggle {...{ chartType, setChartType }} />
</WidgetFooter>
)}
</Widget>
</>
);

View File

@@ -2,9 +2,7 @@
import { useState } from 'react';
import { useEventQueryFilters } from '@/hooks/useEventQueryFilters';
import { pushModal } from '@/modals';
import { cn } from '@/utils/cn';
import { ScanEyeIcon } from 'lucide-react';
import type { IChartType } from '@openpanel/validation';
@@ -12,7 +10,7 @@ import { LazyChart } from '../report/chart/LazyChart';
import { Widget, WidgetBody } from '../widget';
import { OverviewChartToggle } from './overview-chart-toggle';
import OverviewDetailsButton from './overview-details-button';
import { WidgetButtons, WidgetHead } from './overview-widget';
import { WidgetButtons, WidgetFooter, WidgetHead } from './overview-widget';
import { useOverviewOptions } from './useOverviewOptions';
import { useOverviewWidget } from './useOverviewWidget';
@@ -282,10 +280,7 @@ export default function OverviewTopSources({
<>
<Widget className="col-span-6 md:col-span-3">
<WidgetHead>
<div className="title">
{widget.title}
<OverviewChartToggle {...{ chartType, setChartType }} />
</div>
<div className="title">{widget.title}</div>
<WidgetButtons>
{widgets.map((w) => (
@@ -335,8 +330,11 @@ export default function OverviewTopSources({
}
}}
/>
<OverviewDetailsButton chart={widget.chart} />
</WidgetBody>
<WidgetFooter>
<OverviewDetailsButton chart={widget.chart} />
<OverviewChartToggle {...{ chartType, setChartType }} />
</WidgetFooter>
</Widget>
</>
);

View File

@@ -85,7 +85,7 @@ export function WidgetButtons({
<div
ref={container}
className={cn(
'-mb-px -mt-2 flex flex-wrap justify-start self-stretch px-4 transition-opacity [&_button.active]:border-b-2 [&_button.active]:border-black [&_button.active]:opacity-100 dark:[&_button.active]:border-white [&_button]:whitespace-nowrap [&_button]:py-1 [&_button]:text-xs [&_button]:opacity-50',
'-mb-px -mt-2 flex flex-wrap justify-start self-stretch px-4 transition-opacity [&_button.active]:border-b-2 [&_button.active]:border-black [&_button.active]:opacity-100 dark:[&_button.active]:border-white [&_button]:whitespace-nowrap [&_button]:py-1 [&_button]:text-sm [&_button]:opacity-50',
className
)}
style={{ gap }}
@@ -93,7 +93,12 @@ export function WidgetButtons({
>
{Children.map(children, (child, index) => {
return (
<div className={cn('flex', slice < index ? hidden : 'opacity-100')}>
<div
className={cn(
'flex [&_button]:leading-normal',
slice < index ? hidden : 'opacity-100'
)}
>
{child}
</div>
);
@@ -123,3 +128,21 @@ export function WidgetButtons({
</div>
);
}
export function WidgetFooter({
className,
children,
...props
}: WidgetHeadProps) {
return (
<div
className={cn(
'flex rounded-b-md border-t bg-def-100 p-2 py-1',
className
)}
{...props}
>
{children}
</div>
);
}

View File

@@ -0,0 +1,62 @@
import { cn } from '@/utils/cn';
import Link from 'next/link';
export function PageTabs({
children,
className,
}: {
children: React.ReactNode;
className?: string;
}) {
return (
<div className={cn('overflow-x-auto', className)}>
<div className="flex gap-4 whitespace-nowrap text-3xl font-semibold">
{children}
</div>
</div>
);
}
export function PageTabsLink({
href,
children,
isActive = false,
}: {
href: string;
children: React.ReactNode;
isActive?: boolean;
}) {
return (
<Link
className={cn(
'inline-block opacity-100 transition-transform hover:translate-y-[-1px]',
isActive ? 'opacity-100' : 'opacity-50'
)}
href={href}
>
{children}
</Link>
);
}
export function PageTabsItem({
onClick,
children,
isActive = false,
}: {
onClick: () => void;
children: React.ReactNode;
isActive?: boolean;
}) {
return (
<button
className={cn(
'inline-block opacity-100 transition-transform hover:translate-y-[-1px]',
isActive ? 'opacity-100' : 'opacity-50'
)}
onClick={onClick}
>
{children}
</button>
);
}

View File

@@ -77,17 +77,6 @@ export function Pagination({
disabled={isNextDisabled}
icon={ChevronRightIcon}
/>
{size === 'base' && (
<Button
variant="outline"
size="icon"
onClick={() => setCursor(lastCursor)}
disabled={isNextDisabled}
className="max-sm:hidden"
icon={ChevronsRightIcon}
/>
)}
</div>
);
}

View File

@@ -43,7 +43,7 @@ export function ProfileAvatar({
size === 'lg'
? 'text-lg'
: size === 'sm'
? 'text-xs'
? 'text-sm'
: size === 'xs'
? 'text-[8px]'
: 'text-base',

View File

@@ -0,0 +1,102 @@
import { ProjectLink } from '@/components/links';
import { SerieIcon } from '@/components/report/chart/SerieIcon';
import { Tooltiper } from '@/components/ui/tooltip';
import { formatDateTime, formatTime } from '@/utils/date';
import { getProfileName } from '@/utils/getters';
import type { ColumnDef } from '@tanstack/react-table';
import { isToday } from 'date-fns';
import type { IServiceProfile } from '@openpanel/db';
import { ProfileAvatar } from '../profile-avatar';
export function useColumns(type?: 'profiles' | 'power-users') {
const columns: ColumnDef<IServiceProfile>[] = [
{
accessorKey: 'name',
header: 'Name',
cell: ({ row }) => {
const profile = row.original;
return (
<ProjectLink
href={`/profiles/${profile.id}`}
className="flex items-center gap-2 font-medium"
title={getProfileName(profile, false)}
>
<ProfileAvatar size="sm" {...profile} />
{getProfileName(profile)}
</ProjectLink>
);
},
},
{
accessorKey: 'country',
header: 'Country',
cell({ row }) {
const { country, city } = row.original.properties;
return (
<div className="flex min-w-0 items-center gap-2">
<SerieIcon name={country} />
<span className="truncate">{city}</span>
</div>
);
},
},
{
accessorKey: 'os',
header: 'OS',
cell({ row }) {
const { os } = row.original.properties;
return (
<div className="flex min-w-0 items-center gap-2">
<SerieIcon name={os} />
<span className="truncate">{os}</span>
</div>
);
},
},
{
accessorKey: 'browser',
header: 'Browser',
cell({ row }) {
const { browser } = row.original.properties;
return (
<div className="flex min-w-0 items-center gap-2">
<SerieIcon name={browser} />
<span className="truncate">{browser}</span>
</div>
);
},
},
{
accessorKey: 'createdAt',
header: 'Last seen',
cell: ({ row }) => {
const profile = row.original;
return (
<Tooltiper asChild content={formatDateTime(profile.createdAt)}>
<div className="text-muted-foreground">
{isToday(profile.createdAt)
? formatTime(profile.createdAt)
: formatDateTime(profile.createdAt)}
</div>
</Tooltiper>
);
},
},
];
if (type === 'power-users') {
columns.unshift({
accessorKey: 'count',
header: 'Events',
cell: ({ row }) => {
const profile = row.original;
// @ts-expect-error
return <div>{profile.count}</div>;
},
});
}
return columns;
}

View File

@@ -0,0 +1,72 @@
import type { Dispatch, SetStateAction } from 'react';
import { DataTable } from '@/components/data-table';
import { FullPageEmptyState } from '@/components/full-page-empty-state';
import { Pagination } from '@/components/pagination';
import { Button } from '@/components/ui/button';
import type { UseQueryResult } from '@tanstack/react-query';
import { GanttChartIcon } from 'lucide-react';
import type { IServiceProfile } from '@openpanel/db';
import { useColumns } from './columns';
type CommonProps = {
type?: 'profiles' | 'power-users';
query: UseQueryResult<IServiceProfile[]>;
};
type Props =
| CommonProps
| (CommonProps & {
cursor: number;
setCursor: Dispatch<SetStateAction<number>>;
});
export const ProfilesTable = ({ type, query, ...props }: Props) => {
const columns = useColumns(type);
const { data, isFetching, isLoading } = query;
if (isLoading) {
return (
<div className="flex flex-col gap-2">
<div className="card h-[74px] w-full animate-pulse items-center justify-between rounded-lg p-4"></div>
<div className="card h-[74px] w-full animate-pulse items-center justify-between rounded-lg p-4"></div>
<div className="card h-[74px] w-full animate-pulse items-center justify-between rounded-lg p-4"></div>
<div className="card h-[74px] w-full animate-pulse items-center justify-between rounded-lg p-4"></div>
<div className="card h-[74px] w-full animate-pulse items-center justify-between rounded-lg p-4"></div>
</div>
);
}
if (data?.length === 0) {
return (
<FullPageEmptyState title="No profiles here" icon={GanttChartIcon}>
<p>Could not find any profiles</p>
{'cursor' in props && props.cursor !== 0 && (
<Button
className="mt-8"
variant="outline"
onClick={() => props.setCursor((p) => p - 1)}
>
Go to previous page
</Button>
)}
</FullPageEmptyState>
);
}
return (
<>
<DataTable data={data ?? []} columns={columns} />
{'cursor' in props && (
<Pagination
className="mt-2"
setCursor={props.setCursor}
cursor={props.cursor}
count={Infinity}
take={50}
loading={isFetching}
/>
)}
</>
);
};

View File

@@ -23,7 +23,7 @@ function ProjectCard({ id, name, organizationSlug }: IServiceProject) {
<ProjectChart id={id} />
</Suspense>
</div>
<div className="flex justify-end gap-4 text-sm">
<div className="flex justify-end gap-4 ">
<Suspense>
<ProjectMetrics id={id} />
</Suspense>

View File

@@ -62,10 +62,10 @@ export function PreviousDiffIndicator({
const renderIcon = () => {
if (state === 'positive') {
return <ArrowUpIcon strokeWidth={3} size={12} color="#000" />;
return <ArrowUpIcon strokeWidth={3} size={10} color="#000" />;
}
if (state === 'negative') {
return <ArrowDownIcon strokeWidth={3} size={12} color="#000" />;
return <ArrowDownIcon strokeWidth={3} size={10} color="#000" />;
}
return null;
};
@@ -74,7 +74,7 @@ export function PreviousDiffIndicator({
<>
<div
className={cn(
'flex items-center gap-1 font-medium',
'font-mono flex items-center gap-1 font-medium',
size === 'lg' && 'gap-2',
className
)}

View File

@@ -89,21 +89,21 @@ export function MetricCard({
)}
</AutoSizer>
</div>
<div className="relative">
<div className="col relative gap-2">
<div className="flex items-center justify-between gap-2">
<div className="flex min-w-0 items-center gap-2 text-left font-semibold">
<span className="overflow-hidden text-ellipsis whitespace-nowrap text-muted-foreground">
<div className="flex min-w-0 items-center gap-2 text-left">
<span className="truncate text-muted-foreground">
<SerieName name={serie.names} />
</span>
</div>
</div>
<div className="flex items-end justify-between">
<div className="overflow-hidden text-ellipsis whitespace-nowrap text-2xl font-bold">
<div className="font-mono truncate text-3xl font-bold">
{renderValue(serie.metrics[metric], 'ml-1 font-light text-xl')}
</div>
<PreviousDiffIndicator
{...previous}
className="text-xs text-muted-foreground"
className="text-sm text-muted-foreground"
/>
</div>
</div>

View File

@@ -37,7 +37,7 @@ export function ReportBarChart({ data }: ReportBarChartProps) {
return (
<div
className={cn(
'flex flex-col text-xs',
'flex flex-col text-sm',
editMode ? 'card gap-2 p-4 text-base' : '-m-3 gap-1'
)}
>
@@ -60,9 +60,9 @@ export function ReportBarChart({ data }: ReportBarChartProps) {
: {})}
>
<div
className="absolute bottom-0 left-0 top-0 rounded bg-def-200"
className="absolute bottom-0.5 left-1 right-1 top-0.5 rounded bg-def-200"
style={{
width: `${(serie.metrics.sum / maxCount) * 100}%`,
width: `calc(${(serie.metrics.sum / maxCount) * 100}% - 8px)`,
}}
/>
<div className="relative z-10 flex w-full flex-1 items-center gap-4 overflow-hidden px-3 py-2">
@@ -70,7 +70,7 @@ export function ReportBarChart({ data }: ReportBarChartProps) {
<SerieIcon name={serie.names[0]} />
<SerieName name={serie.names} />
</div>
<div className="flex flex-shrink-0 items-center justify-end gap-4">
<div className="font-mono flex flex-shrink-0 items-center justify-end gap-4">
<PreviousDiffIndicator
{...serie.metrics.previous?.[metric]}
/>

View File

@@ -42,7 +42,7 @@ export function ReportChartTooltip({
const hidden = sorted.slice(limit);
return (
<div className="flex min-w-[180px] flex-col gap-2 rounded-xl border bg-card p-3 text-sm shadow-xl">
<div className="flex min-w-[180px] flex-col gap-2 rounded-xl border bg-card p-3 shadow-xl">
{visible.map((item, index) => {
// If we have a <Cell /> component, payload can be nested
const payload = item.payload.payload ?? item.payload;
@@ -65,20 +65,22 @@ export function ReportChartTooltip({
className="w-[3px] rounded-full"
style={{ background: data.color }}
/>
<div className="flex flex-1 flex-col">
<div className="col flex-1 gap-1">
<div className="flex items-center gap-1">
<SerieIcon name={data.names} />
<SerieName name={data.names} />
</div>
<div className="flex justify-between gap-8">
<div>{number.formatWithUnit(data.count, unit)}</div>
<div className="flex gap-1">
<PreviousDiffIndicator {...data.previous}>
{!!data.previous &&
`(${number.formatWithUnit(data.previous.value, unit)})`}
</PreviousDiffIndicator>
<div className="font-mono flex justify-between gap-8 font-medium">
<div className="row gap-1">
{number.formatWithUnit(data.count, unit)}
{!!data.previous && (
<span className="text-muted-foreground">
({number.formatWithUnit(data.previous.value, unit)})
</span>
)}
</div>
<PreviousDiffIndicator {...data.previous} />
</div>
</div>
</div>

View File

@@ -1,6 +1,6 @@
'use client';
import React, { useCallback, useMemo } from 'react';
import React, { useCallback } from 'react';
import { useFormatDateInterval } from '@/hooks/useFormatDateInterval';
import { useNumber } from '@/hooks/useNumerFormatter';
import { useRechartDataModel } from '@/hooks/useRechartDataModel';
@@ -10,24 +10,19 @@ import type { IChartData } from '@/trpc/client';
import { cn } from '@/utils/cn';
import { getChartColor } from '@/utils/theme';
import { isSameDay, isSameHour, isSameMonth } from 'date-fns';
import { SplineIcon } from 'lucide-react';
import { last, pathOr } from 'ramda';
import { last } from 'ramda';
import {
Area,
CartesianGrid,
ComposedChart,
Legend,
Line,
LineChart,
ReferenceLine,
Tooltip,
XAxis,
YAxis,
} from 'recharts';
import type { IServiceReference } from '@openpanel/db';
import type { IChartLineType, IInterval } from '@openpanel/validation';
import { getYAxisWidth } from './chart-utils';
import { useChartContext } from './ChartProvider';
import { ReportChartTooltip } from './ReportChartTooltip';

View File

@@ -48,6 +48,7 @@ interface ChartRootShortcutProps {
chartType?: IChartProps['chartType'];
interval?: IChartProps['interval'];
events: IChartProps['events'];
breakdowns?: IChartProps['breakdowns'];
}
export const ChartRootShortcut = ({
@@ -57,12 +58,13 @@ export const ChartRootShortcut = ({
chartType = 'linear',
interval = 'day',
events,
breakdowns,
}: ChartRootShortcutProps) => {
return (
<ChartRoot
projectId={projectId}
range={range}
breakdowns={[]}
breakdowns={breakdowns ?? []}
previous={previous}
chartType={chartType}
interval={interval}

View File

@@ -132,7 +132,7 @@ export function FunnelSteps({
</div>
{finalStep ? (
<div className={cn('flex flex-col items-center p-4')}>
<div className="text-xs font-medium uppercase">
<div className="text-sm font-medium uppercase">
Conversion
</div>
<div
@@ -143,13 +143,13 @@ export function FunnelSteps({
>
{round(step.percent, 1)}%
</div>
<div className="mt-0 text-sm font-medium uppercase text-muted-foreground">
<div className="mt-0 font-medium uppercase text-muted-foreground">
Converted {step.current} of {totalSessions} sessions
</div>
</div>
) : (
<div className={cn('flex flex-col items-center p-4')}>
<div className="text-xs font-medium uppercase">Dropoff</div>
<div className="text-sm font-medium uppercase">Dropoff</div>
<div
className={cn(
'text-3xl font-bold uppercase',
@@ -158,7 +158,7 @@ export function FunnelSteps({
>
{round(step.dropoff.percent, 1)}%
</div>
<div className="mt-0 text-sm font-medium uppercase text-muted-foreground">
<div className="mt-0 font-medium uppercase text-muted-foreground">
Lost {step.dropoff.count} sessions
</div>
</div>

View File

@@ -1,17 +1,14 @@
'use client';
import { ColorSquare } from '@/components/color-square';
import { AutoSizer } from '@/components/react-virtualized-auto-sizer';
import { TooltipComplete } from '@/components/tooltip-complete';
import { Progress } from '@/components/ui/progress';
import { Widget, WidgetBody } from '@/components/widget';
import type { RouterOutputs } from '@/trpc/client';
import { cn } from '@/utils/cn';
import { round } from '@/utils/math';
import { getChartColor } from '@/utils/theme';
import { AlertCircleIcon, TrendingUp } from 'lucide-react';
import { AlertCircleIcon } from 'lucide-react';
import { last } from 'ramda';
import { Cell, Pie, PieChart } from 'recharts';
import { getPreviousMetric } from '@openpanel/common';
import { alphabetIds } from '@openpanel/constants';
@@ -40,7 +37,7 @@ function InsightCard({
}) {
return (
<div className="flex flex-col rounded-lg border border-border p-4 py-3">
<span className="text-sm">{title}</span>
<span className="">{title}</span>
<div className="whitespace-nowrap text-lg">{children}</div>
</div>
);

View File

@@ -54,7 +54,7 @@ export function EventPropertiesCombobox({
>
<button
className={cn(
'flex items-center gap-1 rounded-md border border-border p-1 px-2 text-xs font-medium leading-none',
'flex items-center gap-1 rounded-md border border-border p-1 px-2 text-sm font-medium leading-none',
!event.property && 'border-destructive text-destructive'
)}
>

View File

@@ -106,7 +106,7 @@ export function ReportEvents() {
</div>
{/* Segment and Filter buttons */}
<div className="flex gap-2 p-2 pt-0 text-sm">
<div className="flex gap-2 p-2 pt-0 ">
<DropdownMenuComposed
onChange={(segment) => {
dispatch(
@@ -148,7 +148,7 @@ export function ReportEvents() {
]}
label="Segment"
>
<button className="flex items-center gap-1 rounded-md border border-border bg-card p-1 px-2 text-xs font-medium leading-none">
<button className="flex items-center gap-1 rounded-md border border-border bg-card p-1 px-2 text-sm font-medium leading-none">
{event.segment === 'user' ? (
<>
<Users size={12} /> Unique users
@@ -216,7 +216,7 @@ export function ReportEvents() {
/>
</div>
<label
className="mt-4 flex cursor-pointer select-none items-center gap-2 text-sm font-medium"
className="mt-4 flex cursor-pointer select-none items-center gap-2 font-medium"
htmlFor="previous"
>
<Checkbox

View File

@@ -103,7 +103,7 @@ export function FilterItem({ filter, event }: FilterProps) {
<ColorSquare className="bg-emerald-500">
<SlidersHorizontal size={10} />
</ColorSquare>
<div className="flex flex-1 text-sm">
<div className="flex flex-1 ">
<RenderDots truncate>{filter.name}</RenderDots>
</div>
<Button variant="ghost" size="sm" onClick={removeFilter}>

View File

@@ -63,7 +63,7 @@ export function FiltersCombobox({ event }: FiltersComboboxProps) {
);
}}
>
<button className="flex items-center gap-1 rounded-md border border-border bg-card p-1 px-2 text-xs font-medium leading-none">
<button className="flex items-center gap-1 rounded-md border border-border bg-card p-1 px-2 text-sm font-medium leading-none">
<FilterIcon size={12} /> Add filter
</button>
</Combobox>

View File

@@ -0,0 +1,86 @@
'use client';
import * as React from 'react';
import { Button } from '@/components/ui/button';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { CheckIcon, MoreHorizontalIcon, PlusIcon } from 'lucide-react';
import { useTheme } from 'next-themes';
import { ProjectLink } from './links';
interface Props {
className?: string;
}
export default function SettingsToggle({ className }: Props) {
const { setTheme, theme } = useTheme();
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="icon" className={className}>
<MoreHorizontalIcon className="h-[1.2rem] w-[1.2rem]" />
<span className="sr-only">Toggle settings</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="center" className="w-56">
<DropdownMenuItem asChild>
<ProjectLink href="/reports">
Create report
<DropdownMenuShortcut>
<PlusIcon className="h-4 w-4" />
</DropdownMenuShortcut>
</ProjectLink>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuLabel>Settings</DropdownMenuLabel>
<DropdownMenuItem asChild>
<ProjectLink href="/settings/organization">Organization</ProjectLink>
</DropdownMenuItem>
<DropdownMenuItem asChild>
<ProjectLink href="/settings/projects">Projects</ProjectLink>
</DropdownMenuItem>
<DropdownMenuItem asChild>
<ProjectLink href="/settings/profile">Your profile</ProjectLink>
</DropdownMenuItem>
<DropdownMenuItem asChild>
<ProjectLink href="/settings/references">References</ProjectLink>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuSub>
<DropdownMenuSubTrigger className="flex w-full items-center justify-between">
Theme
<DropdownMenuShortcut>{theme}</DropdownMenuShortcut>
</DropdownMenuSubTrigger>
<DropdownMenuSubContent className="p-0">
{['system', 'light', 'dark'].map((themeOption) => (
<DropdownMenuItem
key={themeOption}
onClick={() => setTheme(themeOption)}
className="capitalize"
>
{themeOption}
{theme === themeOption && (
<CheckIcon className="ml-2 h-4 w-4" />
)}
</DropdownMenuItem>
))}
</DropdownMenuSubContent>
</DropdownMenuSub>
<DropdownMenuSeparator />
<DropdownMenuItem className="text-red-600">Logout</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}

View File

@@ -0,0 +1,112 @@
import { TooltipComplete } from '@/components/tooltip-complete';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { api } from '@/trpc/client';
import type { ColumnDef, Row } from '@tanstack/react-table';
import { MoreHorizontalIcon } from 'lucide-react';
import { useRouter } from 'next/navigation';
import { pathOr } from 'ramda';
import { toast } from 'sonner';
import type { IServiceInvite, IServiceProject } from '@openpanel/db';
export function useColumns(
projects: IServiceProject[]
): ColumnDef<IServiceInvite>[] {
return [
{
accessorKey: 'email',
header: 'Mail',
cell: ({ row }) => (
<div className="font-medium">{row.original.email}</div>
),
},
{
accessorKey: 'role',
header: 'Role',
},
{
accessorKey: 'createdAt',
header: 'Created',
cell: ({ row }) => (
<TooltipComplete
content={new Date(row.original.createdAt).toLocaleString()}
>
{new Date(row.original.createdAt).toLocaleDateString()}
</TooltipComplete>
),
},
{
accessorKey: 'access',
header: 'Access',
cell: ({ row }) => {
const access = pathOr<string[]>([], ['meta', 'access'], row.original);
return (
<>
{access.map((id) => {
const project = projects.find((p) => p.id === id);
if (!project) {
return (
<Badge key={id} className="mr-1">
Unknown
</Badge>
);
}
return (
<Badge key={id} color="blue" className="mr-1">
{project.name}
</Badge>
);
})}
{access.length === 0 && (
<Badge variant={'secondary'}>All projects</Badge>
)}
</>
);
},
},
{
id: 'actions',
cell: ({ row }) => {
return <ActionCell row={row} />;
},
},
];
}
function ActionCell({ row }: { row: Row<IServiceInvite> }) {
const router = useRouter();
const revoke = api.organization.revokeInvite.useMutation({
onSuccess() {
toast.success(`Invite for ${row.original.email} revoked`);
router.refresh();
},
onError() {
toast.error(`Failed to revoke invite for ${row.original.email}`);
},
});
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button icon={MoreHorizontalIcon} size="icon" variant={'outline'} />
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem
className="text-destructive"
onClick={() => {
revoke.mutate({ memberId: row.original.id });
}}
>
Revoke invite
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}

View File

@@ -0,0 +1,46 @@
'use client';
import { DataTable } from '@/components/data-table';
import { FullPageEmptyState } from '@/components/full-page-empty-state';
import { GanttChartIcon } from 'lucide-react';
import type { IServiceInvite, IServiceProject } from '@openpanel/db';
import { useColumns } from './columns';
type CommonProps = {
projects: IServiceProject[];
data: IServiceInvite[];
};
type Props = CommonProps;
export const InvitesTable = ({ projects, data }: Props) => {
const columns = useColumns(projects);
if (!data) {
return (
<div className="flex flex-col gap-2">
<div className="card h-[74px] w-full animate-pulse items-center justify-between rounded-lg p-4"></div>
<div className="card h-[74px] w-full animate-pulse items-center justify-between rounded-lg p-4"></div>
<div className="card h-[74px] w-full animate-pulse items-center justify-between rounded-lg p-4"></div>
<div className="card h-[74px] w-full animate-pulse items-center justify-between rounded-lg p-4"></div>
<div className="card h-[74px] w-full animate-pulse items-center justify-between rounded-lg p-4"></div>
</div>
);
}
if (data?.length === 0) {
return (
<FullPageEmptyState title="No members here" icon={GanttChartIcon}>
<p>Could not find any members</p>
</FullPageEmptyState>
);
}
return (
<>
<DataTable data={data ?? []} columns={columns} />
</>
);
};

View File

@@ -0,0 +1,140 @@
import { useState } from 'react';
import { TooltipComplete } from '@/components/tooltip-complete';
import { Button } from '@/components/ui/button';
import { ComboboxAdvanced } from '@/components/ui/combobox-advanced';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { api } from '@/trpc/client';
import type { ColumnDef, Row } from '@tanstack/react-table';
import { MoreHorizontalIcon } from 'lucide-react';
import { useRouter } from 'next/navigation';
import { toast } from 'sonner';
import type { IServiceMember, IServiceProject } from '@openpanel/db';
export function useColumns(projects: IServiceProject[]) {
const columns: ColumnDef<IServiceMember>[] = [
{
accessorKey: 'user',
header: 'Name',
cell: ({ row }) => {
const user = row.original.user;
if (!user) return null;
return [user.firstName, user.lastName].filter(Boolean).join(' ');
},
},
{
accessorKey: 'email',
header: 'Email',
cell: ({ row }) => {
const user = row.original.user;
if (!user) return null;
return <div className="font-medium">{user.email}</div>;
},
},
{
accessorKey: 'role',
header: 'Role',
},
{
accessorKey: 'createdAt',
header: 'Created',
cell: ({ row }) => (
<TooltipComplete
content={new Date(row.original.createdAt).toLocaleString()}
>
{new Date(row.original.createdAt).toLocaleDateString()}
</TooltipComplete>
),
},
{
accessorKey: 'access',
header: 'Access',
cell: ({ row }) => {
return <AccessCell row={row} projects={projects} />;
},
},
{
id: 'actions',
cell: ({ row }) => {
return <ActionsCell row={row} />;
},
},
];
return columns;
}
function AccessCell({
row,
projects,
}: {
row: Row<IServiceMember>;
projects: IServiceProject[];
}) {
const [access, setAccess] = useState<string[]>(
row.original.access.map((item) => item.projectId)
);
const mutation = api.organization.updateMemberAccess.useMutation();
return (
<ComboboxAdvanced
placeholder="Restrict access to projects"
value={access}
onChange={(newAccess) => {
setAccess(newAccess);
mutation.mutate({
userId: row.original.user!.id,
organizationSlug: row.original.organizationId,
access: newAccess as string[],
});
}}
items={projects.map((item) => ({
label: item.name,
value: item.id,
}))}
/>
);
}
function ActionsCell({ row }: { row: Row<IServiceMember> }) {
const router = useRouter();
const revoke = api.organization.removeMember.useMutation({
onSuccess() {
toast.success(
`${row.original.user?.firstName} has been removed from the organization`
);
router.refresh();
},
onError() {
toast.error(
`Failed to remove ${row.original.user?.firstName} from the organization`
);
},
});
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button icon={MoreHorizontalIcon} size="icon" variant={'outline'} />
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem
className="text-destructive"
onClick={() => {
revoke.mutate({
organizationId: row.original.organizationId,
userId: row.original.id,
});
}}
>
Remove member
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}

View File

@@ -0,0 +1,46 @@
'use client';
import { DataTable } from '@/components/data-table';
import { FullPageEmptyState } from '@/components/full-page-empty-state';
import { GanttChartIcon } from 'lucide-react';
import type { IServiceMember, IServiceProject } from '@openpanel/db';
import { useColumns } from './columns';
type CommonProps = {
projects: IServiceProject[];
data: IServiceMember[];
};
type Props = CommonProps;
export const MembersTable = ({ projects, data }: Props) => {
const columns = useColumns(projects);
if (!data) {
return (
<div className="flex flex-col gap-2">
<div className="card h-[74px] w-full animate-pulse items-center justify-between rounded-lg p-4"></div>
<div className="card h-[74px] w-full animate-pulse items-center justify-between rounded-lg p-4"></div>
<div className="card h-[74px] w-full animate-pulse items-center justify-between rounded-lg p-4"></div>
<div className="card h-[74px] w-full animate-pulse items-center justify-between rounded-lg p-4"></div>
<div className="card h-[74px] w-full animate-pulse items-center justify-between rounded-lg p-4"></div>
</div>
);
}
if (data?.length === 0) {
return (
<FullPageEmptyState title="No members here" icon={GanttChartIcon}>
<p>Could not find any members</p>
</FullPageEmptyState>
);
}
return (
<>
<DataTable data={data ?? []} columns={columns} />
</>
);
};

View File

@@ -43,7 +43,7 @@ const AccordionContent = React.forwardRef<
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Content
ref={ref}
className="overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
className="overflow-hidden transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
{...props}
>
<div className={cn('pb-4 pt-0', className)}>{children}</div>

View File

@@ -90,7 +90,7 @@ const AlertDialogDescription = React.forwardRef<
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Description
ref={ref}
className={cn('text-sm text-muted-foreground', className)}
className={cn(' text-muted-foreground', className)}
{...props}
/>
));

View File

@@ -52,7 +52,7 @@ const AlertDescription = React.forwardRef<
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn('text-sm [&_p]:leading-relaxed', className)}
className={cn(' [&_p]:leading-relaxed', className)}
{...props}
/>
));

View File

@@ -11,7 +11,7 @@ const Avatar = React.forwardRef<
<AvatarPrimitive.Root
ref={ref}
className={cn(
'relative flex h-8 w-8 shrink-0 overflow-hidden rounded-full text-sm',
'relative flex h-8 w-8 shrink-0 overflow-hidden rounded-full ',
className
)}
{...props}

View File

@@ -6,14 +6,14 @@ import { cva } from 'class-variance-authority';
import type { VariantProps } from 'class-variance-authority';
const badgeVariants = cva(
'inline-flex h-[20px] items-center rounded-full border px-1.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
'inline-flex h-[20px] items-center rounded-full border px-1.5 text-sm font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
{
variants: {
variant: {
default:
'border-transparent bg-primary text-primary-foreground hover:bg-primary/80',
secondary:
'bg-def-100 hover:bg-def-100/80 border-transparent text-secondary-foreground',
'border-transparent bg-def-100 text-secondary-foreground hover:bg-def-100/80',
destructive:
'border-transparent bg-destructive-foreground text-destructive hover:bg-destructive/80',
success:

View File

@@ -10,7 +10,7 @@ import { Loader2 } from 'lucide-react';
import Link from 'next/link';
const buttonVariants = cva(
'inline-flex flex-shrink-0 select-none items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
'inline-flex flex-shrink-0 select-none items-center justify-center whitespace-nowrap rounded-md font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
{
variants: {
variant: {

View File

@@ -20,7 +20,7 @@ function Calendar({
months: 'flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0',
month: 'space-y-4',
caption: 'flex justify-center pt-1 relative items-center',
caption_label: 'text-sm font-medium',
caption_label: ' font-medium',
nav: 'space-x-1 flex items-center',
nav_button: cn(
buttonVariants({ variant: 'outline' }),
@@ -33,7 +33,7 @@ function Calendar({
head_cell:
'text-muted-foreground rounded-md w-9 font-normal text-[0.8rem]',
row: 'flex w-full mt-2',
cell: 'h-9 w-9 text-center text-sm p-0 relative [&:has([aria-selected].day-range-end)]:rounded-r-md [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected])]:bg-accent first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md focus-within:relative focus-within:z-20',
cell: 'h-9 w-9 text-center p-0 relative [&:has([aria-selected].day-range-end)]:rounded-r-md [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected])]:bg-accent first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md focus-within:relative focus-within:z-20',
day: cn(
buttonVariants({ variant: 'ghost' }),
'h-9 w-9 p-0 font-normal aria-selected:opacity-100'

View File

@@ -52,7 +52,7 @@ const CheckboxInput = React.forwardRef<
)}
>
<Checkbox ref={ref} {...props} className="relative top-0.5" />
<div className="text-sm font-medium">{props.children}</div>
<div className=" font-medium">{props.children}</div>
</label>
));
CheckboxInput.displayName = 'CheckboxInput';

View File

@@ -45,7 +45,7 @@ const CommandInput = React.forwardRef<
<CommandPrimitive.Input
ref={ref}
className={cn(
'flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50',
'flex h-11 w-full rounded-md bg-transparent py-3 outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50',
className
)}
{...props}
@@ -72,11 +72,7 @@ const CommandEmpty = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Empty>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
>((props, ref) => (
<CommandPrimitive.Empty
ref={ref}
className="py-6 text-center text-sm"
{...props}
/>
<CommandPrimitive.Empty ref={ref} className="py-6 text-center " {...props} />
));
CommandEmpty.displayName = CommandPrimitive.Empty.displayName;
@@ -88,7 +84,7 @@ const CommandGroup = React.forwardRef<
<CommandPrimitive.Group
ref={ref}
className={cn(
'overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground',
'overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-sm [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground',
className
)}
{...props}
@@ -116,7 +112,7 @@ const CommandItem = React.forwardRef<
<CommandPrimitive.Item
ref={ref}
className={cn(
'relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none aria-selected:bg-accent aria-selected:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
'relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 outline-none aria-selected:bg-accent aria-selected:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
className
)}
data-disabled={props.disabled}
@@ -133,7 +129,7 @@ const CommandShortcut = ({
return (
<span
className={cn(
'ml-auto text-xs tracking-widest text-muted-foreground',
'ml-auto text-sm tracking-widest text-muted-foreground',
className
)}
{...props}

View File

@@ -99,7 +99,7 @@ const DialogDescription = React.forwardRef<
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn('text-sm text-muted-foreground', className)}
className={cn(' text-muted-foreground', className)}
{...props}
/>
));

View File

@@ -26,7 +26,7 @@ const DropdownMenuSubTrigger = React.forwardRef<
<DropdownMenuPrimitive.SubTrigger
ref={ref}
className={cn(
'flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent',
'flex cursor-default select-none items-center rounded-sm px-2 py-1.5 outline-none focus:bg-accent data-[state=open]:bg-accent',
inset && 'pl-8',
className
)}
@@ -82,7 +82,7 @@ const DropdownMenuItem = React.forwardRef<
<DropdownMenuPrimitive.Item
ref={ref}
className={cn(
'relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:mr-2',
'relative flex min-h-8 cursor-pointer select-none items-center rounded-sm px-2 py-1.5 outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&>svg]:mr-2',
inset && 'pl-8',
className
)}
@@ -98,7 +98,7 @@ const DropdownMenuCheckboxItem = React.forwardRef<
<DropdownMenuPrimitive.CheckboxItem
ref={ref}
className={cn(
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
className
)}
checked={checked}
@@ -122,7 +122,7 @@ const DropdownMenuRadioItem = React.forwardRef<
<DropdownMenuPrimitive.RadioItem
ref={ref}
className={cn(
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
className
)}
{...props}
@@ -145,11 +145,7 @@ const DropdownMenuLabel = React.forwardRef<
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Label
ref={ref}
className={cn(
'px-2 py-1.5 text-sm font-semibold',
inset && 'pl-8',
className
)}
className={cn('px-2 py-1.5 font-semibold', inset && 'pl-8', className)}
{...props}
/>
));
@@ -173,7 +169,7 @@ const DropdownMenuShortcut = ({
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn('ml-auto text-xs tracking-widest opacity-60', className)}
className={cn('ml-auto text-sm tracking-widest opacity-60', className)}
{...props}
/>
);

View File

@@ -38,7 +38,7 @@ const InputOTPSlot = React.forwardRef<
<div
ref={ref}
className={cn(
'relative flex h-10 w-10 items-center justify-center border-y border-r border-input text-sm transition-all first:rounded-l-md first:border-l last:rounded-r-md',
'relative flex h-10 w-10 items-center justify-center border-y border-r border-input transition-all first:rounded-l-md first:border-l last:rounded-r-md',
isActive && 'z-10 ring-2 ring-ring ring-offset-background',
className
)}

View File

@@ -6,11 +6,11 @@ import type { VariantProps } from 'class-variance-authority';
import { cva } from 'class-variance-authority';
const inputVariant = cva(
'flex w-full rounded-md border border-input bg-card ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50',
'file: flex w-full rounded-md border border-input bg-card ring-offset-background file:border-0 file:bg-transparent file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50',
{
variants: {
size: {
default: 'h-8 px-3 py-2 text-sm',
default: 'h-8 px-3 py-2 ',
large: 'h-12 px-4 py-3 text-lg',
},
},

View File

@@ -15,7 +15,7 @@ export function KeyValue({ href, onClick, name, value }: KeyValueProps) {
return (
<Component
className={cn(
'group flex min-w-0 max-w-full divide-x self-start overflow-hidden rounded-md border border-border text-xs font-medium transition-transform',
'group flex min-w-0 max-w-full divide-x self-start overflow-hidden rounded-md border border-border text-sm font-medium transition-transform',
clickable && 'hover:-translate-y-0.5'
)}
{...{ href, onClick }}
@@ -23,7 +23,7 @@ export function KeyValue({ href, onClick, name, value }: KeyValueProps) {
<div className="bg-black/5 p-1 px-2 capitalize">{name}</div>
<div
className={cn(
'overflow-hidden text-ellipsis whitespace-nowrap bg-card p-1 px-2 font-mono text-highlight',
'font-mono overflow-hidden text-ellipsis whitespace-nowrap bg-card p-1 px-2 text-highlight',
clickable && 'group-hover:underline'
)}
>

View File

@@ -7,7 +7,7 @@ import { cva } from 'class-variance-authority';
import type { VariantProps } from 'class-variance-authority';
const labelVariants = cva(
'mb-2 block text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70'
'mb-2 block font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70'
);
const Label = React.forwardRef<

View File

@@ -0,0 +1,17 @@
import { cn } from '@/utils/cn';
const padding = 'p-4 lg:p-8';
export const Padding = ({
className,
...props
}: React.HtmlHTMLAttributes<HTMLDivElement>) => {
return <div className={cn(padding, className)} {...props} />;
};
export const Spacer = ({
className,
...props
}: React.HtmlHTMLAttributes<HTMLDivElement>) => {
return <div className={cn('h-8', className)} {...props} />;
};

View File

@@ -13,7 +13,7 @@ const Progress = React.forwardRef<
<ProgressPrimitive.Root
ref={ref}
className={cn(
'bg-def-200 relative h-4 w-full min-w-16 overflow-hidden rounded shadow-sm',
'relative h-4 w-full min-w-16 overflow-hidden rounded bg-def-200 shadow-sm',
size == 'sm' && 'h-2',
size == 'lg' && 'h-8',
className
@@ -28,7 +28,7 @@ const Progress = React.forwardRef<
}}
/>
{value && size != 'sm' && (
<div className="z-5 absolute bottom-0 top-0 flex items-center px-2 text-xs font-semibold">
<div className="z-5 absolute bottom-0 top-0 flex items-center px-2 text-sm font-semibold">
<div>{round(value, 2)}%</div>
</div>
)}

View File

@@ -70,7 +70,7 @@ const SheetContent = React.forwardRef<
<SheetPrimitive.Close id="close-sheet" className="hidden" />
<SheetPrimitive.Close
onClick={onClose}
className="data-[state=open]:bg-def-100 absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none"
className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-def-100"
>
<XIcon className="h-4 w-4" />
<span className="sr-only">Close</span>
@@ -126,7 +126,7 @@ const SheetDescription = React.forwardRef<
>(({ className, ...props }, ref) => (
<SheetPrimitive.Description
ref={ref}
className={cn('text-sm text-muted-foreground', className)}
className={cn(' text-muted-foreground', className)}
{...props}
/>
));

View File

@@ -13,7 +13,7 @@ const Table = React.forwardRef<
<div className={cn('relative w-full', overflow && 'overflow-auto')}>
<table
ref={ref}
className={cn('w-full caption-bottom text-sm', className)}
className={cn('w-full caption-bottom', className)}
{...props}
/>
</div>
@@ -75,7 +75,7 @@ const TableHead = React.forwardRef<
<th
ref={ref}
className={cn(
'h-10 border-b border-border bg-def-200 px-4 text-left align-middle text-sm font-medium text-muted-foreground shadow-[0_0_0_0.5px] shadow-border [&:has([role=checkbox])]:pr-0',
'h-10 whitespace-nowrap border-b border-border bg-def-100 px-4 text-left align-middle font-semibold text-muted-foreground shadow-[0_0_0_0.5px] shadow-border [&:has([role=checkbox])]:pr-0',
className
)}
{...props}
@@ -104,7 +104,7 @@ const TableCaption = React.forwardRef<
>(({ className, ...props }, ref) => (
<caption
ref={ref}
className={cn('mt-4 text-sm text-muted-foreground', className)}
className={cn('mt-4 text-muted-foreground', className)}
{...props}
/>
));

View File

@@ -26,7 +26,7 @@ const TabsTrigger = React.forwardRef<
<TabsPrimitive.Trigger
ref={ref}
className={cn(
'inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-card data-[state=active]:text-foreground data-[state=active]:shadow-sm',
'inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-card data-[state=active]:text-foreground data-[state=active]:shadow-sm',
className
)}
{...props}

View File

@@ -8,7 +8,7 @@ const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
return (
<textarea
className={cn(
'flex min-h-[80px] w-full rounded-md border border-input bg-card px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
'flex min-h-[80px] w-full rounded-md border border-input bg-card px-3 py-2 ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
className
)}
ref={ref}

View File

@@ -62,7 +62,7 @@ const ToastAction = React.forwardRef<
<ToastPrimitives.Action
ref={ref}
className={cn(
'hover:bg-def-100 inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium ring-offset-background transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive',
'inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 font-medium ring-offset-background transition-colors hover:bg-def-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive',
className
)}
{...props}
@@ -94,7 +94,7 @@ const ToastTitle = React.forwardRef<
>(({ className, ...props }, ref) => (
<ToastPrimitives.Title
ref={ref}
className={cn('text-sm font-semibold', className)}
className={cn(' font-semibold', className)}
{...props}
/>
));
@@ -106,7 +106,7 @@ const ToastDescription = React.forwardRef<
>(({ className, ...props }, ref) => (
<ToastPrimitives.Description
ref={ref}
className={cn('text-sm opacity-90', className)}
className={cn(' opacity-90', className)}
{...props}
/>
));

View File

@@ -5,7 +5,7 @@ import { cva } from 'class-variance-authority';
import type { VariantProps } from 'class-variance-authority';
const toggleVariants = cva(
'inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground',
'inline-flex items-center justify-center rounded-md font-medium ring-offset-background transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground',
{
variants: {
variant: {

View File

@@ -22,7 +22,7 @@ const TooltipContent = React.forwardRef<
ref={ref}
sideOffset={sideOffset}
className={cn(
'z-50 overflow-hidden rounded-md border bg-popover-foreground px-3 py-1.5 text-sm text-popover shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
'z-50 overflow-hidden rounded-md border bg-popover-foreground px-3 py-1.5 text-popover shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
className
)}
{...props}

View File

@@ -20,7 +20,7 @@ export const WidgetTableHead = ({
return (
<thead
className={cn(
'bg-def-100 text-def-1000 sticky top-0 z-10 border-b border-border text-sm [&_th:last-child]:text-right [&_th]:whitespace-nowrap [&_th]:p-4 [&_th]:py-2 [&_th]:text-left [&_th]:font-medium',
'text-def-1000 sticky top-0 z-10 border-b border-border bg-def-100 [&_th:last-child]:text-right [&_th]:whitespace-nowrap [&_th]:p-4 [&_th]:py-2 [&_th]:text-left [&_th]:font-medium',
className
)}
>
@@ -49,7 +49,7 @@ export function WidgetTable<T>({
{data.map((item) => (
<tr
key={keyExtractor(item)}
className="border-b border-border text-right text-sm last:border-0 [&_td:first-child]:text-left [&_td]:p-4"
className="border-b border-border text-right last:border-0 [&_td:first-child]:text-left [&_td]:p-4"
>
{columns.map((column) => (
<td key={column.name}>{column.render(item)}</td>