From 9075cb3bed27f381084597ac452ade6ada9b30bb Mon Sep 17 00:00:00 2001 From: Ousmane SAMBA Date: Mon, 27 Oct 2025 09:15:11 +0100 Subject: [PATCH] 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 --- changelog/unreleased/pr-23989.toml | 5 ++ .../components/inputs/InputsDotBadge.test.tsx | 86 +++++++++++++++++++ .../src/components/inputs/InputsDotBadge.tsx | 42 +++++++++ .../navigation/MenuItemDotBadge.tsx | 50 +++++++++++ .../src/components/navigation/bindings.ts | 9 +- .../src/hooks/useInputsStates.ts | 85 ++++++++++++++++++ 6 files changed, 276 insertions(+), 1 deletion(-) create mode 100644 changelog/unreleased/pr-23989.toml create mode 100644 graylog2-web-interface/src/components/inputs/InputsDotBadge.test.tsx create mode 100644 graylog2-web-interface/src/components/inputs/InputsDotBadge.tsx create mode 100644 graylog2-web-interface/src/components/navigation/MenuItemDotBadge.tsx create mode 100644 graylog2-web-interface/src/hooks/useInputsStates.ts diff --git a/changelog/unreleased/pr-23989.toml b/changelog/unreleased/pr-23989.toml new file mode 100644 index 0000000000..4c7fc92356 --- /dev/null +++ b/changelog/unreleased/pr-23989.toml @@ -0,0 +1,5 @@ +type = "a" +message = "Add Inputs state dot badge to System and Inputs menu." + +issues = [""] +pulls = ["23989"] diff --git a/graylog2-web-interface/src/components/inputs/InputsDotBadge.test.tsx b/graylog2-web-interface/src/components/inputs/InputsDotBadge.test.tsx new file mode 100644 index 0000000000..f5a9e63535 --- /dev/null +++ b/graylog2-web-interface/src/components/inputs/InputsDotBadge.test.tsx @@ -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 + * . + */ +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('', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('returns null while loading', () => { + asMock(useInputsStates).mockReturnValue({ + refetch: jest.fn(), + isLoading: true, + data: undefined, + }); + + render(); + + 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(); + + 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(); + + const badge = screen.getByTitle(/Some inputs are in failed state or in setup mode\./i); + expect(badge).toBeInTheDocument(); + expect(badge).toHaveTextContent(TEXT); + }); + }); +}); diff --git a/graylog2-web-interface/src/components/inputs/InputsDotBadge.tsx b/graylog2-web-interface/src/components/inputs/InputsDotBadge.tsx new file mode 100644 index 0000000000..54bad82ff3 --- /dev/null +++ b/graylog2-web-interface/src/components/inputs/InputsDotBadge.tsx @@ -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 + * . + */ +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 ( + + ); +}; + +export default InputsDotBadge; diff --git a/graylog2-web-interface/src/components/navigation/MenuItemDotBadge.tsx b/graylog2-web-interface/src/components/navigation/MenuItemDotBadge.tsx new file mode 100644 index 0000000000..dd4afcdcdf --- /dev/null +++ b/graylog2-web-interface/src/components/navigation/MenuItemDotBadge.tsx @@ -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 + * . + */ +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 {text}; + } + + return {text}; +}; + +export default MenuItemDotBadge; diff --git a/graylog2-web-interface/src/components/navigation/bindings.ts b/graylog2-web-interface/src/components/navigation/bindings.ts index 0e316fe717..ab762879c6 100644 --- a/graylog2-web-interface/src/components/navigation/bindings.ts +++ b/graylog2-web-interface/src/components/navigation/bindings.ts @@ -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'] }, diff --git a/graylog2-web-interface/src/hooks/useInputsStates.ts b/graylog2-web-interface/src/hooks/useInputsStates.ts new file mode 100644 index 0000000000..1995c28e6f --- /dev/null +++ b/graylog2-web-interface/src/hooks/useInputsStates.ts @@ -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 + * . + */ +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; +}; + +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;