Drop V2: Tabs component (#5229)

This commit is contained in:
Tuval Simha 2024-08-18 17:33:37 +03:00 committed by GitHub
parent 5c3eaafa0c
commit e1539b5f5f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 166 additions and 119 deletions

View file

@ -24,7 +24,6 @@ import { Input } from '@/components/ui/input';
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
import { useToast } from '@/components/ui/use-toast';
import { UserMenu } from '@/components/ui/user-menu';
import { Tabs } from '@/components/v2/tabs';
import { env } from '@/env/frontend';
import { graphql, useFragment } from '@/gql';
import { ProjectType } from '@/gql/graphql';
@ -44,6 +43,7 @@ import { RateLimitWarn } from '../organization/billing/RateLimitWarn';
import { HiveLink } from '../ui/hive-link';
import { PlusIcon } from '../ui/icon';
import { QueryError } from '../ui/query-error';
import { Tabs, TabsList, TabsTrigger } from '../ui/tabs';
import { OrganizationSelector } from './organization-selectors';
export enum Page {
@ -144,18 +144,18 @@ export function OrganizationLayout({
<div className="relative h-[--tabs-navbar-height] border-b border-gray-800">
<div className="container flex items-center justify-between">
{currentOrganization && meInCurrentOrg ? (
<Tabs value={page}>
<Tabs.List>
<Tabs.Trigger value={Page.Overview} asChild>
<Tabs value={page} className="min-w-[600px]">
<TabsList variant="menu">
<TabsTrigger variant="menu" value={Page.Overview} asChild>
<Link
to="/$organizationId"
params={{ organizationId: currentOrganization.cleanId }}
>
Overview
</Link>
</Tabs.Trigger>
</TabsTrigger>
{canAccessOrganization(OrganizationAccessScope.Members, meInCurrentOrg) && (
<Tabs.Trigger value={Page.Members} asChild>
<TabsTrigger variant="menu" value={Page.Members} asChild>
<Link
to="/$organizationId/view/members"
params={{ organizationId: currentOrganization.cleanId }}
@ -163,51 +163,51 @@ export function OrganizationLayout({
>
Members
</Link>
</Tabs.Trigger>
</TabsTrigger>
)}
{canAccessOrganization(OrganizationAccessScope.Settings, meInCurrentOrg) && (
<>
<Tabs.Trigger value={Page.Policy} asChild>
<TabsTrigger variant="menu" value={Page.Policy} asChild>
<Link
to="/$organizationId/view/policy"
params={{ organizationId: currentOrganization.cleanId }}
>
Policy
</Link>
</Tabs.Trigger>
<Tabs.Trigger value={Page.Settings} asChild>
</TabsTrigger>
<TabsTrigger variant="menu" value={Page.Settings} asChild>
<Link
to="/$organizationId/view/settings"
params={{ organizationId: currentOrganization.cleanId }}
>
Settings
</Link>
</Tabs.Trigger>
</TabsTrigger>
</>
)}
{canAccessOrganization(OrganizationAccessScope.Read, meInCurrentOrg) &&
env.zendeskSupport && (
<Tabs.Trigger value={Page.Support} asChild>
<TabsTrigger variant="menu" value={Page.Support} asChild>
<Link
to="/$organizationId/view/support"
params={{ organizationId: currentOrganization.cleanId }}
>
Support
</Link>
</Tabs.Trigger>
</TabsTrigger>
)}
{getIsStripeEnabled() &&
canAccessOrganization(OrganizationAccessScope.Settings, meInCurrentOrg) && (
<Tabs.Trigger value={Page.Subscription} asChild>
<TabsTrigger variant="menu" value={Page.Subscription} asChild>
<Link
to="/$organizationId/view/subscription"
params={{ organizationId: currentOrganization.cleanId }}
>
Subscription
</Link>
</Tabs.Trigger>
</TabsTrigger>
)}
</Tabs.List>
</TabsList>
</Tabs>
) : (
<div className="flex flex-row gap-x-8 border-b-2 border-b-transparent px-4 py-3">

View file

@ -15,7 +15,6 @@ import { Form, FormControl, FormField, FormItem, FormMessage } from '@/component
import { Input } from '@/components/ui/input';
import { useToast } from '@/components/ui/use-toast';
import { UserMenu } from '@/components/ui/user-menu';
import { Tabs } from '@/components/v2/tabs';
import { graphql } from '@/gql';
import { canAccessProject, ProjectAccessScope, useProjectAccess } from '@/lib/access/project';
import { useToggle } from '@/lib/hooks';
@ -25,6 +24,7 @@ import { Link, useRouter } from '@tanstack/react-router';
import { ProjectMigrationToast } from '../project/migration-toast';
import { HiveLink } from '../ui/hive-link';
import { PlusIcon } from '../ui/icon';
import { Tabs, TabsList, TabsTrigger } from '../ui/tabs';
import { ProjectSelector } from './project-selector';
export enum Page {
@ -130,8 +130,8 @@ export function ProjectLayout({
<div className="container flex items-center justify-between">
{currentOrganization && currentProject ? (
<Tabs value={page}>
<Tabs.List>
<Tabs.Trigger value={Page.Targets} asChild>
<TabsList variant="menu">
<TabsTrigger variant="menu" value={Page.Targets} asChild>
<Link
to="/$organizationId/$projectId"
params={{
@ -141,9 +141,9 @@ export function ProjectLayout({
>
Targets
</Link>
</Tabs.Trigger>
</TabsTrigger>
{canAccessProject(ProjectAccessScope.Alerts, currentOrganization.me) && (
<Tabs.Trigger value={Page.Alerts} asChild>
<TabsTrigger variant="menu" value={Page.Alerts} asChild>
<Link
to="/$organizationId/$projectId/view/alerts"
params={{
@ -153,11 +153,11 @@ export function ProjectLayout({
>
Alerts
</Link>
</Tabs.Trigger>
</TabsTrigger>
)}
{canAccessProject(ProjectAccessScope.Settings, currentOrganization.me) && (
<>
<Tabs.Trigger value={Page.Policy} asChild>
<TabsTrigger variant="menu" value={Page.Policy} asChild>
<Link
to="/$organizationId/$projectId/view/policy"
params={{
@ -167,8 +167,8 @@ export function ProjectLayout({
>
Policy
</Link>
</Tabs.Trigger>
<Tabs.Trigger value={Page.Settings} asChild>
</TabsTrigger>
<TabsTrigger variant="menu" value={Page.Settings} asChild>
<Link
to="/$organizationId/$projectId/view/settings"
params={{
@ -178,10 +178,10 @@ export function ProjectLayout({
>
Settings
</Link>
</Tabs.Trigger>
</TabsTrigger>
</>
)}
</Tabs.List>
</TabsList>
</Tabs>
) : (
<div className="flex flex-row gap-x-8 border-b-2 border-b-transparent px-4 py-3">

View file

@ -22,7 +22,6 @@ import {
} from '@/components/ui/select';
import { UserMenu } from '@/components/ui/user-menu';
import { CopyValue, Tag } from '@/components/v2';
import { Tabs } from '@/components/v2/tabs';
import { graphql } from '@/gql';
import { ProjectType } from '@/gql/graphql';
import { canAccessTarget, TargetAccessScope, useTargetAccess } from '@/lib/access/target';
@ -32,6 +31,7 @@ import { useLastVisitedOrganizationWriter } from '@/lib/last-visited-org';
import { cn } from '@/lib/utils';
import { Link } from '@tanstack/react-router';
import { ProjectMigrationToast } from '../project/migration-toast';
import { Tabs, TabsList, TabsTrigger } from '../ui/tabs';
import { TargetSelector } from './target-selector';
export enum Page {
@ -168,10 +168,10 @@ export const TargetLayout = ({
<div className="container flex items-center justify-between">
{currentOrganization && currentProject && currentTarget ? (
<Tabs className="flex h-full grow flex-col" value={page}>
<Tabs.List>
<TabsList variant="menu">
{canAccessSchema && (
<>
<Tabs.Trigger value={Page.Schema} asChild>
<TabsTrigger variant="menu" value={Page.Schema} asChild>
<Link
to="/$organizationId/$projectId/$targetId"
params={{
@ -182,8 +182,8 @@ export const TargetLayout = ({
>
Schema
</Link>
</Tabs.Trigger>
<Tabs.Trigger value={Page.Checks} asChild>
</TabsTrigger>
<TabsTrigger variant="menu" value={Page.Checks} asChild>
<Link
to="/$organizationId/$projectId/$targetId/checks"
params={{
@ -194,8 +194,8 @@ export const TargetLayout = ({
>
Checks
</Link>
</Tabs.Trigger>
<Tabs.Trigger value={Page.Explorer} asChild>
</TabsTrigger>
<TabsTrigger variant="menu" value={Page.Explorer} asChild>
<Link
to="/$organizationId/$projectId/$targetId/explorer"
params={{
@ -206,8 +206,8 @@ export const TargetLayout = ({
>
Explorer
</Link>
</Tabs.Trigger>
<Tabs.Trigger value={Page.History} asChild>
</TabsTrigger>
<TabsTrigger variant="menu" value={Page.History} asChild>
<Link
to="/$organizationId/$projectId/$targetId/history"
params={{
@ -218,8 +218,8 @@ export const TargetLayout = ({
>
History
</Link>
</Tabs.Trigger>
<Tabs.Trigger value={Page.Insights} asChild>
</TabsTrigger>
<TabsTrigger variant="menu" value={Page.Insights} asChild>
<Link
to="/$organizationId/$projectId/$targetId/insights"
params={{
@ -230,9 +230,9 @@ export const TargetLayout = ({
>
Insights
</Link>
</Tabs.Trigger>
</TabsTrigger>
{currentOrganization.isAppDeploymentsEnabled && (
<Tabs.Trigger value={Page.Apps} asChild>
<TabsTrigger variant="menu" value={Page.Apps} asChild>
<Link
to="/$organizationId/$projectId/$targetId/apps"
params={{
@ -243,9 +243,9 @@ export const TargetLayout = ({
>
Apps
</Link>
</Tabs.Trigger>
</TabsTrigger>
)}
<Tabs.Trigger value={Page.Laboratory} asChild>
<TabsTrigger variant="menu" value={Page.Laboratory} asChild>
<Link
to="/$organizationId/$projectId/$targetId/laboratory"
params={{
@ -256,11 +256,11 @@ export const TargetLayout = ({
>
Laboratory
</Link>
</Tabs.Trigger>
</TabsTrigger>
</>
)}
{canAccessSettings && (
<Tabs.Trigger value={Page.Settings} asChild>
<TabsTrigger variant="menu" value={Page.Settings} asChild>
<Link
to="/$organizationId/$projectId/$targetId/settings"
params={{
@ -272,9 +272,9 @@ export const TargetLayout = ({
>
Settings
</Link>
</Tabs.Trigger>
</TabsTrigger>
)}
</Tabs.List>
</TabsList>
</Tabs>
) : (
<div className="flex flex-row gap-x-8 border-b-2 border-b-transparent px-4 py-3">

View file

@ -1,21 +1,61 @@
'use client';
import React from 'react';
import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '@/lib/utils';
import * as TabsPrimitive from '@radix-ui/react-tabs';
// Define variants for TabsList
const tabsListVariants = cva('relative flex items-center', {
variants: {
variant: {
default:
'bg-muted text-muted-foreground inline-flex h-10 items-center justify-center rounded-md p-1',
menu: 'text-gray-700',
},
},
defaultVariants: {
variant: 'default',
},
});
// Define variants for TabsTrigger
const tabsTriggerVariants = cva('cursor-pointer !appearance-none text-sm font-medium transition', {
variants: {
variant: {
default:
'ring-offset-background focus-visible:ring-ring data-[state=active]:bg-background data-[state=active]:text-foreground inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:opacity-50 data-[state=active]:shadow-sm disabled:cursor-not-allowed active:disabled:pointer-events-none',
menu: 'text-white radix-state-active:border-b-orange-500 border-b-2 border-b-transparent px-4 py-3 hover:border-b-orange-900',
},
},
defaultVariants: {
variant: 'default',
},
});
// Define variants for TabsContent
const tabsContentVariants = cva(
'ring-offset-background focus-visible:ring-ring mt-2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2',
{
variants: {
variant: {
default: 'py-7',
menu: '',
},
},
defaultVariants: {
variant: 'default',
},
},
);
const Tabs = TabsPrimitive.Root;
const TabsList = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.List>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
>(({ className, ...props }, ref) => (
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List> & VariantProps<typeof tabsListVariants>
>(({ className, variant, ...props }, ref) => (
<TabsPrimitive.List
ref={ref}
className={cn(
'bg-muted text-muted-foreground inline-flex h-10 items-center justify-center rounded-md p-1',
className,
)}
className={cn(tabsListVariants({ variant }), className)}
{...props}
/>
));
@ -23,15 +63,12 @@ TabsList.displayName = TabsPrimitive.List.displayName;
const TabsTrigger = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
>(({ className, ...props }, ref) => (
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger> &
VariantProps<typeof tabsTriggerVariants> & { hasBorder?: boolean }
>(({ className, variant, hasBorder = true, ...props }, ref) => (
<TabsPrimitive.Trigger
ref={ref}
className={cn(
'ring-offset-background focus-visible:ring-ring data-[state=active]:bg-background data-[state=active]:text-foreground inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:opacity-50 data-[state=active]:shadow-sm',
'disabled:cursor-not-allowed active:disabled:pointer-events-none', // `:active` allows `:hover` state for using with `TooltipTrigger` with `asChild` prop
className,
)}
className={cn(tabsTriggerVariants({ variant }), hasBorder, className)}
{...props}
/>
));
@ -39,14 +76,12 @@ TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
const TabsContent = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
>(({ className, ...props }, ref) => (
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content> &
VariantProps<typeof tabsContentVariants>
>(({ className, variant, ...props }, ref) => (
<TabsPrimitive.Content
ref={ref}
className={cn(
'ring-offset-background focus-visible:ring-ring mt-2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2',
className,
)}
className={cn(tabsContentVariants({ variant }), className)}
{...props}
/>
));

View file

@ -1,52 +0,0 @@
import { forwardRef, ReactElement } from 'react';
import clsx from 'clsx';
import {
Root,
Content as TabsContent,
TabsContentProps,
List as TabsList,
TabsListProps,
Trigger as TabsTrigger,
TabsTriggerProps,
} from '@radix-ui/react-tabs';
const List = ({ children, className, ...props }: TabsListProps): ReactElement => (
<TabsList className={clsx('relative flex items-center text-gray-700', className)} {...props}>
{children}
</TabsList>
);
const Trigger = forwardRef<any, Omit<TabsTriggerProps, 'className'> & { hasBorder?: boolean }>(
({ children, hasBorder = true, ...props }, forwardedRef /* when has asChild prop */) => (
<TabsTrigger
ref={forwardedRef}
className={clsx(
'!appearance-none', // unset button styles in Safari
'text-sm font-medium text-white transition',
hasBorder
? 'radix-state-active:border-b-orange-500 cursor-pointer border-b-2 border-b-transparent px-4 py-3 hover:border-b-orange-900'
: null,
)}
{...props}
>
{children}
</TabsTrigger>
),
);
const Content = ({
children,
className,
noPadding,
...props
}: TabsContentProps & { noPadding?: boolean }): ReactElement => (
<TabsContent className={clsx(noPadding ? undefined : 'py-7', className)} {...props}>
{children}
</TabsContent>
);
export const Tabs = Object.assign(Root, {
Content,
Trigger,
List,
});

View file

@ -0,0 +1,64 @@
import { MouseEvent, useCallback, useState } from 'react';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Meta, StoryObj } from '@storybook/react';
const meta: Meta<typeof Template> = {
title: 'Components/Tabs',
component: Template,
argTypes: {
variant: {
control: { type: 'inline-radio' },
options: ['default', 'menu'],
},
tabs: {
control: { type: 'object' },
},
},
args: {
tabs: ['Overview', 'Members', 'Policy', 'Settings', 'Support', 'Subscription'],
variant: 'default',
},
};
export default meta;
type Story = StoryObj<typeof Template>;
function Template({ tabs, variant }: { tabs: string[]; variant: 'default' | 'menu' }) {
const [page, setPage] = useState(tabs[0]);
const handleClick = useCallback((event: MouseEvent<HTMLElement>) => {
setPage(event.currentTarget.dataset.value!);
}, []);
return (
<Tabs value={page}>
<TabsList variant={variant}>
{tabs.map(value => (
<TabsTrigger
key={value}
variant={variant}
onClick={handleClick}
value={value}
data-value={value}
>
{value}
</TabsTrigger>
))}
</TabsList>
{tabs.map(value => (
<TabsContent key={value} variant={variant} value={value}>
{value} Tab Content
</TabsContent>
))}
</Tabs>
);
}
export const Default: Story = {};
export const Menu: Story = {
args: {
variant: 'menu',
},
};