Skip to content
Open
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
138 changes: 138 additions & 0 deletions dashboards/src/components/DashboardLinks/DashboardLinks.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
// Copyright The Perses Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import { IconButton, Link as LinkComponent, Menu, MenuItem, Theme, Chip, Stack } from '@mui/material';

Check failure on line 14 in dashboards/src/components/DashboardLinks/DashboardLinks.tsx

View workflow job for this annotation

GitHub Actions / lint-npm

'Theme' is defined but never used. Allowed unused vars must match /^_/u
import LaunchIcon from 'mdi-material-ui/Launch';
import { Link } from '@perses-dev/core';
import { MouseEvent, ReactElement, useState } from 'react';
import { InfoTooltip } from '@perses-dev/components';
import { useReplaceVariablesInString } from '@perses-dev/plugin-system';

export interface DashboardLinksProps {
links: Link[];
}

/**
* Displays dashboard-level links in the toolbar.
* Renders as chips for 1-3 links, or a dropdown menu for more.
* Supports variable replacement in URLs when renderVariables is enabled.
*/
export function DashboardLinks({ links }: DashboardLinksProps): ReactElement | null {
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
const isMenuOpened = Boolean(anchorEl);

const handleOpenMenu = (event: MouseEvent<HTMLButtonElement>): void => {
setAnchorEl(event.currentTarget);
};

const handleClose = (): void => {
setAnchorEl(null);
};

if (links.length === 0) {
return null;
}

// If there is only one link, show it directly as a chip
if (links.length === 1 && links[0]) {
const link = links[0];
return <DashboardLinkChip link={link} />;
}

// If there are 2-3 links, show them all as chips
if (links.length <= 3) {
return (
<Stack direction="row" spacing={0.5} alignItems="center">
{links.map((link) => (
<DashboardLinkChip key={link.url} link={link} />
))}
</Stack>
);
}

Comment on lines +46 to +62
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's ok to remove the first return & go through the loop when there's only 1 link?

Suggested change
// If there is only one link, show it directly as a chip
if (links.length === 1 && links[0]) {
const link = links[0];
return <DashboardLinkChip link={link} />;
}
// If there are 2-3 links, show them all as chips
if (links.length <= 3) {
return (
<Stack direction="row" spacing={0.5} alignItems="center">
{links.map((link) => (
<DashboardLinkChip key={link.url} link={link} />
))}
</Stack>
);
}
// If there are a few links, show them all as chips
if (links.length <= 3) {
return (
<Stack direction="row" spacing={0.5} alignItems="center">
{links.map((link) => (
<DashboardLinkChip key={link.url} link={link} />
))}
</Stack>
);
}

// For more than 3 links, show a menu
return (
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this part is very similar to its PanelLinks counterpart; could you try to factorize & reuse here?

<>
<InfoTooltip description={`${links.length} links`} enterDelay={100}>
<IconButton
aria-label="Dashboard links"
size="small"
onClick={handleOpenMenu}
sx={(theme) => ({ borderRadius: theme.shape.borderRadius, padding: theme.spacing(0.5) })}
>
<LaunchIcon
aria-describedby="links-icon"
fontSize="inherit"
sx={{ color: (theme) => theme.palette.text.secondary }}
/>
</IconButton>
</InfoTooltip>

<Menu
anchorEl={anchorEl}
open={isMenuOpened}
onClose={handleClose}
MenuListProps={{
'aria-labelledby': 'dashboard-links',
}}
>
{links.map((link: Link) => (
<DashboardLinkMenuItem key={link.url} link={link} />
))}
</Menu>
</>
);
}

function DashboardLinkChip({ link }: { link: Link }): ReactElement {
const { url, name, tooltip, targetBlank } = useLink(link);

return (
<InfoTooltip description={tooltip ?? url} enterDelay={100}>
<Chip
label={name ?? url}
component="a"
href={url}
target={targetBlank ? '_blank' : '_self'}
clickable
size="small"
icon={<LaunchIcon fontSize="small" />}
sx={(theme) => ({ height: theme.spacing(3) })}
/>
</InfoTooltip>
);
}

function DashboardLinkMenuItem({ link }: { link: Link }): ReactElement {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same remark as above

const { url, name, tooltip, targetBlank } = useLink(link);

return (
<InfoTooltip description={tooltip ?? url} enterDelay={100}>
<MenuItem component={LinkComponent} href={url} target={targetBlank ? '_blank' : '_self'}>
{name ?? url}
</MenuItem>
</InfoTooltip>
);
}

function useLink(link: Link): Link {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same remark as above

const url = useReplaceVariablesInString(link.url) ?? link.url;
const name = useReplaceVariablesInString(link.name);
const tooltip = useReplaceVariablesInString(link.tooltip);

if (link.renderVariables === false) {
return link;
}

return { ...link, url, name, tooltip };
}
Loading
Loading