diff --git a/apps/design-system/src/subjects/views/connectors/connectors-ref.tsx b/apps/design-system/src/subjects/views/connectors/connectors-ref.tsx index f88d8547f4..fa899fa4d9 100644 --- a/apps/design-system/src/subjects/views/connectors/connectors-ref.tsx +++ b/apps/design-system/src/subjects/views/connectors/connectors-ref.tsx @@ -173,7 +173,6 @@ export const ConnectorsRefPage = ({ Connectors - setIsDrawerOpen(false)} /> diff --git a/apps/design-system/src/subjects/views/delegates/delegate-connectivity.tsx b/apps/design-system/src/subjects/views/delegates/delegate-connectivity.tsx index e5f3e85596..4a95cd66d7 100644 --- a/apps/design-system/src/subjects/views/delegates/delegate-connectivity.tsx +++ b/apps/design-system/src/subjects/views/delegates/delegate-connectivity.tsx @@ -4,7 +4,6 @@ import { defaultTo } from 'lodash-es' import { DelegateConnectivityList, SandboxLayout } from '@harnessio/ui/views' import mockDelegatesList from './mock-delegates-list.json' -import { isDelegateSelected } from './utils' const DelegateConnectivityWrapper = (): JSX.Element => ( @@ -21,7 +20,6 @@ const DelegateConnectivityWrapper = (): JSX.Element => ( useTranslationStore={useTranslationStore} isLoading={false} selectedTags={[]} - isDelegateSelected={isDelegateSelected} /> diff --git a/apps/design-system/src/subjects/views/delegates/delegate-selector.tsx b/apps/design-system/src/subjects/views/delegates/delegate-selector.tsx index c2c6145a64..8b90832d50 100644 --- a/apps/design-system/src/subjects/views/delegates/delegate-selector.tsx +++ b/apps/design-system/src/subjects/views/delegates/delegate-selector.tsx @@ -3,16 +3,15 @@ import { useState } from 'react' import { useTranslationStore } from '@utils/viewUtils' import { defaultTo } from 'lodash-es' -import { Drawer, FormSeparator, Icon, StyledLink } from '@harnessio/ui/components' +import { StyledLink } from '@harnessio/ui/components' import { DelegateSelectionTypes, - DelegateSelectorForm, + DelegateSelectorDrawer, DelegateSelectorFormFields, DelegateSelectorInput } from '@harnessio/ui/views' import mockDelegatesList from './mock-delegates-list.json' -import { getMatchedDelegatesCount, isDelegateSelected } from './utils' const delegatesData = mockDelegatesList.map(delegate => ({ groupId: delegate.groupId, @@ -42,46 +41,6 @@ const mockTagsList = [ const renderSelectedValue = (type: DelegateSelectionTypes | null, tags: string[]) => type === DelegateSelectionTypes.TAGS ? tags.join(', ') : type === DelegateSelectionTypes.ANY ? 'any delegate' : null -/* ---------- DRAWER COMPONENT -------------- */ -interface DrawerProps { - open: boolean - setOpen: (open: boolean) => void - preSelectedTags: string[] - onSubmit: (data: DelegateSelectorFormFields) => void - disableAnyDelegate?: boolean -} - -const DelegateSelectorDrawer = ({ open, setOpen, preSelectedTags, onSubmit, disableAnyDelegate }: DrawerProps) => ( - - - - Delegate selector - -
- Haven't installed a delegate yet? - - Install delegate - -
- setOpen(false)} /> -
- - setOpen(false)} - isDelegateSelected={isDelegateSelected} - getMatchedDelegatesCount={getMatchedDelegatesCount} - preSelectedTags={preSelectedTags} - disableAnyDelegate={disableAnyDelegate} - /> -
-
-) - /* ---------- MAIN COMPONENT -------------------------- */ export const DelegateSelector = () => { /* ---- FIRST (ANY allowed) ---- */ @@ -109,17 +68,25 @@ export const DelegateSelector = () => { return (
select a delegate} + placeholder={select a delegate} value={renderSelectedValue(typeA, tagsA)} label="Delegate selector" onClick={() => setOpenA(true)} onEdit={() => setOpenA(true)} onClear={() => setTagsA([])} renderValue={tag => tag} - className="max-w-xs mb-8" + className="mb-8 max-w-xs" /> - +
{ onEdit={() => setOpenB(true)} onClear={() => setTagsB([])} renderValue={tag => tag} - className="max-w-xs mb-8" + className="mb-8 max-w-xs" /> { setOpen={setOpenB} preSelectedTags={tagsB} onSubmit={handleSubmitB} + tagsList={mockTagsList} + delegatesData={delegatesData} + useTranslationStore={useTranslationStore} disableAnyDelegate />
diff --git a/apps/design-system/src/subjects/views/execution/pipeline-execution-graph.tsx b/apps/design-system/src/subjects/views/execution/pipeline-execution-graph.tsx index 70e7e44861..a8d93da6e1 100644 --- a/apps/design-system/src/subjects/views/execution/pipeline-execution-graph.tsx +++ b/apps/design-system/src/subjects/views/execution/pipeline-execution-graph.tsx @@ -91,7 +91,7 @@ export function StepNodeComponent({ pipelineName="npm_build" /> -
+
({ logs })} diff --git a/apps/design-system/src/subjects/views/secrets/secrets.tsx b/apps/design-system/src/subjects/views/secrets/secrets.tsx index e1c713d5fa..b1f4c9f421 100644 --- a/apps/design-system/src/subjects/views/secrets/secrets.tsx +++ b/apps/design-system/src/subjects/views/secrets/secrets.tsx @@ -4,7 +4,7 @@ import { secretsFormDefinition } from '@utils/secrets/secrets-form-schema' import { useTranslationStore } from '@utils/viewUtils' import { InputFactory } from '@harnessio/forms' -import { Drawer, FormSeparator, Spacer, Text } from '@harnessio/ui/components' +import { Drawer, Spacer, Text } from '@harnessio/ui/components' import { ArrayInput, BooleanInput, @@ -155,7 +155,6 @@ export const SecretsPage = ({ Secret - setIsDrawerOpen(false)} /> {/* */} diff --git a/packages/ui/src/components/drawer.tsx b/packages/ui/src/components/drawer.tsx index 2484cdf2e4..c70405e561 100644 --- a/packages/ui/src/components/drawer.tsx +++ b/packages/ui/src/components/drawer.tsx @@ -1,6 +1,7 @@ import * as React from 'react' import { usePortal } from '@/context' +import { ScrollArea } from '@components/scroll-area' import { cn } from '@utils/cn' import { Drawer as DrawerPrimitive } from 'vaul' @@ -75,7 +76,7 @@ const DrawerContent = React.forwardRef< ) => ( -
+
) DrawerHeader.displayName = 'DrawerHeader' +const DrawerInner = ({ className, ...props }: React.HTMLAttributes) => ( + +
+ +) +DrawerInner.displayName = 'DrawerInner' + const DrawerFooter = ({ className, ...props }: React.HTMLAttributes) => ( -
+
) DrawerFooter.displayName = 'DrawerFooter' @@ -104,7 +115,7 @@ const DrawerTitle = React.forwardRef< >(({ className, ...props }, ref) => ( )) @@ -127,6 +138,7 @@ const Drawer = { Close: DrawerClose, Content: DrawerContent, Header: DrawerHeader, + Inner: DrawerInner, Footer: DrawerFooter, Title: DrawerTitle, Description: DrawerDescription diff --git a/packages/ui/src/components/option.tsx b/packages/ui/src/components/option.tsx index dc4da72f51..775fbd920e 100644 --- a/packages/ui/src/components/option.tsx +++ b/packages/ui/src/components/option.tsx @@ -32,24 +32,29 @@ export const Option: FC = ({ control, id, label, description, ariaS aria-labelledby={`${id}-label`} aria-selected={ariaSelected} > -
{control}
-
- - {description && ( - - {description} - - )} -
+ {control &&
{control}
} + {!!label || + (!!description && ( +
+ {!!label && ( + + )} + {!!description && ( + + {description} + + )} +
+ ))}
) } diff --git a/packages/ui/src/components/radio.tsx b/packages/ui/src/components/radio.tsx index 7580c2821e..de08352c24 100644 --- a/packages/ui/src/components/radio.tsx +++ b/packages/ui/src/components/radio.tsx @@ -28,20 +28,29 @@ RadioGroup.displayName = RadioGroupPrimitive.Root.displayName const RadioButton = forwardRef< ElementRef, ComponentPropsWithoutRef ->(({ className, ...props }, ref) => { +>(({ className, asChild, ...props }, ref) => { return ( - + {asChild ? props.children : } ) }) diff --git a/packages/ui/src/components/stacked-list.tsx b/packages/ui/src/components/stacked-list.tsx index 257ac58ca3..5cfd6af294 100644 --- a/packages/ui/src/components/stacked-list.tsx +++ b/packages/ui/src/components/stacked-list.tsx @@ -41,7 +41,9 @@ interface ListItemProps extends React.ComponentProps<'div'>, VariantProps, 'title'>, VariantProps { title?: React.ReactNode + titleClassName?: string description?: React.ReactNode + descriptionClassName?: string label?: boolean secondary?: boolean primary?: boolean @@ -118,7 +120,18 @@ const ListItem = ({ ListItem.displayName = 'StackedListItem' -const ListField = ({ className, title, description, label, primary, secondary, right, ...props }: ListFieldProps) => ( +const ListField = ({ + className, + title, + titleClassName, + description, + descriptionClassName, + label, + primary, + secondary, + right, + ...props +}: ListFieldProps) => (
{title && (
em]:text-cn-foreground-1 font-normal [&>em]:font-medium [&>em]:not-italic', !!label && 'text-cn-foreground-2', - className + titleClassName )} > {title} @@ -137,7 +150,7 @@ const ListField = ({ className, title, description, label, primary, secondary, r className={cn( 'text-cn-foreground-2 flex gap-2 text-ellipsis whitespace-nowrap', primary ? 'text-sm' : 'text-2', - className + descriptionClassName )} > {description} diff --git a/packages/ui/src/components/table.tsx b/packages/ui/src/components/table.tsx index 1c3e9751c2..3f131410cf 100644 --- a/packages/ui/src/components/table.tsx +++ b/packages/ui/src/components/table.tsx @@ -8,7 +8,7 @@ const tableVariants = cva('w-full text-sm', { variant: { default: 'caption-bottom', asStackedList: - 'rounded-md border [&_td]:px-4 [&_td]:py-2.5 [&_td]:align-top [&_th]:px-4 [&_thead]:bg-cn-background-2' + '[&_thead]:bg-cn-background-2 rounded-md border [&_td]:px-4 [&_td]:py-2.5 [&_td]:align-top [&_th]:px-4' } }, defaultVariants: { @@ -48,6 +48,7 @@ const TableBody = forwardRef< className={cn( '[&_tr:last-child]:border-0', { '[&>tr:hover]:bg-cn-background-hover': hasHighlightOnHover }, + { '[&>tr:hover]:cursor-pointer': hasHighlightOnHover }, className )} {...props} diff --git a/packages/ui/src/views/components/RadioSelect.tsx b/packages/ui/src/views/components/RadioSelect.tsx index 7390ac018e..7db6ea7ee6 100644 --- a/packages/ui/src/views/components/RadioSelect.tsx +++ b/packages/ui/src/views/components/RadioSelect.tsx @@ -1,5 +1,4 @@ -import { StackedList } from '@components/index' -import { RadioGroup } from '@radix-ui/react-radio-group' +import { Icon, Option, RadioButton, RadioGroup, StackedList } from '@components/index' import { cn } from '@utils/cn' export interface RadioOption { @@ -28,47 +27,55 @@ export const RadioSelect = ({ return ( void} id={id} className={className}>
- {options.map(option => ( -
) } - -interface OptionProps { - id: string - control: React.ReactNode -} - -const Option = ({ id, control }: OptionProps) => { - return
{control}
-} diff --git a/packages/ui/src/views/delegates/components/delegate-connectivity-list.tsx b/packages/ui/src/views/delegates/components/delegate-connectivity-list.tsx index 937c08ef22..e79b5bc410 100644 --- a/packages/ui/src/views/delegates/components/delegate-connectivity-list.tsx +++ b/packages/ui/src/views/delegates/components/delegate-connectivity-list.tsx @@ -1,11 +1,10 @@ -import { useEffect } from 'react' - -import { Badge, Icon, NoData, SkeletonList, SkeletonTable, Table } from '@/components' +import { Badge, Icon, NoData, SkeletonList, Table } from '@/components' import { cn } from '@utils/cn' import { timeAgo } from '@utils/utils' import { defaultTo } from 'lodash-es' import { DelegateConnectivityListProps } from '../types' +import { isDelegateSelected } from '../utils' const Title = ({ title }: { title: string }): JSX.Element => ( {title} @@ -15,8 +14,7 @@ export function DelegateConnectivityList({ delegates, useTranslationStore, isLoading, - selectedTags, - isDelegateSelected + selectedTags }: DelegateConnectivityListProps): JSX.Element { const { t } = useTranslationStore() @@ -36,67 +34,56 @@ export function DelegateConnectivityList({ } return ( - + - Delegate - Heartbeat - Tags - Selected + Delegate + Heartbeat + Tags + Selected - {isLoading ? ( - - ) : ( - - {delegates.map( - ({ - groupId, - groupName, - activelyConnected, - lastHeartBeat, - groupCustomSelectors, - groupImplicitSelectors - }) => { - return ( - - -
- - </div> - </Table.Cell> - <Table.Cell className="content-center"> - <div className="inline-flex items-center gap-2"> - <Icon - name="dot" - size={8} - className={cn(activelyConnected ? 'text-icons-success' : 'text-icons-danger')} - /> - {lastHeartBeat ? timeAgo(lastHeartBeat) : null} - </div> - </Table.Cell> - <Table.Cell className="max-w-80 content-center truncate"> + + <Table.Body hasHighlightOnHover> + {delegates.map( + ({ groupId, groupName, activelyConnected, lastHeartBeat, groupCustomSelectors, groupImplicitSelectors }) => { + return ( + <Table.Row key={groupId}> + <Table.Cell className="max-w-28 content-center !py-4"> + <div className="flex items-center gap-2.5"> + <Title title={groupName} /> + </div> + </Table.Cell> + <Table.Cell className="content-center !py-4"> + <div className="inline-flex items-center gap-2"> + <Icon + name="dot" + size={8} + className={cn(activelyConnected ? 'text-icons-success' : 'text-icons-danger')} + /> + {lastHeartBeat ? timeAgo(lastHeartBeat) : null} + </div> + </Table.Cell> + <Table.Cell className="max-w-56 truncate !py-4"> + <div className="flex flex-wrap content-center gap-1.5"> {groupCustomSelectors.map((selector: string) => ( - <Badge variant="soft" theme="merged" key={selector} className="mr-2"> + <Badge variant="soft" theme="merged" key={selector}> {selector} </Badge> ))} - </Table.Cell> - <Table.Cell className="min-w-8 text-right"> - {isDelegateSelected( - [...defaultTo(groupImplicitSelectors, []), ...defaultTo(groupCustomSelectors, [])], - selectedTags || [] - ) && <Icon name="tick" size={12} className="text-icons-success" />} - </Table.Cell> - </Table.Row> - ) - } - )} - </Table.Body> - )} + </div> + </Table.Cell> + <Table.Cell className="content-center !py-4 !align-middle"> + {isDelegateSelected( + [...defaultTo(groupImplicitSelectors, []), ...defaultTo(groupCustomSelectors, [])], + selectedTags || [] + ) && <Icon name="tick" size={12} className="text-icons-success mx-auto" />} + </Table.Cell> + </Table.Row> + ) + } + )} + </Table.Body> </Table.Root> ) } diff --git a/packages/ui/src/views/delegates/delegate-selector-drawer/delegate-selector-drawer.tsx b/packages/ui/src/views/delegates/delegate-selector-drawer/delegate-selector-drawer.tsx new file mode 100644 index 0000000000..d14dac69bd --- /dev/null +++ b/packages/ui/src/views/delegates/delegate-selector-drawer/delegate-selector-drawer.tsx @@ -0,0 +1,60 @@ +import { FC } from 'react' + +import { Drawer, Icon, StyledLink } from '@/components' +import { DelegateSelectorForm, DelegateSelectorFormFields, TranslationStore } from '@/views' + +import { DelegateItem } from '../types' + +interface DelegateSelectorDrawerProps { + open: boolean + setOpen: (open: boolean) => void + preSelectedTags: string[] + tagsList: string[] + onSubmit: (data: DelegateSelectorFormFields) => void + useTranslationStore: () => TranslationStore + delegatesData: DelegateItem[] + disableAnyDelegate?: boolean +} + +const DelegateSelectorDrawer: FC<DelegateSelectorDrawerProps> = ({ + open, + setOpen, + preSelectedTags, + tagsList, + onSubmit, + useTranslationStore, + disableAnyDelegate, + delegatesData +}) => ( + <Drawer.Root open={open} onOpenChange={setOpen} direction="right"> + <Drawer.Content className="w-[716px]"> + <Drawer.Header> + <Drawer.Title>Delegate selector</Drawer.Title> + <Drawer.Close className="sr-only" onClick={() => setOpen(false)} /> + </Drawer.Header> + + <Drawer.Inner> + <div className="px-6 pt-5 leading-[18px]"> + Haven't installed a delegate yet? + <StyledLink className="ml-1 inline-flex items-center" variant="accent" to="#"> + Install delegate <Icon name="attachment-link" className="ml-2" size={12} /> + </StyledLink> + </div> + + <DelegateSelectorForm + delegates={delegatesData} + tagsList={tagsList} + useTranslationStore={useTranslationStore} + isLoading={false} + onFormSubmit={onSubmit} + onBack={() => setOpen(false)} + preSelectedTags={preSelectedTags} + disableAnyDelegate={disableAnyDelegate} + FooterWrapper={Drawer.Footer} + /> + </Drawer.Inner> + </Drawer.Content> + </Drawer.Root> +) + +export { DelegateSelectorDrawer } diff --git a/packages/ui/src/views/delegates/delegate-selector/delegate-selector-form.tsx b/packages/ui/src/views/delegates/delegate-selector/delegate-selector-form.tsx index 4db668dbc4..d058195936 100644 --- a/packages/ui/src/views/delegates/delegate-selector/delegate-selector-form.tsx +++ b/packages/ui/src/views/delegates/delegate-selector/delegate-selector-form.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useState } from 'react' +import { Fragment, useCallback, useEffect, useState } from 'react' import { useForm, type SubmitHandler } from 'react-hook-form' import { @@ -21,6 +21,7 @@ import { z } from 'zod' import { DelegateConnectivityList } from '../components/delegate-connectivity-list' import { DelegateItem } from '../types' +import { getMatchedDelegatesCount } from '../utils' export enum DelegateSelectionTypes { ANY = 'any', @@ -57,10 +58,9 @@ export interface DelegateSelectorFormProps { onBack: () => void apiError?: string isLoading: boolean - isDelegateSelected: (selectors: string[], tags: string[]) => boolean - getMatchedDelegatesCount: (delegates: DelegateItem[], tags: string[]) => number preSelectedTags?: string[] disableAnyDelegate?: boolean + FooterWrapper?: React.ComponentType<{ children: React.ReactNode }> } export const DelegateSelectorForm = (props: DelegateSelectorFormProps): JSX.Element => { @@ -72,10 +72,9 @@ export const DelegateSelectorForm = (props: DelegateSelectorFormProps): JSX.Elem onBack, apiError = null, isLoading, - isDelegateSelected, - getMatchedDelegatesCount, preSelectedTags, - disableAnyDelegate + disableAnyDelegate, + FooterWrapper = Fragment } = props const { t } = useTranslationStore() const [searchTag, setSearchTag] = useState('') @@ -151,10 +150,10 @@ export const DelegateSelectorForm = (props: DelegateSelectorFormProps): JSX.Elem ) return ( - <SandboxLayout.Content className="h-full px-0 pt-0"> - <Spacer size={5} /> - <FormWrapper className="flex h-full flex-col" onSubmit={handleSubmit(onSubmit)}> - <Fieldset className="mb-0"> + <SandboxLayout.Content className="h-full p-0"> + <Spacer size={6} /> + <FormWrapper className="flex h-full flex-col gap-y-6" onSubmit={handleSubmit(onSubmit)}> + <Fieldset className="mb-0 px-6"> <RadioSelect id="type" {...register('type')} @@ -169,11 +168,11 @@ export const DelegateSelectorForm = (props: DelegateSelectorFormProps): JSX.Elem <Alert.Description>{apiError?.toString()}</Alert.Description> </Alert.Container> )} - <FormSeparator /> + <FormSeparator className="mx-6" /> {delegateType === DelegateSelectionTypes.TAGS && ( - <> - <Fieldset className="py-2"> + <div className="px-6"> + <Fieldset> {/* TAGS */} <MultiSelect {...register('tags')} @@ -190,34 +189,37 @@ export const DelegateSelectorForm = (props: DelegateSelectorFormProps): JSX.Elem error={errors.tags?.message?.toString()} /> </Fieldset> - <Text size={4}>Test Delegate connectivity</Text> - <p>Matches: {matchedDelegates}</p> + <Text className="mb-5 mt-9 leading-none tracking-tight" size={3} weight="medium" as="p"> + Test delegate connectivity + </Text> + <Text className="text-cn-foreground-4 mb-3 mt-5 font-medium leading-tight" size={2} as="p"> + Matches: {matchedDelegates} + </Text> <DelegateConnectivityList delegates={delegates} useTranslationStore={useTranslationStore} isLoading={isLoading} selectedTags={selectedTags.map(tag => tag.id)} - isDelegateSelected={isDelegateSelected} /> - </> + </div> )} - <div className="absolute inset-x-0 bottom-0 bg-cn-background-2 p-4 shadow-md"> - <ControlGroup> - <ButtonGroup className="flex flex-row justify-between"> - <Button type="button" variant="ghost" onClick={onBack}> - Back - </Button> - <Button type="submit"> - Connect  - {delegateType === DelegateSelectionTypes.TAGS ? matchedDelegates : 'any'}  - {delegateType === DelegateSelectionTypes.TAGS && matchedDelegates > 1 ? 'delegates' : 'delegate'} - </Button> - </ButtonGroup> - </ControlGroup> - </div> - - <div className="pb-16"></div> + <FooterWrapper> + <div className="bg-cn-background-2 inset-x-0 bottom-0 px-6 py-5 shadow-md"> + <ControlGroup> + <ButtonGroup className="flex flex-row justify-between"> + <Button type="button" variant="outline" onClick={onBack}> + Back + </Button> + <Button type="submit"> + Connect  + {delegateType === DelegateSelectionTypes.TAGS ? matchedDelegates : 'any'}  + {delegateType === DelegateSelectionTypes.TAGS && matchedDelegates > 1 ? 'delegates' : 'delegate'} + </Button> + </ButtonGroup> + </ControlGroup> + </div> + </FooterWrapper> </FormWrapper> </SandboxLayout.Content> ) diff --git a/packages/ui/src/views/delegates/index.ts b/packages/ui/src/views/delegates/index.ts index 5469339fbb..bd56cac160 100644 --- a/packages/ui/src/views/delegates/index.ts +++ b/packages/ui/src/views/delegates/index.ts @@ -2,3 +2,4 @@ export * from './types' export * from './components/delegate-connectivity-list' export * from './delegate-selector/delegate-selector-form' export * from './delegate-selector/delegate-selector-input' +export * from './delegate-selector-drawer/delegate-selector-drawer' diff --git a/packages/ui/src/views/delegates/types.ts b/packages/ui/src/views/delegates/types.ts index 9ad469f4af..3dba1cf880 100644 --- a/packages/ui/src/views/delegates/types.ts +++ b/packages/ui/src/views/delegates/types.ts @@ -5,7 +5,6 @@ export interface DelegateConnectivityListProps { useTranslationStore: () => TranslationStore isLoading: boolean selectedTags?: string[] - isDelegateSelected: (selectors: string[], tags: string[]) => boolean } export interface DelegateItem { diff --git a/apps/design-system/src/subjects/views/delegates/utils.ts b/packages/ui/src/views/delegates/utils.ts similarity index 93% rename from apps/design-system/src/subjects/views/delegates/utils.ts rename to packages/ui/src/views/delegates/utils.ts index c3f1cec36f..40a8ccb936 100644 --- a/apps/design-system/src/subjects/views/delegates/utils.ts +++ b/packages/ui/src/views/delegates/utils.ts @@ -1,4 +1,4 @@ -import { DelegateItem } from '@harnessio/ui/views' +import { DelegateItem } from './types' export const isDelegateSelected = (delegateSelectors: string[], tags: string[] = []) => { if (!tags?.length) {