mirror of
https://github.com/Graylog2/graylog2-server.git
synced 2026-03-13 09:32:21 +08:00
Input stop/start button UX fixes (#24659)
* fix Input stop/start button permissions * fix: initial button state after creating an input. * cl * fix review comment --------- Co-authored-by: Ousmane SAMBA <ousmane@graylog.com> Co-authored-by: Laura Bergenthal-Grotlüschen <laura.bergenthalgrotlueschen@graylog.com>
This commit is contained in:
committed by
GitHub
parent
98409b9520
commit
895601595f
5
changelog/unreleased/pr-24659.toml
Normal file
5
changelog/unreleased/pr-24659.toml
Normal file
@@ -0,0 +1,5 @@
|
||||
type = "f"
|
||||
message = "Input stop/start button UX fixes"
|
||||
|
||||
issues = ["24235", "graylog-plugin-enterprise#12659"]
|
||||
pulls = ["24659"]
|
||||
@@ -243,7 +243,7 @@ declare module 'graylog-web-plugin/plugin' {
|
||||
indexsets_field_restrictions: 'edit';
|
||||
indices: 'read' | 'changestate' | 'failures';
|
||||
input_types: 'create';
|
||||
inputs: 'create' | 'edit' | 'read' | 'terminate';
|
||||
inputs: 'create' | 'edit' | 'read' | 'terminate' | 'changestate';
|
||||
journal: 'read';
|
||||
jvmstats: 'read';
|
||||
lbstatus: 'change';
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
/*
|
||||
* 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 useFeature from 'hooks/useFeature';
|
||||
import useSendTelemetry from 'logic/telemetry/useSendTelemetry';
|
||||
import useLocation from 'routing/useLocation';
|
||||
import type { Input } from 'components/messageloaders/Types';
|
||||
import type { InputStates } from 'hooks/useInputsStates';
|
||||
import { asMock } from 'helpers/mocking';
|
||||
|
||||
import InputStateControl from './InputStateControl';
|
||||
|
||||
jest.mock('logic/telemetry/useSendTelemetry', () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn(),
|
||||
}));
|
||||
jest.mock('routing/useLocation', () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn(),
|
||||
}));
|
||||
jest.mock('hooks/useFeature');
|
||||
|
||||
const baseInput: Input = {
|
||||
id: 'input-id',
|
||||
title: 'Test Input',
|
||||
name: 'Test Input',
|
||||
type: 'org.graylog2.inputs.raw.udp.RawUDPInput',
|
||||
global: true,
|
||||
node: 'node-1',
|
||||
created_at: '2024-01-01T00:00:00.000Z',
|
||||
creator_user_id: 'user',
|
||||
content_pack: false,
|
||||
static_fields: {},
|
||||
attributes: {},
|
||||
};
|
||||
|
||||
const renderSUT = (inputStates: InputStates, featureEnabled = true) => {
|
||||
asMock(useFeature).mockReturnValue(featureEnabled);
|
||||
asMock(useSendTelemetry).mockReturnValue(jest.fn());
|
||||
asMock(useLocation).mockReturnValue({
|
||||
pathname: '/system/inputs',
|
||||
search: '',
|
||||
hash: '',
|
||||
state: null,
|
||||
key: 'mock-key',
|
||||
});
|
||||
|
||||
return render(<InputStateControl input={baseInput} inputStates={inputStates} openWizard={jest.fn()} />);
|
||||
};
|
||||
|
||||
describe('InputStateControl', () => {
|
||||
it('shows setup state when feature is enabled and input state is not loaded yet', async () => {
|
||||
renderSUT({});
|
||||
|
||||
expect(await screen.findByRole('button', { name: /set-up input/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('falls back to start when feature is disabled and input state is not loaded yet', async () => {
|
||||
renderSUT({}, false);
|
||||
|
||||
expect(await screen.findByRole('button', { name: /start input/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows stop when input is running', async () => {
|
||||
const runningStates: InputStates = {
|
||||
[baseInput.id]: {
|
||||
node1: {
|
||||
id: baseInput.id,
|
||||
state: 'RUNNING',
|
||||
detailed_message: null,
|
||||
message_input: {
|
||||
title: baseInput.title,
|
||||
global: baseInput.global,
|
||||
name: baseInput.name,
|
||||
content_pack: '',
|
||||
id: baseInput.id,
|
||||
created_at: baseInput.created_at,
|
||||
type: baseInput.type,
|
||||
creator_user_id: baseInput.creator_user_id,
|
||||
attributes: baseInput.attributes,
|
||||
static_fields: baseInput.static_fields,
|
||||
node: baseInput.node,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
renderSUT(runningStates);
|
||||
|
||||
expect(await screen.findByRole('button', { name: /stop input/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows start after stopping an input instead of setup', async () => {
|
||||
const runningStates: InputStates = {
|
||||
[baseInput.id]: {
|
||||
node1: {
|
||||
id: baseInput.id,
|
||||
state: 'RUNNING',
|
||||
detailed_message: null,
|
||||
message_input: {
|
||||
title: baseInput.title,
|
||||
global: baseInput.global,
|
||||
name: baseInput.name,
|
||||
content_pack: '',
|
||||
id: baseInput.id,
|
||||
created_at: baseInput.created_at,
|
||||
type: baseInput.type,
|
||||
creator_user_id: baseInput.creator_user_id,
|
||||
attributes: baseInput.attributes,
|
||||
static_fields: baseInput.static_fields,
|
||||
node: baseInput.node,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const { rerender } = renderSUT(runningStates);
|
||||
|
||||
expect(await screen.findByRole('button', { name: /stop input/i })).toBeInTheDocument();
|
||||
|
||||
rerender(<InputStateControl input={baseInput} inputStates={{}} openWizard={jest.fn()} />);
|
||||
|
||||
expect(await screen.findByRole('button', { name: /start input/i })).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -28,6 +28,7 @@ import { TELEMETRY_EVENT_TYPE } from 'logic/telemetry/Constants';
|
||||
import { Button } from 'components/bootstrap';
|
||||
import { INPUT_SETUP_MODE_FEATURE_FLAG } from 'components/inputs/InputSetupWizard';
|
||||
import type { InputStates } from 'hooks/useInputsStates';
|
||||
import useIsInitialUnknownInputState from 'components/inputs/hooks/useIsInitialUnknownInputState';
|
||||
|
||||
type Props = {
|
||||
input: Input;
|
||||
@@ -40,6 +41,7 @@ const InputStateControl = ({ input, openWizard, inputStates }: Props) => {
|
||||
const { pathname } = useLocation();
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
const inputSetupFeatureFlagIsEnabled = useFeature(INPUT_SETUP_MODE_FEATURE_FLAG);
|
||||
const isInitialUnknownState = useIsInitialUnknownInputState(inputStates, input.id);
|
||||
const startInput = () => {
|
||||
setIsLoading(true);
|
||||
|
||||
@@ -75,7 +77,10 @@ const InputStateControl = ({ input, openWizard, inputStates }: Props) => {
|
||||
openWizard();
|
||||
};
|
||||
|
||||
if (inputSetupFeatureFlagIsEnabled && isInputInSetupMode(inputStates, input.id)) {
|
||||
if (
|
||||
inputSetupFeatureFlagIsEnabled &&
|
||||
(isInputInSetupMode(inputStates, input.id) || isInitialUnknownState)
|
||||
) {
|
||||
return (
|
||||
<Button bsStyle="warning" bsSize="xsmall" onClick={setupInput}>
|
||||
Set-up Input
|
||||
|
||||
@@ -27,6 +27,7 @@ import useSendTelemetry from 'logic/telemetry/useSendTelemetry';
|
||||
import { asMock } from 'helpers/mocking';
|
||||
import useFeature from 'hooks/useFeature';
|
||||
import useInputMutations from 'hooks/useInputMutations';
|
||||
import usePermissions from 'hooks/usePermissions';
|
||||
|
||||
import InputsActions from './InputsActions';
|
||||
|
||||
@@ -46,6 +47,7 @@ jest.mock('routing/useLocation', () => ({
|
||||
}));
|
||||
jest.mock('hooks/useFeature');
|
||||
jest.mock('hooks/useInputMutations');
|
||||
jest.mock('hooks/usePermissions');
|
||||
jest.mock('stores/inputs/InputStatesStore', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
@@ -149,6 +151,7 @@ describe('InputsActions', () => {
|
||||
updateInput: updateInputMock,
|
||||
deleteInput: deleteInputMock,
|
||||
} as any);
|
||||
asMock(usePermissions).mockReturnValue({ isPermitted: () => true });
|
||||
});
|
||||
|
||||
it('renders Received messages button with correct query for standard input', () => {
|
||||
@@ -179,6 +182,38 @@ describe('InputsActions', () => {
|
||||
expect(await screen.findByText(/InputSetupWizard/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders input state controls when user has changestate permission', async () => {
|
||||
const input = {
|
||||
...baseInput,
|
||||
id: 'input-changestate',
|
||||
title: 'Input with changestate only',
|
||||
type: 'org.graylog2.inputs.gelf.udp.GELFUDPInput',
|
||||
};
|
||||
const isPermitted = jest.fn((permission) => permission === `inputs:changestate:${input.id}`);
|
||||
|
||||
asMock(usePermissions).mockReturnValue({ isPermitted });
|
||||
|
||||
renderSUT(input);
|
||||
|
||||
expect(await screen.findByRole('button', { name: /set up input/i })).toBeInTheDocument();
|
||||
expect(isPermitted).toHaveBeenCalledWith(`inputs:changestate:${input.id}`);
|
||||
});
|
||||
|
||||
it('does not render input state controls without changestate permission', () => {
|
||||
const input = {
|
||||
...baseInput,
|
||||
id: 'input-no-changestate',
|
||||
title: 'Input without changestate',
|
||||
type: 'org.graylog2.inputs.gelf.udp.GELFUDPInput',
|
||||
};
|
||||
|
||||
asMock(usePermissions).mockReturnValue({ isPermitted: () => false });
|
||||
|
||||
renderSUT(input);
|
||||
|
||||
expect(screen.queryByRole('button', { name: /set up input/i })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('opens Static Field form when Add static field is selected', async () => {
|
||||
const input = {
|
||||
...baseInput,
|
||||
|
||||
@@ -155,7 +155,7 @@ const InputsActions = ({ input, inputTypes: _, inputTypeDescriptions, currentNod
|
||||
</LinkContainer>
|
||||
</IfPermitted>
|
||||
|
||||
<IfPermitted permissions={[`inputs:edit:${input.id}`, `input_types:create:${input.type}`]}>
|
||||
<IfPermitted permissions={`inputs:changestate:${input.id}`}>
|
||||
{!isLoadingInputStates && (
|
||||
<InputStateControl
|
||||
key={`input-state-control-${input.id}`}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* 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 { useEffect, useMemo } from 'react';
|
||||
|
||||
import type { InputStates } from 'hooks/useInputsStates';
|
||||
|
||||
const useIsInitialUnknownInputState = (inputStates: InputStates, inputId: string) => {
|
||||
const seenInputIds = useMemo(() => new Set<string>(), []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!inputStates) {
|
||||
return;
|
||||
}
|
||||
|
||||
Object.keys(inputStates).forEach((id) => {
|
||||
seenInputIds.add(id);
|
||||
});
|
||||
}, [inputStates, seenInputIds]);
|
||||
|
||||
const hasKnownState = !!inputStates?.[inputId];
|
||||
|
||||
return !hasKnownState && !seenInputIds.has(inputId);
|
||||
};
|
||||
|
||||
export default useIsInitialUnknownInputState;
|
||||
Reference in New Issue
Block a user