Skip to content

Commit

Permalink
Implement option to display links in search sidebar. (#20638)
Browse files Browse the repository at this point in the history
* Implement option to display links in search sidebar.

* Render sidebar links conditonally

* Simplify code

* Make sidebar components reusable

* Make `multi` prop of `StreamsFitler` configurable.

* Rename plugin key

* Fix linter hints

* Fixing TS error

* Fix TS error
  • Loading branch information
linuspahl authored Oct 16, 2024
1 parent 9e097a3 commit c7fff37
Show file tree
Hide file tree
Showing 4 changed files with 126 additions and 80 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,14 @@ const Container = styled.div`
`;

type Props = {
disabled?: boolean
value?: Array<string>
disabled?: boolean,
value?: Array<string>,
streams: Array<{ key: string, value: string }>,
onChange: (newStreamIds: Array<string>) => void,
multi?: boolean,
};

const StreamsFilter = ({ disabled = false, value = [], streams, onChange }: Props) => {
const StreamsFilter = ({ disabled = false, value = [], streams, onChange, multi = true }: Props) => {
const sendTelemetry = useSendTelemetry();
const selectedStreams = value.join(',');
const placeholder = 'Select streams the search should include. Searches in all streams if empty.';
Expand Down Expand Up @@ -63,7 +64,7 @@ const StreamsFilter = ({ disabled = false, value = [], streams, onChange }: Prop
inputId="streams-filter"
onChange={handleChange}
options={options}
multi
multi={multi}
value={selectedStreams} />
</Container>
);
Expand Down
135 changes: 71 additions & 64 deletions graylog2-web-interface/src/views/components/sidebar/NavItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,53 +15,44 @@
* <http://www.mongodb.com/licensing/server-side-public-license>.
*/
import * as React from 'react';
import PropTypes from 'prop-types';
import styled, { css } from 'styled-components';

import Icon from 'components/common/Icon';
import type { IconName } from 'components/common/Icon';

export type NavItemProps = {
isSelected?: boolean
title: string,
icon: IconName,
onClick: () => void,
showTitleOnHover?: boolean,
sidebarIsPinned: boolean,
disabled?: boolean,
ariaLabel: string,
};
import { Link } from 'components/common/router';

type ContainerProps = {
$isSelected: boolean,
$sidebarIsPinned: boolean,
$disabled: boolean,
$isLink: boolean,
};

const Container = styled.button<ContainerProps>(({ theme: { colors, fonts }, $isSelected, $sidebarIsPinned, $disabled }) => css`
const Container = styled.button<ContainerProps>(({ theme: { colors, fonts }, $isSelected, $sidebarIsPinned, $disabled, $isLink }) => css`
position: relative;
z-index: 4; /* to render over SidebarNav::before */
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 40px;
text-align: center;
cursor: ${$disabled ? 'not-allowed' : 'pointer'};
font-size: ${fonts.size.h3};
z-index: 4; /* to render over SidebarNav::before */
cursor: ${$disabled ? 'not-allowed' : 'pointer'};
color: ${colors.variant.darkest.default};
background: ${$isSelected ? colors.gray[90] : colors.global.contentBackground};
background: transparent;
border: 0;
padding: 0;
&:hover {
color: ${$isSelected ? colors.variant.darkest.default : colors.variant.darker.default};
background: ${$isSelected ? colors.gray[80] : colors.variant.lightest.default};
&:active > span {
background: ${colors.variant.lighter.default};
}
&:active {
background: ${colors.variant.lighter.default};
&:hover {
text-decoration: none;
}
/* stylelint-disable selector-max-empty-lines, indentation */
${($isSelected && !$sidebarIsPinned) && css`
${$isSelected && !$isLink && !$sidebarIsPinned && css`
&::before,
&::after {
content: '';
Expand All @@ -76,48 +67,55 @@ const Container = styled.button<ContainerProps>(({ theme: { colors, fonts }, $is
transform: skewY(-45deg);
top: calc(50% - 12px);
}
&::after {
transform: skewY(45deg);
bottom: calc(50% - 12px);
}
`}
`}
/* stylelint-enable selector-max-empty-lines, indentation */
`);

type IconWrapProps = {
$showTitleOnHover: boolean,
$isSelected: boolean,
$sidebarIsPinned: boolean,
$sidebarIsPinned?: boolean,
$disabled: boolean,
$isLink: boolean,
}
const IconWrap = styled.span<IconWrapProps>(({ $showTitleOnHover, $isSelected, $disabled, $sidebarIsPinned, theme: { colors } }) => css`
const IconWrap = styled.span<IconWrapProps>(({
$isSelected, $disabled, $isLink,
$sidebarIsPinned, theme: { colors },
}) => css`
display: flex;
width: 100%;
height: 100%;
width: ${$isLink ? '40px' : '100%'};
height: ${$isLink ? '40px' : '100%'};
align-items: center;
justify-content: center;
position: relative;
opacity: ${$disabled ? 0.65 : 1};
background: ${$isSelected ? colors.gray[90] : colors.global.contentBackground};
border-radius: ${$isLink ? '50%' : '0'};
&:hover {
color: ${$isSelected ? colors.variant.darkest.default : colors.variant.darker.default};
background: ${$isSelected ? colors.gray[80] : colors.variant.lightest.default};
+ div {
display: ${($showTitleOnHover && !$isSelected) ? 'flex' : 'none'};
display: ${$isLink || !$isSelected ? 'flex' : 'none'};
}
&::after {
display: ${($showTitleOnHover) ? 'block' : 'none'};
&::after {
display: block;
}
}
&::after {
display: ${$isSelected ? 'block' : 'none'};
box-shadow: ${($isSelected && !$sidebarIsPinned) ? `inset 2px -2px 2px 0 ${colors.global.navigationBoxShadow}` : 'none'};
background-color: ${$isSelected ? colors.global.contentBackground : colors.variant.lightest.info};
border: ${$isSelected ? 'none' : `1px solid ${colors.variant.light.info}`};
display: ${$isSelected && !$isLink ? 'block' : 'none'};
box-shadow: ${$isSelected && !$sidebarIsPinned && !$isLink ? `inset 2px -2px 2px 0 ${colors.global.navigationBoxShadow}` : 'none'};
background-color: ${$isSelected && !$isLink ? colors.global.contentBackground : colors.variant.lightest.info};
border: ${$isSelected && !$isLink ? 'none' : `1px solid ${colors.variant.light.info}`};
content: ' ';
position: absolute;
left: 82.5%;
left: ${$isLink ? '89%' : '82.5%'};
top: calc(50% - 9px);
width: 18px;
height: 18px;
Expand All @@ -139,6 +137,7 @@ const Title = styled.div(({ theme: { colors, fonts } }) => css`
z-index: 4;
border-radius: 0 3px 3px 0;
align-items: center;
white-space: nowrap;
span {
color: ${colors.variant.darker.info};
Expand All @@ -148,30 +147,38 @@ const Title = styled.div(({ theme: { colors, fonts } }) => css`
}
`);

const NavItem = ({ isSelected = false, title, icon, onClick, showTitleOnHover = true, sidebarIsPinned, disabled = false, ariaLabel }: NavItemProps) => (
<Container aria-label={ariaLabel}
$isSelected={isSelected}
onClick={!disabled ? onClick : undefined}
title={showTitleOnHover ? '' : title}
$sidebarIsPinned={sidebarIsPinned}
$disabled={disabled}>
<IconWrap $showTitleOnHover={showTitleOnHover}
$isSelected={isSelected}
$sidebarIsPinned={sidebarIsPinned}
$disabled={disabled}>
<Icon name={icon} type="regular" />
</IconWrap>
{(showTitleOnHover && !isSelected) && <Title><span>{title}</span></Title>}
</Container>
);
export type Props = {
isSelected?: boolean,
title: string,
icon: IconName,
onClick?: () => void,
sidebarIsPinned?: boolean,
disabled?: boolean,
ariaLabel: string,
linkTarget?: string,
};

NavItem.propTypes = {
icon: PropTypes.node.isRequired,
isSelected: PropTypes.bool,
showTitleOnHover: PropTypes.bool,
sidebarIsPinned: PropTypes.bool.isRequired,
title: PropTypes.string.isRequired,
disabled: PropTypes.bool,
const NavItem = ({ isSelected = false, title, icon, onClick = undefined, sidebarIsPinned = false, disabled = false, ariaLabel, linkTarget = undefined }: Props) => {
const isLink = !!linkTarget;
const containerProps = isLink ? { as: Link, to: linkTarget, $isLink: true } : { $isLink: false };

return (
<Container {...containerProps}
aria-label={ariaLabel}
$isSelected={isSelected}
onClick={!disabled ? onClick : undefined}
title={title}
$sidebarIsPinned={sidebarIsPinned}
$disabled={disabled}>
<IconWrap $isLink={isLink}
$isSelected={isSelected}
$sidebarIsPinned={sidebarIsPinned}
$disabled={disabled}>
<Icon name={icon} type="regular" />
</IconWrap>
{(isLink ? true : !isSelected) && <Title><span>{title}</span></Title>}
</Container>
);
};

export default NavItem;
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,21 @@ import * as React from 'react';
import styled, { css } from 'styled-components';

import type { SidebarAction } from 'views/components/sidebar/sidebarActions';
import type { IconName } from 'components/common/Icon';
import usePluginEntities from 'hooks/usePluginEntities';
import useLocation from 'routing/useLocation';

import NavItem from './NavItem';
import type { SidebarSection } from './sidebarSections';

type Props = {
activeSection: SidebarSection | undefined | null,
sections: Array<SidebarSection>,
actions: Array<SidebarAction>,
selectSidebarSection: (sectionKey: string) => void,
sidebarIsPinned: boolean,
export type SidebarPage = {
key: string,
title: string,
icon: IconName,
link: string,
};

const Container = styled.div<{ $isOpen: boolean, $sidebarIsPinned: boolean }>(({ $isOpen, $sidebarIsPinned, theme }) => css`
export const Container = styled.div<{ $isOpen?: boolean, $sidebarIsPinned?: boolean }>(({ $isOpen, $sidebarIsPinned, theme }) => css`
background: ${theme.colors.global.navigationBackground};
color: ${theme.utils.contrastingColor(theme.colors.global.navigationBackground, 'AA')};
box-shadow: ${($sidebarIsPinned && $isOpen) ? 'none' : `3px 3px 3px ${theme.colors.global.navigationBoxShadow}`};
Expand All @@ -53,7 +55,7 @@ const Container = styled.div<{ $isOpen: boolean, $sidebarIsPinned: boolean }>(({
}
`);

const SectionList = styled.div`
export const Section = styled.div`
> * {
margin-bottom: 5px;
Expand All @@ -72,12 +74,38 @@ const HorizontalRuleWrapper = styled.div`
}
`;

type Props = {
activeSection: SidebarSection | undefined | null,
sections: Array<SidebarSection>,
actions: Array<SidebarAction>,
selectSidebarSection: (sectionKey: string) => void,
sidebarIsPinned: boolean,
};

const SidebarNavigation = ({ sections, activeSection, selectSidebarSection, sidebarIsPinned, actions }: Props) => {
const activeSectionKey = activeSection?.key;
const { pathname } = useLocation();
const links = usePluginEntities('views.searchDataSources');
const accessibleLinks = links.filter((link) => (link.useCondition ? !!link.useCondition() : true));

return (
<Container $sidebarIsPinned={sidebarIsPinned} $isOpen={!!activeSection}>
<SectionList>
{accessibleLinks?.length > 0 && (
<>
<Section>
{accessibleLinks.map(({ icon, title, link, key }) => (
<NavItem isSelected={link === pathname}
ariaLabel={`Open ${title}`}
icon={icon}
key={key}
linkTarget={link}
title={title} />
))}
</Section>
<HorizontalRuleWrapper><hr /></HorizontalRuleWrapper>
</>
)}
<Section>
{sections.map(({ key, icon, title }) => {
const isSelected = activeSectionKey === key;

Expand All @@ -91,13 +119,13 @@ const SidebarNavigation = ({ sections, activeSection, selectSidebarSection, side
sidebarIsPinned={sidebarIsPinned} />
);
})}
</SectionList>
</Section>
<HorizontalRuleWrapper><hr /></HorizontalRuleWrapper>
<SectionList>
<Section>
{actions.map(({ key, Component }) => (
<Component key={key} sidebarIsPinned={sidebarIsPinned} />
))}
</SectionList>
</Section>
</Container>
);
};
Expand Down
10 changes: 10 additions & 0 deletions graylog2-web-interface/src/views/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import type * as Immutable from 'immutable';
import type { FormikErrors } from 'formik';
import type { Reducer, AnyAction } from '@reduxjs/toolkit';

import type { IconName } from 'components/common/Icon';
import type Widget from 'views/logic/widgets/Widget';
import type { ActionDefinition } from 'views/components/actions/ActionHandler';
import type { VisualizationComponent } from 'views/components/aggregationbuilder/AggregationBuilder';
Expand Down Expand Up @@ -449,6 +450,14 @@ export type FieldUnitType = 'size' | 'time' | 'percent';

export type FieldUnitsFormValues = Record<string, {abbrev: string; unitType: FieldUnitType}>;

export type SearchDataSource = {
key: string,
title: string,
icon: IconName,
link: string,
useCondition: () => boolean,
}

declare module 'graylog-web-plugin/plugin' {
export interface PluginExports {
creators?: Array<Creator>;
Expand Down Expand Up @@ -500,6 +509,7 @@ declare module 'graylog-web-plugin/plugin' {
'views.hooks.copyPageToDashboard'?: Array<CopyParamsToView>;
'views.hooks.removingWidget'?: Array<RemovingWidgetHook>;
'views.overrides.widgetEdit'?: Array<React.ComponentType<OverrideProps>>;
'views.searchDataSources'?: Array<SearchDataSource>;
'views.widgets.actions'?: Array<WidgetActionType>;
'views.widgets.exportAction'?: Array<{ action: WidgetActionType, useCondition: () => boolean }>;
'views.reducers'?: Array<ViewsReducer>;
Expand Down

0 comments on commit c7fff37

Please sign in to comment.