Add Inputs state dot badge to System and Inputs menu (#23989)

* add Inputs state dot badge to System and Inputs menu

* add changelog

* extract menu dot badge to own component

* adjust title

* remove "or false"

* add license header
This commit is contained in:
Ousmane SAMBA
2025-10-27 09:15:11 +01:00
committed by GitHub
parent 254c0ea3b7
commit 9075cb3bed
6 changed files with 276 additions and 1 deletions

View File

@@ -0,0 +1,5 @@
type = "a"
message = "Add Inputs state dot badge to System and Inputs menu."
issues = [""]
pulls = ["23989"]

View File

@@ -0,0 +1,86 @@
/*
* Copyright (C) 2020 Graylog, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the Server Side Public License, version 1,
* as published by MongoDB, Inc.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* Server Side Public License for more details.
*
* You should have received a copy of the Server Side Public License
* along with this program. If not, see
* <http://www.mongodb.com/licensing/server-side-public-license>.
*/
import * as React from 'react';
import { render, screen } from 'wrappedTestingLibrary';
import { asMock } from 'helpers/mocking';
import type { InputStateSummary } from 'hooks/useInputsStates';
import useInputsStates from 'hooks/useInputsStates';
import InputsDotBadge from './InputsDotBadge';
jest.mock('hooks/useInputsStates');
const TEXT = 'Inputs';
describe('<InputsDotBadge />', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('returns null while loading', () => {
asMock(useInputsStates).mockReturnValue({
refetch: jest.fn(),
isLoading: true,
data: undefined,
});
render(<InputsDotBadge text={TEXT} />);
expect(screen.queryByText(TEXT)).not.toBeInTheDocument();
});
it('renders plain text when there are no failed/failing/setup inputs', () => {
asMock(useInputsStates).mockReturnValue({
refetch: jest.fn(),
isLoading: false,
data: {
states: [
{ id: '1', state: 'RUNNING' } as InputStateSummary,
{ id: '2', state: 'STARTING' } as InputStateSummary,
],
},
});
render(<InputsDotBadge text={TEXT} />);
const textEl = screen.getByText(TEXT);
expect(textEl).toBeInTheDocument();
expect(textEl).not.toHaveAttribute('title', 'Some inputs are in failed state or in setup mode.');
});
describe.each(['FAILED', 'FAILING', 'SETUP'])('renders badge when an input state is %s', (problemState) => {
it(`shows badge (dot) with tooltip for state ${problemState}`, () => {
asMock(useInputsStates).mockReturnValue({
refetch: jest.fn(),
isLoading: false,
data: {
states: [
{ id: '1', state: 'RUNNING' } as InputStateSummary,
{ id: '2', state: problemState } as InputStateSummary,
],
},
});
render(<InputsDotBadge text={TEXT} />);
const badge = screen.getByTitle(/Some inputs are in failed state or in setup mode\./i);
expect(badge).toBeInTheDocument();
expect(badge).toHaveTextContent(TEXT);
});
});
});

View File

@@ -0,0 +1,42 @@
/*
* Copyright (C) 2020 Graylog, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the Server Side Public License, version 1,
* as published by MongoDB, Inc.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* Server Side Public License for more details.
*
* You should have received a copy of the Server Side Public License
* along with this program. If not, see
* <http://www.mongodb.com/licensing/server-side-public-license>.
*/
import * as React from 'react';
import useInputsStates from 'hooks/useInputsStates';
import MenuItemDotBadge from 'components/navigation/MenuItemDotBadge';
const InputsDotBadge = ({ text }: { text: string }) => {
const { data, isLoading } = useInputsStates();
if (isLoading) {
return null;
}
const hasFailedOrSetupInputs = data?.states.some((inputState) =>
['FAILED', 'FAILING', 'SETUP'].includes(inputState.state),
);
return (
<MenuItemDotBadge
text={text}
title="Some inputs are in failed state or in setup mode."
showDot={hasFailedOrSetupInputs}
/>
);
};
export default InputsDotBadge;

View File

