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

Implement option to display links in search sidebar. #20638

Merged
merged 9 commits into from
Oct 16, 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
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
Loading