Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Stacked sidebar #125

Merged
merged 2 commits into from
Dec 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 })
)}
/>
);
}
);
Loading