@@ -0,0 +1,50 @@
/*
* Copyright (C) 2020 Graylog, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the Server Side Public License, version 1,
* as published by MongoDB, Inc.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* Server Side Public License for more details.
*
* You should have received a copy of the Server Side Public License
* along with this program. If not, see
* <http://www.mongodb.com/licensing/server-side-public-license>.
*/
import * as React from 'react';
import styled from 'styled-components';
const Badge = styled.span`
position: relative;
&::after {
display: 'block';
content: ' ';
position: absolute;
width: 8px;
height: 8px;
background-color: ${({ theme }) => theme.colors.brand.primary};
border-radius: 50%;
top: 0;
right: -12px;
}
`;
type Props = {
text: string;
title: string;
showDot: boolean;
};
const MenuItemDotBadge = ({ text, title, showDot }: Props) => {
if (!showDot) {
return <span>{text}</span>;
}
return <Badge title={title}>{text}</Badge>;
};
export default MenuItemDotBadge;

View File

@@ -17,6 +17,7 @@
import type { PluginExports } from 'graylog-web-plugin/plugin';
import InputsDotBadge from 'components/inputs/InputsDotBadge';
import Routes from 'routing/Routes';
import filterMenuItems, { filterCloudMenuItems } from 'util/conditional/filterMenuItems';
import AppConfig from 'util/AppConfig';
@@ -45,6 +46,7 @@ const navigationBindings: PluginExports = {
},
{
description: SYSTEM_DROPDOWN_TITLE,
BadgeComponent: InputsDotBadge,
position: { last: true },
children: filterCloudMenuItems(
filterMenuItems(
@@ -60,7 +62,12 @@ const navigationBindings: PluginExports = {
description: 'Cluster Configuration',
permissions: ['clusterconfiguration:read'],
},
{ path: Routes.SYSTEM.INPUTS, description: 'Inputs', permissions: ['inputs:read'] },
{
path: Routes.SYSTEM.INPUTS,
description: 'Inputs',
permissions: ['inputs:read'],
BadgeComponent: InputsDotBadge,
},
{ path: Routes.SYSTEM.OUTPUTS, description: 'Outputs', permissions: ['outputs:read'] },
{ path: Routes.SYSTEM.INDICES.LIST, description: 'Indices', permissions: ['indices:read'] },
{ path: Routes.SYSTEM.LOGGING, description: 'Logging', permissions: ['loggers:read'] },

View File

@@ -0,0 +1,85 @@
/*
* Copyright (C) 2020 Graylog, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the Server Side Public License, version 1,
* as published by MongoDB, Inc.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* Server Side Public License for more details.
*
* You should have received a copy of the Server Side Public License
* along with this program. If not, see
* <http://www.mongodb.com/licensing/server-side-public-license>.
*/
import { useQuery } from '@tanstack/react-query';
import { SystemInputStates } from '@graylog/server-api';
import { defaultOnError } from 'util/conditional/onError';
export const INPUTS_STATES_QUERY_KEY = ['inputs_states'];
export type InputState = 'RUNNING' | 'FAILED' | 'STOPPED' | 'STARTING' | 'FAILING' | 'SETUP';
export type InputSummary = {
creator_user_id: string;
node: string;
name: string;
created_at: string;
global: boolean;
attributes: {
[key: string]: object;
};
id: string;
title: string;
type: string;
content_pack: string;
static_fields: {
[key: string]: string;
};
};
export type InputStateSummary = {
detailed_message: string;
started_at: string;
id: string;
state: string;
message_input: InputSummary;
};
export type InputStatesList = {
states: Array<InputStateSummary>;
};
type Options = {
enabled: boolean;
};
const useInputsStates = (
{ enabled }: Options = { enabled: true },
): {
data: InputStatesList | undefined;
refetch: () => void;
isLoading: boolean;
} => {
const { data, refetch, isLoading } = useQuery({
queryKey: INPUTS_STATES_QUERY_KEY,
queryFn: () =>
defaultOnError(
SystemInputStates.list(),
'Loading inputs states failed with status',
'Could not load inputs states',
),
enabled,
});
return {
data: data || { states: [] },
refetch,
isLoading,
};
};
export default useInputsStates;