mirror of
https://github.com/hatchet-dev/hatchet.git
synced 2026-04-29 05:59:52 -05:00
Chore: Remove the v0 frontend (#2611)
* feat: remove all v0 paths * chore: run knip * chore: knip, allow removals * chore: rm old components * fix: gen * fix: rm redirect * fix: rm some v0 stuff * fix: rm more unused stuff * fix: rm more * fix: more unused stuff * chore: gen * fix: redirect from root * fix: revert doc changes
This commit is contained in:
@@ -22,7 +22,6 @@
|
||||
"@lukemorales/query-key-factory": "^1.3.4",
|
||||
"@monaco-editor/react": "^4.7.0",
|
||||
"@radix-ui/react-accordion": "^1.2.3",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.7",
|
||||
"@radix-ui/react-avatar": "^1.1.3",
|
||||
"@radix-ui/react-checkbox": "^1.1.4",
|
||||
"@radix-ui/react-collapsible": "^1.1.3",
|
||||
@@ -31,10 +30,8 @@
|
||||
"@radix-ui/react-hover-card": "^1.1.6",
|
||||
"@radix-ui/react-icons": "^1.3.2",
|
||||
"@radix-ui/react-label": "^2.1.2",
|
||||
"@radix-ui/react-menubar": "^1.1.6",
|
||||
"@radix-ui/react-popover": "^1.1.6",
|
||||
"@radix-ui/react-radio-group": "^1.2.3",
|
||||
"@radix-ui/react-scroll-area": "^1.2.3",
|
||||
"@radix-ui/react-select": "^2.1.6",
|
||||
"@radix-ui/react-separator": "^1.1.2",
|
||||
"@radix-ui/react-slot": "^1.1.2",
|
||||
@@ -42,36 +39,17 @@
|
||||
"@radix-ui/react-tabs": "^1.1.3",
|
||||
"@radix-ui/react-toast": "^1.2.6",
|
||||
"@radix-ui/react-tooltip": "^1.1.8",
|
||||
"@rjsf/core": "^5.24.8",
|
||||
"@rjsf/utils": "^5.24.8",
|
||||
"@rjsf/validator-ajv8": "^5.24.8",
|
||||
"@sentry/react": "^7.120.3",
|
||||
"@sentry/vite-plugin": "^2.23.0",
|
||||
"@tanstack/react-query": "^5.71.1",
|
||||
"@tanstack/react-query-devtools": "^5.74.6",
|
||||
"@tanstack/react-table": "^8.21.2",
|
||||
"@types/lodash": "^4.17.20",
|
||||
"@visx/axis": "^3.12.0",
|
||||
"@visx/brush": "^3.12.0",
|
||||
"@visx/curve": "^3.12.0",
|
||||
"@visx/event": "^3.12.0",
|
||||
"@visx/gradient": "^3.12.0",
|
||||
"@visx/grid": "^3.12.0",
|
||||
"@visx/group": "^3.12.0",
|
||||
"@visx/mock-data": "^3.12.0",
|
||||
"@visx/pattern": "^3.12.0",
|
||||
"@visx/responsive": "^3.12.0",
|
||||
"@visx/scale": "^3.12.0",
|
||||
"@visx/shape": "^3.12.0",
|
||||
"@visx/text": "^3.12.0",
|
||||
"@visx/tooltip": "^3.12.0",
|
||||
"@visx/vendor": "^3.12.0",
|
||||
"ansi-to-html": "^0.7.2",
|
||||
"axios": "^1.12.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^0.2.1",
|
||||
"cron-parser": "^4.9.0",
|
||||
"cronstrue": "^2.57.0",
|
||||
"dagre": "^0.8.5",
|
||||
"date-fns": "^3.6.0",
|
||||
@@ -82,8 +60,6 @@
|
||||
"lucide-react": "^0.446.0",
|
||||
"monaco-themes": "^0.4.4",
|
||||
"posthog-js": "^1.298.1",
|
||||
"prism-react-renderer": "^2.4.1",
|
||||
"prismjs": "^1.30.0",
|
||||
"qs": "^6.14.0",
|
||||
"react": "^18.3.1",
|
||||
"react-day-picker": "^8.10.1",
|
||||
@@ -94,7 +70,6 @@
|
||||
"react-syntax-highlighter": "^15.6.1",
|
||||
"reactflow": "^11.11.4",
|
||||
"recharts": "^2.15.1",
|
||||
"shiki": "^3.2.2",
|
||||
"tailwind-merge": "^2.6.0",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"timeago-react": "^3.0.7",
|
||||
@@ -104,7 +79,6 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/dagre": "^0.7.52",
|
||||
"@types/dompurify": "^3.2.0",
|
||||
"@types/node": "^20.17.28",
|
||||
"@types/qs": "^6.9.18",
|
||||
"@types/react": "^18.3.20",
|
||||
@@ -117,7 +91,6 @@
|
||||
"eslint": "^8.57.1",
|
||||
"eslint-config-airbnb-typescript": "^17.1.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-import-resolver-typescript": "^3.10.0",
|
||||
"eslint-plugin-import": "^2.31.0",
|
||||
"eslint-plugin-prettier": "^5.2.5",
|
||||
"eslint-plugin-react": "^7.37.4",
|
||||
@@ -126,13 +99,10 @@
|
||||
"eslint-plugin-unused-imports": "^3.2.0",
|
||||
"postcss": "^8.5.3",
|
||||
"prettier": "^3.5.3",
|
||||
"swagger-typescript-api": "^13.0.28",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"ts-unused-exports": "^11.0.1",
|
||||
"tsx": "^4.19.3",
|
||||
"typescript": "^5.8.2",
|
||||
"vite": "^6.3.6",
|
||||
"vite-plugin-eslint": "^1.8.1"
|
||||
"vite": "^6.3.6"
|
||||
},
|
||||
"pnpm": {
|
||||
"overrides": {
|
||||
|
||||
Generated
+13
-1861
File diff suppressed because it is too large
Load Diff
@@ -1,2 +0,0 @@
|
||||
export * from './payment-methods';
|
||||
export * from './subscription';
|
||||
@@ -1,111 +0,0 @@
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
FaCcAmex,
|
||||
FaCcDiscover,
|
||||
FaCcMastercard,
|
||||
FaCcVisa,
|
||||
FaCreditCard,
|
||||
FaCcDinersClub,
|
||||
FaCcJcb,
|
||||
} from 'react-icons/fa';
|
||||
|
||||
import { LuBanknote } from 'react-icons/lu';
|
||||
|
||||
import { IconType } from 'react-icons/lib';
|
||||
import { TenantContextType } from '@/lib/outlet';
|
||||
import { useOutletContext } from 'react-router-dom';
|
||||
import { useApiError } from '@/lib/hooks';
|
||||
import { useState } from 'react';
|
||||
import { Spinner } from '@/components/ui/loading';
|
||||
import { TenantPaymentMethod } from '@/lib/api/generated/cloud/data-contracts';
|
||||
import { cloudApi } from '@/lib/api/api';
|
||||
|
||||
const ccIcons: Record<string, IconType> = {
|
||||
visa: FaCcVisa,
|
||||
mastercard: FaCcMastercard,
|
||||
amex: FaCcAmex,
|
||||
discover: FaCcDiscover,
|
||||
dinersclub: FaCcDinersClub,
|
||||
jcb: FaCcJcb,
|
||||
generic: FaCreditCard,
|
||||
link: LuBanknote,
|
||||
};
|
||||
|
||||
export interface PaymentMethodsProps {
|
||||
hasMethods?: boolean;
|
||||
methods?: TenantPaymentMethod[];
|
||||
}
|
||||
|
||||
export function PaymentMethods({
|
||||
methods = [],
|
||||
hasMethods,
|
||||
}: PaymentMethodsProps) {
|
||||
const { tenant } = useOutletContext<TenantContextType>();
|
||||
const { handleApiError } = useApiError({});
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const manageClicked = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const link = await cloudApi.billingPortalLinkGet(tenant.metadata.id);
|
||||
if (link.data.url) {
|
||||
window.location.href = link.data.url;
|
||||
}
|
||||
} catch (e) {
|
||||
handleApiError(e as any);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mx-auto py-8 px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex flex-row justify-between items-center">
|
||||
<h3 className="text-xl font-semibold leading-tight text-foreground">
|
||||
Payment Methods
|
||||
</h3>
|
||||
</div>
|
||||
{hasMethods ? (
|
||||
<>
|
||||
{methods.map((method, i) => {
|
||||
const Icon =
|
||||
method.brand in ccIcons
|
||||
? ccIcons[method.brand]
|
||||
: ccIcons.generic;
|
||||
return (
|
||||
<div key={i} className="flex flex-row items-center gap-4 mb-4">
|
||||
<div className="flex flex-col mt-4 text-sm">
|
||||
<div className="flex flex-row gap-2 items-center">
|
||||
<Icon size={24} />
|
||||
{method.brand.toUpperCase()}
|
||||
{method.last4 && ` *** *** ${method.last4} `}
|
||||
{method.expiration && `(Expires ${method.expiration})`}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<div className="mt-4">
|
||||
<Button onClick={manageClicked} variant="default">
|
||||
{loading ? <Spinner /> : 'Manage Payment Methods'}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="mt-4">
|
||||
<p className="">
|
||||
No payment methods added. Payment method is required to upgrade
|
||||
your subscription.
|
||||
</p>
|
||||
<div className="mt-4">
|
||||
<Button onClick={manageClicked} variant="default">
|
||||
{loading ? <Spinner /> : 'Add a Payment Method'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,270 +0,0 @@
|
||||
import { ConfirmDialog } from '@/components/molecules/confirm-dialog';
|
||||
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Card,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@/components/ui/card';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Spinner } from '@/components/ui/loading';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { queries } from '@/lib/api';
|
||||
import { cloudApi } from '@/lib/api/api';
|
||||
import {
|
||||
TenantSubscription,
|
||||
SubscriptionPlan,
|
||||
Coupon,
|
||||
} from '@/lib/api/generated/cloud/data-contracts';
|
||||
import { useApiError } from '@/lib/hooks';
|
||||
import { TenantContextType } from '@/lib/outlet';
|
||||
import queryClient from '@/query-client';
|
||||
import { ExclamationTriangleIcon } from '@radix-ui/react-icons';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useOutletContext } from 'react-router-dom';
|
||||
|
||||
interface SubscriptionProps {
|
||||
active?: TenantSubscription;
|
||||
plans?: SubscriptionPlan[];
|
||||
hasPaymentMethods?: boolean;
|
||||
coupons?: Coupon[];
|
||||
}
|
||||
|
||||
export const Subscription: React.FC<SubscriptionProps> = ({
|
||||
active,
|
||||
plans,
|
||||
coupons,
|
||||
hasPaymentMethods,
|
||||
}) => {
|
||||
// Implement the logic for the Subscription component here
|
||||
|
||||
const [loading, setLoading] = useState<string>();
|
||||
const [showAnnual, setShowAnnual] = useState<boolean>(false);
|
||||
const [isChangeConfirmOpen, setChangeConfirmOpen] = useState<
|
||||
SubscriptionPlan | undefined
|
||||
>(undefined);
|
||||
|
||||
const { tenant } = useOutletContext<TenantContextType>();
|
||||
const { handleApiError } = useApiError({});
|
||||
const [portalLoading, setPortalLoading] = useState(false);
|
||||
|
||||
const manageClicked = async () => {
|
||||
try {
|
||||
if (portalLoading) {
|
||||
return;
|
||||
}
|
||||
setPortalLoading(true);
|
||||
const link = await cloudApi.billingPortalLinkGet(tenant.metadata.id);
|
||||
window.open(link.data.url, '_blank');
|
||||
} catch (e) {
|
||||
handleApiError(e as any);
|
||||
} finally {
|
||||
setPortalLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const subscriptionMutation = useMutation({
|
||||
mutationKey: ['user:update:logout'],
|
||||
mutationFn: async ({ plan_code }: { plan_code: string }) => {
|
||||
const [plan, period] = plan_code.split(':');
|
||||
setLoading(plan_code);
|
||||
await cloudApi.subscriptionUpsert(tenant.metadata.id, { plan, period });
|
||||
},
|
||||
onSuccess: async () => {
|
||||
await Promise.all([
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: queries.tenantResourcePolicy.get(tenant.metadata.id)
|
||||
.queryKey,
|
||||
}),
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: queries.cloud.billing(tenant.metadata.id).queryKey,
|
||||
}),
|
||||
]);
|
||||
|
||||
setLoading(undefined);
|
||||
},
|
||||
onError: handleApiError,
|
||||
});
|
||||
|
||||
const activePlanCode = useMemo(
|
||||
() =>
|
||||
active?.plan
|
||||
? [active.plan, active.period].filter((x) => !!x).join(':')
|
||||
: 'free',
|
||||
[active],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
return setShowAnnual(active?.period?.includes('yearly') || false);
|
||||
}, [active]);
|
||||
|
||||
const sortedPlans = useMemo(() => {
|
||||
return plans
|
||||
?.filter(
|
||||
(v) =>
|
||||
v.plan_code === 'free' ||
|
||||
(showAnnual
|
||||
? v.period?.includes('yearly')
|
||||
: v.period?.includes('monthly')),
|
||||
)
|
||||
.sort((a, b) => a.amount_cents - b.amount_cents);
|
||||
}, [plans, showAnnual]);
|
||||
|
||||
const isUpgrade = useCallback(
|
||||
(plan: SubscriptionPlan) => {
|
||||
if (!active) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const activePlan = sortedPlans?.find(
|
||||
(p) => p.plan_code === activePlanCode,
|
||||
);
|
||||
|
||||
const activeAmount = activePlan?.amount_cents || 0;
|
||||
|
||||
return plan.amount_cents > activeAmount;
|
||||
},
|
||||
[active, activePlanCode, sortedPlans],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ConfirmDialog
|
||||
isOpen={!!isChangeConfirmOpen}
|
||||
title={'Confirm Change Plan'}
|
||||
submitVariant="default"
|
||||
description={
|
||||
<>
|
||||
Are you sure you'd like to change to {isChangeConfirmOpen?.name}{' '}
|
||||
plan?
|
||||
<br />
|
||||
<br />
|
||||
Upgrades will be prorated and downgrades will take effect at the end
|
||||
of the billing period.
|
||||
</>
|
||||
}
|
||||
submitLabel={'Change Plan'}
|
||||
onSubmit={async () => {
|
||||
await subscriptionMutation.mutateAsync({
|
||||
plan_code: isChangeConfirmOpen!.plan_code,
|
||||
});
|
||||
setLoading(undefined);
|
||||
setChangeConfirmOpen(undefined);
|
||||
}}
|
||||
onCancel={() => setChangeConfirmOpen(undefined)}
|
||||
isLoading={!!loading}
|
||||
/>
|
||||
<div className="mx-auto py-8 px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex flex-row justify-between items-center">
|
||||
<h3 className="text-xl font-semibold leading-tight text-foreground flex flex-row gap-2">
|
||||
Subscription
|
||||
{coupons?.map((coupon, i) => (
|
||||
<Badge key={`c${i}`} variant="successful">
|
||||
{coupon.name} coupon applied
|
||||
</Badge>
|
||||
))}
|
||||
</h3>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Switch
|
||||
id="sa"
|
||||
checked={showAnnual}
|
||||
onClick={() => {
|
||||
setShowAnnual((checkedState) => !checkedState);
|
||||
}}
|
||||
/>
|
||||
<Label htmlFor="sa" className="text-sm">
|
||||
Annual Billing{' '}
|
||||
<Badge variant="inProgress" className="ml-2">
|
||||
Save up to 20%
|
||||
</Badge>
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-gray-700 dark:text-gray-300 my-4">
|
||||
For plan details, please visit{' '}
|
||||
<a
|
||||
href="https://hatchet.run/pricing"
|
||||
className="underline"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
our pricing page
|
||||
</a>{' '}
|
||||
or{' '}
|
||||
<a href="https://hatchet.run/office-hours" className="underline">
|
||||
contact us
|
||||
</a>{' '}
|
||||
if you have custom requirements.
|
||||
</p>
|
||||
{!hasPaymentMethods && (
|
||||
<Alert variant="warn" className="mb-4">
|
||||
<ExclamationTriangleIcon className="h-4 w-4" />
|
||||
<AlertTitle className="font-semibold">
|
||||
No Payment Method.
|
||||
</AlertTitle>
|
||||
<AlertDescription>
|
||||
A payment method is required to upgrade your subscription, please{' '}
|
||||
<a onClick={manageClicked} className="underline pointer" href="#">
|
||||
add one
|
||||
</a>{' '}
|
||||
first.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-3">
|
||||
{sortedPlans?.map((plan, i) => (
|
||||
<Card className="bg-muted/30 gap-4 flex-col flex" key={i}>
|
||||
<CardHeader>
|
||||
<CardTitle className="tracking-wide text-sm">
|
||||
{plan.name}
|
||||
</CardTitle>
|
||||
<CardDescription className="py-4">
|
||||
$
|
||||
{(
|
||||
plan.amount_cents /
|
||||
100 /
|
||||
(plan.period == 'yearly' ? 12 : 1)
|
||||
).toLocaleString()}{' '}
|
||||
per month billed {plan.period}*
|
||||
</CardDescription>
|
||||
<CardDescription>
|
||||
<Button
|
||||
disabled={
|
||||
!hasPaymentMethods ||
|
||||
plan.plan_code === activePlanCode ||
|
||||
loading === plan.plan_code
|
||||
}
|
||||
variant={
|
||||
plan.plan_code !== activePlanCode ? 'default' : 'outline'
|
||||
}
|
||||
onClick={() => setChangeConfirmOpen(plan)}
|
||||
>
|
||||
{loading === plan.plan_code ? (
|
||||
<Spinner />
|
||||
) : plan.plan_code === activePlanCode ? (
|
||||
'Active'
|
||||
) : isUpgrade(plan) ? (
|
||||
'Upgrade'
|
||||
) : (
|
||||
'Downgrade'
|
||||
)}
|
||||
</Button>
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
{active?.note && <p className="mt-4">{active?.note}</p>}
|
||||
<p className="text-sm text-gray-500 mt-4">
|
||||
* subscription fee billed upfront {showAnnual ? 'yearly' : 'monthly'},
|
||||
overages billed at the end of each month for usage in that month
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,218 +0,0 @@
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import AnsiToHtml from 'ansi-to-html';
|
||||
import DOMPurify from 'dompurify';
|
||||
|
||||
const convert = new AnsiToHtml({
|
||||
newline: true,
|
||||
bg: 'transparent',
|
||||
});
|
||||
|
||||
export interface ExtendedLogLine {
|
||||
badge?: React.ReactNode;
|
||||
/** @format date-time */
|
||||
timestamp?: string;
|
||||
instance?: string;
|
||||
line: string;
|
||||
}
|
||||
|
||||
type LogProps = {
|
||||
logs: ExtendedLogLine[];
|
||||
onTopReached: () => void;
|
||||
onBottomReached: () => void;
|
||||
autoScroll?: boolean;
|
||||
};
|
||||
|
||||
const options: Intl.DateTimeFormatOptions = {
|
||||
year: 'numeric',
|
||||
month: 'numeric',
|
||||
day: 'numeric',
|
||||
hour: 'numeric',
|
||||
minute: 'numeric',
|
||||
second: 'numeric',
|
||||
};
|
||||
|
||||
const LoggingComponent: React.FC<LogProps> = ({
|
||||
logs,
|
||||
onTopReached,
|
||||
onBottomReached,
|
||||
autoScroll = true,
|
||||
}) => {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
const [lastTopCall, setLastTopCall] = useState<number>(0);
|
||||
const [lastBottomCall, setLastBottomCall] = useState<number>(0);
|
||||
const [firstMount, setFirstMount] = useState<boolean>(true);
|
||||
const previousScrollHeightRef = useRef<number>(0);
|
||||
|
||||
const handleScroll = () => {
|
||||
if (!containerRef.current) {
|
||||
return;
|
||||
}
|
||||
const { scrollTop, scrollHeight, clientHeight } = containerRef.current;
|
||||
previousScrollHeightRef.current = scrollHeight;
|
||||
const now = Date.now();
|
||||
|
||||
if (scrollTop === 0 && now - lastTopCall >= 1000) {
|
||||
if (logs.length > 0) {
|
||||
onTopReached();
|
||||
}
|
||||
setLastTopCall(now);
|
||||
} else if (
|
||||
scrollTop + clientHeight >= scrollHeight &&
|
||||
now - lastBottomCall >= 1000
|
||||
) {
|
||||
if (logs.length > 0) {
|
||||
onBottomReached();
|
||||
}
|
||||
setLastBottomCall(now);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
const container = containerRef.current;
|
||||
|
||||
if (container && container.scrollHeight > container.clientHeight) {
|
||||
if (firstMount && autoScroll) {
|
||||
container.scrollTo({
|
||||
top: container.scrollHeight,
|
||||
behavior: 'smooth',
|
||||
});
|
||||
|
||||
setFirstMount(false);
|
||||
}
|
||||
}
|
||||
}, 250);
|
||||
}, [containerRef, firstMount, autoScroll]);
|
||||
|
||||
useEffect(() => {
|
||||
if (refreshing) {
|
||||
const timer = setTimeout(() => {
|
||||
setRefreshing(false);
|
||||
}, 1000);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [refreshing]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!autoScroll) {
|
||||
return;
|
||||
}
|
||||
|
||||
const container = containerRef.current;
|
||||
if (!container) {
|
||||
return;
|
||||
}
|
||||
|
||||
const previousScrollHeight = previousScrollHeightRef.current;
|
||||
const currentScrollHeight = container.scrollHeight;
|
||||
const { scrollTop, clientHeight } = container;
|
||||
|
||||
const isAtBottom = scrollTop + clientHeight >= previousScrollHeight;
|
||||
|
||||
if (!isAtBottom) {
|
||||
const newScrollTop =
|
||||
scrollTop + (currentScrollHeight - previousScrollHeight);
|
||||
container.scrollTo({ top: newScrollTop });
|
||||
} else {
|
||||
container.scrollTo({ top: currentScrollHeight, behavior: 'smooth' });
|
||||
}
|
||||
}, [logs, autoScroll]);
|
||||
|
||||
const showLogs =
|
||||
logs.length > 0
|
||||
? logs
|
||||
: [
|
||||
{
|
||||
line: 'Waiting for logs...',
|
||||
timestamp: new Date().toISOString(),
|
||||
instance: 'Hatchet',
|
||||
},
|
||||
];
|
||||
|
||||
const sortedLogs = [...showLogs].sort((a, b) => {
|
||||
if (!a.timestamp || !b.timestamp) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime();
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
className="w-full mx-auto overflow-y-auto p-6 text-indigo-300 font-mono text-xs rounded-md max-h-[25rem] min-h-[25rem] bg-muted scrollbar-thin scrollbar-track-muted scrollbar-thumb-muted-foreground"
|
||||
ref={containerRef}
|
||||
onScroll={handleScroll}
|
||||
>
|
||||
{refreshing && (
|
||||
<div className="absolute top-0 left-0 right-0 bg-gray-800 text-white p-2 text-center">
|
||||
Refreshing...
|
||||
</div>
|
||||
)}
|
||||
{sortedLogs.map((log, i) => {
|
||||
const sanitizedHtml = DOMPurify.sanitize(
|
||||
convert.toHtml(log.line || ''),
|
||||
{
|
||||
USE_PROFILES: { html: true },
|
||||
},
|
||||
);
|
||||
|
||||
const logHash = log.timestamp + generateHash(log.line);
|
||||
|
||||
return (
|
||||
<p
|
||||
key={logHash}
|
||||
className="pb-2 break-all overflow-x-hidden whitespace-pre-wrap"
|
||||
id={'log' + i}
|
||||
>
|
||||
{log.badge}
|
||||
{log.timestamp && (
|
||||
<span className="text-gray-500 mr-2 ml--2">
|
||||
{new Date(log.timestamp)
|
||||
.toLocaleString('sv', options)
|
||||
.replace(',', '.')
|
||||
.replace(' ', 'T')}
|
||||
</span>
|
||||
)}
|
||||
{log.instance && (
|
||||
<span className="text-foreground dark:text-white mr-2 ml--2">
|
||||
{log.instance}
|
||||
</span>
|
||||
)}
|
||||
<span
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: sanitizedHtml,
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const generateHash = (input: string | undefined): string => {
|
||||
if (!input) {
|
||||
return Math.random().toString(36).substring(2, 15);
|
||||
}
|
||||
const trimmedInput = input.substring(0, 50);
|
||||
return cyrb53(trimmedInput) + '';
|
||||
};
|
||||
|
||||
// source: https://github.com/bryc/code/blob/master/jshash/experimental/cyrb53.js
|
||||
const cyrb53 = function (str: string, seed = 0) {
|
||||
let h1 = 0xdeadbeef ^ seed,
|
||||
h2 = 0x41c6ce57 ^ seed;
|
||||
for (let i = 0, ch; i < str.length; i++) {
|
||||
ch = str.charCodeAt(i);
|
||||
h1 = Math.imul(h1 ^ ch, 2654435761);
|
||||
h2 = Math.imul(h2 ^ ch, 1597334677);
|
||||
}
|
||||
h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507);
|
||||
h1 ^= Math.imul(h2 ^ (h2 >>> 13), 3266489909);
|
||||
h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507);
|
||||
h2 ^= Math.imul(h1 ^ (h1 >>> 13), 3266489909);
|
||||
return 4294967296 * (2097151 & h2) + (h1 >>> 0);
|
||||
};
|
||||
|
||||
export default LoggingComponent;
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import type { ToastActionElement, ToastProps } from '@/components/ui/toast';
|
||||
import type { ToastActionElement, ToastProps } from '@/components/v1/ui/toast';
|
||||
|
||||
const TOAST_LIMIT = 1;
|
||||
const TOAST_REMOVE_DELAY = 1000000;
|
||||
@@ -69,7 +69,7 @@ const addToRemoveQueue = (toastId: string) => {
|
||||
toastTimeouts.set(toastId, timeout);
|
||||
};
|
||||
|
||||
export const reducer = (state: State, action: Action): State => {
|
||||
const reducer = (state: State, action: Action): State => {
|
||||
switch (action.type) {
|
||||
case 'ADD_TOAST':
|
||||
return {
|
||||
@@ -188,4 +188,4 @@ function useToast() {
|
||||
};
|
||||
}
|
||||
|
||||
export { useToast, toast };
|
||||
export { useToast };
|
||||
|
||||
@@ -1,360 +0,0 @@
|
||||
import React, { useMemo, useCallback } from 'react';
|
||||
import { Group } from '@visx/group';
|
||||
import { AreaClosed, Line, Bar } from '@visx/shape';
|
||||
import {
|
||||
withTooltip,
|
||||
TooltipWithBounds,
|
||||
Tooltip,
|
||||
defaultStyles,
|
||||
} from '@visx/tooltip';
|
||||
import { GridRows, GridColumns } from '@visx/grid';
|
||||
import { WithTooltipProvidedProps } from '@visx/tooltip/lib/enhancers/withTooltip';
|
||||
import { scaleTime, scaleLinear } from '@visx/scale';
|
||||
import { AxisLeft, AxisBottom } from '@visx/axis';
|
||||
import { LinearGradient } from '@visx/gradient';
|
||||
import { curveMonotoneX } from '@visx/curve';
|
||||
import { localPoint } from '@visx/event';
|
||||
import { max, extent, bisector } from '@visx/vendor/d3-array';
|
||||
import { timeFormat } from '@visx/vendor/d3-time-format';
|
||||
import { Text } from '@visx/text';
|
||||
|
||||
const getDate = (d: MetricValue) => d.date;
|
||||
const getValue = (d: MetricValue) => d.value;
|
||||
|
||||
// format to 2 decimal places
|
||||
export const format2Dec = (d: number) => {
|
||||
if (!d.toFixed) {
|
||||
return '0.00';
|
||||
}
|
||||
|
||||
return `${d.toFixed(2)}`;
|
||||
};
|
||||
|
||||
const bisectDate = bisector<MetricValue, Date>((d) => d.date).left;
|
||||
|
||||
export interface MetricValue {
|
||||
date: Date;
|
||||
value: number;
|
||||
}
|
||||
type TooltipData = MetricValue;
|
||||
|
||||
const formatDate = timeFormat('%y-%m-%d %I:%M:%S');
|
||||
|
||||
const accentColor = '#ffffff44';
|
||||
const background = '#1E293B';
|
||||
const background2 = '#8c77e0';
|
||||
const accentColorDark = '#8c77e0';
|
||||
|
||||
const tooltipStyles = {
|
||||
...defaultStyles,
|
||||
border: '1px solid white',
|
||||
color: 'white',
|
||||
background,
|
||||
};
|
||||
|
||||
const axisColor = '#cecece';
|
||||
|
||||
const axisBottomTickLabelProps = {
|
||||
textAnchor: 'middle' as const,
|
||||
fontFamily: 'Arial',
|
||||
fontSize: 10,
|
||||
fill: axisColor,
|
||||
};
|
||||
|
||||
const axisLeftTickLabelProps = {
|
||||
dx: '-0.25em',
|
||||
dy: '0.25em',
|
||||
fontFamily: 'Arial',
|
||||
fontSize: 10,
|
||||
textAnchor: 'end' as const,
|
||||
fill: axisColor,
|
||||
};
|
||||
|
||||
export const formatPercentTooltip = (d: number) => `${format2Dec(d)}%`;
|
||||
|
||||
type AreaChartProps = {
|
||||
data: MetricValue[];
|
||||
kind: 'area' | 'bar';
|
||||
gradientColor?: string;
|
||||
width: number;
|
||||
height: number;
|
||||
hideBottomAxis?: boolean;
|
||||
hideLeftAxis?: boolean;
|
||||
children?: React.ReactNode;
|
||||
yLabel?: string;
|
||||
xLabel?: string;
|
||||
yDomain?: [number, number];
|
||||
xDomain?: [Date, Date];
|
||||
centerText?: string;
|
||||
tooltipFormat?: (d: number) => string;
|
||||
};
|
||||
|
||||
export default withTooltip<AreaChartProps, TooltipData>(
|
||||
({
|
||||
data,
|
||||
kind,
|
||||
gradientColor = background2,
|
||||
width,
|
||||
height,
|
||||
hideBottomAxis = false,
|
||||
hideLeftAxis = false,
|
||||
children,
|
||||
yLabel,
|
||||
xLabel,
|
||||
yDomain,
|
||||
xDomain,
|
||||
centerText,
|
||||
showTooltip,
|
||||
hideTooltip,
|
||||
tooltipFormat,
|
||||
tooltipData,
|
||||
tooltipTop = 0,
|
||||
tooltipLeft = 0,
|
||||
}: AreaChartProps & WithTooltipProvidedProps<TooltipData>) => {
|
||||
if (width < 10) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const innerWidth = width;
|
||||
const innerHeight = height;
|
||||
|
||||
const dateScale = useMemo(
|
||||
() =>
|
||||
scaleTime<number>({
|
||||
range: [0, width],
|
||||
domain: xDomain || (extent(data, getDate) as [Date, Date]),
|
||||
}),
|
||||
[width, data, xDomain],
|
||||
);
|
||||
|
||||
const yScale = useMemo(
|
||||
() =>
|
||||
scaleLinear<number>({
|
||||
range: [height, 0],
|
||||
domain: yDomain || [0, 1.3 * (max(data, getValue) || 0)],
|
||||
nice: true,
|
||||
}),
|
||||
[height, data, yDomain],
|
||||
);
|
||||
|
||||
const handleTooltip = useCallback(
|
||||
(
|
||||
event:
|
||||
| React.TouchEvent<SVGRectElement>
|
||||
| React.MouseEvent<SVGRectElement>,
|
||||
) => {
|
||||
const { x } = localPoint(event) || { x: 0 };
|
||||
const x0 = dateScale.invert(x);
|
||||
const index = bisectDate(data, x0, 1);
|
||||
const d0 = data[index - 1];
|
||||
const d1 = data[index];
|
||||
let d = d0;
|
||||
if (d1 && getDate(d1)) {
|
||||
d =
|
||||
x0.valueOf() - getDate(d0).valueOf() >
|
||||
getDate(d1).valueOf() - x0.valueOf()
|
||||
? d1
|
||||
: d0;
|
||||
}
|
||||
|
||||
showTooltip({
|
||||
tooltipData: d,
|
||||
tooltipLeft: x,
|
||||
tooltipTop: yScale(getValue(d)),
|
||||
});
|
||||
},
|
||||
[showTooltip, yScale, dateScale, data],
|
||||
);
|
||||
|
||||
let barWidth = innerWidth / data.length;
|
||||
|
||||
if (barWidth <= 5) {
|
||||
barWidth = 6;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<svg width={width} height={height} overflow={'visible'}>
|
||||
{centerText && (
|
||||
<Text className="fill-foreground" x="50%" y="50%" dx={-200}>
|
||||
{centerText}
|
||||
</Text>
|
||||
)}
|
||||
<rect
|
||||
x={0}
|
||||
y={0}
|
||||
width={innerWidth}
|
||||
height={innerHeight}
|
||||
fill="url(#area-background-gradient)"
|
||||
rx={14}
|
||||
/>
|
||||
<GridRows
|
||||
left={0}
|
||||
scale={yScale}
|
||||
width={innerWidth}
|
||||
height={innerHeight}
|
||||
strokeDasharray="1,3"
|
||||
stroke={accentColor}
|
||||
strokeOpacity={0.6}
|
||||
pointerEvents="none"
|
||||
/>
|
||||
<GridColumns
|
||||
top={0}
|
||||
left={0}
|
||||
scale={dateScale}
|
||||
width={innerWidth}
|
||||
height={innerHeight}
|
||||
strokeDasharray="1,3"
|
||||
stroke={accentColor}
|
||||
strokeOpacity={0.6}
|
||||
pointerEvents="none"
|
||||
/>
|
||||
<Group height={height} width={width}>
|
||||
<LinearGradient
|
||||
id="gradient"
|
||||
from={gradientColor}
|
||||
fromOpacity={1}
|
||||
to={gradientColor}
|
||||
toOpacity={0.2}
|
||||
height={innerHeight}
|
||||
/>
|
||||
{kind == 'bar' &&
|
||||
data.map((d, i) => {
|
||||
if (i == 0) {
|
||||
return (
|
||||
<Bar
|
||||
key={i}
|
||||
x={dateScale(getDate(d)) || 0}
|
||||
y={yScale(getValue(d)) || 0}
|
||||
width={(barWidth - 4) / 2}
|
||||
height={innerHeight - yScale(getValue(d)) || 0}
|
||||
fill="url(#gradient)"
|
||||
rx={2}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Bar
|
||||
key={i}
|
||||
x={(dateScale(getDate(d)) || 0) - barWidth / 2}
|
||||
y={yScale(getValue(d)) || 0}
|
||||
width={barWidth - 4}
|
||||
height={innerHeight - yScale(getValue(d)) || 0}
|
||||
fill="url(#gradient)"
|
||||
rx={2}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{kind == 'area' && (
|
||||
<AreaClosed<MetricValue>
|
||||
data={data}
|
||||
x={(d) => dateScale(d.date) || 0}
|
||||
y={(d) => yScale(d.value) || 0}
|
||||
yScale={yScale}
|
||||
strokeWidth={1}
|
||||
stroke="url(#gradient)"
|
||||
fill="url(#gradient)"
|
||||
curve={curveMonotoneX}
|
||||
height={innerHeight}
|
||||
/>
|
||||
)}
|
||||
{!hideBottomAxis && (
|
||||
<AxisBottom
|
||||
top={height}
|
||||
scale={dateScale}
|
||||
numTicks={width > 520 ? 10 : 5}
|
||||
stroke={axisColor}
|
||||
tickStroke={axisColor}
|
||||
tickLabelProps={axisBottomTickLabelProps}
|
||||
label={xLabel}
|
||||
/>
|
||||
)}
|
||||
{!hideLeftAxis && (
|
||||
<AxisLeft
|
||||
scale={yScale}
|
||||
numTicks={5}
|
||||
stroke={axisColor}
|
||||
tickStroke={axisColor}
|
||||
tickLabelProps={axisLeftTickLabelProps}
|
||||
label={yLabel}
|
||||
labelClassName="text-white fill-foreground"
|
||||
/>
|
||||
)}
|
||||
{children}
|
||||
</Group>
|
||||
<Bar
|
||||
x={0}
|
||||
y={0}
|
||||
width={innerWidth}
|
||||
height={innerHeight}
|
||||
fill="transparent"
|
||||
rx={14}
|
||||
onTouchStart={handleTooltip}
|
||||
onTouchMove={handleTooltip}
|
||||
onMouseMove={handleTooltip}
|
||||
onMouseLeave={() => hideTooltip()}
|
||||
/>
|
||||
{data.length > 0 && tooltipData && (
|
||||
<g>
|
||||
<Line
|
||||
from={{ x: tooltipLeft, y: 0 }}
|
||||
to={{ x: tooltipLeft, y: innerHeight + 0 }}
|
||||
stroke={accentColorDark}
|
||||
strokeWidth={2}
|
||||
pointerEvents="none"
|
||||
strokeDasharray="5,2"
|
||||
/>
|
||||
<circle
|
||||
cx={tooltipLeft}
|
||||
cy={tooltipTop + 1}
|
||||
r={4}
|
||||
fill="black"
|
||||
fillOpacity={0.1}
|
||||
stroke="black"
|
||||
strokeOpacity={0.1}
|
||||
strokeWidth={2}
|
||||
pointerEvents="none"
|
||||
/>
|
||||
<circle
|
||||
cx={tooltipLeft}
|
||||
cy={tooltipTop}
|
||||
r={4}
|
||||
fill={accentColorDark}
|
||||
stroke="white"
|
||||
strokeWidth={2}
|
||||
pointerEvents="none"
|
||||
/>
|
||||
</g>
|
||||
)}
|
||||
</svg>
|
||||
{data.length > 0 && tooltipData && (
|
||||
<div>
|
||||
<TooltipWithBounds
|
||||
key={Math.random()}
|
||||
top={tooltipTop - 24}
|
||||
left={tooltipLeft}
|
||||
style={tooltipStyles}
|
||||
>
|
||||
{tooltipFormat
|
||||
? tooltipFormat(getValue(tooltipData))
|
||||
: getValue(tooltipData)}
|
||||
</TooltipWithBounds>
|
||||
<Tooltip
|
||||
top={innerHeight - 14}
|
||||
left={tooltipLeft}
|
||||
style={{
|
||||
...defaultStyles,
|
||||
minWidth: 72,
|
||||
textAlign: 'center',
|
||||
transform: 'translateX(-50%)',
|
||||
}}
|
||||
>
|
||||
{formatDate(getDate(tooltipData))}
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -1,454 +0,0 @@
|
||||
import { useState, useMemo, useRef } from 'react';
|
||||
import {
|
||||
CartesianGrid,
|
||||
XAxis,
|
||||
YAxis,
|
||||
ReferenceArea,
|
||||
ResponsiveContainer,
|
||||
Bar,
|
||||
BarChart,
|
||||
LineChart,
|
||||
Line,
|
||||
Area,
|
||||
AreaChart,
|
||||
} from 'recharts';
|
||||
import {
|
||||
ChartConfig,
|
||||
ChartContainer,
|
||||
ChartTooltip,
|
||||
ChartTooltipContent,
|
||||
} from '@/components/ui/chart';
|
||||
import { capitalize, cn } from '@/lib/utils';
|
||||
|
||||
export type DataPoint<T extends string> = Record<T, number> & {
|
||||
date: string;
|
||||
};
|
||||
|
||||
const getNextActiveLabel = (activeLabel: string, data: DataPoint<string>[]) => {
|
||||
const currentIndex = data.findIndex((d) => d.date === activeLabel);
|
||||
if (currentIndex === -1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// if we're at the end of the data, determine the time between the last two data points and add that to the last date
|
||||
if (currentIndex === data.length - 1) {
|
||||
const lastDate = new Date(data[currentIndex].date);
|
||||
const secondLastDate = new Date(data[currentIndex - 1].date);
|
||||
const diff = lastDate.getTime() - secondLastDate.getTime();
|
||||
return new Date(lastDate.getTime() + diff).toISOString();
|
||||
}
|
||||
|
||||
return data[currentIndex + 1]?.date || activeLabel;
|
||||
};
|
||||
|
||||
const getPrevActiveLabel = (activeLabel: string, data: DataPoint<string>[]) => {
|
||||
const currentIndex = data.findIndex((d) => d.date === activeLabel);
|
||||
if (currentIndex === -1) {
|
||||
return activeLabel;
|
||||
}
|
||||
|
||||
// if we're at the start of the data, determine the time between the first two data points and subtract that from the first date
|
||||
if (currentIndex === 0) {
|
||||
const firstDate = new Date(data[currentIndex].date);
|
||||
const secondDate = new Date(data[currentIndex + 1].date);
|
||||
const diff = secondDate.getTime() - firstDate.getTime();
|
||||
return new Date(firstDate.getTime() - diff).toISOString();
|
||||
}
|
||||
|
||||
return data[currentIndex - 1]?.date || activeLabel;
|
||||
};
|
||||
|
||||
type ZoomableChartProps<T extends string> = {
|
||||
data: DataPoint<T>[];
|
||||
colors?: Record<string, string>;
|
||||
zoom?: (startTime: string, endTime: string) => void;
|
||||
showYAxis?: boolean;
|
||||
kind: 'bar' | 'line' | 'area';
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export function ZoomableChart<T extends string>({
|
||||
data,
|
||||
colors,
|
||||
zoom,
|
||||
showYAxis = true,
|
||||
kind = 'bar',
|
||||
className,
|
||||
}: ZoomableChartProps<T>) {
|
||||
const [refAreaLeft, setRefAreaLeft] = useState<string | null>(null);
|
||||
const [refAreaRight, setRefAreaRight] = useState<string | null>(null);
|
||||
const [actualRefAreaLeft, setActualRefAreaLeft] = useState<string | null>(
|
||||
null,
|
||||
);
|
||||
const [actualRefAreaRight, setActualRefAreaRight] = useState<string | null>(
|
||||
null,
|
||||
);
|
||||
const [isSelecting, setIsSelecting] = useState(false);
|
||||
const chartRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const chartConfig = useMemo<ChartConfig>(() => {
|
||||
const keys = Object.keys(data[0] || {}).filter((key) => key !== 'date');
|
||||
return keys.reduce<ChartConfig>((acc, key, index) => {
|
||||
let color = `hsl(${(index * 360) / keys.length}, 70%, 50%)`;
|
||||
|
||||
if (colors && colors[key]) {
|
||||
color = colors[key];
|
||||
}
|
||||
|
||||
if (index < 5) {
|
||||
color = `hsl(var(--chart-${index + 1}))`;
|
||||
}
|
||||
|
||||
acc[key] = {
|
||||
label: capitalize(key),
|
||||
color: colors?.[key] || color,
|
||||
};
|
||||
return acc;
|
||||
}, {});
|
||||
}, [data, colors]);
|
||||
|
||||
const handleMouseDown = (e: any) => {
|
||||
if (e.activeLabel) {
|
||||
setRefAreaLeft(e.activeLabel);
|
||||
setActualRefAreaLeft(getPrevActiveLabel(e.activeLabel, data));
|
||||
setIsSelecting(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleMouseMove = (e: any) => {
|
||||
if (isSelecting && e.activeLabel) {
|
||||
setRefAreaRight(e.activeLabel);
|
||||
setActualRefAreaRight(getNextActiveLabel(e.activeLabel, data));
|
||||
}
|
||||
};
|
||||
|
||||
const handleMouseUp = () => {
|
||||
if (actualRefAreaLeft && actualRefAreaRight) {
|
||||
const [left, right] = [actualRefAreaLeft, actualRefAreaRight].sort();
|
||||
zoom?.(left, right);
|
||||
}
|
||||
setRefAreaLeft(null);
|
||||
setActualRefAreaLeft(null);
|
||||
setRefAreaRight(null);
|
||||
setActualRefAreaRight(null);
|
||||
setIsSelecting(false);
|
||||
};
|
||||
|
||||
const minDate = new Date(
|
||||
Math.min(...data.map((d) => new Date(d.date).getTime())),
|
||||
);
|
||||
const maxDate = new Date(
|
||||
Math.max(...data.map((d) => new Date(d.date).getTime())),
|
||||
);
|
||||
|
||||
const formatXAxis = (tickItem: string) => {
|
||||
const date = new Date(tickItem);
|
||||
const timeDiff = maxDate.getTime() - minDate.getTime();
|
||||
const oneDay = 24 * 60 * 60 * 1000;
|
||||
const sevenDays = 7 * oneDay;
|
||||
|
||||
if (timeDiff > sevenDays) {
|
||||
return date.toLocaleDateString([], { month: 'short', day: 'numeric' });
|
||||
} else if (timeDiff > oneDay) {
|
||||
return `${date.toLocaleDateString([], { month: 'short', day: 'numeric' })} ${date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}`;
|
||||
} else {
|
||||
return date.toLocaleTimeString([], {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// remove date from dataKeys
|
||||
const dataKeys = Object.keys(data[0] || {}).filter((key) => key !== 'date');
|
||||
|
||||
return (
|
||||
<ChartContainer
|
||||
config={chartConfig}
|
||||
className={cn('w-full h-[200px] min-h-[200px]', className)}
|
||||
>
|
||||
<div className="h-full" ref={chartRef} style={{ touchAction: 'none' }}>
|
||||
{getChildChart(kind, {
|
||||
data,
|
||||
showYAxis,
|
||||
formatXAxis,
|
||||
handleMouseDown,
|
||||
handleMouseMove,
|
||||
handleMouseUp,
|
||||
refAreaLeft,
|
||||
refAreaRight,
|
||||
chartConfig,
|
||||
dataKeys,
|
||||
})}
|
||||
</div>
|
||||
</ChartContainer>
|
||||
);
|
||||
}
|
||||
|
||||
function getChildChart<T extends string>(
|
||||
kind: 'bar' | 'line' | 'area',
|
||||
props: ChildChartProps<T>,
|
||||
) {
|
||||
switch (kind) {
|
||||
case 'bar':
|
||||
return <ChildBarChart {...props} />;
|
||||
case 'line':
|
||||
return <ChildLineChart {...props} />;
|
||||
case 'area':
|
||||
return <ChildAreaChart {...props} />;
|
||||
}
|
||||
}
|
||||
|
||||
type ChildChartProps<T extends string> = {
|
||||
data: DataPoint<T>[];
|
||||
showYAxis?: boolean;
|
||||
formatXAxis: (tickItem: string) => string;
|
||||
handleMouseDown: (e: any) => void;
|
||||
handleMouseMove: (e: any) => void;
|
||||
handleMouseUp: () => void;
|
||||
refAreaLeft: string | null;
|
||||
refAreaRight: string | null;
|
||||
chartConfig: ChartConfig;
|
||||
dataKeys: string[];
|
||||
};
|
||||
|
||||
function ChildBarChart<T extends string>({
|
||||
data,
|
||||
showYAxis = true,
|
||||
formatXAxis,
|
||||
handleMouseDown,
|
||||
handleMouseMove,
|
||||
handleMouseUp,
|
||||
refAreaLeft,
|
||||
refAreaRight,
|
||||
chartConfig,
|
||||
dataKeys,
|
||||
}: ChildChartProps<T>) {
|
||||
return (
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<BarChart
|
||||
data={data}
|
||||
margin={{
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
}}
|
||||
onMouseDown={handleMouseDown}
|
||||
onMouseMove={handleMouseMove}
|
||||
onMouseUp={handleMouseUp}
|
||||
>
|
||||
<CartesianGrid vertical={false} />
|
||||
<XAxis
|
||||
dataKey="date"
|
||||
tickFormatter={formatXAxis}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
tickMargin={4}
|
||||
minTickGap={16}
|
||||
style={{ fontSize: '10px', userSelect: 'none' }}
|
||||
/>
|
||||
{showYAxis && (
|
||||
<YAxis
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
tickMargin={4}
|
||||
style={{ fontSize: '10px', userSelect: 'none' }}
|
||||
/>
|
||||
)}
|
||||
<ChartTooltip
|
||||
content={
|
||||
<ChartTooltipContent
|
||||
className="w-[150px] sm:w-[200px] font-mono text-xs sm:text-xs"
|
||||
labelFormatter={(value) => new Date(value).toLocaleString()}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
{dataKeys.map((key) => (
|
||||
<Bar
|
||||
key={key}
|
||||
type="monotone"
|
||||
dataKey={key}
|
||||
stroke={chartConfig[key].color}
|
||||
fillOpacity={1}
|
||||
fill={chartConfig[key].color}
|
||||
isAnimationActive={false}
|
||||
/>
|
||||
))}
|
||||
|
||||
{refAreaLeft && refAreaRight && (
|
||||
<ReferenceArea
|
||||
x1={refAreaLeft}
|
||||
x2={refAreaRight}
|
||||
strokeOpacity={0.3}
|
||||
fill="hsl(var(--foreground))"
|
||||
fillOpacity={0.1}
|
||||
/>
|
||||
)}
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
);
|
||||
}
|
||||
|
||||
function ChildLineChart<T extends string>({
|
||||
data,
|
||||
showYAxis = true,
|
||||
formatXAxis,
|
||||
handleMouseDown,
|
||||
handleMouseMove,
|
||||
handleMouseUp,
|
||||
refAreaLeft,
|
||||
refAreaRight,
|
||||
chartConfig,
|
||||
dataKeys,
|
||||
}: ChildChartProps<T>) {
|
||||
return (
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<LineChart
|
||||
data={data}
|
||||
margin={{
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
}}
|
||||
onMouseDown={handleMouseDown}
|
||||
onMouseMove={handleMouseMove}
|
||||
onMouseUp={handleMouseUp}
|
||||
>
|
||||
<CartesianGrid vertical={false} />
|
||||
<XAxis
|
||||
dataKey="date"
|
||||
tickFormatter={formatXAxis}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
tickMargin={4}
|
||||
minTickGap={16}
|
||||
style={{ fontSize: '10px', userSelect: 'none' }}
|
||||
/>
|
||||
{showYAxis && (
|
||||
<YAxis
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
tickMargin={4}
|
||||
style={{ fontSize: '10px', userSelect: 'none' }}
|
||||
/>
|
||||
)}
|
||||
<ChartTooltip
|
||||
content={
|
||||
<ChartTooltipContent
|
||||
className="w-[150px] sm:w-[200px] font-mono text-xs sm:text-xs"
|
||||
labelFormatter={(value) => new Date(value).toLocaleString()}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
{dataKeys.map((key) => {
|
||||
return (
|
||||
<Line
|
||||
key={key}
|
||||
type="monotone"
|
||||
dot={false}
|
||||
dataKey={key}
|
||||
stroke={chartConfig[key].color}
|
||||
fillOpacity={1}
|
||||
fill={chartConfig[key].color}
|
||||
isAnimationActive={false}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
{refAreaLeft && refAreaRight && (
|
||||
<ReferenceArea
|
||||
x1={refAreaLeft}
|
||||
x2={refAreaRight}
|
||||
strokeOpacity={0.3}
|
||||
fill="hsl(var(--foreground))"
|
||||
fillOpacity={0.1}
|
||||
/>
|
||||
)}
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
);
|
||||
}
|
||||
|
||||
function ChildAreaChart<T extends string>({
|
||||
data,
|
||||
showYAxis = true,
|
||||
formatXAxis,
|
||||
handleMouseDown,
|
||||
handleMouseMove,
|
||||
handleMouseUp,
|
||||
refAreaLeft,
|
||||
refAreaRight,
|
||||
chartConfig,
|
||||
dataKeys,
|
||||
}: ChildChartProps<T>) {
|
||||
return (
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<AreaChart
|
||||
data={data}
|
||||
margin={{
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
}}
|
||||
onMouseDown={handleMouseDown}
|
||||
onMouseMove={handleMouseMove}
|
||||
onMouseUp={handleMouseUp}
|
||||
>
|
||||
<CartesianGrid vertical={false} />
|
||||
<XAxis
|
||||
dataKey="date"
|
||||
tickFormatter={formatXAxis}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
tickMargin={4}
|
||||
minTickGap={16}
|
||||
style={{ fontSize: '10px', userSelect: 'none' }}
|
||||
/>
|
||||
{showYAxis && (
|
||||
<YAxis
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
tickMargin={4}
|
||||
style={{ fontSize: '10px', userSelect: 'none' }}
|
||||
/>
|
||||
)}
|
||||
<ChartTooltip
|
||||
content={
|
||||
<ChartTooltipContent
|
||||
className="w-[150px] sm:w-[200px] font-mono text-xs sm:text-xs"
|
||||
labelFormatter={(value) => new Date(value).toLocaleString()}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
{dataKeys.map((key) => {
|
||||
return (
|
||||
<Area
|
||||
key={key}
|
||||
type="monotone"
|
||||
dot={false}
|
||||
dataKey={key}
|
||||
stroke={chartConfig[key].color}
|
||||
fillOpacity={0.6}
|
||||
fill={chartConfig[key].color}
|
||||
isAnimationActive={false}
|
||||
stackId="a"
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
{refAreaLeft && refAreaRight && (
|
||||
<ReferenceArea
|
||||
x1={refAreaLeft}
|
||||
x2={refAreaRight}
|
||||
strokeOpacity={0.3}
|
||||
fill="hsl(var(--foreground))"
|
||||
fillOpacity={0.1}
|
||||
/>
|
||||
)}
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
);
|
||||
}
|
||||
@@ -1,296 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import { CheckIcon, CircleIcon, PlusCircledIcon } from '@radix-ui/react-icons';
|
||||
import { Column } from '@tanstack/react-table';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
CommandSeparator,
|
||||
} from '@/components/ui/command';
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from '@/components/ui/popover';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { ToolbarType } from '../data-table/data-table-toolbar';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { BiX } from 'react-icons/bi';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
|
||||
interface DataTableFacetedFilterProps<TData, TValue> {
|
||||
column?: Column<TData, TValue>;
|
||||
title?: string;
|
||||
type?: ToolbarType;
|
||||
options?: {
|
||||
label: string;
|
||||
value: string;
|
||||
icon?: React.ComponentType<{ className?: string }>;
|
||||
}[];
|
||||
}
|
||||
|
||||
const keyValuePairSchema = z.object({
|
||||
key: z.string().min(1, 'Key is required'),
|
||||
value: z.string().min(1, 'Value is required'),
|
||||
});
|
||||
|
||||
const arrayInputSchema = z.object({
|
||||
values: z.string().min(1, 'At least one value is required'),
|
||||
});
|
||||
|
||||
type KeyValuePair = z.infer<typeof keyValuePairSchema>;
|
||||
type ArrayInput = z.infer<typeof arrayInputSchema>;
|
||||
|
||||
export function DataTableFacetedFilter<TData, TValue>({
|
||||
column,
|
||||
title,
|
||||
type = ToolbarType.Checkbox,
|
||||
options,
|
||||
}: DataTableFacetedFilterProps<TData, TValue>) {
|
||||
return (
|
||||
<Combobox
|
||||
values={column?.getFilterValue() as string[]}
|
||||
title={title}
|
||||
type={type}
|
||||
options={options}
|
||||
setValues={(values) => column?.setFilterValue(values)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function Combobox({
|
||||
values = [],
|
||||
title,
|
||||
icon,
|
||||
type = ToolbarType.Checkbox,
|
||||
options,
|
||||
setValues,
|
||||
}: {
|
||||
values?: string[];
|
||||
icon?: JSX.Element;
|
||||
title?: string;
|
||||
type?: ToolbarType;
|
||||
options?: {
|
||||
label: string;
|
||||
value: string;
|
||||
icon?: React.ComponentType<{ className?: string }>;
|
||||
}[];
|
||||
setValues: (selectedValues: string[]) => void;
|
||||
}) {
|
||||
const { register, handleSubmit, reset } = useForm<KeyValuePair | ArrayInput>({
|
||||
resolver: zodResolver(
|
||||
type === ToolbarType.KeyValue ? keyValuePairSchema : arrayInputSchema,
|
||||
),
|
||||
defaultValues:
|
||||
type === ToolbarType.KeyValue ? { key: '', value: '' } : { values: '' },
|
||||
});
|
||||
|
||||
const submit = (data: KeyValuePair | ArrayInput) => {
|
||||
if ('key' in data) {
|
||||
values.push(`${data.key}:${data.value}`);
|
||||
} else {
|
||||
data.values.split(',').forEach((value) => values.push(value.trim()));
|
||||
}
|
||||
setValues(values);
|
||||
reset();
|
||||
};
|
||||
|
||||
const remove = (filter: string) => {
|
||||
const index = values.indexOf(filter);
|
||||
if (index > -1) {
|
||||
values.splice(index, 1);
|
||||
}
|
||||
setValues(values);
|
||||
};
|
||||
|
||||
return (
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant="outline" size="sm" className="h-8 border-dashed">
|
||||
{icon || <PlusCircledIcon className="mr-2 h-4 w-4" />}
|
||||
{title}
|
||||
{values.length > 0 && (
|
||||
<>
|
||||
<Separator orientation="vertical" className="mx-2 h-4" />
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="rounded-sm px-1 font-normal lg:hidden"
|
||||
>
|
||||
{type == ToolbarType.Radio
|
||||
? // get the label of the value
|
||||
options?.find(({ value }) => value == values[0])?.label ||
|
||||
values[0]
|
||||
: values.length}
|
||||
</Badge>
|
||||
<div className="hidden space-x-1 lg:flex">
|
||||
{values.length > 2 ? (
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="rounded-sm px-1 font-normal"
|
||||
>
|
||||
{values.length} selected
|
||||
</Badge>
|
||||
) : (
|
||||
values.map((option, index) => (
|
||||
<Badge
|
||||
key={index}
|
||||
variant="secondary"
|
||||
className="rounded-sm px-1 font-normal flex items-center space-x-1"
|
||||
>
|
||||
{options?.find(({ value }) => value == option)?.label ||
|
||||
option}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
className="ml-2"
|
||||
onClick={() => remove(option)}
|
||||
>
|
||||
<BiX className="h-3 w-3" />
|
||||
</Button>
|
||||
</Badge>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[300px] p-2" align="start">
|
||||
{[ToolbarType.Array, ToolbarType.KeyValue].includes(type) && (
|
||||
<div>
|
||||
<div className="">
|
||||
{values.map((filter, index) => (
|
||||
<Badge
|
||||
key={index}
|
||||
variant="secondary"
|
||||
className="mr-2 mb-2 rounded-sm px-1 font-normal flex items-center space-x-1 pl-2"
|
||||
>
|
||||
<span className="grow">{filter}</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="ml-2 shrink-0"
|
||||
onClick={() => remove(filter)}
|
||||
>
|
||||
<BiX className="h-4 w-4" />
|
||||
</Button>
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
<form onSubmit={handleSubmit(submit)}>
|
||||
{type === ToolbarType.KeyValue ? (
|
||||
<div className="flex items-center space-x-2 mb-2">
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Key"
|
||||
{...register('key')}
|
||||
className="flex-1"
|
||||
/>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Value"
|
||||
{...register('value')}
|
||||
className="flex-1"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="mb-2">
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Enter values (comma-separated)"
|
||||
{...register('values')}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<Button type="submit" className="w-full" size="sm">
|
||||
Add {title} Filter
|
||||
</Button>
|
||||
{values.length > 0 && (
|
||||
<Button
|
||||
onClick={() => setValues([])}
|
||||
className="w-full mt-2"
|
||||
size="sm"
|
||||
variant={'ghost'}
|
||||
>
|
||||
Reset
|
||||
</Button>
|
||||
)}
|
||||
</form>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{[ToolbarType.Checkbox, ToolbarType.Radio].includes(type) && (
|
||||
<Command>
|
||||
<CommandInput placeholder={title} />
|
||||
<CommandList>
|
||||
<CommandEmpty>No results found.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{options?.map((option) => {
|
||||
const isSelected = values.indexOf(option.value) != -1;
|
||||
return (
|
||||
<CommandItem
|
||||
key={option.value}
|
||||
onSelect={() => {
|
||||
if (isSelected) {
|
||||
values.splice(values.indexOf(option.value), 1);
|
||||
} else {
|
||||
if (type == 'radio') {
|
||||
values = [];
|
||||
}
|
||||
values.push(option.value);
|
||||
}
|
||||
setValues(values);
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary',
|
||||
isSelected
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: 'opacity-50 [&_svg]:invisible',
|
||||
)}
|
||||
>
|
||||
{type === 'checkbox' ? (
|
||||
<CheckIcon className={cn('h-4 w-4')} />
|
||||
) : (
|
||||
<CircleIcon className={cn('h-4 w-4')} />
|
||||
)}
|
||||
</div>
|
||||
{option.icon && (
|
||||
<option.icon className="mr-2 h-4 w-4 text-gray-700 dark:text-gray-300" />
|
||||
)}
|
||||
<span>{option.label}</span>
|
||||
</CommandItem>
|
||||
);
|
||||
})}
|
||||
</CommandGroup>
|
||||
{values.length > 0 && (
|
||||
<>
|
||||
<CommandSeparator />
|
||||
<CommandGroup>
|
||||
<CommandItem
|
||||
onSelect={() => setValues([])}
|
||||
className="justify-center text-center"
|
||||
>
|
||||
Reset
|
||||
</CommandItem>
|
||||
</CommandGroup>
|
||||
</>
|
||||
)}
|
||||
</CommandList>
|
||||
</Command>
|
||||
)}
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
import { Button, ButtonProps } from '@/components/ui/button';
|
||||
import { Spinner } from '@/components/ui/loading.tsx';
|
||||
import { Button, ButtonProps } from '@/components/v1/ui/button';
|
||||
import { Spinner } from '@/components/v1/ui/loading.tsx';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
} from '@/components/v1/ui/dialog';
|
||||
|
||||
interface ConfirmDialogProps {
|
||||
title: string;
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
import {
|
||||
ArrowDownIcon,
|
||||
ArrowUpIcon,
|
||||
CaretSortIcon,
|
||||
EyeNoneIcon,
|
||||
} from '@radix-ui/react-icons';
|
||||
import { Column } from '@tanstack/react-table';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
|
||||
interface DataTableColumnHeaderProps<TData, TValue>
|
||||
extends React.HTMLAttributes<HTMLDivElement> {
|
||||
column: Column<TData, TValue>;
|
||||
title: string;
|
||||
}
|
||||
|
||||
export function DataTableColumnHeader<TData, TValue>({
|
||||
column,
|
||||
title,
|
||||
className,
|
||||
}: DataTableColumnHeaderProps<TData, TValue>) {
|
||||
if (!column.getCanSort()) {
|
||||
return <div className={cn(className, 'text-xs')}>{title}</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn('flex items-center space-x-2', className)}>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="-ml-3 h-8 data-[state=open]:bg-accent"
|
||||
>
|
||||
<span>{title}</span>
|
||||
{column.getIsSorted() === 'desc' ? (
|
||||
<ArrowDownIcon className="ml-2 h-4 w-4" />
|
||||
) : column.getIsSorted() === 'asc' ? (
|
||||
<ArrowUpIcon className="ml-2 h-4 w-4" />
|
||||
) : (
|
||||
<CaretSortIcon className="ml-2 h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start">
|
||||
<DropdownMenuItem onClick={() => column.toggleSorting(false)}>
|
||||
<ArrowUpIcon className="mr-2 h-3.5 w-3.5 text-gray-600 dark:text-gray-400/70" />
|
||||
Asc
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => column.toggleSorting(true)}>
|
||||
<ArrowDownIcon className="mr-2 h-3.5 w-3.5 text-gray-600 dark:text-gray-400/70" />
|
||||
Desc
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={() => column.toggleVisibility(false)}>
|
||||
<EyeNoneIcon className="mr-2 h-3.5 w-3.5 text-gray-600 dark:text-gray-400/70" />
|
||||
Hide
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import { Column } from '@tanstack/react-table';
|
||||
|
||||
import { ToolbarType } from './data-table-toolbar';
|
||||
import { Combobox } from '../combobox/combobox';
|
||||
|
||||
interface DataTableFacetedFilterProps<TData, TValue> {
|
||||
column?: Column<TData, TValue>;
|
||||
title?: string;
|
||||
type?: ToolbarType;
|
||||
options?: {
|
||||
label: string;
|
||||
value: string;
|
||||
icon?: React.ComponentType<{ className?: string }>;
|
||||
}[];
|
||||
}
|
||||
|
||||
export function DataTableFacetedFilter<TData, TValue>({
|
||||
column,
|
||||
title,
|
||||
type = ToolbarType.Checkbox,
|
||||
options,
|
||||
}: DataTableFacetedFilterProps<TData, TValue>) {
|
||||
return (
|
||||
<Combobox
|
||||
values={column?.getFilterValue() as string[]}
|
||||
title={title}
|
||||
type={type}
|
||||
options={options}
|
||||
setValues={(values) => column?.setFilterValue(values)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,121 +0,0 @@
|
||||
import {
|
||||
ChevronLeftIcon,
|
||||
ChevronRightIcon,
|
||||
DoubleArrowLeftIcon,
|
||||
DoubleArrowRightIcon,
|
||||
} from '@radix-ui/react-icons';
|
||||
import { Table } from '@tanstack/react-table';
|
||||
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { Label } from '@radix-ui/react-label';
|
||||
|
||||
interface DataTablePaginationProps<TData> {
|
||||
table: Table<TData>;
|
||||
onSetPageSize?: (pageSize: number) => void;
|
||||
showSelectedRows?: boolean;
|
||||
}
|
||||
|
||||
export function DataTablePagination<TData>({
|
||||
table,
|
||||
onSetPageSize,
|
||||
showSelectedRows = true,
|
||||
}: DataTablePaginationProps<TData>) {
|
||||
const pagination = table.getState().pagination;
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between px-2">
|
||||
<div className="flex-1 text-sm text-gray-600 dark:text-gray-400">
|
||||
{showSelectedRows && (
|
||||
<div>
|
||||
{table.getFilteredSelectedRowModel().rows.length} of{' '}
|
||||
{table.getFilteredRowModel().rows.length} row(s) selected.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-6 lg:space-x-8">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Label
|
||||
className="text-sm font-medium text-gray-600 dark:text-gray-400"
|
||||
htmlFor="rows-per-page"
|
||||
id="rows-per-page-label"
|
||||
>
|
||||
Rows per page
|
||||
</Label>
|
||||
<Select
|
||||
value={`${pagination.pageSize}`}
|
||||
onValueChange={(value) => {
|
||||
table.setPageSize(Number(value));
|
||||
if (onSetPageSize) {
|
||||
onSetPageSize(Number(value));
|
||||
}
|
||||
}}
|
||||
>
|
||||
<SelectTrigger
|
||||
className="h-8 w-[70px]"
|
||||
id="rows-per-page"
|
||||
aria-labelledby="rows-per-page-label"
|
||||
>
|
||||
<SelectValue placeholder={pagination.pageSize} />
|
||||
</SelectTrigger>
|
||||
<SelectContent side="top">
|
||||
{[50, 100, 200, 500].map((pageSize) => (
|
||||
<SelectItem key={pageSize} value={`${pageSize}`}>
|
||||
{pageSize}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="flex w-[100px] items-center justify-center text-sm font-medium">
|
||||
Page {pagination.pageIndex + 1} of {table.getPageCount()}
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="hidden h-8 w-8 p-0 lg:flex"
|
||||
onClick={() => table.setPageIndex(0)}
|
||||
disabled={!table.getCanPreviousPage()}
|
||||
>
|
||||
<span className="sr-only">Go to first page</span>
|
||||
<DoubleArrowLeftIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="h-8 w-8 p-0"
|
||||
onClick={() => table.previousPage()}
|
||||
disabled={!table.getCanPreviousPage()}
|
||||
>
|
||||
<span className="sr-only">Go to previous page</span>
|
||||
<ChevronLeftIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="h-8 w-8 p-0"
|
||||
onClick={() => table.nextPage()}
|
||||
disabled={!table.getCanNextPage()}
|
||||
>
|
||||
<span className="sr-only">Go to next page</span>
|
||||
<ChevronRightIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="hidden h-8 w-8 p-0 lg:flex"
|
||||
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
|
||||
disabled={!table.getCanNextPage()}
|
||||
>
|
||||
<span className="sr-only">Go to last page</span>
|
||||
<DoubleArrowRightIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
import { DotsHorizontalIcon } from '@radix-ui/react-icons';
|
||||
import { Row } from '@tanstack/react-table';
|
||||
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
|
||||
import { IDGetter } from './data-table';
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
TooltipContent,
|
||||
} from '@/components/ui/tooltip';
|
||||
|
||||
interface DataTableRowActionsProps<TData extends IDGetter> {
|
||||
row: Row<TData>;
|
||||
actions?: {
|
||||
label: string;
|
||||
onClick: (data: TData) => void;
|
||||
disabled?: boolean | string;
|
||||
}[];
|
||||
}
|
||||
|
||||
export function DataTableRowActions<TData extends IDGetter>({
|
||||
row,
|
||||
actions,
|
||||
}: DataTableRowActionsProps<TData>) {
|
||||
if (!actions?.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="flex h-8 w-8 p-0 data-[state=open]:bg-muted"
|
||||
>
|
||||
<DotsHorizontalIcon className="h-4 w-4" />
|
||||
<span className="sr-only">Open menu</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-[160px]">
|
||||
{actions?.map((action) => (
|
||||
<TooltipProvider key={action.label}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger className="w-full">
|
||||
<DropdownMenuItem
|
||||
onClick={() => action.onClick(row.original)}
|
||||
disabled={!!action.disabled}
|
||||
className="w-full"
|
||||
>
|
||||
{action.label}
|
||||
</DropdownMenuItem>
|
||||
</TooltipTrigger>
|
||||
{action.disabled && (
|
||||
<TooltipContent>{action.disabled}</TooltipContent>
|
||||
)}
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
import { Cross2Icon } from '@radix-ui/react-icons';
|
||||
import { Table } from '@tanstack/react-table';
|
||||
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { DataTableViewOptions } from './data-table-view-options';
|
||||
|
||||
import { DataTableFacetedFilter } from './data-table-faceted-filter';
|
||||
import { Input } from '@/components/ui/input.tsx';
|
||||
import { Spinner } from '@/components/ui/loading';
|
||||
|
||||
export interface FilterOption {
|
||||
label: string;
|
||||
value: string;
|
||||
icon?: React.ComponentType<{ className?: string }>;
|
||||
}
|
||||
|
||||
export enum ToolbarType {
|
||||
Checkbox = 'checkbox',
|
||||
Radio = 'radio',
|
||||
KeyValue = 'key-value',
|
||||
Array = 'array',
|
||||
}
|
||||
|
||||
export type ToolbarFilters = {
|
||||
columnId: string;
|
||||
title: string;
|
||||
type?: ToolbarType;
|
||||
options?: FilterOption[];
|
||||
}[];
|
||||
|
||||
interface DataTableToolbarProps<TData> {
|
||||
table: Table<TData>;
|
||||
filters: ToolbarFilters;
|
||||
actions: JSX.Element[];
|
||||
setSearch?: (search: string) => void;
|
||||
search?: string;
|
||||
showColumnToggle?: boolean;
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
export function DataTableToolbar<TData>({
|
||||
table,
|
||||
filters,
|
||||
actions,
|
||||
setSearch,
|
||||
search,
|
||||
showColumnToggle,
|
||||
isLoading = false,
|
||||
}: DataTableToolbarProps<TData>) {
|
||||
const isFiltered = table.getState().columnFilters?.length > 0;
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex flex-1 items-center space-x-2">
|
||||
{setSearch && (
|
||||
<Input
|
||||
placeholder="Search..."
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
className="h-8 w-[150px] lg:w-[250px]"
|
||||
/>
|
||||
)}
|
||||
{filters.map((filter) => (
|
||||
<DataTableFacetedFilter
|
||||
key={filter.columnId}
|
||||
column={table.getColumn(filter.columnId)}
|
||||
title={filter.title}
|
||||
type={filter.type}
|
||||
options={filter.options}
|
||||
/>
|
||||
))}
|
||||
{isFiltered && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => table.resetColumnFilters()}
|
||||
className="h-8 px-2 lg:px-3"
|
||||
>
|
||||
Reset
|
||||
<Cross2Icon className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-row gap-4 items-center">
|
||||
{isLoading && <Spinner />}
|
||||
{actions && actions.length > 0 && actions}
|
||||
{showColumnToggle && <DataTableViewOptions table={table} />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
import { DropdownMenuTrigger } from '@radix-ui/react-dropdown-menu';
|
||||
import { MixerHorizontalIcon } from '@radix-ui/react-icons';
|
||||
import { Table } from '@tanstack/react-table';
|
||||
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
|
||||
interface DataTableViewOptionsProps<TData> {
|
||||
table: Table<TData>;
|
||||
}
|
||||
|
||||
export function DataTableViewOptions<TData>({
|
||||
table,
|
||||
}: DataTableViewOptionsProps<TData>) {
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="ml-auto hidden h-8 lg:flex"
|
||||
>
|
||||
<MixerHorizontalIcon className="mr-2 h-4 w-4" />
|
||||
View
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-[150px]">
|
||||
<DropdownMenuLabel>Toggle columns</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
{table
|
||||
.getAllColumns()
|
||||
.filter(
|
||||
(column) =>
|
||||
typeof column.accessorFn !== 'undefined' && column.getCanHide(),
|
||||
)
|
||||
.map((column) => {
|
||||
return (
|
||||
<DropdownMenuCheckboxItem
|
||||
key={column.id}
|
||||
className="capitalize"
|
||||
checked={column.getIsVisible()}
|
||||
onCheckedChange={(value) => column.toggleVisibility(!!value)}
|
||||
>
|
||||
{column.id}
|
||||
</DropdownMenuCheckboxItem>
|
||||
);
|
||||
})}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
@@ -1,272 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import {
|
||||
ColumnDef,
|
||||
ColumnFiltersState,
|
||||
OnChangeFn,
|
||||
PaginationState,
|
||||
Row,
|
||||
RowSelectionState,
|
||||
SortingState,
|
||||
VisibilityState,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
getFacetedRowModel,
|
||||
getFacetedUniqueValues,
|
||||
getFilteredRowModel,
|
||||
getPaginationRowModel,
|
||||
getSortedRowModel,
|
||||
useReactTable,
|
||||
} from '@tanstack/react-table';
|
||||
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@/components/ui/table';
|
||||
|
||||
import { DataTablePagination } from './data-table-pagination';
|
||||
import { DataTableToolbar, ToolbarFilters } from './data-table-toolbar';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
export interface IDGetter {
|
||||
metadata: {
|
||||
id: string;
|
||||
};
|
||||
getRow?: () => JSX.Element;
|
||||
onClick?: () => void;
|
||||
isExpandable?: boolean;
|
||||
}
|
||||
|
||||
interface DataTableProps<TData extends IDGetter, TValue> {
|
||||
columns: ColumnDef<TData, TValue>[];
|
||||
data: TData[];
|
||||
error?: Error | null;
|
||||
filters: ToolbarFilters;
|
||||
actions?: JSX.Element[];
|
||||
sorting?: SortingState;
|
||||
setSorting?: OnChangeFn<SortingState>;
|
||||
setSearch?: (search: string) => void;
|
||||
search?: string;
|
||||
columnFilters?: ColumnFiltersState;
|
||||
setColumnFilters?: OnChangeFn<ColumnFiltersState>;
|
||||
pagination?: PaginationState;
|
||||
setPagination?: OnChangeFn<PaginationState>;
|
||||
showSelectedRows?: boolean;
|
||||
pageCount?: number;
|
||||
onSetPageSize?: (pageSize: number) => void;
|
||||
showColumnToggle?: boolean;
|
||||
columnVisibility?: VisibilityState;
|
||||
setColumnVisibility?: OnChangeFn<VisibilityState>;
|
||||
rowSelection?: RowSelectionState;
|
||||
setRowSelection?: OnChangeFn<RowSelectionState>;
|
||||
isLoading?: boolean;
|
||||
enableRowSelection?: boolean;
|
||||
getRowId?:
|
||||
| ((
|
||||
originalRow: TData,
|
||||
index: number,
|
||||
parent?: Row<TData> | undefined,
|
||||
) => string)
|
||||
| undefined;
|
||||
manualSorting?: boolean;
|
||||
manualFiltering?: boolean;
|
||||
}
|
||||
|
||||
interface ExtraDataTableProps {
|
||||
emptyState?: JSX.Element;
|
||||
card?: {
|
||||
containerStyle?: string;
|
||||
component: React.FC<any> | ((data: any) => JSX.Element);
|
||||
};
|
||||
}
|
||||
|
||||
export function DataTable<TData extends IDGetter, TValue>({
|
||||
columns,
|
||||
error,
|
||||
data,
|
||||
filters,
|
||||
actions = [],
|
||||
sorting,
|
||||
setSorting,
|
||||
setSearch,
|
||||
search,
|
||||
columnFilters,
|
||||
setColumnFilters,
|
||||
pagination,
|
||||
setPagination,
|
||||
pageCount,
|
||||
onSetPageSize,
|
||||
showSelectedRows = true,
|
||||
showColumnToggle,
|
||||
columnVisibility,
|
||||
setColumnVisibility,
|
||||
rowSelection,
|
||||
setRowSelection,
|
||||
isLoading,
|
||||
getRowId,
|
||||
emptyState,
|
||||
card,
|
||||
manualSorting = true,
|
||||
manualFiltering = true,
|
||||
}: DataTableProps<TData, TValue> & ExtraDataTableProps) {
|
||||
const loadingNoData = isLoading && !data.length;
|
||||
|
||||
const tableData = React.useMemo(
|
||||
() => (loadingNoData ? Array(10).fill({ metadata: {} }) : data),
|
||||
[loadingNoData, data],
|
||||
);
|
||||
|
||||
const tableColumns = React.useMemo(
|
||||
() =>
|
||||
loadingNoData
|
||||
? columns.map((column) => ({
|
||||
...column,
|
||||
cell: () => <Skeleton className="h-4 w-[100px]" />,
|
||||
}))
|
||||
: columns,
|
||||
[loadingNoData, columns],
|
||||
);
|
||||
|
||||
const table = useReactTable({
|
||||
data: tableData,
|
||||
columns: tableColumns,
|
||||
state: {
|
||||
sorting,
|
||||
columnVisibility,
|
||||
rowSelection: rowSelection || {},
|
||||
columnFilters,
|
||||
pagination,
|
||||
},
|
||||
pageCount,
|
||||
enableRowSelection: !!rowSelection,
|
||||
onRowSelectionChange: setRowSelection,
|
||||
onSortingChange: setSorting,
|
||||
onColumnFiltersChange: setColumnFilters,
|
||||
onColumnVisibilityChange: setColumnVisibility,
|
||||
onPaginationChange: setPagination,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getFilteredRowModel: getFilteredRowModel(),
|
||||
getPaginationRowModel: getPaginationRowModel(),
|
||||
getSortedRowModel: getSortedRowModel(),
|
||||
getFacetedRowModel: getFacetedRowModel(),
|
||||
getFacetedUniqueValues: getFacetedUniqueValues(),
|
||||
manualSorting,
|
||||
manualFiltering,
|
||||
manualPagination: true,
|
||||
getRowId,
|
||||
});
|
||||
|
||||
const getTableRow = (row: Row<TData>) => {
|
||||
if (row.original.getRow) {
|
||||
return row.original.getRow();
|
||||
}
|
||||
|
||||
return (
|
||||
<TableRow
|
||||
key={row.id}
|
||||
data-state={row.getIsSelected() && 'selected'}
|
||||
className={cn(
|
||||
row.original.isExpandable && 'cursor-pointer hover:bg-muted',
|
||||
)}
|
||||
onClick={row.original.onClick}
|
||||
>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell key={cell.id}>
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
);
|
||||
};
|
||||
|
||||
const getTable = () => (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => {
|
||||
return (
|
||||
<TableHead key={header.id} colSpan={header.colSpan}>
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext(),
|
||||
)}
|
||||
</TableHead>
|
||||
);
|
||||
})}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{error ? (
|
||||
<TableRow className="p-4 text-center text-red-500">
|
||||
<TableCell colSpan={columns.length}>
|
||||
{error.message || 'An error occurred.'}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : table.getRowModel().rows?.length ? (
|
||||
table.getRowModel().rows.map((row) => getTableRow(row))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={columns.length} className="h-24 text-center">
|
||||
{emptyState || 'No results.'}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
);
|
||||
|
||||
const getCards = () => (
|
||||
<div
|
||||
className={
|
||||
card?.containerStyle ||
|
||||
'grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3'
|
||||
}
|
||||
>
|
||||
{error
|
||||
? error.message || 'An error occurred.'
|
||||
: table.getRowModel().rows?.length
|
||||
? table
|
||||
.getRowModel()
|
||||
.rows.map((row) =>
|
||||
card?.component
|
||||
? card?.component({ data: row.original })
|
||||
: null,
|
||||
)
|
||||
: emptyState || 'No results.'}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{(setSearch || actions || (filters && filters.length > 0)) && (
|
||||
<DataTableToolbar
|
||||
table={table}
|
||||
filters={filters}
|
||||
isLoading={isLoading}
|
||||
actions={actions}
|
||||
search={search}
|
||||
setSearch={setSearch}
|
||||
showColumnToggle={showColumnToggle}
|
||||
/>
|
||||
)}
|
||||
<div className={`rounded-md ${!card && 'border'}`}>
|
||||
{!card ? getTable() : getCards()}
|
||||
</div>
|
||||
{pagination && (
|
||||
<DataTablePagination
|
||||
table={table}
|
||||
onSetPageSize={onSetPageSize}
|
||||
showSelectedRows={showSelectedRows}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
|
||||
export interface BannerProps {
|
||||
message: JSX.Element;
|
||||
type?: 'info' | 'warning' | 'success' | 'error';
|
||||
actionText?: string;
|
||||
onAction?: () => void;
|
||||
}
|
||||
|
||||
export const Banner: React.FC<BannerProps> = ({
|
||||
message,
|
||||
type = 'info',
|
||||
actionText,
|
||||
onAction,
|
||||
}) => {
|
||||
const getBgColor = () => {
|
||||
switch (type) {
|
||||
case 'warning':
|
||||
return 'bg-amber-50 dark:bg-amber-900/20 text-amber-800 dark:text-amber-200';
|
||||
case 'success':
|
||||
return 'bg-green-50 dark:bg-green-900/20 text-green-800 dark:text-green-200';
|
||||
case 'error':
|
||||
return 'bg-red-50 dark:bg-red-900/20 text-red-800 dark:text-red-200';
|
||||
default:
|
||||
return 'bg-blue-50 dark:bg-blue-900/20 text-blue-800 dark:text-blue-200';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`w-full py-2 px-4 h-12 ${getBgColor()}`}>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex flex-1 items-center">
|
||||
<p className="text-sm font-medium">{message}</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{actionText && onAction && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={onAction}
|
||||
className="text-xs font-medium hover:bg-opacity-20"
|
||||
>
|
||||
{actionText}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Button } from '@/components/v1/ui/button';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -8,10 +8,10 @@ import {
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
} from '@/components/v1/ui/dropdown-menu';
|
||||
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import api, { TenantMember, TenantVersion, User } from '@/lib/api';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import api, { TenantMember, User } from '@/lib/api';
|
||||
import { useApiError } from '@/lib/hooks';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import hatchet from '@/assets/hatchet_logo.png';
|
||||
@@ -29,12 +29,9 @@ import {
|
||||
} from 'react-icons/bi';
|
||||
import { Menu } from 'lucide-react';
|
||||
import { useTheme } from '@/components/theme-provider';
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import useApiMeta from '@/pages/auth/hooks/use-api-meta';
|
||||
import { VersionInfo } from '@/pages/main/info/components/version-info';
|
||||
import { useTenant } from '@/lib/atoms';
|
||||
import { routes } from '@/router';
|
||||
import { Banner, BannerProps } from './banner';
|
||||
import {
|
||||
Breadcrumb,
|
||||
BreadcrumbItem,
|
||||
@@ -45,11 +42,12 @@ import {
|
||||
} from '@/components/v1/ui/breadcrumb';
|
||||
import { useBreadcrumbs } from '@/hooks/use-breadcrumbs';
|
||||
import { usePendingInvites } from '@/hooks/use-pending-invites';
|
||||
import { useTenantDetails } from '@/hooks/use-tenant';
|
||||
|
||||
function HelpDropdown() {
|
||||
const meta = useApiMeta();
|
||||
const navigate = useNavigate();
|
||||
const { tenant } = useTenant();
|
||||
const { tenant } = useTenantDetails();
|
||||
|
||||
const hasPylon = useMemo(() => {
|
||||
if (!meta.data?.pylonAppId) {
|
||||
@@ -101,11 +99,11 @@ function HelpDropdown() {
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
if (tenant?.version === TenantVersion.V1) {
|
||||
navigate(`/tenants/${tenant.metadata.id}/onboarding/get-started`);
|
||||
} else {
|
||||
navigate('/onboarding/get-started');
|
||||
if (!tenant) {
|
||||
return;
|
||||
}
|
||||
|
||||
navigate(`/tenants/${tenant.metadata.id}/onboarding/get-started`);
|
||||
}}
|
||||
>
|
||||
<BiSolidGraduation className="mr-2" />
|
||||
@@ -118,7 +116,6 @@ function HelpDropdown() {
|
||||
|
||||
function AccountDropdown({ user }: { user: User }) {
|
||||
const navigate = useNavigate();
|
||||
const { tenant } = useTenant();
|
||||
|
||||
const { handleApiError } = useApiError({});
|
||||
|
||||
@@ -176,14 +173,6 @@ function AccountDropdown({ user }: { user: User }) {
|
||||
<DropdownMenuItem>
|
||||
<VersionInfo />
|
||||
</DropdownMenuItem>
|
||||
{tenant?.version == TenantVersion.V1 &&
|
||||
location.pathname.includes(`/tenants/${tenant.metadata.id}`) && (
|
||||
<DropdownMenuItem
|
||||
onClick={() => navigate('/workflow-runs?previewV0=true')}
|
||||
>
|
||||
View Legacy V0 Data
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={() => toggleTheme()}>
|
||||
Toggle Theme
|
||||
@@ -200,76 +189,15 @@ function AccountDropdown({ user }: { user: User }) {
|
||||
interface MainNavProps {
|
||||
user: User;
|
||||
tenantMemberships: TenantMember[];
|
||||
setHasBanner?: (state: boolean) => void;
|
||||
}
|
||||
|
||||
export default function MainNav({ user, setHasBanner }: MainNavProps) {
|
||||
export default function MainNav({ user }: MainNavProps) {
|
||||
const { toggleSidebarOpen } = useSidebar();
|
||||
const { theme } = useTheme();
|
||||
const { tenant } = useTenant();
|
||||
const { pathname } = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const breadcrumbs = useBreadcrumbs();
|
||||
|
||||
const tenantedRoutes = useMemo(
|
||||
() =>
|
||||
routes
|
||||
.at(0)
|
||||
?.children?.find((r) => r.path?.startsWith('/tenants/'))
|
||||
?.children?.find(
|
||||
(r) => r.path?.startsWith('/tenants/') && r.children?.length,
|
||||
)
|
||||
?.children?.map((c) => c.path)
|
||||
?.map((p) => p?.replace('/tenants/:tenant', '')) || [],
|
||||
[],
|
||||
);
|
||||
|
||||
const tenantVersion = tenant?.version || TenantVersion.V0;
|
||||
|
||||
const banner: BannerProps | undefined = useMemo(() => {
|
||||
const pathnameWithoutTenant = pathname.replace(
|
||||
`/tenants/${tenant?.metadata.id}`,
|
||||
'',
|
||||
);
|
||||
|
||||
const shouldShowVersionUpgradeButton =
|
||||
tenantedRoutes.includes(pathnameWithoutTenant) && // It is a versioned route
|
||||
!pathname.startsWith('/tenants') && // The user is not already on the v1 version
|
||||
tenantVersion === TenantVersion.V1; // The tenant is on the v1 version
|
||||
|
||||
if (shouldShowVersionUpgradeButton) {
|
||||
return {
|
||||
message: (
|
||||
<>
|
||||
You are viewing legacy V0 data for a tenant that was upgraded to V1
|
||||
runtime.
|
||||
</>
|
||||
),
|
||||
type: 'warning',
|
||||
actionText: 'View V1',
|
||||
onAction: () => {
|
||||
navigate({
|
||||
pathname: `/tenants/${tenant?.metadata.id}${pathname}`,
|
||||
search: '?previewV0=false',
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return;
|
||||
}, [navigate, pathname, tenantVersion, tenantedRoutes, tenant?.metadata.id]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!setHasBanner) {
|
||||
return;
|
||||
}
|
||||
setHasBanner(!!banner);
|
||||
}, [setHasBanner, banner]);
|
||||
|
||||
return (
|
||||
<div className="fixed top-0 w-screen z-50">
|
||||
{banner && <Banner {...banner} />}
|
||||
|
||||
<div className="h-16 border-b bg-background">
|
||||
<div className="flex h-16 items-center pr-4 pl-4">
|
||||
<div className="flex flex-row items-center gap-x-8">
|
||||
|
||||
@@ -1,114 +0,0 @@
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { cn } from '@/lib/utils';
|
||||
import {
|
||||
BuildingOffice2Icon,
|
||||
// ChartBarSquareIcon,
|
||||
CheckIcon,
|
||||
} from '@heroicons/react/24/outline';
|
||||
import invariant from 'tiny-invariant';
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
CommandSeparator,
|
||||
} from '@/components/ui/command';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Tenant, TenantMember, TenantVersion } from '@/lib/api';
|
||||
import { CaretSortIcon, PlusCircledIcon } from '@radix-ui/react-icons';
|
||||
import {
|
||||
PopoverTrigger,
|
||||
Popover,
|
||||
PopoverContent,
|
||||
} from '@radix-ui/react-popover';
|
||||
import React from 'react';
|
||||
import { useTenant } from '@/lib/atoms';
|
||||
import { Spinner } from '@/components/ui/loading.tsx';
|
||||
import useApiMeta from '@/pages/auth/hooks/use-api-meta';
|
||||
|
||||
interface TenantSwitcherProps {
|
||||
className?: string;
|
||||
memberships: TenantMember[];
|
||||
currTenant: Tenant;
|
||||
}
|
||||
export function TenantSwitcher({
|
||||
className,
|
||||
memberships,
|
||||
currTenant,
|
||||
}: TenantSwitcherProps) {
|
||||
const meta = useApiMeta();
|
||||
const { setTenant: setCurrTenant } = useTenant();
|
||||
const [open, setOpen] = React.useState(false);
|
||||
|
||||
if (!currTenant) {
|
||||
return <Spinner />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
aria-expanded={open}
|
||||
aria-label="Select a team"
|
||||
className={cn('w-full justify-between', className)}
|
||||
>
|
||||
<BuildingOffice2Icon className="mr-2 h-4 w-4" />
|
||||
{currTenant.name}
|
||||
<CaretSortIcon className="ml-auto h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent side="right" className="w-full p-0 mb-6 z-50">
|
||||
<Command className="min-w-[260px]" value={currTenant.slug}>
|
||||
<CommandList>
|
||||
<CommandEmpty>No tenants found.</CommandEmpty>
|
||||
{memberships.map((membership) => (
|
||||
<CommandItem
|
||||
key={membership.metadata.id}
|
||||
onSelect={() => {
|
||||
invariant(membership.tenant);
|
||||
setCurrTenant(membership.tenant);
|
||||
setOpen(false);
|
||||
|
||||
if (membership.tenant.version === TenantVersion.V1) {
|
||||
// Hack to wait for next event loop tick so local storage is updated
|
||||
setTimeout(() => {
|
||||
window.location.href = `/tenants/${membership.tenant?.metadata.id}`;
|
||||
}, 0);
|
||||
}
|
||||
}}
|
||||
value={membership.tenant?.slug}
|
||||
className="text-sm cursor-pointer"
|
||||
>
|
||||
<BuildingOffice2Icon className="mr-2 h-4 w-4" />
|
||||
{membership.tenant?.name}
|
||||
<CheckIcon
|
||||
className={cn(
|
||||
'ml-auto h-4 w-4',
|
||||
currTenant.slug === membership.tenant?.slug
|
||||
? 'opacity-100'
|
||||
: 'opacity-0',
|
||||
)}
|
||||
/>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandList>
|
||||
{meta.data?.allowCreateTenant && (
|
||||
<>
|
||||
<CommandSeparator />
|
||||
<CommandList>
|
||||
<Link to="/onboarding/create-tenant">
|
||||
<CommandItem className="text-sm cursor-pointer">
|
||||
<PlusCircledIcon className="mr-2 h-4 w-4" />
|
||||
New Tenant
|
||||
</CommandItem>
|
||||
</Link>
|
||||
</CommandList>
|
||||
</>
|
||||
)}
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import TimeAgo from 'timeago-react';
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from '@/components/ui/tooltip';
|
||||
|
||||
interface RelativeDateProps {
|
||||
date?: Date | string;
|
||||
future?: boolean;
|
||||
}
|
||||
|
||||
const RelativeDate: React.FC<RelativeDateProps> = ({
|
||||
date = '',
|
||||
future = false,
|
||||
}) => {
|
||||
const formattedDate = useMemo(
|
||||
() => (typeof date === 'string' ? new Date(date) : date),
|
||||
[date],
|
||||
);
|
||||
|
||||
const [countdown, setCountdown] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
if (future) {
|
||||
const updateCountdown = () => {
|
||||
const currentDate = new Date();
|
||||
const timeDiff = formattedDate.getTime() - currentDate.getTime();
|
||||
|
||||
if (timeDiff <= 0) {
|
||||
setCountdown('');
|
||||
return;
|
||||
}
|
||||
|
||||
const days = Math.floor(timeDiff / (1000 * 3600 * 24));
|
||||
const hours = Math.floor(
|
||||
(timeDiff % (1000 * 3600 * 24)) / (1000 * 3600),
|
||||
);
|
||||
const minutes = Math.floor((timeDiff % (1000 * 3600)) / (1000 * 60));
|
||||
const seconds = Math.floor((timeDiff % (1000 * 60)) / 1000);
|
||||
|
||||
const countdownParts = [];
|
||||
if (days > 0) {
|
||||
countdownParts.push(`${days}d`);
|
||||
}
|
||||
if (hours > 0 || days > 0) {
|
||||
countdownParts.push(`${hours}h`);
|
||||
}
|
||||
if (minutes > 0 || hours > 0 || days > 0) {
|
||||
countdownParts.push(`${minutes}m`);
|
||||
}
|
||||
countdownParts.push(`${seconds}s`);
|
||||
|
||||
setCountdown(countdownParts.join(' '));
|
||||
};
|
||||
|
||||
updateCountdown();
|
||||
const countdownInterval = setInterval(updateCountdown, 1000);
|
||||
|
||||
return () => {
|
||||
clearInterval(countdownInterval);
|
||||
};
|
||||
}
|
||||
}, [formattedDate, future]);
|
||||
|
||||
if (date == '0001-01-01T00:00:00Z') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
onFocusCapture={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
{future && countdown ? (
|
||||
<>{countdown}</>
|
||||
) : (
|
||||
<TimeAgo datetime={formattedDate} />
|
||||
)}
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="z-[80]">
|
||||
{formattedDate.toLocaleString()}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default RelativeDate;
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useTenantDetails } from '@/hooks/use-tenant';
|
||||
import { User } from '@/lib/api';
|
||||
import { useTenant } from '@/lib/atoms';
|
||||
import useApiMeta from '@/pages/auth/hooks/use-api-meta';
|
||||
import React, { PropsWithChildren, useEffect, useMemo } from 'react';
|
||||
|
||||
@@ -13,7 +13,7 @@ const SupportChat: React.FC<PropsWithChildren & SupportChatProps> = ({
|
||||
}) => {
|
||||
const meta = useApiMeta();
|
||||
|
||||
const { tenant } = useTenant();
|
||||
const { tenant } = useTenantDetails();
|
||||
|
||||
const APP_ID = useMemo(() => {
|
||||
if (!meta.data?.pylonAppId) {
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
import { add, format } from 'date-fns';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Calendar } from '@/components/ui/calendar';
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from '@/components/ui/popover';
|
||||
import { TimePicker } from './time-picker';
|
||||
import { CalendarIcon } from '@radix-ui/react-icons';
|
||||
|
||||
type DateTimePickerProps = {
|
||||
date: Date | undefined;
|
||||
setDate: (date: Date | undefined) => void;
|
||||
label?: string;
|
||||
};
|
||||
|
||||
const formatDateWithLabel = (date: Date, label?: string) => {
|
||||
const formattedDate =
|
||||
format(date, 'PPP HH:mm') + ' (' + format(date, 'z') + ')';
|
||||
|
||||
if (!label) {
|
||||
return formattedDate;
|
||||
}
|
||||
|
||||
return label + ': ' + formattedDate;
|
||||
};
|
||||
|
||||
export function DateTimePicker({ date, setDate, label }: DateTimePickerProps) {
|
||||
/**
|
||||
* carry over the current time when a user clicks a new day
|
||||
* instead of resetting to 00:00
|
||||
*/
|
||||
const handleSelect = (newDay: Date | undefined) => {
|
||||
if (!newDay) {
|
||||
return;
|
||||
}
|
||||
if (!date) {
|
||||
setDate(newDay);
|
||||
return;
|
||||
}
|
||||
const diff = newDay.getTime() - date.getTime();
|
||||
const diffInDays = diff / (1000 * 60 * 60 * 24);
|
||||
const newDateFull = add(date, { days: Math.ceil(diffInDays) });
|
||||
setDate(newDateFull);
|
||||
};
|
||||
|
||||
return (
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant={'outline'}
|
||||
className={cn(
|
||||
'w-fit justify-start text-left font-normal text-xs',
|
||||
!date && 'text-muted-foreground',
|
||||
)}
|
||||
>
|
||||
<CalendarIcon className="mr-2 h-4 w-4" />
|
||||
{date ? formatDateWithLabel(date, label) : <span>{label}</span>}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-0">
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={date}
|
||||
onSelect={(d) => handleSelect(d)}
|
||||
initialFocus
|
||||
/>
|
||||
<div className="p-3 border-t border-border">
|
||||
<TimePicker setDate={setDate} date={date} />
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
@@ -1,149 +0,0 @@
|
||||
import { Input } from '@/components/ui/input';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
import React from 'react';
|
||||
import {
|
||||
Period,
|
||||
TimePickerType,
|
||||
getArrowByType,
|
||||
getDateByType,
|
||||
setDateByType,
|
||||
} from './time-picker-utils';
|
||||
|
||||
export interface TimePickerInputProps
|
||||
extends React.InputHTMLAttributes<HTMLInputElement> {
|
||||
picker: TimePickerType;
|
||||
date: Date | undefined;
|
||||
setDate: (date: Date | undefined) => void;
|
||||
period?: Period;
|
||||
onRightFocus?: () => void;
|
||||
onLeftFocus?: () => void;
|
||||
}
|
||||
|
||||
const TimePickerInput = React.forwardRef<
|
||||
HTMLInputElement,
|
||||
TimePickerInputProps
|
||||
>(
|
||||
(
|
||||
{
|
||||
className,
|
||||
value,
|
||||
id,
|
||||
name,
|
||||
date = new Date(new Date().setHours(0, 0, 0, 0)),
|
||||
setDate,
|
||||
onChange,
|
||||
onKeyDown,
|
||||
picker,
|
||||
period,
|
||||
onLeftFocus,
|
||||
onRightFocus,
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const [flag, setFlag] = React.useState<boolean>(false);
|
||||
const [prevIntKey, setPrevIntKey] = React.useState<string>('0');
|
||||
|
||||
/**
|
||||
* allow the user to enter the second digit within 2 seconds
|
||||
* otherwise start again with entering first digit
|
||||
*/
|
||||
React.useEffect(() => {
|
||||
if (flag) {
|
||||
const timer = setTimeout(() => {
|
||||
setFlag(false);
|
||||
}, 2000);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [flag]);
|
||||
|
||||
const calculatedValue = React.useMemo(() => {
|
||||
return getDateByType(date, picker);
|
||||
}, [date, picker]);
|
||||
|
||||
const calculateNewValue = (key: string) => {
|
||||
/*
|
||||
* If picker is '12hours' and the first digit is 0, then the second digit is automatically set to 1.
|
||||
* The second entered digit will break the condition and the value will be set to 10-12.
|
||||
*/
|
||||
if (picker === '12hours') {
|
||||
if (flag && calculatedValue.slice(1, 2) === '1' && prevIntKey === '0') {
|
||||
return '0' + key;
|
||||
}
|
||||
}
|
||||
|
||||
return !flag ? '0' + key : calculatedValue.slice(1, 2) + key;
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === 'Tab') {
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
if (e.key === 'ArrowRight') {
|
||||
onRightFocus?.();
|
||||
}
|
||||
if (e.key === 'ArrowLeft') {
|
||||
onLeftFocus?.();
|
||||
}
|
||||
if (['ArrowUp', 'ArrowDown'].includes(e.key)) {
|
||||
const step = e.key === 'ArrowUp' ? 1 : -1;
|
||||
const newValue = getArrowByType(calculatedValue, step, picker);
|
||||
if (flag) {
|
||||
setFlag(false);
|
||||
}
|
||||
const tempDate = new Date(date);
|
||||
setDate(setDateByType(tempDate, newValue, picker, period));
|
||||
}
|
||||
if (e.key >= '0' && e.key <= '9') {
|
||||
if (picker === '12hours') {
|
||||
setPrevIntKey(e.key);
|
||||
}
|
||||
|
||||
const newValue = calculateNewValue(e.key);
|
||||
if (flag) {
|
||||
onRightFocus?.();
|
||||
}
|
||||
setFlag((prev) => !prev);
|
||||
const tempDate = new Date(date);
|
||||
setDate(setDateByType(tempDate, newValue, picker, period));
|
||||
}
|
||||
};
|
||||
|
||||
const valueToTwoDigits = (value: number) => {
|
||||
return value < 10 ? `0${value}` : value;
|
||||
};
|
||||
|
||||
return (
|
||||
<Input
|
||||
ref={ref}
|
||||
id={id || picker}
|
||||
name={name || picker}
|
||||
className={cn(
|
||||
'w-[48px] text-xs text-center font-mono tabular-nums caret-transparent focus:bg-accent focus:text-accent-foreground [&::-webkit-inner-spin-button]:appearance-none',
|
||||
className,
|
||||
)}
|
||||
value={value || valueToTwoDigits(parseInt(calculatedValue))}
|
||||
onChange={(e) => {
|
||||
e.preventDefault();
|
||||
onChange?.(e);
|
||||
}}
|
||||
min={0}
|
||||
max={59}
|
||||
type="number"
|
||||
inputMode="numeric"
|
||||
onKeyDown={(e) => {
|
||||
onKeyDown?.(e);
|
||||
handleKeyDown(e);
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
TimePickerInput.displayName = 'TimePickerInput';
|
||||
|
||||
export { TimePickerInput };
|
||||
@@ -1,226 +0,0 @@
|
||||
/**
|
||||
* regular expression to check for valid hour format (01-23)
|
||||
*/
|
||||
export function isValidHour(value: string) {
|
||||
return /^(0[0-9]|1[0-9]|2[0-3])$/.test(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* regular expression to check for valid 12 hour format (01-12)
|
||||
*/
|
||||
export function isValid12Hour(value: string) {
|
||||
return /^(0[1-9]|1[0-2])$/.test(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* regular expression to check for valid minute format (00-59)
|
||||
*/
|
||||
export function isValidMinuteOrSecond(value: string) {
|
||||
return /^[0-5][0-9]$/.test(value);
|
||||
}
|
||||
|
||||
type GetValidNumberConfig = { max: number; min?: number; loop?: boolean };
|
||||
|
||||
export function getValidNumber(
|
||||
value: string,
|
||||
{ max, min = 0, loop = false }: GetValidNumberConfig,
|
||||
) {
|
||||
let numericValue = parseInt(value, 10);
|
||||
|
||||
if (!isNaN(numericValue)) {
|
||||
if (!loop) {
|
||||
if (numericValue > max) {
|
||||
numericValue = max;
|
||||
}
|
||||
if (numericValue < min) {
|
||||
numericValue = min;
|
||||
}
|
||||
} else {
|
||||
if (numericValue > max) {
|
||||
numericValue = min;
|
||||
}
|
||||
if (numericValue < min) {
|
||||
numericValue = max;
|
||||
}
|
||||
}
|
||||
return numericValue.toString().padStart(2, '0');
|
||||
}
|
||||
|
||||
return '00';
|
||||
}
|
||||
|
||||
export function getValidHour(value: string) {
|
||||
if (isValidHour(value)) {
|
||||
return value;
|
||||
}
|
||||
return getValidNumber(value, { max: 23 });
|
||||
}
|
||||
|
||||
export function getValid12Hour(value: string) {
|
||||
if (isValid12Hour(value)) {
|
||||
return value;
|
||||
}
|
||||
return getValidNumber(value, { min: 1, max: 12 });
|
||||
}
|
||||
|
||||
export function getValidMinuteOrSecond(value: string) {
|
||||
if (isValidMinuteOrSecond(value)) {
|
||||
return value;
|
||||
}
|
||||
return getValidNumber(value, { max: 59 });
|
||||
}
|
||||
|
||||
type GetValidArrowNumberConfig = {
|
||||
min: number;
|
||||
max: number;
|
||||
step: number;
|
||||
};
|
||||
|
||||
export function getValidArrowNumber(
|
||||
value: string,
|
||||
{ min, max, step }: GetValidArrowNumberConfig,
|
||||
) {
|
||||
let numericValue = parseInt(value, 10);
|
||||
if (!isNaN(numericValue)) {
|
||||
numericValue += step;
|
||||
return getValidNumber(String(numericValue), { min, max, loop: true });
|
||||
}
|
||||
return '00';
|
||||
}
|
||||
|
||||
export function getValidArrowHour(value: string, step: number) {
|
||||
return getValidArrowNumber(value, { min: 0, max: 23, step });
|
||||
}
|
||||
|
||||
export function getValidArrow12Hour(value: string, step: number) {
|
||||
return getValidArrowNumber(value, { min: 1, max: 12, step });
|
||||
}
|
||||
|
||||
export function getValidArrowMinuteOrSecond(value: string, step: number) {
|
||||
return getValidArrowNumber(value, { min: 0, max: 59, step });
|
||||
}
|
||||
|
||||
export function setMinutes(date: Date, value: string) {
|
||||
const minutes = getValidMinuteOrSecond(value);
|
||||
date.setMinutes(parseInt(minutes, 10));
|
||||
return date;
|
||||
}
|
||||
|
||||
export function setSeconds(date: Date, value: string) {
|
||||
const seconds = getValidMinuteOrSecond(value);
|
||||
date.setSeconds(parseInt(seconds, 10));
|
||||
return date;
|
||||
}
|
||||
|
||||
export function setHours(date: Date, value: string) {
|
||||
const hours = getValidHour(value);
|
||||
date.setHours(parseInt(hours, 10));
|
||||
return date;
|
||||
}
|
||||
|
||||
export function set12Hours(date: Date, value: string, period: Period) {
|
||||
const hours = parseInt(getValid12Hour(value), 10);
|
||||
const convertedHours = convert12HourTo24Hour(hours, period);
|
||||
date.setHours(convertedHours);
|
||||
return date;
|
||||
}
|
||||
|
||||
export type TimePickerType = 'minutes' | 'seconds' | 'hours' | '12hours';
|
||||
export type Period = 'AM' | 'PM';
|
||||
|
||||
export function setDateByType(
|
||||
date: Date,
|
||||
value: string,
|
||||
type: TimePickerType,
|
||||
period?: Period,
|
||||
) {
|
||||
switch (type) {
|
||||
case 'minutes':
|
||||
return setMinutes(date, value);
|
||||
case 'seconds':
|
||||
return setSeconds(date, value);
|
||||
case 'hours':
|
||||
return setHours(date, value);
|
||||
case '12hours': {
|
||||
if (!period) {
|
||||
return date;
|
||||
}
|
||||
return set12Hours(date, value, period);
|
||||
}
|
||||
default:
|
||||
return date;
|
||||
}
|
||||
}
|
||||
|
||||
export function getDateByType(date: Date, type: TimePickerType) {
|
||||
switch (type) {
|
||||
case 'minutes':
|
||||
return getValidMinuteOrSecond(String(date.getMinutes()));
|
||||
case 'seconds':
|
||||
return getValidMinuteOrSecond(String(date.getSeconds()));
|
||||
case 'hours':
|
||||
return getValidHour(String(date.getHours()));
|
||||
case '12hours':
|
||||
return getValid12Hour(String(display12HourValue(date.getHours())));
|
||||
default:
|
||||
return '00';
|
||||
}
|
||||
}
|
||||
|
||||
export function getArrowByType(
|
||||
value: string,
|
||||
step: number,
|
||||
type: TimePickerType,
|
||||
) {
|
||||
switch (type) {
|
||||
case 'minutes':
|
||||
return getValidArrowMinuteOrSecond(value, step);
|
||||
case 'seconds':
|
||||
return getValidArrowMinuteOrSecond(value, step);
|
||||
case 'hours':
|
||||
return getValidArrowHour(value, step);
|
||||
case '12hours':
|
||||
return getValidArrow12Hour(value, step);
|
||||
default:
|
||||
return '00';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* handles value change of 12-hour input
|
||||
* 12:00 PM is 12:00
|
||||
* 12:00 AM is 00:00
|
||||
*/
|
||||
export function convert12HourTo24Hour(hour: number, period: Period) {
|
||||
if (period === 'PM') {
|
||||
if (hour <= 11) {
|
||||
return hour + 12;
|
||||
} else {
|
||||
return hour;
|
||||
}
|
||||
} else if (period === 'AM') {
|
||||
if (hour === 12) {
|
||||
return 0;
|
||||
}
|
||||
return hour;
|
||||
}
|
||||
return hour;
|
||||
}
|
||||
|
||||
/**
|
||||
* time is stored in the 24-hour form,
|
||||
* but needs to be displayed to the user
|
||||
* in its 12-hour representation
|
||||
*/
|
||||
export function display12HourValue(hours: number) {
|
||||
if (hours === 0 || hours === 12) {
|
||||
return '12';
|
||||
}
|
||||
if (hours >= 22) {
|
||||
return `${hours - 12}`;
|
||||
}
|
||||
if (hours % 12 > 9) {
|
||||
return `${hours}`;
|
||||
}
|
||||
return `0${hours % 12}`;
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { TimePickerInput } from './time-picker-input';
|
||||
|
||||
interface TimePickerProps {
|
||||
date: Date | undefined;
|
||||
setDate: (date: Date | undefined) => void;
|
||||
}
|
||||
|
||||
export function TimePicker({ date, setDate }: TimePickerProps) {
|
||||
const minuteRef = React.useRef<HTMLInputElement>(null);
|
||||
const hourRef = React.useRef<HTMLInputElement>(null);
|
||||
const secondRef = React.useRef<HTMLInputElement>(null);
|
||||
|
||||
return (
|
||||
<div className="flex items-end gap-2">
|
||||
<div className="grid gap-1 text-center">
|
||||
<Label htmlFor="hours" className="text-xs">
|
||||
Hours
|
||||
</Label>
|
||||
<TimePickerInput
|
||||
picker="hours"
|
||||
date={date}
|
||||
setDate={setDate}
|
||||
ref={hourRef}
|
||||
onRightFocus={() => minuteRef.current?.focus()}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-1 text-center">
|
||||
<Label htmlFor="minutes" className="text-xs">
|
||||
Minutes
|
||||
</Label>
|
||||
<TimePickerInput
|
||||
picker="minutes"
|
||||
date={date}
|
||||
setDate={setDate}
|
||||
ref={minuteRef}
|
||||
onLeftFocus={() => hourRef.current?.focus()}
|
||||
onRightFocus={() => secondRef.current?.focus()}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-1 text-center">
|
||||
<Label htmlFor="seconds" className="text-xs">
|
||||
Seconds
|
||||
</Label>
|
||||
<TimePickerInput
|
||||
picker="seconds"
|
||||
date={date}
|
||||
setDate={setDate}
|
||||
ref={secondRef}
|
||||
onLeftFocus={() => minuteRef.current?.focus()}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import * as AccordionPrimitive from '@radix-ui/react-accordion';
|
||||
import { ChevronDownIcon } from '@radix-ui/react-icons';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const Accordion = AccordionPrimitive.Root;
|
||||
|
||||
const AccordionItem = React.forwardRef<
|
||||
React.ElementRef<typeof AccordionPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AccordionPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn('border-b', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
AccordionItem.displayName = 'AccordionItem';
|
||||
|
||||
const AccordionTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof AccordionPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger> & {
|
||||
hideChevron?: boolean;
|
||||
}
|
||||
>(({ className, children, hideChevron, ...props }, ref) => (
|
||||
<AccordionPrimitive.Header className="flex">
|
||||
<AccordionPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'flex flex-1 items-center justify-between py-4 text-sm font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
{!hideChevron && (
|
||||
<ChevronDownIcon className="h-4 w-4 shrink-0 text-gray-700 dark:text-gray-300 transition-transform duration-200" />
|
||||
)}
|
||||
</AccordionPrimitive.Trigger>
|
||||
</AccordionPrimitive.Header>
|
||||
));
|
||||
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;
|
||||
|
||||
const AccordionContent = React.forwardRef<
|
||||
React.ElementRef<typeof AccordionPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<AccordionPrimitive.Content
|
||||
ref={ref}
|
||||
className="overflow-hidden text-sm data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
|
||||
{...props}
|
||||
>
|
||||
<div className={cn('pb-4 pt-0', className)}>{children}</div>
|
||||
</AccordionPrimitive.Content>
|
||||
));
|
||||
AccordionContent.displayName = AccordionPrimitive.Content.displayName;
|
||||
|
||||
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
|
||||
@@ -1,60 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import { cva, type VariantProps } from 'class-variance-authority';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const alertVariants = cva(
|
||||
'relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: 'bg-background text-foreground',
|
||||
destructive:
|
||||
'border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive',
|
||||
warn: 'border-yellow-400 dark:border-yellow-800',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const Alert = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
|
||||
>(({ className, variant, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
role="alert"
|
||||
className={cn(alertVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
Alert.displayName = 'Alert';
|
||||
|
||||
const AlertTitle = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLHeadingElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<h5
|
||||
ref={ref}
|
||||
className={cn('mb-1 font-medium leading-none tracking-tight', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
AlertTitle.displayName = 'AlertTitle';
|
||||
|
||||
const AlertDescription = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn('text-sm [&_p]:leading-relaxed', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
AlertDescription.displayName = 'AlertDescription';
|
||||
|
||||
export { Alert, AlertTitle, AlertDescription };
|
||||
@@ -1,48 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import * as AvatarPrimitive from '@radix-ui/react-avatar';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const Avatar = React.forwardRef<
|
||||
React.ElementRef<typeof AvatarPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AvatarPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
Avatar.displayName = AvatarPrimitive.Root.displayName;
|
||||
|
||||
const AvatarImage = React.forwardRef<
|
||||
React.ElementRef<typeof AvatarPrimitive.Image>,
|
||||
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AvatarPrimitive.Image
|
||||
ref={ref}
|
||||
className={cn('aspect-square h-full w-full', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
AvatarImage.displayName = AvatarPrimitive.Image.displayName;
|
||||
|
||||
const AvatarFallback = React.forwardRef<
|
||||
React.ElementRef<typeof AvatarPrimitive.Fallback>,
|
||||
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AvatarPrimitive.Fallback
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'flex h-full w-full items-center justify-center rounded-full bg-muted',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;
|
||||
|
||||
export { Avatar, AvatarImage, AvatarFallback };
|
||||
@@ -1,42 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import { cva, type VariantProps } from 'class-variance-authority';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const badgeVariants = cva(
|
||||
'inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs 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 shadow',
|
||||
secondary:
|
||||
'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
||||
destructive:
|
||||
'border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80',
|
||||
outline: 'text-foreground',
|
||||
outlineDestructive:
|
||||
'border border-destructive rounded-sm px-1 font-normal text-red-800 dark:text-red-300 bg-transparent',
|
||||
successful:
|
||||
'border-transparent rounded-sm px-1 font-normal text-green-800 dark:text-green-300 bg-green-500/20 ring-green-500/30',
|
||||
failed:
|
||||
'border-transparent rounded-sm px-1 font-normal text-red-800 dark:text-red-300 bg-red-500/20 ring-red-500/30',
|
||||
inProgress:
|
||||
'border-transparent rounded-sm px-1 font-normal text-yellow-800 dark:text-yellow-300 bg-yellow-500/20 ring-yellow-500/30',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
},
|
||||
},
|
||||
);
|
||||
export interface BadgeProps
|
||||
extends React.HTMLAttributes<HTMLDivElement>,
|
||||
VariantProps<typeof badgeVariants> {}
|
||||
|
||||
function Badge({ className, variant, ...props }: BadgeProps) {
|
||||
return (
|
||||
<span className={cn(badgeVariants({ variant }), className)} {...props} />
|
||||
);
|
||||
}
|
||||
|
||||
export { Badge, badgeVariants };
|
||||
@@ -1,115 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import { ChevronRightIcon, DotsHorizontalIcon } from '@radix-ui/react-icons';
|
||||
import { Slot } from '@radix-ui/react-slot';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const Breadcrumb = React.forwardRef<
|
||||
HTMLElement,
|
||||
React.ComponentPropsWithoutRef<'nav'> & {
|
||||
separator?: React.ReactNode;
|
||||
}
|
||||
>(({ ...props }, ref) => <nav ref={ref} aria-label="breadcrumb" {...props} />);
|
||||
Breadcrumb.displayName = 'Breadcrumb';
|
||||
|
||||
const BreadcrumbList = React.forwardRef<
|
||||
HTMLOListElement,
|
||||
React.ComponentPropsWithoutRef<'ol'>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ol
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
BreadcrumbList.displayName = 'BreadcrumbList';
|
||||
|
||||
const BreadcrumbItem = React.forwardRef<
|
||||
HTMLLIElement,
|
||||
React.ComponentPropsWithoutRef<'li'>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<li
|
||||
ref={ref}
|
||||
className={cn('inline-flex items-center gap-1.5', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
BreadcrumbItem.displayName = 'BreadcrumbItem';
|
||||
|
||||
const BreadcrumbLink = React.forwardRef<
|
||||
HTMLAnchorElement,
|
||||
React.ComponentPropsWithoutRef<'a'> & {
|
||||
asChild?: boolean;
|
||||
}
|
||||
>(({ asChild, className, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : 'a';
|
||||
|
||||
return (
|
||||
<Comp
|
||||
ref={ref}
|
||||
className={cn('transition-colors hover:text-foreground', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
BreadcrumbLink.displayName = 'BreadcrumbLink';
|
||||
|
||||
const BreadcrumbPage = React.forwardRef<
|
||||
HTMLSpanElement,
|
||||
React.ComponentPropsWithoutRef<'span'>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<span
|
||||
ref={ref}
|
||||
role="link"
|
||||
aria-disabled="true"
|
||||
aria-current="page"
|
||||
className={cn('font-normal text-foreground', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
BreadcrumbPage.displayName = 'BreadcrumbPage';
|
||||
|
||||
const BreadcrumbSeparator = ({
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<'li'>) => (
|
||||
<li
|
||||
role="presentation"
|
||||
aria-hidden="true"
|
||||
className={cn('[&>svg]:size-3.5', className)}
|
||||
{...props}
|
||||
>
|
||||
{children ?? <ChevronRightIcon />}
|
||||
</li>
|
||||
);
|
||||
BreadcrumbSeparator.displayName = 'BreadcrumbSeparator';
|
||||
|
||||
const BreadcrumbEllipsis = ({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<'span'>) => (
|
||||
<span
|
||||
role="presentation"
|
||||
aria-hidden="true"
|
||||
className={cn('flex h-9 w-9 items-center justify-center', className)}
|
||||
{...props}
|
||||
>
|
||||
<DotsHorizontalIcon className="h-4 w-4" />
|
||||
<span className="sr-only">More</span>
|
||||
</span>
|
||||
);
|
||||
BreadcrumbEllipsis.displayName = 'BreadcrumbElipssis';
|
||||
|
||||
export {
|
||||
Breadcrumb,
|
||||
BreadcrumbList,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
BreadcrumbPage,
|
||||
BreadcrumbSeparator,
|
||||
BreadcrumbEllipsis,
|
||||
};
|
||||
@@ -1,58 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import { Slot } from '@radix-ui/react-slot';
|
||||
import { cva, type VariantProps } from 'class-variance-authority';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const buttonVariants = cva(
|
||||
'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
'bg-primary text-primary-foreground shadow hover:bg-primary/90',
|
||||
destructive:
|
||||
'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90',
|
||||
outline:
|
||||
'border border-input bg-transparent shadow-sm hover:bg-accent hover:text-accent-foreground',
|
||||
secondary:
|
||||
'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80',
|
||||
ghost: 'hover:bg-accent hover:text-accent-foreground',
|
||||
link: 'text-primary underline-offset-4 hover:underline',
|
||||
},
|
||||
size: {
|
||||
default: 'h-9 px-4 py-2',
|
||||
sm: 'h-8 rounded-md px-3 text-xs',
|
||||
lg: 'h-10 rounded-md px-8',
|
||||
icon: 'h-9 w-9',
|
||||
xs: '',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
size: 'default',
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
export interface ButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
asChild?: boolean;
|
||||
}
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : 'button';
|
||||
return (
|
||||
<Comp
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
Button.displayName = 'Button';
|
||||
|
||||
export { Button, buttonVariants };
|
||||
@@ -1,70 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import { ChevronLeftIcon, ChevronRightIcon } from '@radix-ui/react-icons';
|
||||
import { DayPicker } from 'react-day-picker';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
import { buttonVariants } from '@/components/ui/button';
|
||||
|
||||
export type CalendarProps = React.ComponentProps<typeof DayPicker>;
|
||||
|
||||
function Calendar({
|
||||
className,
|
||||
classNames,
|
||||
showOutsideDays = true,
|
||||
...props
|
||||
}: CalendarProps) {
|
||||
return (
|
||||
<DayPicker
|
||||
showOutsideDays={showOutsideDays}
|
||||
className={cn('p-3', className)}
|
||||
classNames={{
|
||||
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',
|
||||
nav: 'space-x-1 flex items-center',
|
||||
nav_button: cn(
|
||||
buttonVariants({ variant: 'outline' }),
|
||||
'h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100',
|
||||
),
|
||||
nav_button_previous: 'absolute left-1',
|
||||
nav_button_next: 'absolute right-1',
|
||||
table: 'w-full border-collapse space-y-1',
|
||||
head_row: 'flex',
|
||||
head_cell:
|
||||
'text-muted-foreground rounded-md w-8 font-normal text-[0.8rem]',
|
||||
row: 'flex w-full mt-2',
|
||||
cell: cn(
|
||||
'relative p-0 text-center text-sm focus-within:relative focus-within:z-20 [&:has([aria-selected])]:bg-accent [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected].day-range-end)]:rounded-r-md',
|
||||
props.mode === 'range'
|
||||
? '[&:has(>.day-range-end)]:rounded-r-md [&:has(>.day-range-start)]:rounded-l-md first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md'
|
||||
: '[&:has([aria-selected])]:rounded-md',
|
||||
),
|
||||
day: cn(
|
||||
buttonVariants({ variant: 'ghost' }),
|
||||
'h-8 w-8 p-0 font-normal aria-selected:opacity-100',
|
||||
),
|
||||
day_range_start: 'day-range-start',
|
||||
day_range_end: 'day-range-end',
|
||||
day_selected:
|
||||
'bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground',
|
||||
day_today: 'bg-accent text-accent-foreground',
|
||||
day_outside:
|
||||
'day-outside text-muted-foreground opacity-50 aria-selected:bg-accent/50 aria-selected:text-muted-foreground aria-selected:opacity-30',
|
||||
day_disabled: 'text-muted-foreground opacity-50',
|
||||
day_range_middle:
|
||||
'aria-selected:bg-accent aria-selected:text-accent-foreground',
|
||||
day_hidden: 'invisible',
|
||||
...classNames,
|
||||
}}
|
||||
components={{
|
||||
IconLeft: () => <ChevronLeftIcon className="h-4 w-4" />,
|
||||
IconRight: () => <ChevronRightIcon className="h-4 w-4" />,
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
Calendar.displayName = 'Calendar';
|
||||
|
||||
export { Calendar };
|
||||
@@ -1,83 +0,0 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const Card = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'rounded-xl border bg-card text-card-foreground shadow',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
Card.displayName = 'Card';
|
||||
|
||||
const CardHeader = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn('flex flex-col space-y-1.5 p-6', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
CardHeader.displayName = 'CardHeader';
|
||||
|
||||
const CardTitle = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLHeadingElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<h3
|
||||
ref={ref}
|
||||
className={cn('font-semibold leading-none tracking-tight', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
CardTitle.displayName = 'CardTitle';
|
||||
|
||||
const CardDescription = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<p
|
||||
ref={ref}
|
||||
className={cn('text-sm text-muted-foreground', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
CardDescription.displayName = 'CardDescription';
|
||||
|
||||
const CardContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn('p-6 pt-0', className)} {...props} />
|
||||
));
|
||||
CardContent.displayName = 'CardContent';
|
||||
|
||||
const CardFooter = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn('flex items-center p-6 pt-0', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
CardFooter.displayName = 'CardFooter';
|
||||
|
||||
export {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardFooter,
|
||||
CardTitle,
|
||||
CardDescription,
|
||||
CardContent,
|
||||
};
|
||||
@@ -1,363 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import * as RechartsPrimitive from 'recharts';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
// Format: { THEME_NAME: CSS_SELECTOR }
|
||||
const THEMES = { light: '', dark: '.dark' } as const;
|
||||
|
||||
export type ChartConfig = {
|
||||
[k in string]: {
|
||||
label?: React.ReactNode;
|
||||
icon?: React.ComponentType;
|
||||
} & (
|
||||
| { color?: string; theme?: never }
|
||||
| { color?: never; theme: Record<keyof typeof THEMES, string> }
|
||||
);
|
||||
};
|
||||
|
||||
type ChartContextProps = {
|
||||
config: ChartConfig;
|
||||
};
|
||||
|
||||
const ChartContext = React.createContext<ChartContextProps | null>(null);
|
||||
|
||||
function useChart() {
|
||||
const context = React.useContext(ChartContext);
|
||||
|
||||
if (!context) {
|
||||
throw new Error('useChart must be used within a <ChartContainer />');
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
const ChartContainer = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentProps<'div'> & {
|
||||
config: ChartConfig;
|
||||
children: React.ComponentProps<
|
||||
typeof RechartsPrimitive.ResponsiveContainer
|
||||
>['children'];
|
||||
}
|
||||
>(({ id, className, children, config, ...props }, ref) => {
|
||||
const uniqueId = React.useId();
|
||||
const chartId = `chart-${id || uniqueId.replace(/:/g, '')}`;
|
||||
|
||||
return (
|
||||
<ChartContext.Provider value={{ config }}>
|
||||
<div
|
||||
data-chart={chartId}
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex aspect-video justify-center text-xs [&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-none [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-sector]:outline-none [&_.recharts-surface]:outline-none",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChartStyle id={chartId} config={config} />
|
||||
<RechartsPrimitive.ResponsiveContainer>
|
||||
{children}
|
||||
</RechartsPrimitive.ResponsiveContainer>
|
||||
</div>
|
||||
</ChartContext.Provider>
|
||||
);
|
||||
});
|
||||
ChartContainer.displayName = 'Chart';
|
||||
|
||||
const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
|
||||
const colorConfig = Object.entries(config).filter(
|
||||
([, config]) => config.theme || config.color,
|
||||
);
|
||||
|
||||
if (!colorConfig.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<style
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: Object.entries(THEMES)
|
||||
.map(
|
||||
([theme, prefix]) => `
|
||||
${prefix} [data-chart=${id}] {
|
||||
${colorConfig
|
||||
.map(([key, itemConfig]) => {
|
||||
const color =
|
||||
itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||
|
||||
itemConfig.color;
|
||||
return color ? ` --color-${key}: ${color};` : null;
|
||||
})
|
||||
.join('\n')}
|
||||
}
|
||||
`,
|
||||
)
|
||||
.join('\n'),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const ChartTooltip = RechartsPrimitive.Tooltip;
|
||||
|
||||
const ChartTooltipContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
|
||||
React.ComponentProps<'div'> & {
|
||||
hideLabel?: boolean;
|
||||
hideIndicator?: boolean;
|
||||
indicator?: 'line' | 'dot' | 'dashed';
|
||||
nameKey?: string;
|
||||
labelKey?: string;
|
||||
}
|
||||
>(
|
||||
(
|
||||
{
|
||||
active,
|
||||
payload,
|
||||
className,
|
||||
indicator = 'dot',
|
||||
hideLabel = false,
|
||||
hideIndicator = false,
|
||||
label,
|
||||
labelFormatter,
|
||||
labelClassName,
|
||||
formatter,
|
||||
color,
|
||||
nameKey,
|
||||
labelKey,
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const { config } = useChart();
|
||||
|
||||
const tooltipLabel = React.useMemo(() => {
|
||||
if (hideLabel || !payload?.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const [item] = payload;
|
||||
const key = `${labelKey || item.dataKey || item.name || 'value'}`;
|
||||
const itemConfig = getPayloadConfigFromPayload(config, item, key);
|
||||
const value =
|
||||
!labelKey && typeof label === 'string'
|
||||
? config[label as keyof typeof config]?.label || label
|
||||
: itemConfig?.label;
|
||||
|
||||
if (labelFormatter) {
|
||||
return (
|
||||
<div className={cn('font-medium', labelClassName)}>
|
||||
{labelFormatter(value, payload)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <div className={cn('font-medium', labelClassName)}>{value}</div>;
|
||||
}, [
|
||||
label,
|
||||
labelFormatter,
|
||||
payload,
|
||||
hideLabel,
|
||||
labelClassName,
|
||||
config,
|
||||
labelKey,
|
||||
]);
|
||||
|
||||
if (!active || !payload?.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const nestLabel = payload.length === 1 && indicator !== 'dot';
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'grid min-w-[8rem] items-start gap-1.5 rounded-lg border border-border/50 bg-background px-2.5 py-1.5 text-xs shadow-xl',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{!nestLabel ? tooltipLabel : null}
|
||||
<div className="grid gap-1.5">
|
||||
{payload.map((item, index) => {
|
||||
const key = `${nameKey || item.name || item.dataKey || 'value'}`;
|
||||
const itemConfig = getPayloadConfigFromPayload(config, item, key);
|
||||
const indicatorColor = color || item.payload.fill || item.color;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={item.dataKey}
|
||||
className={cn(
|
||||
'flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground',
|
||||
indicator === 'dot' && 'items-center',
|
||||
)}
|
||||
>
|
||||
{formatter && item?.value !== undefined && item.name ? (
|
||||
formatter(item.value, item.name, item, index, item.payload)
|
||||
) : (
|
||||
<>
|
||||
{itemConfig?.icon ? (
|
||||
<itemConfig.icon />
|
||||
) : (
|
||||
!hideIndicator && (
|
||||
<div
|
||||
className={cn(
|
||||
'shrink-0 rounded-[2px] border-[--color-border] bg-[--color-bg]',
|
||||
{
|
||||
'h-2.5 w-2.5': indicator === 'dot',
|
||||
'w-1': indicator === 'line',
|
||||
'w-0 border-[1.5px] border-dashed bg-transparent':
|
||||
indicator === 'dashed',
|
||||
'my-0.5': nestLabel && indicator === 'dashed',
|
||||
},
|
||||
)}
|
||||
style={
|
||||
{
|
||||
'--color-bg': indicatorColor,
|
||||
'--color-border': indicatorColor,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
<div
|
||||
className={cn(
|
||||
'flex flex-1 justify-between leading-none',
|
||||
nestLabel ? 'items-end' : 'items-center',
|
||||
)}
|
||||
>
|
||||
<div className="grid gap-1.5">
|
||||
{nestLabel ? tooltipLabel : null}
|
||||
<span className="text-muted-foreground">
|
||||
{itemConfig?.label || item.name}
|
||||
</span>
|
||||
</div>
|
||||
{item.value && (
|
||||
<span className="font-mono font-medium tabular-nums text-foreground">
|
||||
{item.value.toLocaleString()}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
ChartTooltipContent.displayName = 'ChartTooltip';
|
||||
|
||||
const ChartLegend = RechartsPrimitive.Legend;
|
||||
|
||||
const ChartLegendContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentProps<'div'> &
|
||||
Pick<RechartsPrimitive.LegendProps, 'payload' | 'verticalAlign'> & {
|
||||
hideIcon?: boolean;
|
||||
nameKey?: string;
|
||||
}
|
||||
>(
|
||||
(
|
||||
{ className, hideIcon = false, payload, verticalAlign = 'bottom', nameKey },
|
||||
ref,
|
||||
) => {
|
||||
const { config } = useChart();
|
||||
|
||||
if (!payload?.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'flex items-center justify-center gap-4',
|
||||
verticalAlign === 'top' ? 'pb-3' : 'pt-3',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{payload.map((item) => {
|
||||
const key = `${nameKey || item.dataKey || 'value'}`;
|
||||
const itemConfig = getPayloadConfigFromPayload(config, item, key);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={item.value}
|
||||
className={cn(
|
||||
'flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground',
|
||||
)}
|
||||
>
|
||||
{itemConfig?.icon && !hideIcon ? (
|
||||
<itemConfig.icon />
|
||||
) : (
|
||||
<div
|
||||
className="h-2 w-2 shrink-0 rounded-[2px]"
|
||||
style={{
|
||||
backgroundColor: item.color,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{itemConfig?.label}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
ChartLegendContent.displayName = 'ChartLegend';
|
||||
|
||||
// Helper to extract item config from a payload.
|
||||
function getPayloadConfigFromPayload(
|
||||
config: ChartConfig,
|
||||
payload: unknown,
|
||||
key: string,
|
||||
) {
|
||||
if (typeof payload !== 'object' || payload === null) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const payloadPayload =
|
||||
'payload' in payload &&
|
||||
typeof payload.payload === 'object' &&
|
||||
payload.payload !== null
|
||||
? payload.payload
|
||||
: undefined;
|
||||
|
||||
let configLabelKey: string = key;
|
||||
|
||||
if (
|
||||
key in payload &&
|
||||
typeof payload[key as keyof typeof payload] === 'string'
|
||||
) {
|
||||
configLabelKey = payload[key as keyof typeof payload] as string;
|
||||
} else if (
|
||||
payloadPayload &&
|
||||
key in payloadPayload &&
|
||||
typeof payloadPayload[key as keyof typeof payloadPayload] === 'string'
|
||||
) {
|
||||
configLabelKey = payloadPayload[
|
||||
key as keyof typeof payloadPayload
|
||||
] as string;
|
||||
}
|
||||
|
||||
return configLabelKey in config
|
||||
? config[configLabelKey]
|
||||
: config[key as keyof typeof config];
|
||||
}
|
||||
|
||||
export {
|
||||
ChartContainer,
|
||||
ChartTooltip,
|
||||
ChartTooltipContent,
|
||||
ChartLegend,
|
||||
ChartLegendContent,
|
||||
ChartStyle,
|
||||
};
|
||||
@@ -1,28 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import * as CheckboxPrimitive from '@radix-ui/react-checkbox';
|
||||
import { CheckIcon } from '@radix-ui/react-icons';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const Checkbox = React.forwardRef<
|
||||
React.ElementRef<typeof CheckboxPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CheckboxPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'peer h-4 w-4 shrink-0 rounded-sm border border-primary shadow focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<CheckboxPrimitive.Indicator
|
||||
className={cn('flex items-center justify-center text-current')}
|
||||
>
|
||||
<CheckIcon className="h-4 w-4" />
|
||||
</CheckboxPrimitive.Indicator>
|
||||
</CheckboxPrimitive.Root>
|
||||
));
|
||||
Checkbox.displayName = CheckboxPrimitive.Root.displayName;
|
||||
|
||||
export { Checkbox };
|
||||
@@ -1,219 +0,0 @@
|
||||
import CopyToClipboard from './copy-to-clipboard';
|
||||
import { cn } from '@/lib/utils';
|
||||
import Editor, { DiffEditor, Monaco } from '@monaco-editor/react';
|
||||
import 'monaco-themes/themes/Pastels on Dark.json';
|
||||
import { useTheme } from '../theme-provider';
|
||||
|
||||
interface CodeEditorProps {
|
||||
code: string;
|
||||
setCode?: (code: string | undefined) => void;
|
||||
language: string;
|
||||
className?: string;
|
||||
height?: string;
|
||||
width?: string;
|
||||
copy?: boolean;
|
||||
wrapLines?: boolean;
|
||||
lineNumbers?: boolean;
|
||||
}
|
||||
|
||||
export function CodeEditor({
|
||||
code,
|
||||
setCode,
|
||||
language,
|
||||
className,
|
||||
height,
|
||||
width,
|
||||
copy = true,
|
||||
wrapLines = true,
|
||||
lineNumbers = false,
|
||||
}: CodeEditorProps) {
|
||||
const { theme } = useTheme();
|
||||
|
||||
const setEditorTheme = (monaco: Monaco) => {
|
||||
monaco.editor.defineTheme('pastels-on-dark', getMonacoTheme());
|
||||
monaco.editor.setTheme('pastels-on-dark');
|
||||
};
|
||||
|
||||
const editorTheme = theme === 'dark' ? 'pastels-on-dark' : '';
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
className,
|
||||
'w-full h-fit relative rounded-lg overflow-hidden',
|
||||
)}
|
||||
>
|
||||
<Editor
|
||||
beforeMount={setEditorTheme}
|
||||
language={language}
|
||||
value={code}
|
||||
onChange={setCode}
|
||||
width={width || '100%'}
|
||||
height={height || '400px'}
|
||||
theme={editorTheme}
|
||||
options={{
|
||||
minimap: { enabled: false },
|
||||
wordWrap: wrapLines ? 'on' : 'off',
|
||||
lineNumbers: lineNumbers
|
||||
? function (lineNumber) {
|
||||
return `<span style="padding-right:8px">${lineNumber}</span>`;
|
||||
}
|
||||
: 'off',
|
||||
theme: editorTheme,
|
||||
autoDetectHighContrast: true,
|
||||
readOnly: !setCode,
|
||||
scrollbar: { vertical: 'hidden', horizontal: 'hidden' },
|
||||
showFoldingControls: language == 'json' ? 'always' : 'never',
|
||||
lineDecorationsWidth: 0,
|
||||
overviewRulerBorder: false,
|
||||
colorDecorators: false,
|
||||
hideCursorInOverviewRuler: true,
|
||||
contextmenu: false,
|
||||
}}
|
||||
/>
|
||||
{copy && (
|
||||
<CopyToClipboard
|
||||
className="absolute top-2 right-2"
|
||||
text={code.trim()}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function DiffCodeEditor({
|
||||
code,
|
||||
setCode,
|
||||
language,
|
||||
className,
|
||||
height,
|
||||
width,
|
||||
copy,
|
||||
originalValue,
|
||||
wrapLines = true,
|
||||
}: CodeEditorProps & {
|
||||
originalValue: string;
|
||||
}) {
|
||||
const setEditorTheme = (monaco: Monaco) => {
|
||||
monaco.editor.defineTheme('pastels-on-dark', getMonacoTheme());
|
||||
|
||||
monaco.editor.setTheme('pastels-on-dark');
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
className,
|
||||
'w-full h-fit relative rounded-lg overflow-hidden',
|
||||
)}
|
||||
>
|
||||
<DiffEditor
|
||||
beforeMount={setEditorTheme}
|
||||
language={language}
|
||||
width={width || '100%'}
|
||||
height={height || '400px'}
|
||||
theme="pastels-on-dark"
|
||||
original={originalValue}
|
||||
modified={code}
|
||||
options={{
|
||||
minimap: { enabled: false },
|
||||
wordWrap: wrapLines ? 'on' : 'off',
|
||||
lineNumbers: 'off',
|
||||
readOnly: !setCode,
|
||||
scrollbar: { vertical: 'hidden', horizontal: 'hidden' },
|
||||
showFoldingControls: language == 'json' ? 'always' : 'never',
|
||||
lineDecorationsWidth: 0,
|
||||
overviewRulerBorder: false,
|
||||
colorDecorators: false,
|
||||
hideCursorInOverviewRuler: true,
|
||||
contextmenu: false,
|
||||
}}
|
||||
/>
|
||||
{copy && (
|
||||
<CopyToClipboard
|
||||
className="absolute top-2 right-2"
|
||||
text={code.trim()}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
type BuiltinTheme = 'vs' | 'vs-dark' | 'hc-black' | 'hc-light';
|
||||
|
||||
const getMonacoTheme = () => {
|
||||
return {
|
||||
base: 'vs-dark' as BuiltinTheme,
|
||||
inherit: true,
|
||||
rules: [
|
||||
{
|
||||
background: '0D0D0D',
|
||||
token: '',
|
||||
},
|
||||
{
|
||||
foreground: '473c45',
|
||||
token: 'comment',
|
||||
},
|
||||
{
|
||||
foreground: 'c0c5ce',
|
||||
token: 'string',
|
||||
},
|
||||
{
|
||||
foreground: 'a8885a',
|
||||
token: 'constant',
|
||||
},
|
||||
{
|
||||
foreground: '4FB4D7',
|
||||
token: 'variable.parameter',
|
||||
},
|
||||
{
|
||||
foreground: '596380',
|
||||
token: 'variable.other',
|
||||
},
|
||||
{
|
||||
foreground: '728059',
|
||||
token: 'keyword - keyword.operator',
|
||||
},
|
||||
{
|
||||
foreground: '728059',
|
||||
token: 'keyword.operator.logical',
|
||||
},
|
||||
{
|
||||
foreground: '9ebf60',
|
||||
token: 'storage',
|
||||
},
|
||||
{
|
||||
foreground: '6078bf',
|
||||
token: 'entity',
|
||||
},
|
||||
{
|
||||
fontStyle: 'italic',
|
||||
token: 'entity.other.inherited-class',
|
||||
},
|
||||
{
|
||||
foreground: '8a4b66',
|
||||
token: 'support',
|
||||
},
|
||||
{
|
||||
foreground: '893062',
|
||||
token: 'support.type.exception',
|
||||
},
|
||||
{
|
||||
background: '5f0047',
|
||||
token: 'invalid',
|
||||
},
|
||||
{
|
||||
background: '371d28',
|
||||
token: 'meta.function.section',
|
||||
},
|
||||
],
|
||||
colors: {
|
||||
'editor.foreground': '#c0c5ce',
|
||||
'editor.background': '#1e293b',
|
||||
'editor.selectionBackground': '#40002F',
|
||||
'editor.lineHighlightBackground': '#00000012',
|
||||
'editorCursor.foreground': '#7F005D',
|
||||
'editorWhitespace.foreground': '#BFBFBF',
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -1,77 +0,0 @@
|
||||
import { Light as SyntaxHighlighter } from 'react-syntax-highlighter';
|
||||
|
||||
import typescript from 'react-syntax-highlighter/dist/esm/languages/hljs/typescript';
|
||||
import yaml from 'react-syntax-highlighter/dist/esm/languages/hljs/yaml';
|
||||
import json from 'react-syntax-highlighter/dist/esm/languages/hljs/json';
|
||||
|
||||
import {
|
||||
anOldHope,
|
||||
atomOneLight,
|
||||
} from 'react-syntax-highlighter/dist/esm/styles/hljs';
|
||||
import CopyToClipboard from './copy-to-clipboard';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useTheme } from '../theme-provider';
|
||||
|
||||
SyntaxHighlighter.registerLanguage('typescript', typescript);
|
||||
SyntaxHighlighter.registerLanguage('yaml', yaml);
|
||||
SyntaxHighlighter.registerLanguage('json', json);
|
||||
|
||||
export function CodeHighlighter({
|
||||
code,
|
||||
copyCode,
|
||||
language,
|
||||
className,
|
||||
maxHeight,
|
||||
minHeight,
|
||||
maxWidth,
|
||||
copy = true,
|
||||
wrapLines = true,
|
||||
}: {
|
||||
code: string;
|
||||
copyCode?: string;
|
||||
language: string;
|
||||
className?: string;
|
||||
maxHeight?: string;
|
||||
minHeight?: string;
|
||||
maxWidth?: string;
|
||||
copy?: boolean;
|
||||
wrapLines?: boolean;
|
||||
}) {
|
||||
const { theme } = useTheme();
|
||||
|
||||
return (
|
||||
<div className={cn('w-full h-fit relative bg-muted rounded-lg', className)}>
|
||||
<SyntaxHighlighter
|
||||
language={language}
|
||||
style={theme == 'dark' ? anOldHope : atomOneLight}
|
||||
wrapLines={wrapLines}
|
||||
lineProps={{
|
||||
style: { wordBreak: 'break-all', whiteSpace: 'pre-wrap' },
|
||||
}}
|
||||
customStyle={{
|
||||
cursor: 'default',
|
||||
borderRadius: '0.5rem',
|
||||
maxHeight: maxHeight,
|
||||
minHeight: minHeight,
|
||||
maxWidth: maxWidth,
|
||||
fontFamily:
|
||||
"ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace",
|
||||
fontSize: '0.75rem',
|
||||
lineHeight: '1rem',
|
||||
padding: '0.5rem',
|
||||
paddingRight: '2rem',
|
||||
flex: '1',
|
||||
background: 'transparent',
|
||||
}}
|
||||
>
|
||||
{code.trim()}
|
||||
</SyntaxHighlighter>
|
||||
{copy && (
|
||||
<CopyToClipboard
|
||||
className="absolute top-2 right-2"
|
||||
text={(copyCode || code).trim()}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
import * as CollapsiblePrimitive from '@radix-ui/react-collapsible';
|
||||
|
||||
const Collapsible = CollapsiblePrimitive.Root;
|
||||
|
||||
const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger;
|
||||
|
||||
const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent;
|
||||
|
||||
export { Collapsible, CollapsibleTrigger, CollapsibleContent };
|
||||
@@ -1,157 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import { type DialogProps } from '@radix-ui/react-dialog';
|
||||
import { MagnifyingGlassIcon } from '@radix-ui/react-icons';
|
||||
import { Command as CommandPrimitive } from 'cmdk';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Dialog, DialogContent } from '@/components/ui/dialog';
|
||||
|
||||
const Command = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
Command.displayName = CommandPrimitive.displayName;
|
||||
|
||||
interface CommandDialogProps extends DialogProps {}
|
||||
|
||||
const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
|
||||
return (
|
||||
<Dialog {...props}>
|
||||
<DialogContent className="overflow-hidden p-0">
|
||||
<Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
|
||||
{children}
|
||||
</Command>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
const CommandInput = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Input>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
|
||||
>(({ className, ...props }, ref) => {
|
||||
// noinspection HtmlUnknownAttribute
|
||||
return (
|
||||
// eslint-disable-next-line react/no-unknown-property
|
||||
<div className="flex items-center border-b px-3" cmdk-input-wrapper="">
|
||||
<MagnifyingGlassIcon className="mr-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
<CommandPrimitive.Input
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
CommandInput.displayName = CommandPrimitive.Input.displayName;
|
||||
|
||||
const CommandList = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.List>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive.List
|
||||
ref={ref}
|
||||
className={cn('max-h-[300px] overflow-y-auto overflow-x-hidden', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
CommandList.displayName = CommandPrimitive.List.displayName;
|
||||
|
||||
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}
|
||||
/>
|
||||
));
|
||||
|
||||
CommandEmpty.displayName = CommandPrimitive.Empty.displayName;
|
||||
|
||||
const CommandGroup = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Group>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<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',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
CommandGroup.displayName = CommandPrimitive.Group.displayName;
|
||||
|
||||
const CommandSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn('-mx-1 h-px bg-border', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
CommandSeparator.displayName = CommandPrimitive.Separator.displayName;
|
||||
|
||||
const CommandItem = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'relative flex cursor-default 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',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
CommandItem.displayName = CommandPrimitive.Item.displayName;
|
||||
|
||||
const CommandShortcut = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
'ml-auto text-xs tracking-widest text-gray-700 dark:text-gray-300',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
CommandShortcut.displayName = 'CommandShortcut';
|
||||
|
||||
export {
|
||||
Command,
|
||||
CommandDialog,
|
||||
CommandInput,
|
||||
CommandList,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandItem,
|
||||
CommandShortcut,
|
||||
CommandSeparator,
|
||||
};
|
||||
@@ -1,51 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Button } from './button';
|
||||
import { CopyIcon } from '@radix-ui/react-icons';
|
||||
import { CheckIcon } from '@heroicons/react/24/outline';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
type Props = {
|
||||
text: string;
|
||||
className?: string;
|
||||
withText?: boolean;
|
||||
onCopy?: () => void;
|
||||
};
|
||||
|
||||
const CopyToClipboard: React.FC<Props> = ({
|
||||
text,
|
||||
className,
|
||||
withText,
|
||||
onCopy,
|
||||
}) => {
|
||||
const [successCopy, setSuccessCopy] = useState(false);
|
||||
|
||||
return (
|
||||
<Button
|
||||
className={cn(
|
||||
className,
|
||||
withText
|
||||
? 'cursor-pointer flex flex-row gap-2 items-center mt-2'
|
||||
: 'w-6 h-6 p-0 cursor-pointer',
|
||||
)}
|
||||
variant={withText ? 'default' : 'ghost'}
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(text);
|
||||
setSuccessCopy(true);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
||||
onCopy && onCopy();
|
||||
setTimeout(() => {
|
||||
setSuccessCopy(false);
|
||||
}, 2000);
|
||||
}}
|
||||
>
|
||||
{successCopy ? (
|
||||
<CheckIcon className="w-4 h-4" />
|
||||
) : (
|
||||
<CopyIcon className="w-4 h-4" />
|
||||
)}
|
||||
{withText && (successCopy ? 'Copied' : 'Copy to clipboard')}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
export default CopyToClipboard;
|
||||
@@ -1,119 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import * as DialogPrimitive from '@radix-ui/react-dialog';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const Dialog = DialogPrimitive.Root;
|
||||
|
||||
const DialogTrigger = DialogPrimitive.Trigger;
|
||||
|
||||
const DialogPortal = DialogPrimitive.Portal;
|
||||
|
||||
const DialogClose = DialogPrimitive.Close;
|
||||
|
||||
const DialogOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Overlay
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'fixed inset-0 z-50 bg-background/80 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
|
||||
|
||||
const DialogContent = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<DialogPortal>
|
||||
<DialogOverlay />
|
||||
<DialogPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
{/* <DialogPrimitive.Close 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-accent data-[state=open]:text-muted-foreground">
|
||||
<Cross2Icon className="h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close> */}
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
));
|
||||
DialogContent.displayName = DialogPrimitive.Content.displayName;
|
||||
|
||||
const DialogHeader = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
'flex flex-col space-y-1.5 text-center sm:text-left',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
DialogHeader.displayName = 'DialogHeader';
|
||||
|
||||
const DialogFooter = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
'flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
DialogFooter.displayName = 'DialogFooter';
|
||||
|
||||
const DialogTitle = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'text-lg font-semibold leading-none tracking-tight',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DialogTitle.displayName = DialogPrimitive.Title.displayName;
|
||||
|
||||
const DialogDescription = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn('text-sm text-gray-700 dark:text-gray-300', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DialogDescription.displayName = DialogPrimitive.Description.displayName;
|
||||
|
||||
export {
|
||||
Dialog,
|
||||
DialogPortal,
|
||||
DialogOverlay,
|
||||
DialogTrigger,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogFooter,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
};
|
||||
@@ -1,203 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';
|
||||
import {
|
||||
CheckIcon,
|
||||
ChevronRightIcon,
|
||||
DotFilledIcon,
|
||||
} from '@radix-ui/react-icons';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const DropdownMenu = DropdownMenuPrimitive.Root;
|
||||
|
||||
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
|
||||
|
||||
const DropdownMenuGroup = DropdownMenuPrimitive.Group;
|
||||
|
||||
const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
|
||||
|
||||
const DropdownMenuSub = DropdownMenuPrimitive.Sub;
|
||||
|
||||
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
|
||||
|
||||
const DropdownMenuSubTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
|
||||
inset?: boolean;
|
||||
}
|
||||
>(({ className, inset, children, ...props }, ref) => (
|
||||
<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',
|
||||
inset && 'pl-8',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronRightIcon className="ml-auto h-4 w-4" />
|
||||
</DropdownMenuPrimitive.SubTrigger>
|
||||
));
|
||||
DropdownMenuSubTrigger.displayName =
|
||||
DropdownMenuPrimitive.SubTrigger.displayName;
|
||||
|
||||
const DropdownMenuSubContent = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.SubContent
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-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}
|
||||
/>
|
||||
));
|
||||
DropdownMenuSubContent.displayName =
|
||||
DropdownMenuPrimitive.SubContent.displayName;
|
||||
|
||||
const DropdownMenuContent = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
|
||||
>(({ className, sideOffset = 4, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Portal>
|
||||
<DropdownMenuPrimitive.Content
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
'z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md',
|
||||
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-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}
|
||||
/>
|
||||
</DropdownMenuPrimitive.Portal>
|
||||
));
|
||||
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
|
||||
|
||||
const DropdownMenuItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
|
||||
inset?: boolean;
|
||||
}
|
||||
>(({ className, inset, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'relative flex cursor-default 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',
|
||||
inset && 'pl-8',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
|
||||
|
||||
const DropdownMenuCheckboxItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
|
||||
>(({ className, children, checked, ...props }, ref) => (
|
||||
<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',
|
||||
className,
|
||||
)}
|
||||
checked={checked}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<CheckIcon className="h-4 w-4" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.CheckboxItem>
|
||||
));
|
||||
DropdownMenuCheckboxItem.displayName =
|
||||
DropdownMenuPrimitive.CheckboxItem.displayName;
|
||||
|
||||
const DropdownMenuRadioItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<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',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<DotFilledIcon className="h-4 w-4 fill-current" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.RadioItem>
|
||||
));
|
||||
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
|
||||
|
||||
const DropdownMenuLabel = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
|
||||
inset?: boolean;
|
||||
}
|
||||
>(({ className, inset, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Label
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'px-2 py-1.5 text-sm font-semibold',
|
||||
inset && 'pl-8',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
|
||||
|
||||
const DropdownMenuSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn('-mx-1 my-1 h-px bg-muted', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
|
||||
|
||||
const DropdownMenuShortcut = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||
return (
|
||||
<span
|
||||
className={cn('ml-auto text-xs tracking-widest opacity-60', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
DropdownMenuShortcut.displayName = 'DropdownMenuShortcut';
|
||||
|
||||
export {
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuRadioGroup,
|
||||
};
|
||||
@@ -1,221 +0,0 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { Input } from '@/components/v1/ui/input';
|
||||
import { Textarea } from './textarea';
|
||||
import { Button } from '@/components/v1/ui/button';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { TrashIcon } from '@radix-ui/react-icons';
|
||||
|
||||
export type KeyValueType = {
|
||||
key: string;
|
||||
value: string;
|
||||
hidden: boolean;
|
||||
locked: boolean;
|
||||
deleted: boolean;
|
||||
id?: string; // Optional ID for existing env vars
|
||||
isEditing?: boolean; // Whether the value is being edited
|
||||
hint?: string; // Hint value for existing secrets
|
||||
};
|
||||
|
||||
type PropsType = {
|
||||
label?: string;
|
||||
values: KeyValueType[];
|
||||
setValues?: (x: KeyValueType[]) => void;
|
||||
disabled?: boolean;
|
||||
fileUpload?: boolean;
|
||||
secretOption?: boolean;
|
||||
onDeleteIds?: (ids: string[]) => void; // Callback for deleted IDs
|
||||
onAdd?: (values: KeyValueType[]) => void; // Callback for added values
|
||||
onUpdate?: (values: KeyValueType[]) => void; // Callback for updated values
|
||||
};
|
||||
|
||||
const EnvGroupArray: React.FC<PropsType> = ({
|
||||
label,
|
||||
values,
|
||||
setValues = () => {},
|
||||
disabled,
|
||||
secretOption,
|
||||
onDeleteIds,
|
||||
onAdd,
|
||||
onUpdate,
|
||||
}) => {
|
||||
useEffect(() => {
|
||||
if (!values) {
|
||||
setValues([]);
|
||||
}
|
||||
}, [setValues, values]);
|
||||
|
||||
const handleValueChange = (index: number, key: string, value: any) => {
|
||||
const newValues = [...values];
|
||||
const entry = newValues[index];
|
||||
|
||||
// If this is an existing secret and we're editing the value
|
||||
if (key === 'value' && entry.hint) {
|
||||
// Only set isEditing to true if the value is different from the hint
|
||||
// and not empty (which would be the case when no changes are made)
|
||||
if (value !== entry.hint && value !== '') {
|
||||
newValues[index] = { ...entry, [key]: value, isEditing: true };
|
||||
}
|
||||
} else {
|
||||
newValues[index] = { ...entry, [key]: value };
|
||||
}
|
||||
|
||||
setValues(newValues);
|
||||
|
||||
// If this is an existing entry (has an ID), notify about updates
|
||||
if (entry.id && onUpdate && value !== entry.hint) {
|
||||
onUpdate([newValues[index]]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteToggle = (index: number) => {
|
||||
const newValues = [...values];
|
||||
const entry = newValues[index];
|
||||
const newDeletedState = !entry.deleted;
|
||||
|
||||
newValues[index] = { ...entry, deleted: newDeletedState };
|
||||
setValues(newValues);
|
||||
|
||||
// If this is an existing env var with an ID, notify parent of deleted IDs
|
||||
if (entry.id && onDeleteIds) {
|
||||
const deletedIds = values
|
||||
.filter((v) => v.id && v.deleted)
|
||||
.map((v) => v.id as string);
|
||||
onDeleteIds(deletedIds);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemove = (index: number) => {
|
||||
const entry = values[index];
|
||||
|
||||
if (entry.id) {
|
||||
// For existing entries, mark as deleted
|
||||
const newValues = [...values];
|
||||
newValues[index] = { ...entry, deleted: true };
|
||||
setValues(newValues);
|
||||
|
||||
if (onDeleteIds) {
|
||||
const deletedIds = values
|
||||
.filter((v) => v.id && v.deleted)
|
||||
.map((v) => v.id as string);
|
||||
onDeleteIds(deletedIds);
|
||||
}
|
||||
} else {
|
||||
// For new entries, remove completely
|
||||
const newValues = values.filter((_, i) => i !== index);
|
||||
setValues(newValues);
|
||||
|
||||
if (onAdd) {
|
||||
onAdd(newValues.filter((v) => !v.id));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{label && <div className="mb-2 text-white">{label}</div>}
|
||||
{values?.map((entry: KeyValueType, i: number) => (
|
||||
<div
|
||||
className={cn(
|
||||
'mb-2 flex items-center gap-2',
|
||||
entry.deleted && 'opacity-50 [&>*]:line-through',
|
||||
)}
|
||||
key={i}
|
||||
>
|
||||
<Input
|
||||
placeholder="ex: key"
|
||||
value={entry.key}
|
||||
onChange={(e) => handleValueChange(i, 'key', e.target.value)}
|
||||
disabled={disabled || entry.locked || entry.deleted}
|
||||
className={cn(
|
||||
'w-64',
|
||||
entry.locked && 'bg-gray-200 cursor-not-allowed',
|
||||
)}
|
||||
/>
|
||||
{entry.hidden ? (
|
||||
<Input
|
||||
placeholder={entry.hint}
|
||||
value={entry.isEditing ? entry.value : undefined}
|
||||
onChange={(e) => handleValueChange(i, 'value', e.target.value)}
|
||||
type="password"
|
||||
disabled={disabled || entry.locked || entry.deleted}
|
||||
className={cn(
|
||||
'flex-1',
|
||||
entry.locked && 'bg-gray-200 cursor-not-allowed',
|
||||
!entry.isEditing && entry.hint && 'text-gray-400',
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<Textarea
|
||||
placeholder={entry.hint}
|
||||
value={entry.isEditing ? entry.value : undefined}
|
||||
onChange={(e: any) =>
|
||||
handleValueChange(i, 'value', e.target.value)
|
||||
}
|
||||
rows={entry.value?.split('\n').length || 0}
|
||||
disabled={disabled || entry.locked || entry.deleted}
|
||||
className={cn(
|
||||
'flex-1',
|
||||
entry.locked && 'bg-gray-200 cursor-not-allowed',
|
||||
!entry.isEditing && entry.hint && 'text-gray-400',
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{secretOption && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() =>
|
||||
!entry.locked && handleValueChange(i, 'hidden', !entry.hidden)
|
||||
}
|
||||
disabled={entry.locked || entry.deleted}
|
||||
>
|
||||
{entry.hidden ? 'Unlock' : 'Lock'}
|
||||
</Button>
|
||||
)}
|
||||
{!disabled && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
if (entry.id) {
|
||||
handleDeleteToggle(i);
|
||||
} else {
|
||||
handleRemove(i);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<TrashIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{!disabled && (
|
||||
<div className="flex items-center">
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
const newEntry = {
|
||||
key: '',
|
||||
value: '',
|
||||
hidden: false,
|
||||
locked: false,
|
||||
deleted: false,
|
||||
};
|
||||
const newValues = [...values, newEntry];
|
||||
setValues(newValues);
|
||||
|
||||
if (onAdd) {
|
||||
onAdd([...values.filter((v) => !v.id), newEntry]);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Add row
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default EnvGroupArray;
|
||||
@@ -1,44 +0,0 @@
|
||||
import { ObjectFieldTemplateProps } from '@rjsf/utils';
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
} from '@/components/ui/accordion';
|
||||
|
||||
import { DEFAULT_COLLAPSED } from '../json-form';
|
||||
|
||||
export const CollapsibleSection = (props: ObjectFieldTemplateProps) => {
|
||||
if (!props.title) {
|
||||
return props.properties.map((element, i) => (
|
||||
<div className="property-wrapper ml-4" key={i}>
|
||||
{element.content}
|
||||
</div>
|
||||
));
|
||||
}
|
||||
|
||||
return (
|
||||
<Accordion
|
||||
type="single"
|
||||
collapsible
|
||||
className="w-full"
|
||||
defaultValue={DEFAULT_COLLAPSED.includes(props.title) ? 'closed' : 'open'}
|
||||
>
|
||||
<AccordionItem value="open">
|
||||
<AccordionTrigger>{props.title}</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
{props.description}
|
||||
{props.properties?.length > 0 ? (
|
||||
props.properties.map((element, i) => (
|
||||
<div className="property-wrapper ml-4" key={i}>
|
||||
{element.content}
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="ml-4">empty state</div>
|
||||
)}
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
);
|
||||
};
|
||||
@@ -1,100 +0,0 @@
|
||||
import { BaseInputTemplateProps, getInputProps } from '@rjsf/utils';
|
||||
import {
|
||||
ChangeEvent,
|
||||
FocusEvent,
|
||||
KeyboardEvent,
|
||||
useContext,
|
||||
useEffect,
|
||||
useRef,
|
||||
} from 'react';
|
||||
import { JSONFormContext } from '../json-form';
|
||||
|
||||
export function DynamicSizeInputTemplate(props: BaseInputTemplateProps) {
|
||||
const {
|
||||
schema,
|
||||
id,
|
||||
options,
|
||||
label,
|
||||
value,
|
||||
type,
|
||||
placeholder,
|
||||
required,
|
||||
disabled,
|
||||
readonly,
|
||||
autofocus,
|
||||
onChange,
|
||||
onChangeOverride,
|
||||
onBlur,
|
||||
onFocus,
|
||||
rawErrors,
|
||||
hideError,
|
||||
uiSchema,
|
||||
registry,
|
||||
formContext,
|
||||
...rest
|
||||
} = props;
|
||||
|
||||
const ref = useRef<HTMLTextAreaElement>(null);
|
||||
const { form } = useContext(JSONFormContext);
|
||||
|
||||
const onTextChange = ({
|
||||
target: { value: val },
|
||||
}: ChangeEvent<HTMLTextAreaElement>) => {
|
||||
// Use the options.emptyValue if it is specified and newVal is also an empty string
|
||||
onChange(val === '' ? options.emptyValue || '' : val);
|
||||
};
|
||||
const onTextBlur = ({
|
||||
target: { value: val },
|
||||
}: FocusEvent<HTMLTextAreaElement>) => onBlur(id, val);
|
||||
const onTextFocus = ({
|
||||
target: { value: val },
|
||||
}: FocusEvent<HTMLTextAreaElement>) => onFocus(id, val);
|
||||
|
||||
const inputProps = { ...rest, ...getInputProps(schema, type, options) };
|
||||
delete inputProps.hideLabel; // hideLabel is not a valid prop for textarea
|
||||
|
||||
const setHeight = (e: HTMLTextAreaElement) => {
|
||||
e.style.height = 'auto';
|
||||
e.style.height = `${e.scrollHeight}px`;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// Call adjustHeight whenever the watched value changes externally
|
||||
if (!ref.current) {
|
||||
return;
|
||||
}
|
||||
setHeight(ref.current);
|
||||
}, [ref.current?.value]);
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
if (!form?.current) {
|
||||
return;
|
||||
}
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
form.current.submit();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<textarea
|
||||
ref={ref}
|
||||
id={id}
|
||||
value={value}
|
||||
placeholder={placeholder}
|
||||
disabled={disabled}
|
||||
readOnly={readonly}
|
||||
autoFocus={autofocus}
|
||||
className="overflow-y-hidden"
|
||||
onKeyDown={handleKeyDown}
|
||||
onChange={
|
||||
(onChangeOverride as
|
||||
| ((event: ChangeEvent<HTMLTextAreaElement>) => void)
|
||||
| undefined) || onTextChange
|
||||
}
|
||||
onBlur={onTextBlur}
|
||||
onFocus={onTextFocus}
|
||||
{...inputProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import * as HoverCardPrimitive from '@radix-ui/react-hover-card';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const HoverCard = HoverCardPrimitive.Root;
|
||||
|
||||
const HoverCardTrigger = HoverCardPrimitive.Trigger;
|
||||
|
||||
const HoverCardContent = React.forwardRef<
|
||||
React.ElementRef<typeof HoverCardPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof HoverCardPrimitive.Content>
|
||||
>(({ className, align = 'center', sideOffset = 4, ...props }, ref) => (
|
||||
<HoverCardPrimitive.Content
|
||||
ref={ref}
|
||||
align={align}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
'z-50 w-64 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-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}
|
||||
/>
|
||||
));
|
||||
HoverCardContent.displayName = HoverCardPrimitive.Content.displayName;
|
||||
|
||||
export { HoverCard, HoverCardTrigger, HoverCardContent };
|
||||
@@ -1,148 +0,0 @@
|
||||
type IconProps = React.HTMLAttributes<SVGElement>;
|
||||
|
||||
export const Icons = {
|
||||
logo: (props: IconProps) => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" {...props}>
|
||||
<rect width="256" height="256" fill="none" />
|
||||
<line
|
||||
x1="208"
|
||||
y1="128"
|
||||
x2="128"
|
||||
y2="208"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="16"
|
||||
/>
|
||||
<line
|
||||
x1="192"
|
||||
y1="40"
|
||||
x2="40"
|
||||
y2="192"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="16"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
twitter: (props: IconProps) => (
|
||||
<svg
|
||||
{...props}
|
||||
height="23"
|
||||
viewBox="0 0 1200 1227"
|
||||
width="23"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M714.163 519.284L1160.89 0H1055.03L667.137 450.887L357.328 0H0L468.492 681.821L0 1226.37H105.866L515.491 750.218L842.672 1226.37H1200L714.137 519.284H714.163ZM569.165 687.828L521.697 619.934L144.011 79.6944H306.615L611.412 515.685L658.88 583.579L1055.08 1150.3H892.476L569.165 687.854V687.828Z" />
|
||||
</svg>
|
||||
),
|
||||
gitHub: (props: IconProps) => (
|
||||
<svg viewBox="0 0 438.549 438.549" {...props}>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M409.132 114.573c-19.608-33.596-46.205-60.194-79.798-79.8-33.598-19.607-70.277-29.408-110.063-29.408-39.781 0-76.472 9.804-110.063 29.408-33.596 19.605-60.192 46.204-79.8 79.8C9.803 148.168 0 184.854 0 224.63c0 47.78 13.94 90.745 41.827 128.906 27.884 38.164 63.906 64.572 108.063 79.227 5.14.954 8.945.283 11.419-1.996 2.475-2.282 3.711-5.14 3.711-8.562 0-.571-.049-5.708-.144-15.417a2549.81 2549.81 0 01-.144-25.406l-6.567 1.136c-4.187.767-9.469 1.092-15.846 1-6.374-.089-12.991-.757-19.842-1.999-6.854-1.231-13.229-4.086-19.13-8.559-5.898-4.473-10.085-10.328-12.56-17.556l-2.855-6.57c-1.903-4.374-4.899-9.233-8.992-14.559-4.093-5.331-8.232-8.945-12.419-10.848l-1.999-1.431c-1.332-.951-2.568-2.098-3.711-3.429-1.142-1.331-1.997-2.663-2.568-3.997-.572-1.335-.098-2.43 1.427-3.289 1.525-.859 4.281-1.276 8.28-1.276l5.708.853c3.807.763 8.516 3.042 14.133 6.851 5.614 3.806 10.229 8.754 13.846 14.842 4.38 7.806 9.657 13.754 15.846 17.847 6.184 4.093 12.419 6.136 18.699 6.136 6.28 0 11.704-.476 16.274-1.423 4.565-.952 8.848-2.383 12.847-4.285 1.713-12.758 6.377-22.559 13.988-29.41-10.848-1.14-20.601-2.857-29.264-5.14-8.658-2.286-17.605-5.996-26.835-11.14-9.235-5.137-16.896-11.516-22.985-19.126-6.09-7.614-11.088-17.61-14.987-29.979-3.901-12.374-5.852-26.648-5.852-42.826 0-23.035 7.52-42.637 22.557-58.817-7.044-17.318-6.379-36.732 1.997-58.24 5.52-1.715 13.706-.428 24.554 3.853 10.85 4.283 18.794 7.952 23.84 10.994 5.046 3.041 9.089 5.618 12.135 7.708 17.705-4.947 35.976-7.421 54.818-7.421s37.117 2.474 54.823 7.421l10.849-6.849c7.419-4.57 16.18-8.758 26.262-12.565 10.088-3.805 17.802-4.853 23.134-3.138 8.562 21.509 9.325 40.922 2.279 58.24 15.036 16.18 22.559 35.787 22.559 58.817 0 16.178-1.958 30.497-5.853 42.966-3.9 12.471-8.941 22.457-15.125 29.979-6.191 7.521-13.901 13.85-23.131 18.986-9.232 5.14-18.182 8.85-26.84 11.136-8.662 2.286-18.415 4.004-29.263 5.146 9.894 8.562 14.842 22.077 14.842 40.539v60.237c0 3.422 1.19 6.279 3.572 8.562 2.379 2.279 6.136 2.95 11.276 1.995 44.163-14.653 80.185-41.062 108.068-79.226 27.88-38.161 41.825-81.126 41.825-128.906-.01-39.771-9.818-76.454-29.414-110.049z"
|
||||
></path>
|
||||
</svg>
|
||||
),
|
||||
radix: (props: IconProps) => (
|
||||
<svg viewBox="0 0 25 25" fill="none" {...props}>
|
||||
<path
|
||||
d="M12 25C7.58173 25 4 21.4183 4 17C4 12.5817 7.58173 9 12 9V25Z"
|
||||
fill="currentcolor"
|
||||
></path>
|
||||
<path d="M12 0H4V8H12V0Z" fill="currentcolor"></path>
|
||||
<path
|
||||
d="M17 8C19.2091 8 21 6.20914 21 4C21 1.79086 19.2091 0 17 0C14.7909 0 13 1.79086 13 4C13 6.20914 14.7909 8 17 8Z"
|
||||
fill="currentcolor"
|
||||
></path>
|
||||
</svg>
|
||||
),
|
||||
aria: (props: IconProps) => (
|
||||
<svg role="img" viewBox="0 0 24 24" fill="currentColor" {...props}>
|
||||
<path d="M13.966 22.624l-1.69-4.281H8.122l3.892-9.144 5.662 13.425zM8.884 1.376H0v21.248zm15.116 0h-8.884L24 22.624Z" />
|
||||
</svg>
|
||||
),
|
||||
npm: (props: IconProps) => (
|
||||
<svg viewBox="0 0 24 24" {...props}>
|
||||
<path
|
||||
d="M1.763 0C.786 0 0 .786 0 1.763v20.474C0 23.214.786 24 1.763 24h20.474c.977 0 1.763-.786 1.763-1.763V1.763C24 .786 23.214 0 22.237 0zM5.13 5.323l13.837.019-.009 13.836h-3.464l.01-10.382h-3.456L12.04 19.17H5.113z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
yarn: (props: IconProps) => (
|
||||
<svg viewBox="0 0 24 24" {...props}>
|
||||
<path
|
||||
d="M12 0C5.375 0 0 5.375 0 12s5.375 12 12 12 12-5.375 12-12S18.625 0 12 0zm.768 4.105c.183 0 .363.053.525.157.125.083.287.185.755 1.154.31-.088.468-.042.551-.019.204.056.366.19.463.375.477.917.542 2.553.334 3.605-.241 1.232-.755 2.029-1.131 2.576.324.329.778.899 1.117 1.825.278.774.31 1.478.273 2.015a5.51 5.51 0 0 0 .602-.329c.593-.366 1.487-.917 2.553-.931.714-.009 1.269.445 1.353 1.103a1.23 1.23 0 0 1-.945 1.362c-.649.158-.95.278-1.821.843-1.232.797-2.539 1.242-3.012 1.39a1.686 1.686 0 0 1-.704.343c-.737.181-3.266.315-3.466.315h-.046c-.783 0-1.214-.241-1.45-.491-.658.329-1.51.19-2.122-.134a1.078 1.078 0 0 1-.58-1.153 1.243 1.243 0 0 1-.153-.195c-.162-.25-.528-.936-.454-1.946.056-.723.556-1.367.88-1.71a5.522 5.522 0 0 1 .408-2.256c.306-.727.885-1.348 1.32-1.737-.32-.537-.644-1.367-.329-2.21.227-.602.412-.936.82-1.08h-.005c.199-.074.389-.153.486-.259a3.418 3.418 0 0 1 2.298-1.103c.037-.093.079-.185.125-.283.31-.658.639-1.029 1.024-1.168a.94.94 0 0 1 .328-.06zm.006.7c-.507.016-1.001 1.519-1.001 1.519s-1.27-.204-2.266.871c-.199.218-.468.334-.746.44-.079.028-.176.023-.417.672-.371.991.625 2.094.625 2.094s-1.186.839-1.626 1.881c-.486 1.144-.338 2.261-.338 2.261s-.843.732-.899 1.487c-.051.663.139 1.2.343 1.515.227.343.51.176.51.176s-.561.653-.037.931c.477.25 1.283.394 1.71-.037.31-.31.371-1.001.486-1.283.028-.065.12.111.209.199.097.093.264.195.264.195s-.755.324-.445 1.066c.102.246.468.403 1.066.398.222-.005 2.664-.139 3.313-.296.375-.088.505-.283.505-.283s1.566-.431 2.998-1.357c.917-.598 1.293-.76 2.034-.936.612-.148.57-1.098-.241-1.084-.839.009-1.575.44-2.196.825-1.163.718-1.742.672-1.742.672l-.018-.032c-.079-.13.371-1.293-.134-2.678-.547-1.515-1.413-1.881-1.344-1.997.297-.5 1.038-1.297 1.334-2.78.176-.899.13-2.377-.269-3.151-.074-.144-.732.241-.732.241s-.616-1.371-.788-1.483a.271.271 0 0 0-.157-.046z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
pnpm: (props: IconProps) => (
|
||||
<svg viewBox="0 0 24 24" {...props}>
|
||||
<path
|
||||
d="M0 0v7.5h7.5V0zm8.25 0v7.5h7.498V0zm8.25 0v7.5H24V0zM8.25 8.25v7.5h7.498v-7.5zm8.25 0v7.5H24v-7.5zM0 16.5V24h7.5v-7.5zm8.25 0V24h7.498v-7.5zm8.25 0V24H24v-7.5z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
react: (props: IconProps) => (
|
||||
<svg viewBox="0 0 24 24" {...props}>
|
||||
<path
|
||||
d="M14.23 12.004a2.236 2.236 0 0 1-2.235 2.236 2.236 2.236 0 0 1-2.236-2.236 2.236 2.236 0 0 1 2.235-2.236 2.236 2.236 0 0 1 2.236 2.236zm2.648-10.69c-1.346 0-3.107.96-4.888 2.622-1.78-1.653-3.542-2.602-4.887-2.602-.41 0-.783.093-1.106.278-1.375.793-1.683 3.264-.973 6.365C1.98 8.917 0 10.42 0 12.004c0 1.59 1.99 3.097 5.043 4.03-.704 3.113-.39 5.588.988 6.38.32.187.69.275 1.102.275 1.345 0 3.107-.96 4.888-2.624 1.78 1.654 3.542 2.603 4.887 2.603.41 0 .783-.09 1.106-.275 1.374-.792 1.683-3.263.973-6.365C22.02 15.096 24 13.59 24 12.004c0-1.59-1.99-3.097-5.043-4.032.704-3.11.39-5.587-.988-6.38-.318-.184-.688-.277-1.092-.278zm-.005 1.09v.006c.225 0 .406.044.558.127.666.382.955 1.835.73 3.704-.054.46-.142.945-.25 1.44-.96-.236-2.006-.417-3.107-.534-.66-.905-1.345-1.727-2.035-2.447 1.592-1.48 3.087-2.292 4.105-2.295zm-9.77.02c1.012 0 2.514.808 4.11 2.28-.686.72-1.37 1.537-2.02 2.442-1.107.117-2.154.298-3.113.538-.112-.49-.195-.964-.254-1.42-.23-1.868.054-3.32.714-3.707.19-.09.4-.127.563-.132zm4.882 3.05c.455.468.91.992 1.36 1.564-.44-.02-.89-.034-1.345-.034-.46 0-.915.01-1.36.034.44-.572.895-1.096 1.345-1.565zM12 8.1c.74 0 1.477.034 2.202.093.406.582.802 1.203 1.183 1.86.372.64.71 1.29 1.018 1.946-.308.655-.646 1.31-1.013 1.95-.38.66-.773 1.288-1.18 1.87-.728.063-1.466.098-2.21.098-.74 0-1.477-.035-2.202-.093-.406-.582-.802-1.204-1.183-1.86-.372-.64-.71-1.29-1.018-1.946.303-.657.646-1.313 1.013-1.954.38-.66.773-1.286 1.18-1.868.728-.064 1.466-.098 2.21-.098zm-3.635.254c-.24.377-.48.763-.704 1.16-.225.39-.435.782-.635 1.174-.265-.656-.49-1.31-.676-1.947.64-.15 1.315-.283 2.015-.386zm7.26 0c.695.103 1.365.23 2.006.387-.18.632-.405 1.282-.66 1.933-.2-.39-.41-.783-.64-1.174-.225-.392-.465-.774-.705-1.146zm3.063.675c.484.15.944.317 1.375.498 1.732.74 2.852 1.708 2.852 2.476-.005.768-1.125 1.74-2.857 2.475-.42.18-.88.342-1.355.493-.28-.958-.646-1.956-1.1-2.98.45-1.017.81-2.01 1.085-2.964zm-13.395.004c.278.96.645 1.957 1.1 2.98-.45 1.017-.812 2.01-1.086 2.964-.484-.15-.944-.318-1.37-.5-1.732-.737-2.852-1.706-2.852-2.474 0-.768 1.12-1.742 2.852-2.476.42-.18.88-.342 1.356-.494zm11.678 4.28c.265.657.49 1.312.676 1.948-.64.157-1.316.29-2.016.39.24-.375.48-.762.705-1.158.225-.39.435-.788.636-1.18zm-9.945.02c.2.392.41.783.64 1.175.23.39.465.772.705 1.143-.695-.102-1.365-.23-2.006-.386.18-.63.406-1.282.66-1.933zM17.92 16.32c.112.493.2.968.254 1.423.23 1.868-.054 3.32-.714 3.708-.147.09-.338.128-.563.128-1.012 0-2.514-.807-4.11-2.28.686-.72 1.37-1.536 2.02-2.44 1.107-.118 2.154-.3 3.113-.54zm-11.83.01c.96.234 2.006.415 3.107.532.66.905 1.345 1.727 2.035 2.446-1.595 1.483-3.092 2.295-4.11 2.295-.22-.005-.406-.05-.553-.132-.666-.38-.955-1.834-.73-3.703.054-.46.142-.944.25-1.438zm4.56.64c.44.02.89.034 1.345.034.46 0 .915-.01 1.36-.034-.44.572-.895 1.095-1.345 1.565-.455-.47-.91-.993-1.36-1.565z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
tailwind: (props: IconProps) => (
|
||||
<svg viewBox="0 0 24 24" {...props}>
|
||||
<path
|
||||
d="M12.001,4.8c-3.2,0-5.2,1.6-6,4.8c1.2-1.6,2.6-2.2,4.2-1.8c0.913,0.228,1.565,0.89,2.288,1.624 C13.666,10.618,15.027,12,18.001,12c3.2,0,5.2-1.6,6-4.8c-1.2,1.6-2.6,2.2-4.2,1.8c-0.913-0.228-1.565-0.89-2.288-1.624 C16.337,6.182,14.976,4.8,12.001,4.8z M6.001,12c-3.2,0-5.2,1.6-6,4.8c1.2-1.6,2.6-2.2,4.2-1.8c0.913,0.228,1.565,0.89,2.288,1.624 c1.177,1.194,2.538,2.576,5.512,2.576c3.2,0,5.2-1.6,6-4.8c-1.2,1.6-2.6,2.2-4.2,1.8c-0.913-0.228-1.565-0.89-2.288-1.624 C10.337,13.382,8.976,12,6.001,12z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
google: (props: IconProps) => (
|
||||
<svg role="img" viewBox="0 0 24 24" {...props}>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M12.48 10.92v3.28h7.84c-.24 1.84-.853 3.187-1.787 4.133-1.147 1.147-2.933 2.4-6.053 2.4-4.827 0-8.6-3.893-8.6-8.72s3.773-8.72 8.6-8.72c2.6 0 4.507 1.027 5.907 2.347l2.307-2.307C18.747 1.44 16.133 0 12.48 0 5.867 0 .307 5.387.307 12s5.56 12 12.173 12c3.573 0 6.267-1.173 8.373-3.36 2.16-2.16 2.84-5.213 2.84-7.667 0-.76-.053-1.467-.173-2.053H12.48z"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
apple: (props: IconProps) => (
|
||||
<svg role="img" viewBox="0 0 24 24" {...props}>
|
||||
<path
|
||||
d="M12.152 6.896c-.948 0-2.415-1.078-3.96-1.04-2.04.027-3.91 1.183-4.961 3.014-2.117 3.675-.546 9.103 1.519 12.09 1.013 1.454 2.208 3.09 3.792 3.039 1.52-.065 2.09-.987 3.935-.987 1.831 0 2.35.987 3.96.948 1.637-.026 2.676-1.48 3.676-2.948 1.156-1.688 1.636-3.325 1.662-3.415-.039-.013-3.182-1.221-3.22-4.857-.026-3.04 2.48-4.494 2.597-4.559-1.429-2.09-3.623-2.324-4.39-2.376-2-.156-3.675 1.09-4.61 1.09zM15.53 3.83c.843-1.012 1.4-2.427 1.245-3.83-1.207.052-2.662.805-3.532 1.818-.78.896-1.454 2.338-1.273 3.714 1.338.104 2.715-.688 3.559-1.701"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
paypal: (props: IconProps) => (
|
||||
<svg role="img" viewBox="0 0 24 24" {...props}>
|
||||
<path
|
||||
d="M7.076 21.337H2.47a.641.641 0 0 1-.633-.74L4.944.901C5.026.382 5.474 0 5.998 0h7.46c2.57 0 4.578.543 5.69 1.81 1.01 1.15 1.304 2.42 1.012 4.287-.023.143-.047.288-.077.437-.983 5.05-4.349 6.797-8.647 6.797h-2.19c-.524 0-.968.382-1.05.9l-1.12 7.106zm14.146-14.42a3.35 3.35 0 0 0-.607-.541c-.013.076-.026.175-.041.254-.93 4.778-4.005 7.201-9.138 7.201h-2.19a.563.563 0 0 0-.556.479l-1.187 7.527h-.506l-.24 1.516a.56.56 0 0 0 .554.647h3.882c.46 0 .85-.334.922-.788.06-.26.76-4.852.816-5.09a.932.932 0 0 1 .923-.788h.58c3.76 0 6.705-1.528 7.565-5.946.36-1.847.174-3.388-.777-4.471z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
spinner: (props: IconProps) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
{...props}
|
||||
>
|
||||
<path d="M21 12a9 9 0 1 1-6.219-8.56" />
|
||||
</svg>
|
||||
),
|
||||
};
|
||||
@@ -1,25 +0,0 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
export interface InputProps
|
||||
extends React.InputHTMLAttributes<HTMLInputElement> {}
|
||||
|
||||
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
({ className, type, ...props }, ref) => {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
className={cn(
|
||||
'flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50',
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
Input.displayName = 'Input';
|
||||
|
||||
export { Input };
|
||||
@@ -1,159 +0,0 @@
|
||||
import { cn } from '@/lib/utils';
|
||||
import {
|
||||
RJSFSchema,
|
||||
RJSFValidationError,
|
||||
UiSchema,
|
||||
ValidationData,
|
||||
ValidatorType,
|
||||
} from '@rjsf/utils';
|
||||
import Form from '@rjsf/core';
|
||||
import { PlayIcon } from '@radix-ui/react-icons';
|
||||
import { Button } from './button';
|
||||
import { Loading } from './loading';
|
||||
import { CollapsibleSection } from './form-inputs/collapsible-section';
|
||||
import { DynamicSizeInputTemplate } from './form-inputs/dynamic-size-input-template';
|
||||
import { createContext, useRef } from 'react';
|
||||
|
||||
type JSONPrimitive = string | number | boolean | null | Array<JSONPrimitive>;
|
||||
export type JSONType = {
|
||||
[key: string]: JSONType | JSONPrimitive | Array<JSONType>;
|
||||
};
|
||||
|
||||
export const DEFAULT_COLLAPSED = ['advanced', 'user data'];
|
||||
|
||||
class NoValidation implements ValidatorType {
|
||||
validateFormData(): ValidationData<any> {
|
||||
return { errors: [], errorSchema: {} };
|
||||
}
|
||||
|
||||
toErrorList(): RJSFValidationError[] {
|
||||
return [];
|
||||
}
|
||||
|
||||
isValid(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
rawValidation() {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
interface JSONFormContextSchema {
|
||||
form?: React.RefObject<Form>;
|
||||
}
|
||||
|
||||
export const JSONFormContext = createContext<JSONFormContextSchema>({
|
||||
form: undefined,
|
||||
});
|
||||
|
||||
export function JsonForm({
|
||||
inputSchema,
|
||||
inputData,
|
||||
className,
|
||||
setInput,
|
||||
disabled,
|
||||
onSubmit,
|
||||
}: {
|
||||
inputSchema: JSONType;
|
||||
className?: string;
|
||||
inputData: JSONType;
|
||||
setInput: React.Dispatch<React.SetStateAction<string>>;
|
||||
disabled?: boolean;
|
||||
onSubmit: () => void;
|
||||
}) {
|
||||
const formRef = useRef<Form>(null);
|
||||
|
||||
const schema = {
|
||||
...inputSchema,
|
||||
required: undefined,
|
||||
$schema: undefined,
|
||||
properties: {
|
||||
...(inputSchema.properties as any),
|
||||
triggered_by: undefined,
|
||||
advanced: {
|
||||
// Transform the schema to wrap the triggered by field
|
||||
type: 'object',
|
||||
properties: {
|
||||
triggered_by: inputSchema.properties
|
||||
? (inputSchema.properties as any).triggered_by
|
||||
: undefined,
|
||||
},
|
||||
},
|
||||
},
|
||||
} as RJSFSchema;
|
||||
|
||||
delete schema.properties?.triggered_by;
|
||||
|
||||
const uiSchema: UiSchema<any, RJSFSchema, any> = {
|
||||
input: {
|
||||
'ui:title': 'workflow input',
|
||||
},
|
||||
parents: {
|
||||
'ui:title': 'parent step data',
|
||||
},
|
||||
overrides: {
|
||||
'ui:title': 'step overrides',
|
||||
},
|
||||
user_data: {
|
||||
'ui:title': 'user data',
|
||||
},
|
||||
'ui:order': ['input', 'overrides', 'parents', '*'],
|
||||
};
|
||||
|
||||
return (
|
||||
<JSONFormContext.Provider value={{ form: formRef }}>
|
||||
<div
|
||||
className={cn(
|
||||
className,
|
||||
'w-full h-fit relative rounded-lg overflow-hidden',
|
||||
)}
|
||||
>
|
||||
<Form
|
||||
ref={formRef}
|
||||
formData={inputData}
|
||||
schema={schema}
|
||||
disabled={disabled}
|
||||
templates={{
|
||||
BaseInputTemplate: DynamicSizeInputTemplate,
|
||||
ObjectFieldTemplate: CollapsibleSection,
|
||||
}}
|
||||
uiSchema={uiSchema}
|
||||
validator={new NoValidation()}
|
||||
noHtml5Validate={true}
|
||||
onChange={(data) => {
|
||||
// Transform the data to unwrap the advanced fields
|
||||
const formData = { ...data.formData, ...data.formData.advanced };
|
||||
delete formData.advanced;
|
||||
setInput((prev) =>
|
||||
JSON.stringify({
|
||||
...JSON.parse(prev),
|
||||
...formData,
|
||||
}),
|
||||
);
|
||||
}}
|
||||
onSubmit={onSubmit}
|
||||
onError={(e) => {
|
||||
console.error(e);
|
||||
}}
|
||||
>
|
||||
<Button className="w-fit invisible" disabled={disabled}>
|
||||
{disabled ? (
|
||||
<>
|
||||
<Loading />
|
||||
Playing
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<PlayIcon
|
||||
className={cn(disabled ? 'rotate-180' : '', 'h-4 w-4 mr-2')}
|
||||
/>
|
||||
Play Step
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</Form>
|
||||
</div>
|
||||
</JSONFormContext.Provider>
|
||||
);
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import * as LabelPrimitive from '@radix-ui/react-label';
|
||||
import { cva, type VariantProps } from 'class-variance-authority';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const labelVariants = cva(
|
||||
'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70',
|
||||
);
|
||||
|
||||
const Label = React.forwardRef<
|
||||
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
|
||||
VariantProps<typeof labelVariants>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<LabelPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(labelVariants(), className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
Label.displayName = LabelPrimitive.Root.displayName;
|
||||
|
||||
export { Label };
|
||||
@@ -1,14 +0,0 @@
|
||||
import { Icons } from '@/components/ui/icons.tsx';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
export function Spinner() {
|
||||
return <Icons.spinner className="mr-2 h-4 w-4 animate-spin" />;
|
||||
}
|
||||
|
||||
export function Loading({ className }: { className?: string }) {
|
||||
return (
|
||||
<div className={cn('flex flex-row flex-1 w-full h-full', className)}>
|
||||
<Spinner />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,238 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import {
|
||||
CheckIcon,
|
||||
ChevronRightIcon,
|
||||
DotFilledIcon,
|
||||
} from '@radix-ui/react-icons';
|
||||
import * as MenubarPrimitive from '@radix-ui/react-menubar';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const MenubarMenu = MenubarPrimitive.Menu;
|
||||
|
||||
const MenubarGroup = MenubarPrimitive.Group;
|
||||
|
||||
const MenubarPortal = MenubarPrimitive.Portal;
|
||||
|
||||
const MenubarSub = MenubarPrimitive.Sub;
|
||||
|
||||
const MenubarRadioGroup = MenubarPrimitive.RadioGroup;
|
||||
|
||||
const Menubar = React.forwardRef<
|
||||
React.ElementRef<typeof MenubarPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<MenubarPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'flex h-9 items-center space-x-1 rounded-md border bg-background p-1 shadow-sm',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
Menubar.displayName = MenubarPrimitive.Root.displayName;
|
||||
|
||||
const MenubarTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof MenubarPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Trigger>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<MenubarPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'flex cursor-default select-none items-center rounded-sm px-3 py-1 text-sm font-medium outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
MenubarTrigger.displayName = MenubarPrimitive.Trigger.displayName;
|
||||
|
||||
const MenubarSubTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof MenubarPrimitive.SubTrigger>,
|
||||
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.SubTrigger> & {
|
||||
inset?: boolean;
|
||||
}
|
||||
>(({ className, inset, children, ...props }, ref) => (
|
||||
<MenubarPrimitive.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 focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground',
|
||||
inset && 'pl-8',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronRightIcon className="ml-auto h-4 w-4" />
|
||||
</MenubarPrimitive.SubTrigger>
|
||||
));
|
||||
MenubarSubTrigger.displayName = MenubarPrimitive.SubTrigger.displayName;
|
||||
|
||||
const MenubarSubContent = React.forwardRef<
|
||||
React.ElementRef<typeof MenubarPrimitive.SubContent>,
|
||||
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.SubContent>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<MenubarPrimitive.SubContent
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-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}
|
||||
/>
|
||||
));
|
||||
MenubarSubContent.displayName = MenubarPrimitive.SubContent.displayName;
|
||||
|
||||
const MenubarContent = React.forwardRef<
|
||||
React.ElementRef<typeof MenubarPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Content>
|
||||
>(
|
||||
(
|
||||
{ className, align = 'start', alignOffset = -4, sideOffset = 8, ...props },
|
||||
ref,
|
||||
) => (
|
||||
<MenubarPrimitive.Portal>
|
||||
<MenubarPrimitive.Content
|
||||
ref={ref}
|
||||
align={align}
|
||||
alignOffset={alignOffset}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
'z-50 min-w-[12rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-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}
|
||||
/>
|
||||
</MenubarPrimitive.Portal>
|
||||
),
|
||||
);
|
||||
MenubarContent.displayName = MenubarPrimitive.Content.displayName;
|
||||
|
||||
const MenubarItem = React.forwardRef<
|
||||
React.ElementRef<typeof MenubarPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Item> & {
|
||||
inset?: boolean;
|
||||
}
|
||||
>(({ className, inset, ...props }, ref) => (
|
||||
<MenubarPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||
inset && 'pl-8',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
MenubarItem.displayName = MenubarPrimitive.Item.displayName;
|
||||
|
||||
const MenubarCheckboxItem = React.forwardRef<
|
||||
React.ElementRef<typeof MenubarPrimitive.CheckboxItem>,
|
||||
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.CheckboxItem>
|
||||
>(({ className, children, checked, ...props }, ref) => (
|
||||
<MenubarPrimitive.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 focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||
className,
|
||||
)}
|
||||
checked={checked}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<MenubarPrimitive.ItemIndicator>
|
||||
<CheckIcon className="h-4 w-4" />
|
||||
</MenubarPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</MenubarPrimitive.CheckboxItem>
|
||||
));
|
||||
MenubarCheckboxItem.displayName = MenubarPrimitive.CheckboxItem.displayName;
|
||||
|
||||
const MenubarRadioItem = React.forwardRef<
|
||||
React.ElementRef<typeof MenubarPrimitive.RadioItem>,
|
||||
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.RadioItem>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<MenubarPrimitive.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 focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<MenubarPrimitive.ItemIndicator>
|
||||
<DotFilledIcon className="h-4 w-4 fill-current" />
|
||||
</MenubarPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</MenubarPrimitive.RadioItem>
|
||||
));
|
||||
MenubarRadioItem.displayName = MenubarPrimitive.RadioItem.displayName;
|
||||
|
||||
const MenubarLabel = React.forwardRef<
|
||||
React.ElementRef<typeof MenubarPrimitive.Label>,
|
||||
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Label> & {
|
||||
inset?: boolean;
|
||||
}
|
||||
>(({ className, inset, ...props }, ref) => (
|
||||
<MenubarPrimitive.Label
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'px-2 py-1.5 text-sm font-semibold',
|
||||
inset && 'pl-8',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
MenubarLabel.displayName = MenubarPrimitive.Label.displayName;
|
||||
|
||||
const MenubarSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof MenubarPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<MenubarPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn('-mx-1 my-1 h-px bg-muted', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
MenubarSeparator.displayName = MenubarPrimitive.Separator.displayName;
|
||||
|
||||
const MenubarShortcut = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
'ml-auto text-xs tracking-widest text-gray-700 dark:text-gray-300',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
MenubarShortcut.displayname = 'MenubarShortcut';
|
||||
|
||||
export {
|
||||
Menubar,
|
||||
MenubarMenu,
|
||||
MenubarTrigger,
|
||||
MenubarContent,
|
||||
MenubarItem,
|
||||
MenubarSeparator,
|
||||
MenubarLabel,
|
||||
MenubarCheckboxItem,
|
||||
MenubarRadioGroup,
|
||||
MenubarRadioItem,
|
||||
MenubarPortal,
|
||||
MenubarSubContent,
|
||||
MenubarSubTrigger,
|
||||
MenubarGroup,
|
||||
MenubarSub,
|
||||
MenubarShortcut,
|
||||
};
|
||||
@@ -1,37 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import * as PopoverPrimitive from '@radix-ui/react-popover';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const Popover = PopoverPrimitive.Root;
|
||||
|
||||
const PopoverTrigger = PopoverPrimitive.Trigger;
|
||||
|
||||
const PopoverContent = React.forwardRef<
|
||||
React.ElementRef<typeof PopoverPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content> & {
|
||||
container?: HTMLElement | null;
|
||||
}
|
||||
>(
|
||||
(
|
||||
{ className, container, align = 'center', sideOffset = 4, ...props },
|
||||
ref,
|
||||
) => (
|
||||
<PopoverPrimitive.Portal container={container}>
|
||||
<PopoverPrimitive.Content
|
||||
ref={ref}
|
||||
align={align}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
'z-50 w-72 rounded-lg border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-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}
|
||||
/>
|
||||
</PopoverPrimitive.Portal>
|
||||
),
|
||||
);
|
||||
|
||||
PopoverContent.displayName = PopoverPrimitive.Content.displayName;
|
||||
|
||||
export { Popover, PopoverTrigger, PopoverContent };
|
||||
@@ -1,46 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const ScrollArea = React.forwardRef<
|
||||
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<ScrollAreaPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn('relative overflow-hidden', className)}
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
|
||||
{children}
|
||||
</ScrollAreaPrimitive.Viewport>
|
||||
<ScrollBar />
|
||||
<ScrollAreaPrimitive.Corner />
|
||||
</ScrollAreaPrimitive.Root>
|
||||
));
|
||||
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName;
|
||||
|
||||
const ScrollBar = React.forwardRef<
|
||||
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
|
||||
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||
>(({ className, orientation = 'vertical', ...props }, ref) => (
|
||||
<ScrollAreaPrimitive.ScrollAreaScrollbar
|
||||
ref={ref}
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
'flex touch-none select-none transition-colors',
|
||||
orientation === 'vertical' &&
|
||||
'h-full w-2.5 border-l border-l-transparent p-[1px]',
|
||||
orientation === 'horizontal' &&
|
||||
'h-2.5 flex-col border-t border-t-transparent p-[1px]',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
|
||||
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||
));
|
||||
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName;
|
||||
|
||||
export { ScrollArea, ScrollBar };
|
||||
@@ -1,191 +0,0 @@
|
||||
import { Light as SyntaxHighlighter } from 'react-syntax-highlighter';
|
||||
import typescript from 'react-syntax-highlighter/dist/esm/languages/hljs/typescript';
|
||||
import yaml from 'react-syntax-highlighter/dist/esm/languages/hljs/yaml';
|
||||
import json from 'react-syntax-highlighter/dist/esm/languages/hljs/json';
|
||||
import {
|
||||
anOldHope,
|
||||
atomOneLight,
|
||||
} from 'react-syntax-highlighter/dist/esm/styles/hljs';
|
||||
import CopyToClipboard from './copy-to-clipboard';
|
||||
import { useRef, useState } from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useTheme } from '../theme-provider';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import { Button } from './button';
|
||||
import { CaretSortIcon } from '@radix-ui/react-icons';
|
||||
|
||||
SyntaxHighlighter.registerLanguage('typescript', typescript);
|
||||
SyntaxHighlighter.registerLanguage('yaml', yaml);
|
||||
SyntaxHighlighter.registerLanguage('json', json);
|
||||
|
||||
type Secrets = Record<string, string>;
|
||||
|
||||
enum Formats {
|
||||
TABLE = 'table',
|
||||
JSON = 'json',
|
||||
YAML = 'yaml',
|
||||
DOTENV = 'dotenv',
|
||||
CLI = 'cli',
|
||||
}
|
||||
|
||||
export function SecretCopier({
|
||||
secrets,
|
||||
className,
|
||||
maxHeight,
|
||||
maxWidth,
|
||||
copy,
|
||||
onClick,
|
||||
}: {
|
||||
secrets: Secrets;
|
||||
className?: string;
|
||||
maxHeight?: string;
|
||||
maxWidth?: string;
|
||||
copy?: boolean;
|
||||
onClick?: () => void;
|
||||
}) {
|
||||
const { theme } = useTheme();
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
const [format, setFormat] = useState<Formats>(Formats.DOTENV);
|
||||
|
||||
const renderSecrets = () => {
|
||||
switch (format) {
|
||||
case Formats.JSON:
|
||||
return JSON.stringify(secrets, null, 2);
|
||||
case Formats.YAML:
|
||||
return toYAML(secrets);
|
||||
case Formats.TABLE:
|
||||
return (
|
||||
<table className="w-full">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Env Var</th>
|
||||
<th>Value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{Object.entries(secrets).map(([key, value]) => (
|
||||
<tr key={key}>
|
||||
<td>
|
||||
<CopyToClipboard text={key} /> {key}
|
||||
</td>
|
||||
<td>
|
||||
<CopyToClipboard text={value} /> {value}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
case Formats.CLI:
|
||||
return toCliEnv(secrets);
|
||||
case Formats.DOTENV:
|
||||
default:
|
||||
return toDotEnv(secrets);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={cn(className, 'w-full h-fit relative')}>
|
||||
<div className="mb-2 justify-right flex flex-row items-center">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="-ml-3 h-8 data-[state=open]:bg-accent"
|
||||
>
|
||||
<span>{format}</span>
|
||||
<CaretSortIcon className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start">
|
||||
<DropdownMenuItem onClick={() => setFormat(Formats.DOTENV)}>
|
||||
{Formats.DOTENV}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setFormat(Formats.CLI)}>
|
||||
{Formats.CLI}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setFormat(Formats.JSON)}>
|
||||
{Formats.JSON}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setFormat(Formats.YAML)}>
|
||||
{Formats.YAML}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setFormat(Formats.TABLE)}>
|
||||
{Formats.TABLE}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onKeyDown={() => textareaRef.current?.focus()}
|
||||
onClick={() => {
|
||||
textareaRef.current?.focus();
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
||||
onClick && onClick();
|
||||
}}
|
||||
className="relative flex bg-muted rounded-lg"
|
||||
>
|
||||
{format === Formats.TABLE ? (
|
||||
renderSecrets()
|
||||
) : (
|
||||
<SyntaxHighlighter
|
||||
language="text"
|
||||
style={theme === 'dark' ? anOldHope : atomOneLight}
|
||||
wrapLines={false}
|
||||
lineProps={{
|
||||
style: { wordBreak: 'break-all', whiteSpace: 'pre-wrap' },
|
||||
}}
|
||||
customStyle={{
|
||||
cursor: 'default',
|
||||
borderRadius: '0.5rem',
|
||||
maxHeight: maxHeight,
|
||||
maxWidth: maxWidth,
|
||||
fontFamily:
|
||||
"ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace",
|
||||
fontSize: '0.75rem',
|
||||
lineHeight: '1rem',
|
||||
padding: '0.5rem',
|
||||
flex: '1',
|
||||
background: 'transparent',
|
||||
}}
|
||||
>
|
||||
{renderSecrets() as string}
|
||||
</SyntaxHighlighter>
|
||||
)}
|
||||
</div>
|
||||
{copy && format !== Formats.TABLE && (
|
||||
<CopyToClipboard
|
||||
text={renderSecrets() as string}
|
||||
withText
|
||||
onCopy={() => onClick && onClick()}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function toDotEnv(s: Secrets) {
|
||||
return Object.entries(s)
|
||||
.map(([key, value]) => `${key}="${value}"`)
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
function toCliEnv(s: Secrets) {
|
||||
return Object.entries(s)
|
||||
.map(([key, value]) => `export ${key}="${value}"`)
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
function toYAML(s: Secrets) {
|
||||
return Object.entries(s)
|
||||
.map(([key, value]) => `${key}:"${value}"`)
|
||||
.join('\n');
|
||||
}
|
||||
@@ -1,162 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import {
|
||||
CaretSortIcon,
|
||||
CheckIcon,
|
||||
ChevronDownIcon,
|
||||
ChevronUpIcon,
|
||||
} from '@radix-ui/react-icons';
|
||||
import * as SelectPrimitive from '@radix-ui/react-select';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const Select = SelectPrimitive.Root;
|
||||
|
||||
const SelectGroup = SelectPrimitive.Group;
|
||||
|
||||
const SelectValue = SelectPrimitive.Value;
|
||||
|
||||
const SelectTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<SelectPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<SelectPrimitive.Icon asChild>
|
||||
<CaretSortIcon className="h-4 w-4 opacity-50" />
|
||||
</SelectPrimitive.Icon>
|
||||
</SelectPrimitive.Trigger>
|
||||
));
|
||||
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
|
||||
|
||||
const SelectScrollUpButton = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.ScrollUpButton
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'flex cursor-default items-center justify-center py-1',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronUpIcon />
|
||||
</SelectPrimitive.ScrollUpButton>
|
||||
));
|
||||
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;
|
||||
|
||||
const SelectScrollDownButton = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.ScrollDownButton
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'flex cursor-default items-center justify-center py-1',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronDownIcon />
|
||||
</SelectPrimitive.ScrollDownButton>
|
||||
));
|
||||
SelectScrollDownButton.displayName =
|
||||
SelectPrimitive.ScrollDownButton.displayName;
|
||||
|
||||
const SelectContent = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
|
||||
>(({ className, children, position = 'popper', ...props }, ref) => (
|
||||
<SelectPrimitive.Portal>
|
||||
<SelectPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-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',
|
||||
position === 'popper' &&
|
||||
'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1',
|
||||
className,
|
||||
)}
|
||||
position={position}
|
||||
{...props}
|
||||
>
|
||||
<SelectScrollUpButton />
|
||||
<SelectPrimitive.Viewport
|
||||
className={cn(
|
||||
'p-1',
|
||||
position === 'popper' &&
|
||||
'h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]',
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</SelectPrimitive.Viewport>
|
||||
<SelectScrollDownButton />
|
||||
</SelectPrimitive.Content>
|
||||
</SelectPrimitive.Portal>
|
||||
));
|
||||
SelectContent.displayName = SelectPrimitive.Content.displayName;
|
||||
|
||||
const SelectLabel = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Label>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.Label
|
||||
ref={ref}
|
||||
className={cn('px-2 py-1.5 text-sm font-semibold', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
SelectLabel.displayName = SelectPrimitive.Label.displayName;
|
||||
|
||||
const SelectItem = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<SelectPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<SelectPrimitive.ItemIndicator>
|
||||
<CheckIcon className="h-4 w-4" />
|
||||
</SelectPrimitive.ItemIndicator>
|
||||
</span>
|
||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||
</SelectPrimitive.Item>
|
||||
));
|
||||
SelectItem.displayName = SelectPrimitive.Item.displayName;
|
||||
|
||||
const SelectSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn('-mx-1 my-1 h-px bg-muted', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
|
||||
|
||||
export {
|
||||
Select,
|
||||
SelectGroup,
|
||||
SelectValue,
|
||||
SelectTrigger,
|
||||
SelectContent,
|
||||
SelectLabel,
|
||||
SelectItem,
|
||||
SelectSeparator,
|
||||
SelectScrollUpButton,
|
||||
SelectScrollDownButton,
|
||||
};
|
||||
@@ -1,29 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import * as SeparatorPrimitive from '@radix-ui/react-separator';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const Separator = React.forwardRef<
|
||||
React.ElementRef<typeof SeparatorPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
|
||||
>(
|
||||
(
|
||||
{ className, orientation = 'horizontal', decorative = true, ...props },
|
||||
ref,
|
||||
) => (
|
||||
<SeparatorPrimitive.Root
|
||||
ref={ref}
|
||||
decorative={decorative}
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
'shrink-0 bg-border',
|
||||
orientation === 'horizontal' ? 'h-[1px] w-full' : 'h-full w-[1px]',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
);
|
||||
Separator.displayName = SeparatorPrimitive.Root.displayName;
|
||||
|
||||
export { Separator };
|
||||
@@ -1,138 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import * as SheetPrimitive from '@radix-ui/react-dialog';
|
||||
import { Cross2Icon } from '@radix-ui/react-icons';
|
||||
import { cva, type VariantProps } from 'class-variance-authority';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const Sheet = SheetPrimitive.Root;
|
||||
|
||||
const SheetTrigger = SheetPrimitive.Trigger;
|
||||
|
||||
const SheetClose = SheetPrimitive.Close;
|
||||
|
||||
const SheetPortal = SheetPrimitive.Portal;
|
||||
|
||||
const SheetOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof SheetPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SheetPrimitive.Overlay
|
||||
className={cn(
|
||||
'fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
/>
|
||||
));
|
||||
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName;
|
||||
|
||||
const sheetVariants = cva(
|
||||
'fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500 data-[state=open]:animate-in data-[state=closed]:animate-out',
|
||||
{
|
||||
variants: {
|
||||
side: {
|
||||
top: 'inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top',
|
||||
bottom:
|
||||
'inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom',
|
||||
left: 'inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm',
|
||||
right:
|
||||
'inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
side: 'right',
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
interface SheetContentProps
|
||||
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
|
||||
VariantProps<typeof sheetVariants> {}
|
||||
|
||||
const SheetContent = React.forwardRef<
|
||||
React.ElementRef<typeof SheetPrimitive.Content>,
|
||||
SheetContentProps
|
||||
>(({ side = 'right', className, children, ...props }, ref) => (
|
||||
<SheetPortal>
|
||||
<SheetOverlay />
|
||||
<SheetPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(sheetVariants({ side }), className)}
|
||||
{...props}
|
||||
>
|
||||
<SheetPrimitive.Close 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-secondary">
|
||||
<Cross2Icon className="h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</SheetPrimitive.Close>
|
||||
{children}
|
||||
</SheetPrimitive.Content>
|
||||
</SheetPortal>
|
||||
));
|
||||
SheetContent.displayName = SheetPrimitive.Content.displayName;
|
||||
|
||||
const SheetHeader = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
'flex flex-col space-y-2 text-center sm:text-left',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
SheetHeader.displayName = 'SheetHeader';
|
||||
|
||||
const SheetFooter = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
'flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
SheetFooter.displayName = 'SheetFooter';
|
||||
|
||||
const SheetTitle = React.forwardRef<
|
||||
React.ElementRef<typeof SheetPrimitive.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SheetPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn('text-lg font-semibold text-foreground', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
SheetTitle.displayName = SheetPrimitive.Title.displayName;
|
||||
|
||||
const SheetDescription = React.forwardRef<
|
||||
React.ElementRef<typeof SheetPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SheetPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn('text-sm text-muted-foreground', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
SheetDescription.displayName = SheetPrimitive.Description.displayName;
|
||||
|
||||
export {
|
||||
Sheet,
|
||||
SheetPortal,
|
||||
SheetOverlay,
|
||||
SheetTrigger,
|
||||
SheetClose,
|
||||
SheetContent,
|
||||
SheetHeader,
|
||||
SheetFooter,
|
||||
SheetTitle,
|
||||
SheetDescription,
|
||||
};
|
||||
@@ -1,15 +0,0 @@
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
function Skeleton({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) {
|
||||
return (
|
||||
<div
|
||||
className={cn('animate-pulse rounded-md bg-primary/10', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Skeleton };
|
||||
@@ -1,44 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import { cva, type VariantProps } from 'class-variance-authority';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const stepsVariants = cva(
|
||||
'ml-4 mb-12 border-l border-border pl-6 dark:border-border [counter-reset:step] flex flex-col gap-12',
|
||||
);
|
||||
|
||||
interface StepProps {
|
||||
title?: string;
|
||||
children: React.ReactNode;
|
||||
stepNumber?: number; // Add the stepNumber prop
|
||||
}
|
||||
|
||||
const Step = ({ title, children, stepNumber }: StepProps) => (
|
||||
<div className="relative">
|
||||
<div className="absolute w-[33px] h-[33px] border-4 border-muted bg-muted rounded-full text-foreground text-base font-normal text-center mt-[3px] ml-[-41px]">
|
||||
{stepNumber}
|
||||
</div>
|
||||
<div className="pl-12">
|
||||
{title && <h3 className="text-lg font-semibold mb-2">{title}</h3>}
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const Steps = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof stepsVariants>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<div ref={ref} className={cn(stepsVariants(), className)} {...props}>
|
||||
{React.Children.map(children, (child, index) =>
|
||||
React.isValidElement(child)
|
||||
? React.cloneElement(child as React.ReactElement<any>, {
|
||||
stepNumber: index + 1,
|
||||
})
|
||||
: child,
|
||||
)}
|
||||
</div>
|
||||
));
|
||||
|
||||
Steps.displayName = 'Steps';
|
||||
|
||||
export { Steps, Step };
|
||||
@@ -1,27 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import * as SwitchPrimitives from '@radix-ui/react-switch';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const Switch = React.forwardRef<
|
||||
React.ElementRef<typeof SwitchPrimitives.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SwitchPrimitives.Root
|
||||
className={cn(
|
||||
'peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
>
|
||||
<SwitchPrimitives.Thumb
|
||||
className={cn(
|
||||
'pointer-events-none block h-4 w-4 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0',
|
||||
)}
|
||||
/>
|
||||
</SwitchPrimitives.Root>
|
||||
));
|
||||
Switch.displayName = SwitchPrimitives.Root.displayName;
|
||||
|
||||
export { Switch };
|
||||
@@ -1,120 +0,0 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const Table = React.forwardRef<
|
||||
HTMLTableElement,
|
||||
React.HTMLAttributes<HTMLTableElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div className="relative w-full overflow-auto">
|
||||
<table
|
||||
ref={ref}
|
||||
className={cn('w-full caption-bottom text-sm', className)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
));
|
||||
Table.displayName = 'Table';
|
||||
|
||||
const TableHeader = React.forwardRef<
|
||||
HTMLTableSectionElement,
|
||||
React.HTMLAttributes<HTMLTableSectionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<thead ref={ref} className={cn('[&_tr]:border-b', className)} {...props} />
|
||||
));
|
||||
TableHeader.displayName = 'TableHeader';
|
||||
|
||||
const TableBody = React.forwardRef<
|
||||
HTMLTableSectionElement,
|
||||
React.HTMLAttributes<HTMLTableSectionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<tbody
|
||||
ref={ref}
|
||||
className={cn('[&_tr:last-child]:border-0', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TableBody.displayName = 'TableBody';
|
||||
|
||||
const TableFooter = React.forwardRef<
|
||||
HTMLTableSectionElement,
|
||||
React.HTMLAttributes<HTMLTableSectionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<tfoot
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'border-t bg-muted/50 font-medium [&>tr]:last:border-b-0',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TableFooter.displayName = 'TableFooter';
|
||||
|
||||
const TableRow = React.forwardRef<
|
||||
HTMLTableRowElement,
|
||||
React.HTMLAttributes<HTMLTableRowElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<tr
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TableRow.displayName = 'TableRow';
|
||||
|
||||
const TableHead = React.forwardRef<
|
||||
HTMLTableCellElement,
|
||||
React.ThHTMLAttributes<HTMLTableCellElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<th
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'h-10 px-2 text-left align-middle font-medium text-gray-700 dark:text-gray-300 [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TableHead.displayName = 'TableHead';
|
||||
|
||||
const TableCell = React.forwardRef<
|
||||
HTMLTableCellElement,
|
||||
React.TdHTMLAttributes<HTMLTableCellElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<td
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TableCell.displayName = 'TableCell';
|
||||
|
||||
const TableCaption = React.forwardRef<
|
||||
HTMLTableCaptionElement,
|
||||
React.HTMLAttributes<HTMLTableCaptionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<caption
|
||||
ref={ref}
|
||||
className={cn('mt-4 text-sm text-gray-700 dark:text-gray-300', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TableCaption.displayName = 'TableCaption';
|
||||
|
||||
export {
|
||||
Table,
|
||||
TableHeader,
|
||||
TableBody,
|
||||
TableFooter,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TableCell,
|
||||
TableCaption,
|
||||
};
|
||||
@@ -1,67 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import * as TabsPrimitive from '@radix-ui/react-tabs';
|
||||
import { cva } from 'class-variance-authority';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const Tabs = TabsPrimitive.Root;
|
||||
|
||||
const tabsListVariants = cva('', {
|
||||
variants: {
|
||||
layout: {
|
||||
default:
|
||||
'inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-gray-700 dark:text-gray-300',
|
||||
underlined:
|
||||
'w-full justify-start rounded-none border-b bg-transparent p-0',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
layout: 'default',
|
||||
},
|
||||
});
|
||||
|
||||
const TabsList = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.List>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List> & {
|
||||
layout?: 'default' | 'underlined';
|
||||
}
|
||||
>(({ className, layout = 'default', ...props }, ref) => (
|
||||
<TabsPrimitive.List
|
||||
ref={ref}
|
||||
className={cn(tabsListVariants({ layout }), className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TabsList.displayName = TabsPrimitive.List.displayName;
|
||||
|
||||
const tabsTriggerVariants = cva('', {
|
||||
variants: {
|
||||
variant: {
|
||||
underlined:
|
||||
'relative rounded-none border-b-2 border-b-transparent bg-transparent px-3 py-1 text-sm font-medium text-gray-700 dark:text-gray-300 shadow-none transition-none focus-visible:ring-0 data-[state=active]:border-b-primary data-[state=active]:text-foreground data-[state=active]:shadow-none ',
|
||||
default:
|
||||
'inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 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-background data-[state=active]:text-foreground data-[state=active]:shadow',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
},
|
||||
});
|
||||
|
||||
const TabsTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger> & {
|
||||
variant?: 'default' | 'underlined';
|
||||
}
|
||||
>(({ className, variant = 'default', ...props }, ref) => (
|
||||
<TabsPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(tabsTriggerVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
|
||||
|
||||
const TabsContent = TabsPrimitive.Content;
|
||||
|
||||
export { Tabs, TabsList, TabsTrigger, TabsContent };
|
||||
@@ -1,24 +0,0 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
export interface TextareaProps
|
||||
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
|
||||
|
||||
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
|
||||
({ className, ...props }, ref) => {
|
||||
return (
|
||||
<textarea
|
||||
className={cn(
|
||||
'flex w-full rounded-md border min-h-9 border-input bg-transparent px-3 py-2 text-sm shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50',
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
Textarea.displayName = 'Textarea';
|
||||
|
||||
export { Textarea };
|
||||
@@ -1,127 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import { Cross2Icon } from '@radix-ui/react-icons';
|
||||
import * as ToastPrimitives from '@radix-ui/react-toast';
|
||||
import { cva, type VariantProps } from 'class-variance-authority';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const ToastProvider = ToastPrimitives.Provider;
|
||||
|
||||
const ToastViewport = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Viewport>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Viewport
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
ToastViewport.displayName = ToastPrimitives.Viewport.displayName;
|
||||
|
||||
const toastVariants = cva(
|
||||
'group pointer-events-auto relative flex w-full items-center justify-between space-x-2 overflow-hidden rounded-md border p-4 pr-6 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: 'border-foreground bg-background text-foreground',
|
||||
destructive:
|
||||
'destructive group border-destructive bg-destructive text-destructive-foreground',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const Toast = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
|
||||
VariantProps<typeof toastVariants>
|
||||
>(({ className, variant, ...props }, ref) => {
|
||||
return (
|
||||
<ToastPrimitives.Root
|
||||
ref={ref}
|
||||
className={cn(toastVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
Toast.displayName = ToastPrimitives.Root.displayName;
|
||||
|
||||
const ToastAction = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Action>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Action
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium transition-colors hover:bg-secondary focus:outline-none focus:ring-1 focus:ring-ring 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}
|
||||
/>
|
||||
));
|
||||
ToastAction.displayName = ToastPrimitives.Action.displayName;
|
||||
|
||||
const ToastClose = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Close>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Close
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'absolute right-1 top-1 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-1 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600',
|
||||
className,
|
||||
)}
|
||||
toast-close=""
|
||||
{...props}
|
||||
>
|
||||
<Cross2Icon className="h-4 w-4" />
|
||||
</ToastPrimitives.Close>
|
||||
));
|
||||
ToastClose.displayName = ToastPrimitives.Close.displayName;
|
||||
|
||||
const ToastTitle = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Title
|
||||
ref={ref}
|
||||
className={cn('text-sm font-semibold [&+div]:text-xs', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
ToastTitle.displayName = ToastPrimitives.Title.displayName;
|
||||
|
||||
const ToastDescription = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Description
|
||||
ref={ref}
|
||||
className={cn('text-sm opacity-90', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
ToastDescription.displayName = ToastPrimitives.Description.displayName;
|
||||
|
||||
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>;
|
||||
|
||||
type ToastActionElement = React.ReactElement<typeof ToastAction>;
|
||||
|
||||
export {
|
||||
type ToastProps,
|
||||
type ToastActionElement,
|
||||
ToastProvider,
|
||||
ToastViewport,
|
||||
Toast,
|
||||
ToastTitle,
|
||||
ToastDescription,
|
||||
ToastClose,
|
||||
ToastAction,
|
||||
};
|
||||
@@ -1,33 +0,0 @@
|
||||
import { useToast } from '@/components/hooks/use-toast';
|
||||
import {
|
||||
Toast,
|
||||
ToastClose,
|
||||
ToastDescription,
|
||||
ToastProvider,
|
||||
ToastTitle,
|
||||
ToastViewport,
|
||||
} from '@/components/ui/toast';
|
||||
|
||||
export function Toaster() {
|
||||
const { toasts } = useToast();
|
||||
|
||||
return (
|
||||
<ToastProvider>
|
||||
{toasts.map(function ({ id, title, description, action, ...props }) {
|
||||
return (
|
||||
<Toast key={id} {...props}>
|
||||
<div className="grid gap-1">
|
||||
{title && <ToastTitle>{title}</ToastTitle>}
|
||||
{description && (
|
||||
<ToastDescription>{description}</ToastDescription>
|
||||
)}
|
||||
</div>
|
||||
{action}
|
||||
<ToastClose />
|
||||
</Toast>
|
||||
);
|
||||
})}
|
||||
<ToastViewport />
|
||||
</ToastProvider>
|
||||
);
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import * as TooltipPrimitive from '@radix-ui/react-tooltip';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const TooltipProvider = TooltipPrimitive.Provider;
|
||||
|
||||
const Tooltip = TooltipPrimitive.Root;
|
||||
|
||||
const TooltipTrigger = TooltipPrimitive.Trigger;
|
||||
|
||||
const TooltipContent = React.forwardRef<
|
||||
React.ElementRef<typeof TooltipPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
|
||||
>(({ className, sideOffset = 4, ...props }, ref) => (
|
||||
<TooltipPrimitive.Content
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
'z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground 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}
|
||||
/>
|
||||
));
|
||||
TooltipContent.displayName = TooltipPrimitive.Content.displayName;
|
||||
|
||||
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
|
||||
@@ -30,7 +30,7 @@ const ccIcons: Record<string, IconType> = {
|
||||
link: LuBanknote,
|
||||
};
|
||||
|
||||
export interface PaymentMethodsProps {
|
||||
interface PaymentMethodsProps {
|
||||
hasMethods?: boolean;
|
||||
methods?: TenantPaymentMethod[];
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import type { ToastActionElement, ToastProps } from '@/components/ui/toast';
|
||||
import type { ToastActionElement, ToastProps } from '@/components/v1/ui/toast';
|
||||
|
||||
const TOAST_LIMIT = 1;
|
||||
const TOAST_REMOVE_DELAY = 1000000;
|
||||
@@ -68,7 +68,7 @@ const addToRemoveQueue = (toastId: string) => {
|
||||
toastTimeouts.set(toastId, timeout);
|
||||
};
|
||||
|
||||
export const reducer = (state: State, action: Action): State => {
|
||||
const reducer = (state: State, action: Action): State => {
|
||||
switch (action.type) {
|
||||
case 'ADD_TOAST':
|
||||
return {
|
||||
@@ -187,4 +187,4 @@ function useToast() {
|
||||
};
|
||||
}
|
||||
|
||||
export { useToast, toast };
|
||||
export { useToast };
|
||||
|
||||
@@ -1,360 +0,0 @@
|
||||
import React, { useMemo, useCallback } from 'react';
|
||||
import { Group } from '@visx/group';
|
||||
import { AreaClosed, Line, Bar } from '@visx/shape';
|
||||
import {
|
||||
withTooltip,
|
||||
TooltipWithBounds,
|
||||
Tooltip,
|
||||
defaultStyles,
|
||||
} from '@visx/tooltip';
|
||||
import { GridRows, GridColumns } from '@visx/grid';
|
||||
import { WithTooltipProvidedProps } from '@visx/tooltip/lib/enhancers/withTooltip';
|
||||
import { scaleTime, scaleLinear } from '@visx/scale';
|
||||
import { AxisLeft, AxisBottom } from '@visx/axis';
|
||||
import { LinearGradient } from '@visx/gradient';
|
||||
import { curveMonotoneX } from '@visx/curve';
|
||||
import { localPoint } from '@visx/event';
|
||||
import { max, extent, bisector } from '@visx/vendor/d3-array';
|
||||
import { timeFormat } from '@visx/vendor/d3-time-format';
|
||||
import { Text } from '@visx/text';
|
||||
|
||||
const getDate = (d: MetricValue) => d.date;
|
||||
const getValue = (d: MetricValue) => d.value;
|
||||
|
||||
// format to 2 decimal places
|
||||
export const format2Dec = (d: number) => {
|
||||
if (!d.toFixed) {
|
||||
return '0.00';
|
||||
}
|
||||
|
||||
return `${d.toFixed(2)}`;
|
||||
};
|
||||
|
||||
const bisectDate = bisector<MetricValue, Date>((d) => d.date).left;
|
||||
|
||||
export interface MetricValue {
|
||||
date: Date;
|
||||
value: number;
|
||||
}
|
||||
type TooltipData = MetricValue;
|
||||
|
||||
const formatDate = timeFormat('%y-%m-%d %I:%M:%S');
|
||||
|
||||
const accentColor = '#ffffff44';
|
||||
const background = '#1E293B';
|
||||
const background2 = '#8c77e0';
|
||||
const accentColorDark = '#8c77e0';
|
||||
|
||||
const tooltipStyles = {
|
||||
...defaultStyles,
|
||||
border: '1px solid white',
|
||||
color: 'white',
|
||||
background,
|
||||
};
|
||||
|
||||
const axisColor = '#cecece';
|
||||
|
||||
const axisBottomTickLabelProps = {
|
||||
textAnchor: 'middle' as const,
|
||||
fontFamily: 'Arial',
|
||||
fontSize: 10,
|
||||
fill: axisColor,
|
||||
};
|
||||
|
||||
const axisLeftTickLabelProps = {
|
||||
dx: '-0.25em',
|
||||
dy: '0.25em',
|
||||
fontFamily: 'Arial',
|
||||
fontSize: 10,
|
||||
textAnchor: 'end' as const,
|
||||
fill: axisColor,
|
||||
};
|
||||
|
||||
export const formatPercentTooltip = (d: number) => `${format2Dec(d)}%`;
|
||||
|
||||
type AreaChartProps = {
|
||||
data: MetricValue[];
|
||||
kind: 'area' | 'bar';
|
||||
gradientColor?: string;
|
||||
width: number;
|
||||
height: number;
|
||||
hideBottomAxis?: boolean;
|
||||
hideLeftAxis?: boolean;
|
||||
children?: React.ReactNode;
|
||||
yLabel?: string;
|
||||
xLabel?: string;
|
||||
yDomain?: [number, number];
|
||||
xDomain?: [Date, Date];
|
||||
centerText?: string;
|
||||
tooltipFormat?: (d: number) => string;
|
||||
};
|
||||
|
||||
export default withTooltip<AreaChartProps, TooltipData>(
|
||||
({
|
||||
data,
|
||||
kind,
|
||||
gradientColor = background2,
|
||||
width,
|
||||
height,
|
||||
hideBottomAxis = false,
|
||||
hideLeftAxis = false,
|
||||
children,
|
||||
yLabel,
|
||||
xLabel,
|
||||
yDomain,
|
||||
xDomain,
|
||||
centerText,
|
||||
showTooltip,
|
||||
hideTooltip,
|
||||
tooltipFormat,
|
||||
tooltipData,
|
||||
tooltipTop = 0,
|
||||
tooltipLeft = 0,
|
||||
}: AreaChartProps & WithTooltipProvidedProps<TooltipData>) => {
|
||||
if (width < 10) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const innerWidth = width;
|
||||
const innerHeight = height;
|
||||
|
||||
const dateScale = useMemo(
|
||||
() =>
|
||||
scaleTime<number>({
|
||||
range: [0, width],
|
||||
domain: xDomain || (extent(data, getDate) as [Date, Date]),
|
||||
}),
|
||||
[width, data, xDomain],
|
||||
);
|
||||
|
||||
const yScale = useMemo(
|
||||
() =>
|
||||
scaleLinear<number>({
|
||||
range: [height, 0],
|
||||
domain: yDomain || [0, 1.3 * (max(data, getValue) || 0)],
|
||||
nice: true,
|
||||
}),
|
||||
[height, data, yDomain],
|
||||
);
|
||||
|
||||
const handleTooltip = useCallback(
|
||||
(
|
||||
event:
|
||||
| React.TouchEvent<SVGRectElement>
|
||||
| React.MouseEvent<SVGRectElement>,
|
||||
) => {
|
||||
const { x } = localPoint(event) || { x: 0 };
|
||||
const x0 = dateScale.invert(x);
|
||||
const index = bisectDate(data, x0, 1);
|
||||
const d0 = data[index - 1];
|
||||
const d1 = data[index];
|
||||
let d = d0;
|
||||
if (d1 && getDate(d1)) {
|
||||
d =
|
||||
x0.valueOf() - getDate(d0).valueOf() >
|
||||
getDate(d1).valueOf() - x0.valueOf()
|
||||
? d1
|
||||
: d0;
|
||||
}
|
||||
|
||||
showTooltip({
|
||||
tooltipData: d,
|
||||
tooltipLeft: x,
|
||||
tooltipTop: yScale(getValue(d)),
|
||||
});
|
||||
},
|
||||
[showTooltip, yScale, dateScale, data],
|
||||
);
|
||||
|
||||
let barWidth = innerWidth / data.length;
|
||||
|
||||
if (barWidth <= 5) {
|
||||
barWidth = 6;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<svg width={width} height={height} overflow={'visible'}>
|
||||
{centerText && (
|
||||
<Text className="fill-foreground" x="50%" y="50%" dx={-200}>
|
||||
{centerText}
|
||||
</Text>
|
||||
)}
|
||||
<rect
|
||||
x={0}
|
||||
y={0}
|
||||
width={innerWidth}
|
||||
height={innerHeight}
|
||||
fill="url(#area-background-gradient)"
|
||||
rx={14}
|
||||
/>
|
||||
<GridRows
|
||||
left={0}
|
||||
scale={yScale}
|
||||
width={innerWidth}
|
||||
height={innerHeight}
|
||||
strokeDasharray="1,3"
|
||||
stroke={accentColor}
|
||||
strokeOpacity={0.6}
|
||||
pointerEvents="none"
|
||||
/>
|
||||
<GridColumns
|
||||
top={0}
|
||||
left={0}
|
||||
scale={dateScale}
|
||||
width={innerWidth}
|
||||
height={innerHeight}
|
||||
strokeDasharray="1,3"
|
||||
stroke={accentColor}
|
||||
strokeOpacity={0.6}
|
||||
pointerEvents="none"
|
||||
/>
|
||||
<Group height={height} width={width}>
|
||||
<LinearGradient
|
||||
id="gradient"
|
||||
from={gradientColor}
|
||||
fromOpacity={1}
|
||||
to={gradientColor}
|
||||
toOpacity={0.2}
|
||||
height={innerHeight}
|
||||
/>
|
||||
{kind == 'bar' &&
|
||||
data.map((d, i) => {
|
||||
if (i == 0) {
|
||||
return (
|
||||
<Bar
|
||||
key={i}
|
||||
x={dateScale(getDate(d)) || 0}
|
||||
y={yScale(getValue(d)) || 0}
|
||||
width={(barWidth - 4) / 2}
|
||||
height={innerHeight - yScale(getValue(d)) || 0}
|
||||
fill="url(#gradient)"
|
||||
rx={2}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Bar
|
||||
key={i}
|
||||
x={(dateScale(getDate(d)) || 0) - barWidth / 2}
|
||||
y={yScale(getValue(d)) || 0}
|
||||
width={barWidth - 4}
|
||||
height={innerHeight - yScale(getValue(d)) || 0}
|
||||
fill="url(#gradient)"
|
||||
rx={2}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{kind == 'area' && (
|
||||
<AreaClosed<MetricValue>
|
||||
data={data}
|
||||
x={(d) => dateScale(d.date) || 0}
|
||||
y={(d) => yScale(d.value) || 0}
|
||||
yScale={yScale}
|
||||
strokeWidth={1}
|
||||
stroke="url(#gradient)"
|
||||
fill="url(#gradient)"
|
||||
curve={curveMonotoneX}
|
||||
height={innerHeight}
|
||||
/>
|
||||
)}
|
||||
{!hideBottomAxis && (
|
||||
<AxisBottom
|
||||
top={height}
|
||||
scale={dateScale}
|
||||
numTicks={width > 520 ? 10 : 5}
|
||||
stroke={axisColor}
|
||||
tickStroke={axisColor}
|
||||
tickLabelProps={axisBottomTickLabelProps}
|
||||
label={xLabel}
|
||||
/>
|
||||
)}
|
||||
{!hideLeftAxis && (
|
||||
<AxisLeft
|
||||
scale={yScale}
|
||||
numTicks={5}
|
||||
stroke={axisColor}
|
||||
tickStroke={axisColor}
|
||||
tickLabelProps={axisLeftTickLabelProps}
|
||||
label={yLabel}
|
||||
labelClassName="text-white fill-foreground"
|
||||
/>
|
||||
)}
|
||||
{children}
|
||||
</Group>
|
||||
<Bar
|
||||
x={0}
|
||||
y={0}
|
||||
width={innerWidth}
|
||||
height={innerHeight}
|
||||
fill="transparent"
|
||||
rx={14}
|
||||
onTouchStart={handleTooltip}
|
||||
onTouchMove={handleTooltip}
|
||||
onMouseMove={handleTooltip}
|
||||
onMouseLeave={() => hideTooltip()}
|
||||
/>
|
||||
{data.length > 0 && tooltipData && (
|
||||
<g>
|
||||
<Line
|
||||
from={{ x: tooltipLeft, y: 0 }}
|
||||
to={{ x: tooltipLeft, y: innerHeight + 0 }}
|
||||
stroke={accentColorDark}
|
||||
strokeWidth={2}
|
||||
pointerEvents="none"
|
||||
strokeDasharray="5,2"
|
||||
/>
|
||||
<circle
|
||||
cx={tooltipLeft}
|
||||
cy={tooltipTop + 1}
|
||||
r={4}
|
||||
fill="black"
|
||||
fillOpacity={0.1}
|
||||
stroke="black"
|
||||
strokeOpacity={0.1}
|
||||
strokeWidth={2}
|
||||
pointerEvents="none"
|
||||
/>
|
||||
<circle
|
||||
cx={tooltipLeft}
|
||||
cy={tooltipTop}
|
||||
r={4}
|
||||
fill={accentColorDark}
|
||||
stroke="white"
|
||||
strokeWidth={2}
|
||||
pointerEvents="none"
|
||||
/>
|
||||
</g>
|
||||
)}
|
||||
</svg>
|
||||
{data.length > 0 && tooltipData && (
|
||||
<div>
|
||||
<TooltipWithBounds
|
||||
key={Math.random()}
|
||||
top={tooltipTop - 24}
|
||||
left={tooltipLeft}
|
||||
style={tooltipStyles}
|
||||
>
|
||||
{tooltipFormat
|
||||
? tooltipFormat(getValue(tooltipData))
|
||||
: getValue(tooltipData)}
|
||||
</TooltipWithBounds>
|
||||
<Tooltip
|
||||
top={innerHeight - 14}
|
||||
left={tooltipLeft}
|
||||
style={{
|
||||
...defaultStyles,
|
||||
minWidth: 72,
|
||||
textAlign: 'center',
|
||||
transform: 'translateX(-50%)',
|
||||
}}
|
||||
>
|
||||
{formatDate(getDate(tooltipData))}
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -1,6 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import { CheckIcon, PlusCircledIcon } from '@radix-ui/react-icons';
|
||||
import { Column } from '@tanstack/react-table';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Badge } from '@/components/v1/ui/badge';
|
||||
@@ -27,17 +26,6 @@ import { useForm } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
|
||||
interface DataTableFacetedFilterProps<TData, TValue> {
|
||||
column?: Column<TData, TValue>;
|
||||
title?: string;
|
||||
type?: ToolbarType;
|
||||
options?: {
|
||||
label: string;
|
||||
value: string;
|
||||
icon?: React.ComponentType<{ className?: string }>;
|
||||
}[];
|
||||
}
|
||||
|
||||
const keyValuePairSchema = z.object({
|
||||
key: z.string().min(1, 'Key is required'),
|
||||
value: z.string().min(1, 'Value is required'),
|
||||
@@ -50,23 +38,6 @@ const arrayInputSchema = z.object({
|
||||
type KeyValuePair = z.infer<typeof keyValuePairSchema>;
|
||||
type ArrayInput = z.infer<typeof arrayInputSchema>;
|
||||
|
||||
export function DataTableFacetedFilter<TData, TValue>({
|
||||
column,
|
||||
title,
|
||||
type = ToolbarType.Checkbox,
|
||||
options,
|
||||
}: DataTableFacetedFilterProps<TData, TValue>) {
|
||||
return (
|
||||
<Combobox
|
||||
values={column?.getFilterValue() as string[]}
|
||||
title={title}
|
||||
type={type}
|
||||
options={options}
|
||||
setValues={(values) => column?.setFilterValue(values)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function Combobox({
|
||||
values = [],
|
||||
title,
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import { Column } from '@tanstack/react-table';
|
||||
|
||||
import { ToolbarType } from './data-table-toolbar';
|
||||
import { Combobox } from '../combobox/combobox';
|
||||
import { Label } from '../../ui/label';
|
||||
import { Checkbox } from '../../ui/checkbox';
|
||||
|
||||
interface DataTableFacetedFilterProps<TData, TValue> {
|
||||
column?: Column<TData, TValue>;
|
||||
title?: string;
|
||||
type?: ToolbarType;
|
||||
options?: {
|
||||
label: string;
|
||||
value: string;
|
||||
icon?: React.ComponentType<{ className?: string }>;
|
||||
}[];
|
||||
}
|
||||
|
||||
export function DataTableFacetedFilter<TData, TValue>({
|
||||
column,
|
||||
title,
|
||||
type = ToolbarType.Checkbox,
|
||||
options,
|
||||
}: DataTableFacetedFilterProps<TData, TValue>) {
|
||||
const value = column?.getFilterValue();
|
||||
|
||||
if (type === ToolbarType.Switch) {
|
||||
return (
|
||||
<div className="flex items-center space-x-2 border p-2 rounded-md border-dashed h-[32px]">
|
||||
<Checkbox
|
||||
id="toolbar-switch"
|
||||
checked={!!value}
|
||||
onCheckedChange={(e) =>
|
||||
column?.setFilterValue(e.valueOf() === true ? true : undefined)
|
||||
}
|
||||
/>
|
||||
<Label htmlFor="toolbar-switch" className="text-xs">
|
||||
Flatten
|
||||
</Label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Combobox
|
||||
values={typeof value === 'string' ? [value] : (value as string[])}
|
||||
title={title}
|
||||
type={type}
|
||||
options={options}
|
||||
setValues={(values) => column?.setFilterValue(values)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
import { DropdownMenuTrigger } from '@radix-ui/react-dropdown-menu';
|
||||
import { MixerHorizontalIcon } from '@radix-ui/react-icons';
|
||||
import { Table } from '@tanstack/react-table';
|
||||
|
||||
import { Button } from '@/components/v1/ui/button';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
} from '@/components/v1/ui/dropdown-menu';
|
||||
|
||||
interface DataTableViewOptionsProps<TData> {
|
||||
table: Table<TData>;
|
||||
// todo: make this required
|
||||
columnKeyToName?: Record<string, string>;
|
||||
}
|
||||
|
||||
export function DataTableViewOptions<TData>({
|
||||
table,
|
||||
columnKeyToName,
|
||||
}: DataTableViewOptionsProps<TData>) {
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="ml-auto hidden h-8 lg:flex"
|
||||
>
|
||||
<MixerHorizontalIcon className="mr-2 h-4 w-4" />
|
||||
View
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-[180px] z-[70]">
|
||||
<DropdownMenuLabel>Toggle columns</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
{table
|
||||
.getAllColumns()
|
||||
.filter(
|
||||
(column) =>
|
||||
typeof column.accessorFn !== 'undefined' && column.getCanHide(),
|
||||
)
|
||||
.map((column) => {
|
||||
const columnName =
|
||||
(columnKeyToName ?? {})[
|
||||
column.id as keyof typeof columnKeyToName
|
||||
] || column.id;
|
||||
return (
|
||||
<DropdownMenuCheckboxItem
|
||||
key={column.id}
|
||||
className="capitalize"
|
||||
checked={column.getIsVisible()}
|
||||
onCheckedChange={(value) => column.toggleVisibility(!!value)}
|
||||
>
|
||||
{columnName}
|
||||
</DropdownMenuCheckboxItem>
|
||||
);
|
||||
})}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from '@/components/v1/ui/command';
|
||||
import { Tenant, TenantMember, TenantVersion } from '@/lib/api';
|
||||
import { Tenant, TenantMember } from '@/lib/api';
|
||||
import { OrganizationForUser } from '@/lib/api/generated/cloud/data-contracts';
|
||||
import { CaretSortIcon } from '@radix-ui/react-icons';
|
||||
import {
|
||||
@@ -196,13 +196,6 @@ export function OrganizationSelector({
|
||||
|
||||
const handleTenantSelect = (tenant: Tenant) => {
|
||||
setCurrTenant(tenant);
|
||||
|
||||
if (tenant.version === TenantVersion.V0) {
|
||||
// Hack to wait for next event loop tick so local storage is updated
|
||||
setTimeout(() => {
|
||||
window.location.href = `/workflow-runs?tenant=${tenant.metadata.id}`;
|
||||
}, 0);
|
||||
}
|
||||
};
|
||||
|
||||
const toggleOrgExpansion = (orgId: string) => {
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
CommandSeparator,
|
||||
} from '@/components/v1/ui/command';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { TenantMember, TenantVersion } from '@/lib/api';
|
||||
import { TenantMember } from '@/lib/api';
|
||||
import { CaretSortIcon, PlusCircledIcon } from '@radix-ui/react-icons';
|
||||
import {
|
||||
PopoverTrigger,
|
||||
@@ -70,13 +70,6 @@ export function TenantSwitcher({
|
||||
invariant(membership.tenant);
|
||||
setCurrTenant(membership.tenant);
|
||||
setOpen(false);
|
||||
|
||||
if (membership.tenant.version === TenantVersion.V0) {
|
||||
// Hack to wait for next event loop tick so local storage is updated
|
||||
setTimeout(() => {
|
||||
window.location.href = `/workflow-runs?tenant=${membership.tenant?.metadata.id}`;
|
||||
}, 0);
|
||||
}
|
||||
}}
|
||||
value={membership.tenant?.slug}
|
||||
className="text-sm cursor-pointer"
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
import { useTenantDetails } from '@/hooks/use-tenant';
|
||||
import { User } from '@/lib/api';
|
||||
import useApiMeta from '@/pages/auth/hooks/use-api-meta';
|
||||
import React, { PropsWithChildren, useEffect, useMemo } from 'react';
|
||||
|
||||
interface SupportChatProps {
|
||||
user: User;
|
||||
}
|
||||
|
||||
const SupportChat: React.FC<PropsWithChildren & SupportChatProps> = ({
|
||||
user,
|
||||
children,
|
||||
}) => {
|
||||
const meta = useApiMeta();
|
||||
|
||||
const { tenant } = useTenantDetails();
|
||||
|
||||
const APP_ID = useMemo(() => {
|
||||
if (!meta.data?.pylonAppId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return meta.data.pylonAppId;
|
||||
}, [meta]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!APP_ID) {
|
||||
return;
|
||||
}
|
||||
|
||||
const pylonScript = `(function(){var e=window;var t=document;var n=function(){n.e(arguments)};n.q=[];n.e=function(e){n.q.push(e)};e.Pylon=n;var r=function(){var e=t.createElement("script");e.setAttribute("type","text/javascript");e.setAttribute("async","true");e.setAttribute("src","https://widget.usepylon.com/widget/${APP_ID}");var n=t.getElementsByTagName("script")[0];n.parentNode.insertBefore(e,n)};if(t.readyState==="complete"){r()}else if(e.addEventListener){e.addEventListener("load",r,false)}})();`;
|
||||
document.body.appendChild(document.createElement('script')).innerHTML =
|
||||
pylonScript;
|
||||
}, [APP_ID]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!APP_ID || !user) {
|
||||
return;
|
||||
}
|
||||
|
||||
(window as any).pylon = {
|
||||
chat_settings: {
|
||||
app_id: APP_ID,
|
||||
email: user.email,
|
||||
name: user.name,
|
||||
email_hash: user.emailHash,
|
||||
},
|
||||
};
|
||||
|
||||
(window as any).Pylon('setNewIssueCustomFields', {
|
||||
user_id: user.metadata.id,
|
||||
tenant_name: tenant?.name,
|
||||
tenant_slug: tenant?.slug,
|
||||
tenant_id: tenant?.metadata?.id,
|
||||
});
|
||||
}, [user, APP_ID, tenant]);
|
||||
|
||||
return children;
|
||||
};
|
||||
|
||||
export default SupportChat;
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
setDateByType,
|
||||
} from './time-picker-utils';
|
||||
|
||||
export interface TimePickerInputProps
|
||||
interface TimePickerInputProps
|
||||
extends React.InputHTMLAttributes<HTMLInputElement> {
|
||||
picker: TimePickerType;
|
||||
date: Date | undefined;
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
/**
|
||||
* regular expression to check for valid hour format (01-23)
|
||||
*/
|
||||
export function isValidHour(value: string) {
|
||||
function isValidHour(value: string) {
|
||||
return /^(0[0-9]|1[0-9]|2[0-3])$/.test(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* regular expression to check for valid 12 hour format (01-12)
|
||||
*/
|
||||
export function isValid12Hour(value: string) {
|
||||
function isValid12Hour(value: string) {
|
||||
return /^(0[1-9]|1[0-2])$/.test(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* regular expression to check for valid minute format (00-59)
|
||||
*/
|
||||
export function isValidMinuteOrSecond(value: string) {
|
||||
function isValidMinuteOrSecond(value: string) {
|
||||
return /^[0-5][0-9]$/.test(value);
|
||||
}
|
||||
|
||||
type GetValidNumberConfig = { max: number; min?: number; loop?: boolean };
|
||||
|
||||
export function getValidNumber(
|
||||
function getValidNumber(
|
||||
value: string,
|
||||
{ max, min = 0, loop = false }: GetValidNumberConfig,
|
||||
) {
|
||||
@@ -49,21 +49,21 @@ export function getValidNumber(
|
||||
return '00';
|
||||
}
|
||||
|
||||
export function getValidHour(value: string) {
|
||||
function getValidHour(value: string) {
|
||||
if (isValidHour(value)) {
|
||||
return value;
|
||||
}
|
||||
return getValidNumber(value, { max: 23 });
|
||||
}
|
||||
|
||||
export function getValid12Hour(value: string) {
|
||||
function getValid12Hour(value: string) {
|
||||
if (isValid12Hour(value)) {
|
||||
return value;
|
||||
}
|
||||
return getValidNumber(value, { min: 1, max: 12 });
|
||||
}
|
||||
|
||||
export function getValidMinuteOrSecond(value: string) {
|
||||
function getValidMinuteOrSecond(value: string) {
|
||||
if (isValidMinuteOrSecond(value)) {
|
||||
return value;
|
||||
}
|
||||
@@ -76,7 +76,7 @@ type GetValidArrowNumberConfig = {
|
||||
step: number;
|
||||
};
|
||||
|
||||
export function getValidArrowNumber(
|
||||
function getValidArrowNumber(
|
||||
value: string,
|
||||
{ min, max, step }: GetValidArrowNumberConfig,
|
||||
) {
|
||||
@@ -88,37 +88,37 @@ export function getValidArrowNumber(
|
||||
return '00';
|
||||
}
|
||||
|
||||
export function getValidArrowHour(value: string, step: number) {
|
||||
function getValidArrowHour(value: string, step: number) {
|
||||
return getValidArrowNumber(value, { min: 0, max: 23, step });
|
||||
}
|
||||
|
||||
export function getValidArrow12Hour(value: string, step: number) {
|
||||
function getValidArrow12Hour(value: string, step: number) {
|
||||
return getValidArrowNumber(value, { min: 1, max: 12, step });
|
||||
}
|
||||
|
||||
export function getValidArrowMinuteOrSecond(value: string, step: number) {
|
||||
function getValidArrowMinuteOrSecond(value: string, step: number) {
|
||||
return getValidArrowNumber(value, { min: 0, max: 59, step });
|
||||
}
|
||||
|
||||
export function setMinutes(date: Date, value: string) {
|
||||
function setMinutes(date: Date, value: string) {
|
||||
const minutes = getValidMinuteOrSecond(value);
|
||||
date.setMinutes(parseInt(minutes, 10));
|
||||
return date;
|
||||
}
|
||||
|
||||
export function setSeconds(date: Date, value: string) {
|
||||
function setSeconds(date: Date, value: string) {
|
||||
const seconds = getValidMinuteOrSecond(value);
|
||||
date.setSeconds(parseInt(seconds, 10));
|
||||
return date;
|
||||
}
|
||||
|
||||
export function setHours(date: Date, value: string) {
|
||||
function setHours(date: Date, value: string) {
|
||||
const hours = getValidHour(value);
|
||||
date.setHours(parseInt(hours, 10));
|
||||
return date;
|
||||
}
|
||||
|
||||
export function set12Hours(date: Date, value: string, period: Period) {
|
||||
function set12Hours(date: Date, value: string, period: Period) {
|
||||
const hours = parseInt(getValid12Hour(value), 10);
|
||||
const convertedHours = convert12HourTo24Hour(hours, period);
|
||||
date.setHours(convertedHours);
|
||||
@@ -191,7 +191,7 @@ export function getArrowByType(
|
||||
* 12:00 PM is 12:00
|
||||
* 12:00 AM is 00:00
|
||||
*/
|
||||
export function convert12HourTo24Hour(hour: number, period: Period) {
|
||||
function convert12HourTo24Hour(hour: number, period: Period) {
|
||||
if (period === 'PM') {
|
||||
if (hour <= 11) {
|
||||
return hour + 12;
|
||||
@@ -212,7 +212,7 @@ export function convert12HourTo24Hour(hour: number, period: Period) {
|
||||
* but needs to be displayed to the user
|
||||
* in its 12-hour representation
|
||||
*/
|
||||
export function display12HourValue(hours: number) {
|
||||
function display12HourValue(hours: number) {
|
||||
if (hours === 0 || hours === 12) {
|
||||
return '12';
|
||||
}
|
||||
|
||||
@@ -12,10 +12,7 @@ import {
|
||||
import { V1TaskStatus } from '@/lib/api';
|
||||
import { Duration as DateFnsDuration } from 'date-fns';
|
||||
|
||||
export function formatDuration(
|
||||
duration: DateFnsDuration,
|
||||
rawTimeMs: number,
|
||||
): string {
|
||||
function formatDuration(duration: DateFnsDuration, rawTimeMs: number): string {
|
||||
const parts = [];
|
||||
|
||||
if (duration.days) {
|
||||
@@ -48,7 +45,7 @@ export function formatDuration(
|
||||
return parts.join(' ');
|
||||
}
|
||||
|
||||
export const isValidTimestamp = (
|
||||
const isValidTimestamp = (
|
||||
timestamp?: string | Date | null,
|
||||
): timestamp is string | Date => {
|
||||
if (!timestamp) {
|
||||
@@ -78,7 +75,7 @@ const durationVariants = cva('text-sm', {
|
||||
},
|
||||
});
|
||||
|
||||
export interface DurationProps
|
||||
interface DurationProps
|
||||
extends React.HTMLAttributes<HTMLDivElement>,
|
||||
VariantProps<typeof durationVariants> {
|
||||
start?: string | Date | null;
|
||||
|
||||
@@ -43,4 +43,4 @@ function Badge({ className, variant, ...props }: BadgeProps) {
|
||||
);
|
||||
}
|
||||
|
||||
export { Badge, badgeVariants };
|
||||
export { Badge };
|
||||
|
||||
@@ -111,5 +111,4 @@ export {
|
||||
BreadcrumbLink,
|
||||
BreadcrumbPage,
|
||||
BreadcrumbSeparator,
|
||||
BreadcrumbEllipsis,
|
||||
};
|
||||
|
||||
@@ -5,7 +5,7 @@ import { DayPicker } from 'react-day-picker';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { buttonVariants } from '@/components/v1/ui/button';
|
||||
|
||||
export type CalendarProps = React.ComponentProps<typeof DayPicker>;
|
||||
type CalendarProps = React.ComponentProps<typeof DayPicker>;
|
||||
|
||||
function Calendar({
|
||||
className,
|
||||
|
||||
@@ -254,7 +254,7 @@ const ChartTooltipContent = React.forwardRef<
|
||||
);
|
||||
ChartTooltipContent.displayName = 'ChartTooltip';
|
||||
|
||||
const ChartLegend = RechartsPrimitive.Legend;
|
||||
export const ChartLegend = RechartsPrimitive.Legend;
|
||||
|
||||
const ChartLegendContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
@@ -353,11 +353,4 @@ function getPayloadConfigFromPayload(
|
||||
: config[key as keyof typeof config];
|
||||
}
|
||||
|
||||
export {
|
||||
ChartContainer,
|
||||
ChartTooltip,
|
||||
ChartTooltipContent,
|
||||
ChartLegend,
|
||||
ChartLegendContent,
|
||||
ChartStyle,
|
||||
};
|
||||
export { ChartContainer, ChartTooltip, ChartTooltipContent };
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import CopyToClipboard from './copy-to-clipboard';
|
||||
import { cn } from '@/lib/utils';
|
||||
import Editor, { DiffEditor, Monaco } from '@monaco-editor/react';
|
||||
import Editor, { Monaco } from '@monaco-editor/react';
|
||||
import 'monaco-themes/themes/Pastels on Dark.json';
|
||||
import { useTheme } from '@/components/theme-provider';
|
||||
|
||||
@@ -81,63 +81,6 @@ export function CodeEditor({
|
||||
);
|
||||
}
|
||||
|
||||
export function DiffCodeEditor({
|
||||
code = '',
|
||||
setCode,
|
||||
language,
|
||||
className,
|
||||
height,
|
||||
width,
|
||||
copy,
|
||||
originalValue = '',
|
||||
wrapLines = true,
|
||||
}: CodeEditorProps & {
|
||||
originalValue: string;
|
||||
}) {
|
||||
const setEditorTheme = (monaco: Monaco) => {
|
||||
monaco.editor.defineTheme('pastels-on-dark', getMonacoTheme());
|
||||
monaco.editor.setTheme('pastels-on-dark');
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
className,
|
||||
'w-full h-fit relative rounded-lg overflow-hidden',
|
||||
)}
|
||||
>
|
||||
<DiffEditor
|
||||
beforeMount={setEditorTheme}
|
||||
language={language}
|
||||
width={width || '100%'}
|
||||
height={height || '400px'}
|
||||
theme="pastels-on-dark"
|
||||
original={originalValue}
|
||||
modified={code || ''}
|
||||
options={{
|
||||
minimap: { enabled: false },
|
||||
wordWrap: wrapLines ? 'on' : 'off',
|
||||
lineNumbers: 'off',
|
||||
readOnly: !setCode,
|
||||
scrollbar: { vertical: 'hidden', horizontal: 'hidden' },
|
||||
showFoldingControls: language == 'json' ? 'always' : 'never',
|
||||
lineDecorationsWidth: 0,
|
||||
overviewRulerBorder: false,
|
||||
colorDecorators: false,
|
||||
hideCursorInOverviewRuler: true,
|
||||
contextmenu: false,
|
||||
}}
|
||||
/>
|
||||
{copy && (
|
||||
<CopyToClipboard
|
||||
className="absolute top-2 right-2"
|
||||
text={code?.trim() || ''}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
type BuiltinTheme = 'vs' | 'vs-dark' | 'hc-black' | 'hc-light';
|
||||
|
||||
const getMonacoTheme = () => {
|
||||
|
||||
@@ -2,8 +2,8 @@ import * as CollapsiblePrimitive from '@radix-ui/react-collapsible';
|
||||
|
||||
const Collapsible = CollapsiblePrimitive.Root;
|
||||
|
||||
const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger;
|
||||
export const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger;
|
||||
|
||||
const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent;
|
||||
|
||||
export { Collapsible, CollapsibleTrigger, CollapsibleContent };
|
||||
export { Collapsible, CollapsibleContent };
|
||||
|
||||
@@ -23,7 +23,7 @@ Command.displayName = CommandPrimitive.displayName;
|
||||
|
||||
interface CommandDialogProps extends DialogProps {}
|
||||
|
||||
const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
|
||||
export const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
|
||||
return (
|
||||
<Dialog {...props}>
|
||||
<DialogContent className="overflow-hidden p-0">
|
||||
@@ -146,12 +146,10 @@ CommandShortcut.displayName = 'CommandShortcut';
|
||||
|
||||
export {
|
||||
Command,
|
||||
CommandDialog,
|
||||
CommandInput,
|
||||
CommandList,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandItem,
|
||||
CommandShortcut,
|
||||
CommandSeparator,
|
||||
};
|
||||
|
||||
@@ -107,8 +107,6 @@ DialogDescription.displayName = DialogPrimitive.Description.displayName;
|
||||
|
||||
export {
|
||||
Dialog,
|
||||
DialogPortal,
|
||||
DialogOverlay,
|
||||
DialogTrigger,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
|
||||
@@ -12,13 +12,13 @@ const DropdownMenu = DropdownMenuPrimitive.Root;
|
||||
|
||||
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
|
||||
|
||||
const DropdownMenuGroup = DropdownMenuPrimitive.Group;
|
||||
export const DropdownMenuGroup = DropdownMenuPrimitive.Group;
|
||||
|
||||
const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
|
||||
export const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
|
||||
|
||||
const DropdownMenuSub = DropdownMenuPrimitive.Sub;
|
||||
export const DropdownMenuSub = DropdownMenuPrimitive.Sub;
|
||||
|
||||
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
|
||||
export const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
|
||||
|
||||
const DropdownMenuSubTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
|
||||
@@ -141,7 +141,7 @@ const DropdownMenuRadioItem = React.forwardRef<
|
||||
));
|
||||
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
|
||||
|
||||
const DropdownMenuLabel = React.forwardRef<
|
||||
export const DropdownMenuLabel = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
|
||||
inset?: boolean;
|
||||
@@ -171,7 +171,7 @@ const DropdownMenuSeparator = React.forwardRef<
|
||||
));
|
||||
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
|
||||
|
||||
const DropdownMenuShortcut = ({
|
||||
export const DropdownMenuShortcut = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||
@@ -189,15 +189,5 @@ export {
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuRadioGroup,
|
||||
};
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
import { ObjectFieldTemplateProps } from '@rjsf/utils';
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
} from '@/components/v1/ui/accordion';
|
||||
|
||||
import { DEFAULT_COLLAPSED } from '../json-form';
|
||||
|
||||
export const CollapsibleSection = (props: ObjectFieldTemplateProps) => {
|
||||
if (!props.title) {
|
||||
return props.properties.map((element, i) => (
|
||||
<div className="property-wrapper ml-4" key={i}>
|
||||
{element.content}
|
||||
</div>
|
||||
));
|
||||
}
|
||||
|
||||
return (
|
||||
<Accordion
|
||||
type="single"
|
||||
collapsible
|
||||
className="w-full"
|
||||
defaultValue={DEFAULT_COLLAPSED.includes(props.title) ? 'closed' : 'open'}
|
||||
>
|
||||
<AccordionItem value="open">
|
||||
<AccordionTrigger>{props.title}</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
{props.description}
|
||||
{props.properties?.length > 0 ? (
|
||||
props.properties.map((element, i) => (
|
||||
<div className="property-wrapper ml-4" key={i}>
|
||||
{element.content}
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="ml-4">empty state</div>
|
||||
)}
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
);
|
||||
};
|
||||
@@ -1,100 +0,0 @@
|
||||
import { BaseInputTemplateProps, getInputProps } from '@rjsf/utils';
|
||||
import {
|
||||
ChangeEvent,
|
||||
FocusEvent,
|
||||
KeyboardEvent,
|
||||
useContext,
|
||||
useEffect,
|
||||
useRef,
|
||||
} from 'react';
|
||||
import { JSONFormContext } from '../json-form';
|
||||
|
||||
export function DynamicSizeInputTemplate(props: BaseInputTemplateProps) {
|
||||
const {
|
||||
schema,
|
||||
id,
|
||||
options,
|
||||
label,
|
||||
value,
|
||||
type,
|
||||
placeholder,
|
||||
required,
|
||||
disabled,
|
||||
readonly,
|
||||
autofocus,
|
||||
onChange,
|
||||
onChangeOverride,
|
||||
onBlur,
|
||||
onFocus,
|
||||
rawErrors,
|
||||
hideError,
|
||||
uiSchema,
|
||||
registry,
|
||||
formContext,
|
||||
...rest
|
||||
} = props;
|
||||
|
||||
const ref = useRef<HTMLTextAreaElement>(null);
|
||||
const { form } = useContext(JSONFormContext);
|
||||
|
||||
const onTextChange = ({
|
||||
target: { value: val },
|
||||
}: ChangeEvent<HTMLTextAreaElement>) => {
|
||||
// Use the options.emptyValue if it is specified and newVal is also an empty string
|
||||
onChange(val === '' ? options.emptyValue || '' : val);
|
||||
};
|
||||
const onTextBlur = ({
|
||||
target: { value: val },
|
||||
}: FocusEvent<HTMLTextAreaElement>) => onBlur(id, val);
|
||||
const onTextFocus = ({
|
||||
target: { value: val },
|
||||
}: FocusEvent<HTMLTextAreaElement>) => onFocus(id, val);
|
||||
|
||||
const inputProps = { ...rest, ...getInputProps(schema, type, options) };
|
||||
delete inputProps.hideLabel; // hideLabel is not a valid prop for textarea
|
||||
|
||||
const setHeight = (e: HTMLTextAreaElement) => {
|
||||
e.style.height = 'auto';
|
||||
e.style.height = `${e.scrollHeight}px`;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// Call adjustHeight whenever the watched value changes externally
|
||||
if (!ref.current) {
|
||||
return;
|
||||
}
|
||||
setHeight(ref.current);
|
||||
}, [ref.current?.value]);
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
if (!form?.current) {
|
||||
return;
|
||||
}
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
form.current.submit();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<textarea
|
||||
ref={ref}
|
||||
id={id}
|
||||
value={value}
|
||||
placeholder={placeholder}
|
||||
disabled={disabled}
|
||||
readOnly={readonly}
|
||||
autoFocus={autofocus}
|
||||
className="overflow-y-hidden"
|
||||
onKeyDown={handleKeyDown}
|
||||
onChange={
|
||||
(onChangeOverride as
|
||||
| ((event: ChangeEvent<HTMLTextAreaElement>) => void)
|
||||
| undefined) || onTextChange
|
||||
}
|
||||
onBlur={onTextBlur}
|
||||
onFocus={onTextFocus}
|
||||
{...inputProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -24,4 +24,4 @@ const HoverCardContent = React.forwardRef<
|
||||
));
|
||||
HoverCardContent.displayName = HoverCardPrimitive.Content.displayName;
|
||||
|
||||
export { HoverCard, HoverCardTrigger, HoverCardContent };
|
||||
export { HoverCard, HoverCardTrigger };
|
||||
|
||||
@@ -2,8 +2,7 @@ import * as React from 'react';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
export interface InputProps
|
||||
extends React.InputHTMLAttributes<HTMLInputElement> {}
|
||||
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {}
|
||||
|
||||
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
({ className, type, ...props }, ref) => {
|
||||
|
||||
@@ -1,159 +0,0 @@
|
||||
import { cn } from '@/lib/utils';
|
||||
import {
|
||||
RJSFSchema,
|
||||
RJSFValidationError,
|
||||
UiSchema,
|
||||
ValidationData,
|
||||
ValidatorType,
|
||||
} from '@rjsf/utils';
|
||||
import Form from '@rjsf/core';
|
||||
import { PlayIcon } from '@radix-ui/react-icons';
|
||||
import { Button } from './button';
|
||||
import { Loading } from './loading';
|
||||
import { CollapsibleSection } from './form-inputs/collapsible-section';
|
||||
import { DynamicSizeInputTemplate } from './form-inputs/dynamic-size-input-template';
|
||||
import { createContext, useRef } from 'react';
|
||||
|
||||
type JSONPrimitive = string | number | boolean | null | Array<JSONPrimitive>;
|
||||
export type JSONType = {
|
||||
[key: string]: JSONType | JSONPrimitive | Array<JSONType>;
|
||||
};
|
||||
|
||||
export const DEFAULT_COLLAPSED = ['advanced', 'user data'];
|
||||
|
||||
class NoValidation implements ValidatorType {
|
||||
validateFormData(): ValidationData<any> {
|
||||
return { errors: [], errorSchema: {} };
|
||||
}
|
||||
|
||||
toErrorList(): RJSFValidationError[] {
|
||||
return [];
|
||||
}
|
||||
|
||||
isValid(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
rawValidation() {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
interface JSONFormContextSchema {
|
||||
form?: React.RefObject<Form>;
|
||||
}
|
||||
|
||||
export const JSONFormContext = createContext<JSONFormContextSchema>({
|
||||
form: undefined,
|
||||
});
|
||||
|
||||
export function JsonForm({
|
||||
inputSchema,
|
||||
inputData,
|
||||
className,
|
||||
setInput,
|
||||
disabled,
|
||||
onSubmit,
|
||||
}: {
|
||||
inputSchema: JSONType;
|
||||
className?: string;
|
||||
inputData: JSONType;
|
||||
setInput: React.Dispatch<React.SetStateAction<string>>;
|
||||
disabled?: boolean;
|
||||
onSubmit: () => void;
|
||||
}) {
|
||||
const formRef = useRef<Form>(null);
|
||||
|
||||
const schema = {
|
||||
...inputSchema,
|
||||
required: undefined,
|
||||
$schema: undefined,
|
||||
properties: {
|
||||
...(inputSchema.properties as any),
|
||||
triggered_by: undefined,
|
||||
advanced: {
|
||||
// Transform the schema to wrap the triggered by field
|
||||
type: 'object',
|
||||
properties: {
|
||||
triggered_by: inputSchema.properties
|
||||
? (inputSchema.properties as any).triggered_by
|
||||
: undefined,
|
||||
},
|
||||
},
|
||||
},
|
||||
} as RJSFSchema;
|
||||
|
||||
delete schema.properties?.triggered_by;
|
||||
|
||||
const uiSchema: UiSchema<any, RJSFSchema, any> = {
|
||||
input: {
|
||||
'ui:title': 'workflow input',
|
||||
},
|
||||
parents: {
|
||||
'ui:title': 'parent step data',
|
||||
},
|
||||
overrides: {
|
||||
'ui:title': 'step overrides',
|
||||
},
|
||||
user_data: {
|
||||
'ui:title': 'user data',
|
||||
},
|
||||
'ui:order': ['input', 'overrides', 'parents', '*'],
|
||||
};
|
||||
|
||||
return (
|
||||
<JSONFormContext.Provider value={{ form: formRef }}>
|
||||
<div
|
||||
className={cn(
|
||||
className,
|
||||
'w-full h-fit relative rounded-lg overflow-hidden',
|
||||
)}
|
||||
>
|
||||
<Form
|
||||
ref={formRef}
|
||||
formData={inputData}
|
||||
schema={schema}
|
||||
disabled={disabled}
|
||||
templates={{
|
||||
BaseInputTemplate: DynamicSizeInputTemplate,
|
||||
ObjectFieldTemplate: CollapsibleSection,
|
||||
}}
|
||||
uiSchema={uiSchema}
|
||||
validator={new NoValidation()}
|
||||
noHtml5Validate={true}
|
||||
onChange={(data) => {
|
||||
// Transform the data to unwrap the advanced fields
|
||||
const formData = { ...data.formData, ...data.formData.advanced };
|
||||
delete formData.advanced;
|
||||
setInput((prev) =>
|
||||
JSON.stringify({
|
||||
...JSON.parse(prev),
|
||||
...formData,
|
||||
}),
|
||||
);
|
||||
}}
|
||||
onSubmit={onSubmit}
|
||||
onError={(e) => {
|
||||
console.error(e);
|
||||
}}
|
||||
>
|
||||
<Button className="w-fit invisible" disabled={disabled}>
|
||||
{disabled ? (
|
||||
<>
|
||||
<Loading />
|
||||
Playing
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<PlayIcon
|
||||
className={cn(disabled ? 'rotate-180' : '', 'h-4 w-4 mr-2')}
|
||||
/>
|
||||
Play Step
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</Form>
|
||||
</div>
|
||||
</JSONFormContext.Provider>
|
||||
);
|
||||
}
|
||||
@@ -1,238 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import {
|
||||
CheckIcon,
|
||||
ChevronRightIcon,
|
||||
DotFilledIcon,
|
||||
} from '@radix-ui/react-icons';
|
||||
import * as MenubarPrimitive from '@radix-ui/react-menubar';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const MenubarMenu = MenubarPrimitive.Menu;
|
||||
|
||||
const MenubarGroup = MenubarPrimitive.Group;
|
||||
|
||||
const MenubarPortal = MenubarPrimitive.Portal;
|
||||
|
||||
const MenubarSub = MenubarPrimitive.Sub;
|
||||
|
||||
const MenubarRadioGroup = MenubarPrimitive.RadioGroup;
|
||||
|
||||
const Menubar = React.forwardRef<
|
||||
React.ElementRef<typeof MenubarPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<MenubarPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'flex h-9 items-center space-x-1 rounded-md border bg-background p-1 shadow-sm',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
Menubar.displayName = MenubarPrimitive.Root.displayName;
|
||||
|
||||
const MenubarTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof MenubarPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Trigger>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<MenubarPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'flex cursor-default select-none items-center rounded-sm px-3 py-1 text-sm font-medium outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
MenubarTrigger.displayName = MenubarPrimitive.Trigger.displayName;
|
||||
|
||||
const MenubarSubTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof MenubarPrimitive.SubTrigger>,
|
||||
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.SubTrigger> & {
|
||||
inset?: boolean;
|
||||
}
|
||||
>(({ className, inset, children, ...props }, ref) => (
|
||||
<MenubarPrimitive.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 focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground',
|
||||
inset && 'pl-8',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronRightIcon className="ml-auto h-4 w-4" />
|
||||
</MenubarPrimitive.SubTrigger>
|
||||
));
|
||||
MenubarSubTrigger.displayName = MenubarPrimitive.SubTrigger.displayName;
|
||||
|
||||
const MenubarSubContent = React.forwardRef<
|
||||
React.ElementRef<typeof MenubarPrimitive.SubContent>,
|
||||
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.SubContent>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<MenubarPrimitive.SubContent
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-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}
|
||||
/>
|
||||
));
|
||||
MenubarSubContent.displayName = MenubarPrimitive.SubContent.displayName;
|
||||
|
||||
const MenubarContent = React.forwardRef<
|
||||
React.ElementRef<typeof MenubarPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Content>
|
||||
>(
|
||||
(
|
||||
{ className, align = 'start', alignOffset = -4, sideOffset = 8, ...props },
|
||||
ref,
|
||||
) => (
|
||||
<MenubarPrimitive.Portal>
|
||||
<MenubarPrimitive.Content
|
||||
ref={ref}
|
||||
align={align}
|
||||
alignOffset={alignOffset}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
'z-50 min-w-[12rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-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}
|
||||
/>
|
||||
</MenubarPrimitive.Portal>
|
||||
),
|
||||
);
|
||||
MenubarContent.displayName = MenubarPrimitive.Content.displayName;
|
||||
|
||||
const MenubarItem = React.forwardRef<
|
||||
React.ElementRef<typeof MenubarPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Item> & {
|
||||
inset?: boolean;
|
||||
}
|
||||
>(({ className, inset, ...props }, ref) => (
|
||||
<MenubarPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||
inset && 'pl-8',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
MenubarItem.displayName = MenubarPrimitive.Item.displayName;
|
||||
|
||||
const MenubarCheckboxItem = React.forwardRef<
|
||||
React.ElementRef<typeof MenubarPrimitive.CheckboxItem>,
|
||||
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.CheckboxItem>
|
||||
>(({ className, children, checked, ...props }, ref) => (
|
||||
<MenubarPrimitive.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 focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||
className,
|
||||
)}
|
||||
checked={checked}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<MenubarPrimitive.ItemIndicator>
|
||||
<CheckIcon className="h-4 w-4" />
|
||||
</MenubarPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</MenubarPrimitive.CheckboxItem>
|
||||
));
|
||||
MenubarCheckboxItem.displayName = MenubarPrimitive.CheckboxItem.displayName;
|
||||
|
||||
const MenubarRadioItem = React.forwardRef<
|
||||
React.ElementRef<typeof MenubarPrimitive.RadioItem>,
|
||||
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.RadioItem>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<MenubarPrimitive.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 focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<MenubarPrimitive.ItemIndicator>
|
||||
<DotFilledIcon className="h-4 w-4 fill-current" />
|
||||
</MenubarPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</MenubarPrimitive.RadioItem>
|
||||
));
|
||||
MenubarRadioItem.displayName = MenubarPrimitive.RadioItem.displayName;
|
||||
|
||||
const MenubarLabel = React.forwardRef<
|
||||
React.ElementRef<typeof MenubarPrimitive.Label>,
|
||||
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Label> & {
|
||||
inset?: boolean;
|
||||
}
|
||||
>(({ className, inset, ...props }, ref) => (
|
||||
<MenubarPrimitive.Label
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'px-2 py-1.5 text-sm font-semibold',
|
||||
inset && 'pl-8',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
MenubarLabel.displayName = MenubarPrimitive.Label.displayName;
|
||||
|
||||
const MenubarSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof MenubarPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<MenubarPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn('-mx-1 my-1 h-px bg-muted', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
MenubarSeparator.displayName = MenubarPrimitive.Separator.displayName;
|
||||
|
||||
const MenubarShortcut = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
'ml-auto text-xs tracking-widest text-gray-700 dark:text-gray-300',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
MenubarShortcut.displayname = 'MenubarShortcut';
|
||||
|
||||
export {
|
||||
Menubar,
|
||||
MenubarMenu,
|
||||
MenubarTrigger,
|
||||
MenubarContent,
|
||||
MenubarItem,
|
||||
MenubarSeparator,
|
||||
MenubarLabel,
|
||||
MenubarCheckboxItem,
|
||||
MenubarRadioGroup,
|
||||
MenubarRadioItem,
|
||||
MenubarPortal,
|
||||
MenubarSubContent,
|
||||
MenubarSubTrigger,
|
||||
MenubarGroup,
|
||||
MenubarSub,
|
||||
MenubarShortcut,
|
||||
};
|
||||
@@ -1,46 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const ScrollArea = React.forwardRef<
|
||||
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<ScrollAreaPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn('relative overflow-hidden', className)}
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
|
||||
{children}
|
||||
</ScrollAreaPrimitive.Viewport>
|
||||
<ScrollBar />
|
||||
<ScrollAreaPrimitive.Corner />
|
||||
</ScrollAreaPrimitive.Root>
|
||||
));
|
||||
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName;
|
||||
|
||||
const ScrollBar = React.forwardRef<
|
||||
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
|
||||
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||
>(({ className, orientation = 'vertical', ...props }, ref) => (
|
||||
<ScrollAreaPrimitive.ScrollAreaScrollbar
|
||||
ref={ref}
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
'flex touch-none select-none transition-colors',
|
||||
orientation === 'vertical' &&
|
||||
'h-full w-2.5 border-l border-l-transparent p-[1px]',
|
||||
orientation === 'horizontal' &&
|
||||
'h-2.5 flex-col border-t border-t-transparent p-[1px]',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
|
||||
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||
));
|
||||
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName;
|
||||
|
||||
export { ScrollArea, ScrollBar };
|
||||
@@ -10,7 +10,7 @@ import { cn } from '@/lib/utils';
|
||||
|
||||
const Select = SelectPrimitive.Root;
|
||||
|
||||
const SelectGroup = SelectPrimitive.Group;
|
||||
export const SelectGroup = SelectPrimitive.Group;
|
||||
|
||||
const SelectValue = SelectPrimitive.Value;
|
||||
|
||||
@@ -147,15 +147,4 @@ const SelectSeparator = React.forwardRef<
|
||||
));
|
||||
SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
|
||||
|
||||
export {
|
||||
Select,
|
||||
SelectGroup,
|
||||
SelectValue,
|
||||
SelectTrigger,
|
||||
SelectContent,
|
||||
SelectLabel,
|
||||
SelectItem,
|
||||
SelectSeparator,
|
||||
SelectScrollUpButton,
|
||||
SelectScrollDownButton,
|
||||
};
|
||||
export { Select, SelectValue, SelectTrigger, SelectContent, SelectItem };
|
||||
|
||||
@@ -1,138 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import * as SheetPrimitive from '@radix-ui/react-dialog';
|
||||
import { Cross2Icon } from '@radix-ui/react-icons';
|
||||
import { cva, type VariantProps } from 'class-variance-authority';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const Sheet = SheetPrimitive.Root;
|
||||
|
||||
const SheetTrigger = SheetPrimitive.Trigger;
|
||||
|
||||
const SheetClose = SheetPrimitive.Close;
|
||||
|
||||
const SheetPortal = SheetPrimitive.Portal;
|
||||
|
||||
const SheetOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof SheetPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SheetPrimitive.Overlay
|
||||
className={cn(
|
||||
'fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
/>
|
||||
));
|
||||
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName;
|
||||
|
||||
const sheetVariants = cva(
|
||||
'fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500 data-[state=open]:animate-in data-[state=closed]:animate-out',
|
||||
{
|
||||
variants: {
|
||||
side: {
|
||||
top: 'inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top',
|
||||
bottom:
|
||||
'inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom',
|
||||
left: 'inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm',
|
||||
right:
|
||||
'inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
side: 'right',
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
interface SheetContentProps
|
||||
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
|
||||
VariantProps<typeof sheetVariants> {}
|
||||
|
||||
const SheetContent = React.forwardRef<
|
||||
React.ElementRef<typeof SheetPrimitive.Content>,
|
||||
SheetContentProps
|
||||
>(({ side = 'right', className, children, ...props }, ref) => (
|
||||
<SheetPortal>
|
||||
<SheetOverlay />
|
||||
<SheetPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(sheetVariants({ side }), className)}
|
||||
{...props}
|
||||
>
|
||||
<SheetPrimitive.Close 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-secondary">
|
||||
<Cross2Icon className="h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</SheetPrimitive.Close>
|
||||
{children}
|
||||
</SheetPrimitive.Content>
|
||||
</SheetPortal>
|
||||
));
|
||||
SheetContent.displayName = SheetPrimitive.Content.displayName;
|
||||
|
||||
const SheetHeader = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
'flex flex-col space-y-2 text-center sm:text-left',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
SheetHeader.displayName = 'SheetHeader';
|
||||
|
||||
const SheetFooter = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
'flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
SheetFooter.displayName = 'SheetFooter';
|
||||
|
||||
const SheetTitle = React.forwardRef<
|
||||
React.ElementRef<typeof SheetPrimitive.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SheetPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn('text-lg font-semibold text-foreground', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
SheetTitle.displayName = SheetPrimitive.Title.displayName;
|
||||
|
||||
const SheetDescription = React.forwardRef<
|
||||
React.ElementRef<typeof SheetPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SheetPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn('text-sm text-muted-foreground', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
SheetDescription.displayName = SheetPrimitive.Description.displayName;
|
||||
|
||||
export {
|
||||
Sheet,
|
||||
SheetPortal,
|
||||
SheetOverlay,
|
||||
SheetTrigger,
|
||||
SheetClose,
|
||||
SheetContent,
|
||||
SheetHeader,
|
||||
SheetFooter,
|
||||
SheetTitle,
|
||||
SheetDescription,
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user