Skip to content

Commit

Permalink
Stacked sidebar (#125)
Browse files Browse the repository at this point in the history
Signed-off-by: Nik Nasr <[email protected]>
  • Loading branch information
nikrooz authored Dec 13, 2024
1 parent de46f4b commit b4d1aa7
Show file tree
Hide file tree
Showing 10 changed files with 163 additions and 49 deletions.
6 changes: 3 additions & 3 deletions libs/features/invocation-route/src/lib/InvocationId.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import { TruncateWithTooltip } from '@restate/ui/tooltip';
import { Link } from '@restate/ui/link';
import { Invocation } from '@restate/data-access/admin-api';
import { tv } from 'tailwind-variants';
import { useSearchParams } from 'react-router';
import { INVOCATION_QUERY_NAME } from './constants';
import { useActiveSidebarParam } from '@restate/ui/layout';

const styles = tv({
base: 'relative text-zinc-600 font-mono',
Expand Down Expand Up @@ -51,8 +51,8 @@ export function InvocationId({
}) {
const linkRef = useRef<HTMLAnchorElement>(null);
const { base, icon, text, link, container, linkIcon } = styles({ size });
const [searchParams] = useSearchParams();
const isSelected = searchParams.getAll(INVOCATION_QUERY_NAME).includes(id);
const invocationInSidebar = useActiveSidebarParam(INVOCATION_QUERY_NAME);
const isSelected = invocationInSidebar === id;

return (
<div className={base({ className })}>
Expand Down
10 changes: 6 additions & 4 deletions libs/features/overview-route/src/lib/Deployment.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ import {
import { Revision } from './Revision';
import { DEPLOYMENT_QUERY_PARAM } from './constants';
import { Link } from '@restate/ui/link';
import { useSearchParams } from 'react-router';
import { useRef } from 'react';
import { useActiveSidebarParam } from '@restate/ui/layout';

const styles = tv({
base: 'flex flex-row items-center gap-2 relative border -m-1 p-1 transition-all ease-in-out text-code',
Expand All @@ -36,10 +36,12 @@ export function Deployment({
}) {
const { data: { deployments } = {} } = useListDeployments();
const deployment = deploymentId ? deployments?.get(deploymentId) : undefined;
const [searchParams] = useSearchParams();
const activeDeploymentInSidebar = useActiveSidebarParam(
DEPLOYMENT_QUERY_PARAM
);

const isSelected =
searchParams.get(DEPLOYMENT_QUERY_PARAM) === deploymentId &&
highlightSelection;
activeDeploymentInSidebar === deploymentId && highlightSelection;
const linkRef = useRef<HTMLAnchorElement>(null);

if (!deployment) {
Expand Down
7 changes: 6 additions & 1 deletion libs/features/overview-route/src/lib/Details/Service.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -444,7 +444,12 @@ function ServiceForm({
<div className="flex flex-col gap-2">
{sortedRevisions.map((revision) =>
deployments?.[revision]?.map((id) => (
<Deployment deploymentId={id} revision={revision} key={id} />
<Deployment
deploymentId={id}
revision={revision}
key={id}
highlightSelection={false}
/>
))
)}
</div>
Expand Down
6 changes: 3 additions & 3 deletions libs/features/overview-route/src/lib/Service.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import { Deployment } from './Deployment';
import { TruncateWithTooltip } from '@restate/ui/tooltip';
import { Link } from '@restate/ui/link';
import { SERVICE_QUERY_PARAM } from './constants';
import { useSearchParams } from 'react-router';
import { useRef } from 'react';
import { useActiveSidebarParam } from '@restate/ui/layout';

const styles = tv({
base: 'w-full rounded-2xl p2-0.5 pt2-1 border shadow-zinc-800/[0.03] transform transition',
Expand All @@ -32,8 +32,8 @@ export function Service({
const serviceDeployments = service?.deployments;
const revisions = service?.sortedRevisions ?? [];

const [searchParams] = useSearchParams();
const isSelected = searchParams.get(SERVICE_QUERY_PARAM) === serviceName;
const activeServiceInSidebar = useActiveSidebarParam(SERVICE_QUERY_PARAM);
const isSelected = activeServiceInSidebar === serviceName;
const linkRef = useRef<HTMLAnchorElement>(null);

const deploymentRevisionPairs = revisions
Expand Down
14 changes: 13 additions & 1 deletion libs/ui/dropdown/src/lib/DropdownItem.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Icon, IconName } from '@restate/ui/icons';
import { useHrefWithQueryParams } from '@restate/ui/link';
import type { PropsWithChildren } from 'react';
import {
MenuItem as AriaMenuItem,
Expand Down Expand Up @@ -102,10 +103,21 @@ export type DropdownItemProps =
| DropdownNavItemProps;

export function DropdownItem(props: DropdownItemProps) {
const hrefWithQUeryParams = useHrefWithQueryParams({
href: props.href,
preserveQueryParams: true,
mode: 'append',
});

if (isNavItem(props)) {
const { href, value, ...rest } = props;
return (
<StyledDropdownItem {...rest} href={href} id={value} textValue={value} />
<StyledDropdownItem
{...rest}
href={hrefWithQUeryParams}
id={value}
textValue={value}
/>
);
}
if (isCustomItem(props)) {
Expand Down
1 change: 1 addition & 0 deletions libs/ui/layout/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ export {
Complementary,
ComplementaryWithSearchParam,
ComplementaryClose,
useActiveSidebarParam,
} from './lib/Complementary';
95 changes: 72 additions & 23 deletions libs/ui/layout/src/lib/Complementary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { FocusScope } from 'react-aria';
interface ComplementaryProps {
footer?: ReactNode;
onClose?: VoidFunction;
isOnTop?: boolean;
}

// eslint-disable-next-line @typescript-eslint/no-empty-function
Expand All @@ -26,6 +27,7 @@ export function Complementary({
children,
footer,
onClose = noop,
isOnTop = false,
}: PropsWithChildren<ComplementaryProps>) {
if (!children) {
return null;
Expand All @@ -34,25 +36,30 @@ export function Complementary({
return (
<ComplementaryContext.Provider value={{ onClose }}>
<LayoutOutlet zone={LayoutZone.Complementary}>
<FocusScope restoreFocus autoFocus>
<div
data-complementary-content
className="overflow-y-auto min-h-[50vh] bg-white p-3 pt-7 border rounded-xl max-h-[inherit] overflow-auto relative flex-auto"
onKeyDown={(e) => {
if (e.key === 'Escape') {
onClose?.();
}
}}
>
<div tabIndex={0} />
{children}
</div>
{footer && (
<div className="flex gap-2 has-[*]:py-1 has-[*]:pb-0 has-[*]:mt-1 [&>*]:min-w-0 3xl:sticky 3xl:bottom-0 3xl:bg-gray-50/80 3xl:backdrop-blur-xl 3xl:backdrop-saturate-200 rounded-[1rem] 3xl:-mx-1.5 3xl:-mb-1.5 3xl:p-1.5 3xl:pb-1.5 z-10">
{footer}
<div
data-top={isOnTop}
className="[&[data-top=false]]:overflow-hidden duration-250 [&[data-top=true]]:z-[1] [&[data-top=true]]:order-1 transition-all min-h-0 min-w-0 p-1.5 border shadow-lg 3xl:shadow-sm shadow-zinc-800/5 bg-gray-50/80 backdrop-blur-xl backdrop-saturate-200 rounded-[1.125rem] max-h-[inherit] flex flex-col w-full"
>
<FocusScope restoreFocus autoFocus>
<div
data-complementary-content
className="overflow-y-auto bg-white p-3 pt-7 border rounded-xl flex-auto flex flex-col min-h-[50vh] overflow-auto relative max-h-[inherit]"
onKeyDown={(e) => {
if (e.key === 'Escape') {
onClose?.();
}
}}
>
<div tabIndex={0} />
{children}
</div>
)}
</FocusScope>
{footer && (
<div className="flex gap-2 has-[*]:py-1 has-[*]:pb-0 has-[*]:mt-1 [&>*]:min-w-0 3xl:sticky 3xl:bottom-0 3xl:bg-gray-50/80 3xl:backdrop-blur-xl 3xl:backdrop-saturate-200 rounded-[1rem] 3xl:-mx-1.5 3xl:-mb-1.5 3xl:p-1.5 3xl:pb-1.5 z-10">
{footer}
</div>
)}
</FocusScope>
</div>
</LayoutOutlet>
</ComplementaryContext.Provider>
);
Expand Down Expand Up @@ -84,9 +91,36 @@ export function ComplementaryWithSearchParam({
paramName: string;
}
>) {
const [searchParams, setSearchParams] = useSearchParams();
const [searchParams] = useSearchParams();
const paramValues = searchParams.getAll(paramName);

return (
<>
{paramValues.map((paramValue) => (
<ComplementaryWithSearchParamValue
children={children}
footer={footer}
key={paramValue}
paramName={paramName}
paramValue={paramValue}
/>
))}
</>
);
}

const paramValue = searchParams.get(paramName);
function ComplementaryWithSearchParamValue({
children,
footer,
paramName,
paramValue,
}: PropsWithChildren<
Pick<ComplementaryProps, 'footer'> & {
paramName: string;
paramValue: string;
}
>) {
const [searchParams, setSearchParams] = useSearchParams();
const renderedChildren = useMemo(() => {
if (!paramValue) {
return null;
Expand All @@ -96,16 +130,31 @@ export function ComplementaryWithSearchParam({

const onClose = useCallback(() => {
setSearchParams((prev) => {
prev.delete(paramName);
return prev;
return new URLSearchParams(
prev.toString().replace(`${paramName}=${paramValue}`, '')
);
});
}, [paramName, setSearchParams]);
}, [paramName, paramValue, setSearchParams]);
const isOnTop = searchParams
.toString()
.startsWith(`${paramName}=${paramValue}`);

return (
<Complementary
children={renderedChildren}
footer={footer}
onClose={onClose}
isOnTop={isOnTop}
/>
);
}

export function useActiveSidebarParam(paramName: string) {
const [searchParams] = useSearchParams();

if (searchParams.toString().startsWith(paramName)) {
return searchParams.get(paramName) as string;
} else {
return undefined;
}
}
2 changes: 1 addition & 1 deletion libs/ui/layout/src/lib/ComplementaryOutlet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export function ComplementaryOutlet(
<aside className="[&:has(>*>*)]:duration-250 [&:has(>*>*)]:animate-in [&:has(>*>*)]:slide-in-from-right [&:has(>*>*)]:fade-in [&:not(has(>*>*))]:duration-250 flex flex-col 3xl:sticky top-[50%] left-[50%] -translate-x-1/2 -translate-y-1/2 sm:translate-y-0 sm:translate-x-0 sm:top-24 3xl:top-[calc(0.75rem+3.5rem+2.5rem)] 3xl:px-0 3xl:pt-8 [&:not(:has([data-complementary-content]>*))]:hidden fixed z-[100] sm:z-50 lg:right-8 sm:right-6 right-auto sm:left-auto lg:bottom-6 sm:bottom-6 max-h-[90vh] max-w-[100vw] 3xl:max-h-auto sm:max-h-none lg:max-h-none 3xl:max-h-none">
<div
{...props}
className="3xl:h-auto h-full flex-auto 3xl:flex-none p-1.5 border shadow-lg 3xl:shadow-sm shadow-zinc-800/5 bg-gray-50/80 backdrop-blur-xl backdrop-saturate-200 rounded-[1.125rem] max-h-[inherit] flex flex-col max-w-[90vw] w-[350px]"
className="relative [&>*]:row-start-1 [&>*]:row-end-2 [&>*]:col-start-1 [&>*]:col-end-2 [&>[data-top=false]]:absolute [&>[data-top=false]:has(~[data-top=false])]:-right-3 [&>[data-top=false]:has(~[data-top=false])]:top-3 [&>[data-top=false]:has(~[data-top=false])]:bottom-3 [&>*[data-top=false]]:-right-1.5 [&>[data-top=false]]:top-1.5 [&>[data-top=false]]:bottom-1.5 3xl:h-auto h-full flex-auto 3xl:flex-none max-h-[inherit] grid [grid-template-columns:1fr] [grid-template-rows:1fr] max-w-[90vw] w-[350px]"
/>
</aside>
</>
Expand Down
2 changes: 1 addition & 1 deletion libs/ui/layout/src/lib/Layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ export function LayoutOutlet({
return createPortal(
<>
{children}
<div data-variant={variant} />
{zone === LayoutZone.AppBar && <div data-variant={variant} />}
</>,
document.getElementById(ZONE_IDS[zone])!
);
Expand Down
69 changes: 57 additions & 12 deletions libs/ui/link/src/lib/Link.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { focusRing } from '@restate/ui/focus';
import { AriaAttributes, forwardRef } from 'react';
import { AriaAttributes, forwardRef, useMemo } from 'react';
import {
Link as AriaLink,
LinkProps as AriaLinkProps,
composeRenderProps,
} from 'react-aria-components';
import { useSearchParams } from 'react-router';
import { tv } from 'tailwind-variants';

interface LinkProps
Expand All @@ -21,6 +22,7 @@ interface LinkProps
Pick<AriaAttributes, 'aria-current'> {
className?: string;
variant?: 'primary' | 'secondary' | 'button' | 'secondary-button';
preserveQueryParams?: boolean;
}

const styles = tv({
Expand All @@ -43,14 +45,57 @@ const styles = tv({
},
});

export const Link = forwardRef<HTMLAnchorElement, LinkProps>((props, ref) => {
return (
<AriaLink
{...props}
ref={ref}
className={composeRenderProps(props.className, (className, renderProps) =>
styles({ ...renderProps, className, variant: props.variant })
)}
/>
);
});
export function useHrefWithQueryParams({
preserveQueryParams,
href,
mode = 'prepend',
}: {
preserveQueryParams: boolean;
href?: string;
mode?: 'append' | 'prepend';
}) {
const [searchParams] = useSearchParams();

const hrefWithQueryParams = useMemo(() => {
if (preserveQueryParams && href?.startsWith('?')) {
const newSearchParams = new URLSearchParams(href);
let existingSearchParams = new URLSearchParams(searchParams);
Array.from(newSearchParams.entries()).forEach(([key, value]) => {
existingSearchParams = new URLSearchParams(
existingSearchParams.toString().replace(`${key}=${value}`, '')
);
});
const combinedSearchParams = new URLSearchParams([
...(mode === 'prepend' ? newSearchParams : []),
...existingSearchParams,
...(mode === 'append' ? newSearchParams : []),
]);
return '?' + combinedSearchParams.toString();
} else {
return href;
}
}, [preserveQueryParams, href, searchParams, mode]);

return hrefWithQueryParams;
}
export const Link = forwardRef<HTMLAnchorElement, LinkProps>(
({ href, preserveQueryParams = true, ...props }, ref) => {
const hrefWithQueryParams = useHrefWithQueryParams({
href,
preserveQueryParams,
});

return (
<AriaLink
{...props}
href={hrefWithQueryParams}
ref={ref}
className={composeRenderProps(
props.className,
(className, renderProps) =>
styles({ ...renderProps, className, variant: props.variant })
)}
/>
);
}
);

0 comments on commit b4d1aa7

Please sign in to comment.