diff --git a/graylog2-web-interface/src/components/common/TimeUnit.jsx b/graylog2-web-interface/src/components/common/TimeUnit.jsx deleted file mode 100644 index c6d7d7aa8f53..000000000000 --- a/graylog2-web-interface/src/components/common/TimeUnit.jsx +++ /dev/null @@ -1,66 +0,0 @@ -/* - * 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 PropTypes from 'prop-types'; -import React from 'react'; -import createReactClass from 'create-react-class'; - -/** - * Component that renders a time value given in a certain unit. - * It can also use 0 as never if `zeroIsNever` is set. - */ -const TimeUnit = createReactClass({ - displayName: 'TimeUnit', - - propTypes: { - /** Value to display. */ - value: PropTypes.number.isRequired, - /** Unit used in the value. */ - unit: PropTypes.oneOf(['NANOSECONDS', 'MICROSECONDS', 'MILLISECONDS', 'SECONDS', 'MINUTES', 'HOURS', 'DAYS']).isRequired, - /** Specifies if zero should be displayed as never or not. */ - zeroIsNever: PropTypes.bool, - }, - - getDefaultProps() { - return { - zeroIsNever: true, - }; - }, - - UNITS: { - NANOSECONDS: 'nanoseconds', - MICROSECONDS: 'microseconds', - MILLISECONDS: 'milliseconds', - SECONDS: 'seconds', - MINUTES: 'minutes', - HOURS: 'hours', - DAYS: 'days', - }, - - render() { - if (this.props.value === 0 && this.props.zeroIsNever) { - return Never; - } - - return ( - - {this.props.value} {this.UNITS[this.props.unit]} - - ); - }, -}); - -export default TimeUnit; diff --git a/graylog2-web-interface/src/components/common/TimeUnit.tsx b/graylog2-web-interface/src/components/common/TimeUnit.tsx new file mode 100644 index 000000000000..b261d20dcd95 --- /dev/null +++ b/graylog2-web-interface/src/components/common/TimeUnit.tsx @@ -0,0 +1,53 @@ +/* + * 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 type { TimeUnit as TimeUnitString } from './types'; + +/** + * Component that renders a time value given in a certain unit. + * It can also use 0 as never if `zeroIsNever` is set. + */ +type Props = { + value: number, + unit: TimeUnitString, + zeroIsNever?: boolean, +} + +const UNITS = { + NANOSECONDS: 'nanoseconds', + MICROSECONDS: 'microseconds', + MILLISECONDS: 'milliseconds', + SECONDS: 'seconds', + MINUTES: 'minutes', + HOURS: 'hours', + DAYS: 'days', +}; + +const TimeUnit = ({ value, unit, zeroIsNever = true }: Props) => { + if (value === 0 && zeroIsNever) { + return Never; + } + + return ( + + {value} {UNITS[unit]} + + ); +}; + +export default TimeUnit; diff --git a/graylog2-web-interface/src/components/common/TimeUnitInput.jsx b/graylog2-web-interface/src/components/common/TimeUnitInput.tsx similarity index 64% rename from graylog2-web-interface/src/components/common/TimeUnitInput.jsx rename to graylog2-web-interface/src/components/common/TimeUnitInput.tsx index 6a577ff5a4f1..ff92f1f93882 100644 --- a/graylog2-web-interface/src/components/common/TimeUnitInput.jsx +++ b/graylog2-web-interface/src/components/common/TimeUnitInput.tsx @@ -14,10 +14,7 @@ * along with this program. If not, see * . */ -import PropTypes from 'prop-types'; import React from 'react'; -// eslint-disable-next-line no-restricted-imports -import createReactClass from 'create-react-class'; import defaultTo from 'lodash/defaultTo'; import isEqual from 'lodash/isEqual'; import last from 'lodash/last'; @@ -47,7 +44,8 @@ const unitValues = [ 'DAYS', 'MONTHS', 'YEARS', -]; +] as const; +type UnitValue = typeof unitValues[number]; const defaultUnits = [ 'NANOSECONDS', 'MICROSECONDS', @@ -57,7 +55,6 @@ const defaultUnits = [ 'HOURS', 'DAYS', ]; -const unitType = PropTypes.oneOf(unitValues); const StyledInputGroup = styled(InputGroup)` display: flex; @@ -103,81 +100,86 @@ export const extractDurationAndUnit = (duration, timeUnits) => { * and a select that let the user choose the unit used for the given time * value. */ -const TimeUnitInput = createReactClass({ - // eslint-disable-next-line react/no-unused-class-component-methods - displayName: 'TimeUnitInput', - - // eslint-disable-next-line react/no-unused-class-component-methods - propTypes: { - /** - * Function that will be called when the input changes, that is, - * when the field is enabled/disabled, the value or the unit change. - * The function will receive the value, unit, and checked boolean as - * arguments. - */ - update: PropTypes.func.isRequired, - /** Label to use for the field. */ - label: PropTypes.string, - /** Help message to use for the field. */ - help: PropTypes.oneOfType([PropTypes.string, PropTypes.node]), - /** Specifies if this is a required field or not. */ - required: PropTypes.bool, - /** Specifies if the input is enabled or disabled. */ - enabled: PropTypes.bool, - /** Indicates the default enabled state, in case the consumer does not want to handle the enabled state. */ - defaultEnabled: PropTypes.bool, - /** Specifies the value of the input. */ - value: PropTypes.number, - /** Indicates the default value to use, in case value is not provided or set. */ - defaultValue: PropTypes.number, - /** Indicates which unit is used for the value. */ - unit: unitType, - /** Specifies which units should be available in the form. */ - units: PropTypes.arrayOf(unitType), - /** Add an additional class to the label. */ - labelClassName: PropTypes.string, - /** Add an additional class to the input wrapper. */ - wrapperClassName: PropTypes.string, - /** Specifies if the input should render a checkbox. Use this if the enabled state is controlled by another input */ - hideCheckbox: PropTypes.bool, - /** Align unit dropdown menu to the right. */ - pullRight: PropTypes.bool, - /** Lets the user clear the numeric input. */ - clearable: PropTypes.bool, - - name: PropTypes.string, - unitName: PropTypes.string, - }, - - getDefaultProps() { - return { - defaultValue: 1, - value: undefined, - unit: 'SECONDS', - units: defaultUnits, - label: '', - help: '', - name: null, - unitName: null, - required: false, - enabled: undefined, - defaultEnabled: false, - labelClassName: undefined, - wrapperClassName: undefined, - hideCheckbox: false, - pullRight: false, - clearable: false, - }; - }, +type Props = { + /** + * Function that will be called when the input changes, that is, + * when the field is enabled/disabled, the value or the unit change. + * The function will receive the value, unit, and checked boolean as + * arguments. + */ + update: (value: number, unit: string, checked: boolean) => void, + /** Label to use for the field. */ + label?: string, + /** Help message to use for the field. */ + help?: React.ReactNode, + /** Specifies if this is a required field or not. */ + required?: boolean, + /** Specifies if the input is enabled or disabled. */ + enabled?: boolean, + /** Indicates the default enabled state, in case the consumer does not want to handle the enabled state. */ + defaultEnabled?: boolean, + /** Specifies the value of the input. */ + value?: number | string, + /** Indicates the default value to use, in case value is not provided or set. */ + defaultValue?: number, + /** Indicates which unit is used for the value. */ + unit?: string, + /** Specifies which units should be available in the form. */ + units?: Array, + /** Add an additional class to the label. */ + labelClassName?: string, + /** Add an additional class to the input wrapper. */ + wrapperClassName?: string, + /** Specifies if the input should render a checkbox. Use this if the enabled state is controlled by another input */ + hideCheckbox?: boolean, + /** Align unit dropdown menu to the right. */ + pullRight?: boolean, + /** Lets the user clear the numeric input. */ + clearable?: boolean, + + name?: string, + unitName?: string, + // TODO: Added to avoid messing with existing code, should be considered for removal + id?: string, + // TODO: Added to avoid messing with existing code, should be considered for removal + type?: string, +} + +type State = { + enabled: boolean, + unitOptions: Array<{ label: string, value: UnitValue }> +} + +class TimeUnitInput extends React.Component { + static defaultProps = { + defaultValue: 1, + value: undefined, + unit: 'SECONDS', + units: defaultUnits, + label: '', + help: '', + name: null, + unitName: null, + required: false, + enabled: undefined, + defaultEnabled: false, + labelClassName: undefined, + wrapperClassName: undefined, + hideCheckbox: false, + pullRight: false, + clearable: false, + }; - getInitialState() { - const { defaultEnabled, enabled, units } = this.props; + constructor(props) { + super(props); - return { + const { defaultEnabled, enabled, units } = props; + + this.state = { enabled: defaultTo(enabled, defaultEnabled), unitOptions: this._getUnitOptions(units), }; - }, + } UNSAFE_componentWillReceiveProps(nextProps) { const { units } = this.props; @@ -185,21 +187,19 @@ const TimeUnitInput = createReactClass({ if (!isEqual(units, nextProps.units)) { this.setState({ unitOptions: this._getUnitOptions(nextProps.units) }); } - }, + } - _getEffectiveValue() { + _getEffectiveValue = () => { const { defaultValue, value, clearable } = this.props; return clearable ? value : defaultTo(value, defaultValue); - }, + }; - _getUnitOptions(units) { - return unitValues - .filter((value) => units.includes(value)) - .map((value) => ({ value: value, label: value.toLowerCase() })); - }, + _getUnitOptions = (units) => unitValues + .filter((value) => units.includes(value)) + .map((value) => ({ value: value, label: value.toLowerCase() })); - _isChecked() { + _isChecked = () => { const { required, enabled } = this.props; if (required) { @@ -209,9 +209,9 @@ const TimeUnitInput = createReactClass({ const { enabled: enabledState } = this.state; return defaultTo(enabled, enabledState); - }, + }; - _propagateInput(update) { + _propagateInput = (update) => { const { update: onUpdate, unit } = this.props; const previousInput = { value: this._getEffectiveValue(), @@ -221,16 +221,16 @@ const TimeUnitInput = createReactClass({ const nextInput = { ...previousInput, ...update }; onUpdate(nextInput.value, nextInput.unit, nextInput.checked); - }, + }; - _onToggleEnable(e) { + _onToggleEnable = (e) => { const isChecked = e.target.checked; this.setState({ enabled: isChecked }); this._propagateInput({ checked: isChecked }); - }, + }; - _onUpdate(e) { + _onUpdate = (e) => { const { defaultValue, clearable } = this.props; let value; @@ -241,11 +241,11 @@ const TimeUnitInput = createReactClass({ } this._propagateInput({ value: value }); - }, + }; - _onUnitSelect(unit) { + _onUnitSelect = (unit) => { this._propagateInput({ unit: unit }); - }, + }; render() { const { unitOptions } = this.state; @@ -277,8 +277,7 @@ const TimeUnitInput = createReactClass({ aria-label={label || 'Time unit input'} onChange={this._onUpdate} value={defaultTo(this._getEffectiveValue(), '')} /> - o.value === unit)[0].label} @@ -290,7 +289,7 @@ const TimeUnitInput = createReactClass({ ); - }, -}); + } +} export default TimeUnitInput; diff --git a/graylog2-web-interface/src/components/common/types.ts b/graylog2-web-interface/src/components/common/types.ts index e7c437bc4817..ac5b1dc4e667 100644 --- a/graylog2-web-interface/src/components/common/types.ts +++ b/graylog2-web-interface/src/components/common/types.ts @@ -15,3 +15,4 @@ * . */ export type ValidationState = 'error' | 'success' | 'warning'; +export type TimeUnit = 'NANOSECONDS' | 'MICROSECONDS' | 'MILLISECONDS' | 'SECONDS' | 'MINUTES' | 'HOURS' | 'DAYS'; diff --git a/graylog2-web-interface/src/components/content-packs/ContentPackVersions.tsx b/graylog2-web-interface/src/components/content-packs/ContentPackVersions.tsx index 1fe8b94b657d..b01d74c7b617 100644 --- a/graylog2-web-interface/src/components/content-packs/ContentPackVersions.tsx +++ b/graylog2-web-interface/src/components/content-packs/ContentPackVersions.tsx @@ -18,18 +18,19 @@ import React from 'react'; import { DataTable } from 'components/common'; import ContentPackVersionItem from 'components/content-packs/components/ContentPackVersionItem'; -import type { ContentPackVersionsType, ContentPackInstallation } from 'components/content-packs/Types'; +import type { ContentPackInstallation } from 'components/content-packs/Types'; import './ContentPackVersions.css'; +import type ContentPackRevisions from 'logic/content-packs/ContentPackRevisions'; type Props = { - contentPackRevisions: ContentPackVersionsType, + contentPackRevisions: ContentPackRevisions, onDeletePack?: (id: string) => void onChange?: (id: string) => void onInstall?: (id: string, contentPackRev: string, parameters: unknown) => void }; -const headerFormatter = (header) => { +const headerFormatter = (header: React.ReactNode) => { if (header === 'Action') { return ({header}); } diff --git a/graylog2-web-interface/src/components/content-packs/components/ContentPackVersionItem.tsx b/graylog2-web-interface/src/components/content-packs/components/ContentPackVersionItem.tsx index cc9447d5d514..aa9c85f96b42 100644 --- a/graylog2-web-interface/src/components/content-packs/components/ContentPackVersionItem.tsx +++ b/graylog2-web-interface/src/components/content-packs/components/ContentPackVersionItem.tsx @@ -31,11 +31,12 @@ import { MenuItem, Modal, } from 'components/bootstrap'; -import type { ContentPackVersionsType, ContentPackInstallation } from 'components/content-packs/Types'; +import type { ContentPackInstallation } from 'components/content-packs/Types'; +import type ContentPackRevisions from 'logic/content-packs/ContentPackRevisions'; type Props = { pack: ContentPackInstallation - contentPackRevisions: ContentPackVersionsType, + contentPackRevisions: ContentPackRevisions, onDeletePack?: (id: string, rev: number) => void onChange?: (id: string) => void onInstall?: (id: string, contentPackRev: string, parameters: unknown) => void diff --git a/graylog2-web-interface/src/components/enterprise/PluginList.jsx b/graylog2-web-interface/src/components/enterprise/PluginList.tsx similarity index 61% rename from graylog2-web-interface/src/components/enterprise/PluginList.jsx rename to graylog2-web-interface/src/components/enterprise/PluginList.tsx index 729d716ec793..b752a220c66f 100644 --- a/graylog2-web-interface/src/components/enterprise/PluginList.jsx +++ b/graylog2-web-interface/src/components/enterprise/PluginList.tsx @@ -15,43 +15,38 @@ * . */ import React from 'react'; -import createReactClass from 'create-react-class'; import { PluginStore } from 'graylog-web-plugin/plugin'; import { Icon } from 'components/common'; import style from './PluginList.css'; -const PluginList = createReactClass({ - displayName: 'PluginList', +const ENTERPRISE_PLUGINS = { + 'graylog-plugin-enterprise': 'Graylog Plugin Enterprise', +}; - ENTERPRISE_PLUGINS: { - 'graylog-plugin-enterprise': 'Graylog Plugin Enterprise', - }, - - _formatPlugin(pluginName) { +const PluginList = () => { + const _formatPlugin = (pluginName: string) => { const plugin = PluginStore.get().filter((p) => p.metadata.name === pluginName)[0]; return (
  •   - {this.ENTERPRISE_PLUGINS[pluginName]} is {plugin ? 'installed' : 'not installed'} + {ENTERPRISE_PLUGINS[pluginName]} is {plugin ? 'installed' : 'not installed'}
  • ); - }, - - render() { - const enterprisePluginList = Object.keys(this.ENTERPRISE_PLUGINS).map((pluginName) => this._formatPlugin(pluginName)); - - return ( - <> -

    This is the status of Graylog Enterprise modules in this cluster:

    -
      - {enterprisePluginList} -
    - - ); - }, -}); + }; + + const enterprisePluginList = Object.keys(ENTERPRISE_PLUGINS).map((pluginName) => _formatPlugin(pluginName)); + + return ( + <> +

    This is the status of Graylog Enterprise modules in this cluster:

    +
      + {enterprisePluginList} +
    + + ); +}; export default PluginList; diff --git a/graylog2-web-interface/src/components/extractors/ExtractorsList.tsx b/graylog2-web-interface/src/components/extractors/ExtractorsList.tsx index 960cc529143b..02f7ea850a54 100644 --- a/graylog2-web-interface/src/components/extractors/ExtractorsList.tsx +++ b/graylog2-web-interface/src/components/extractors/ExtractorsList.tsx @@ -22,15 +22,17 @@ import Spinner from 'components/common/Spinner'; import AddExtractorWizard from 'components/extractors/AddExtractorWizard'; import EntityList from 'components/common/EntityList'; import { ExtractorsActions, ExtractorsStore } from 'stores/extractors/ExtractorsStore'; -import type { ExtractorType, InputSummary, NodeSummary } from 'stores/extractors/ExtractorsStore'; +import type { ExtractorType } from 'stores/extractors/ExtractorsStore'; import { useStore } from 'stores/connect'; +import type { NodeInfo } from 'stores/nodes/NodesStore'; +import type { Input } from 'components/messageloaders/Types'; import ExtractorsListItem from './ExtractorsListItem'; import ExtractorsSortModal from './ExtractorSortModal'; type Props = { - input: InputSummary, - node: NodeSummary, + input: Input, + node: NodeInfo, }; const fetchExtractors = (inputId: string) => { diff --git a/graylog2-web-interface/src/components/extractors/extractors_configuration/JSONExtractorConfiguration.jsx b/graylog2-web-interface/src/components/extractors/extractors_configuration/JSONExtractorConfiguration.tsx similarity index 88% rename from graylog2-web-interface/src/components/extractors/extractors_configuration/JSONExtractorConfiguration.jsx rename to graylog2-web-interface/src/components/extractors/extractors_configuration/JSONExtractorConfiguration.tsx index cf8c2f6ce58d..e775eb01e79f 100644 --- a/graylog2-web-interface/src/components/extractors/extractors_configuration/JSONExtractorConfiguration.jsx +++ b/graylog2-web-interface/src/components/extractors/extractors_configuration/JSONExtractorConfiguration.tsx @@ -14,9 +14,7 @@ * along with this program. If not, see * . */ -import PropTypes from 'prop-types'; import React from 'react'; -import createReactClass from 'create-react-class'; import { Icon } from 'components/common'; import { Col, Row, Button, Input } from 'components/bootstrap'; @@ -24,43 +22,57 @@ import ExtractorUtils from 'util/ExtractorUtils'; import FormUtils from 'util/FormsUtils'; import ToolsStore from 'stores/tools/ToolsStore'; -const JSONExtractorConfiguration = createReactClass({ - displayName: 'JSONExtractorConfiguration', +type Configuration = { + flatten: boolean, + list_separator: string, + key_separator: string, + kv_separator: string, + replace_key_whitespace: boolean, + key_whitespace_replacement: string, + key_prefix: string, + string: string +}; +type Props = { + configuration: Configuration, + exampleMessage?: string, + onChange: (newConfig: Configuration) => void, + onExtractorPreviewLoad: (content: React.ReactNode) => void, +} +type State = { + configuration: Configuration, + trying: boolean, +} + +class JSONExtractorConfiguration extends React.Component { + private DEFAULT_CONFIGURATION = { + list_separator: ', ', + key_separator: '_', + kv_separator: '=', + key_prefix: '', + replace_key_whitespace: false, + key_whitespace_replacement: '_', + }; - propTypes: { - configuration: PropTypes.object.isRequired, - exampleMessage: PropTypes.string, - onChange: PropTypes.func.isRequired, - onExtractorPreviewLoad: PropTypes.func.isRequired, - }, + constructor(props: Props, context: any) { + super(props, context); - getInitialState() { - return { + this.state = { trying: false, - configuration: this._getEffectiveConfiguration(this.props.configuration), + configuration: this._getEffectiveConfiguration(props.configuration), }; - }, + } componentDidMount() { this.props.onChange(this.state.configuration); - }, + } UNSAFE_componentWillReceiveProps(nextProps) { this.setState({ configuration: this._getEffectiveConfiguration(nextProps.configuration) }); - }, - - DEFAULT_CONFIGURATION: { - list_separator: ', ', - key_separator: '_', - kv_separator: '=', - key_prefix: '', - replace_key_whitespace: false, - key_whitespace_replacement: '_', - }, + } _getEffectiveConfiguration(configuration) { return ExtractorUtils.getEffectiveConfiguration(this.DEFAULT_CONFIGURATION, configuration); - }, + } _onChange(key) { return (event) => { @@ -70,7 +82,7 @@ const JSONExtractorConfiguration = createReactClass({ newConfig[key] = FormUtils.getValueFromInput(event.target); this.props.onChange(newConfig); }; - }, + } _onTryClick() { this.setState({ trying: true }); @@ -94,11 +106,11 @@ const JSONExtractorConfiguration = createReactClass({ }); promise.finally(() => this.setState({ trying: false })); - }, + } _isTryButtonDisabled() { return this.state.trying || !this.props.exampleMessage; - }, + } render() { return ( @@ -178,7 +190,7 @@ const JSONExtractorConfiguration = createReactClass({ ); - }, -}); + } +} export default JSONExtractorConfiguration; diff --git a/graylog2-web-interface/src/components/extractors/extractors_configuration/SubstringExtractorConfiguration.jsx b/graylog2-web-interface/src/components/extractors/extractors_configuration/SubstringExtractorConfiguration.jsx deleted file mode 100644 index 2418ca2d2255..000000000000 --- a/graylog2-web-interface/src/components/extractors/extractors_configuration/SubstringExtractorConfiguration.jsx +++ /dev/null @@ -1,166 +0,0 @@ -/* - * 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 PropTypes from 'prop-types'; -import React from 'react'; -import createReactClass from 'create-react-class'; - -import { Button, Col, Row, Input } from 'components/bootstrap'; -import { Icon } from 'components/common'; -import UserNotification from 'util/UserNotification'; -import ExtractorUtils from 'util/ExtractorUtils'; -import FormUtils from 'util/FormsUtils'; -import ToolsStore from 'stores/tools/ToolsStore'; - -const SubstringExtractorConfiguration = createReactClass({ - displayName: 'SubstringExtractorConfiguration', - - propTypes: { - configuration: PropTypes.object.isRequired, - exampleMessage: PropTypes.string, - onChange: PropTypes.func.isRequired, - onExtractorPreviewLoad: PropTypes.func.isRequired, - }, - - getInitialState() { - return { - trying: false, - configuration: this._getEffectiveConfiguration(this.props.configuration), - }; - }, - - componentDidMount() { - this.props.onChange(this.state.configuration); - }, - - UNSAFE_componentWillReceiveProps(nextProps) { - this.setState({ configuration: this._getEffectiveConfiguration(nextProps.configuration) }); - }, - - DEFAULT_CONFIGURATION: { begin_index: 0, end_index: 1 }, - - _getEffectiveConfiguration(configuration) { - return ExtractorUtils.getEffectiveConfiguration(this.DEFAULT_CONFIGURATION, configuration); - }, - - _onChange(key) { - return (event) => { - this.props.onExtractorPreviewLoad(undefined); - const newConfig = this.state.configuration; - - newConfig[key] = FormUtils.getValueFromInput(event.target); - this.props.onChange(newConfig); - }; - }, - - _verifySubstringInputs() { - const beginIndex = this.beginIndex.getInputDOMNode(); - const endIndex = this.endIndex.getInputDOMNode(); - - if (this.state.configuration.begin_index === undefined || this.state.configuration.begin_index < 0) { - beginIndex.value = 0; - this._onChange('begin_index')({ target: beginIndex }); - } - - if (this.state.configuration.end_index === undefined || this.state.configuration.end_index < 0) { - endIndex.value = 0; - this._onChange('end_index')({ target: endIndex }); - } - - if (this.state.configuration.begin_index > this.state.configuration.end_index) { - beginIndex.value = this.state.configuration.end_index; - this._onChange('begin_index')({ target: beginIndex }); - } - }, - - _onTryClick() { - this.setState({ trying: true }); - - this._verifySubstringInputs(); - - if (this.state.configuration.begin_index === this.state.configuration.end_index) { - this.props.onExtractorPreviewLoad(''); - this.setState({ trying: false }); - } else { - const promise = ToolsStore.testSubstring(this.state.configuration.begin_index, this.state.configuration.end_index, this.props.exampleMessage); - - promise.then((result) => { - if (!result.successful) { - UserNotification.warning('We were not able to run the substring extraction. Please check index boundaries.'); - - return; - } - - this.props.onExtractorPreviewLoad({result.cut}); - }); - - promise.finally(() => this.setState({ trying: false })); - } - }, - - _isTryButtonDisabled() { - const { configuration } = this.state; - - return this.state.trying || configuration.begin_index === undefined || configuration.begin_index < 0 || configuration.end_index === undefined || configuration.end_index < 0 || !this.props.exampleMessage; - }, - - render() { - const endIndexHelpMessage = ( - - Where to end extracting. (Exclusive){' '} - Example: 1,5 cuts love from the string ilovelogs. - - ); - - return ( -
    - { this.beginIndex = beginIndex; }} - id="begin_index" - label="Begin index" - labelClassName="col-md-2" - wrapperClassName="col-md-10" - defaultValue={this.state.configuration.begin_index} - onChange={this._onChange('begin_index')} - min="0" - required - help="Character position from where to start extracting. (Inclusive)" /> - - { this.endIndex = endIndex; }} - id="end_index" - label="End index" - labelClassName="col-md-2" - wrapperClassName="col-md-10" - defaultValue={this.state.configuration.end_index} - onChange={this._onChange('end_index')} - min="0" - required - help={endIndexHelpMessage} /> - - - - - - -
    - ); - }, -}); - -export default SubstringExtractorConfiguration; diff --git a/graylog2-web-interface/src/components/extractors/extractors_configuration/SubstringExtractorConfiguration.tsx b/graylog2-web-interface/src/components/extractors/extractors_configuration/SubstringExtractorConfiguration.tsx new file mode 100644 index 000000000000..f80c6c32213f --- /dev/null +++ b/graylog2-web-interface/src/components/extractors/extractors_configuration/SubstringExtractorConfiguration.tsx @@ -0,0 +1,152 @@ +/* + * 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 { useEffect, useState } from 'react'; + +import { Button, Col, Row, Input } from 'components/bootstrap'; +import { Icon } from 'components/common'; +import UserNotification from 'util/UserNotification'; +import ExtractorUtils from 'util/ExtractorUtils'; +import FormUtils from 'util/FormsUtils'; +import ToolsStore from 'stores/tools/ToolsStore'; + +type Config = { + begin_index: number, + end_index: number, +}; +type Props = { + configuration: Config, + exampleMessage?: string, + onChange: (newConfig: {}) => void, + onExtractorPreviewLoad: (extractor: React.ReactNode) => void, +} +const DEFAULT_CONFIGURATION = { begin_index: 0, end_index: 1 }; +const _getEffectiveConfiguration = (configuration: Config) => ExtractorUtils.getEffectiveConfiguration(DEFAULT_CONFIGURATION, configuration); + +const SubstringExtractorConfiguration = ({ configuration: initialConfig, exampleMessage, onChange, onExtractorPreviewLoad }: Props) => { + const [configuration, setConfig] = useState(_getEffectiveConfiguration(initialConfig)); + const [trying, setTrying] = useState(false); + const [beginIndex, setBeginIndex] = useState(); + const [endIndex, setEndIndex] = useState(); + + useEffect(() => { + onChange(configuration); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const _onChange = (key: string) => (event: { target: HTMLInputElement }) => { + onExtractorPreviewLoad(undefined); + const newConfig = { + ...configuration, + [key]: FormUtils.getValueFromInput(event.target), + }; + setConfig(newConfig); + onChange(newConfig); + }; + + const _verifySubstringInputs = () => { + const _beginIndex = beginIndex.getInputDOMNode(); + const _endIndex = endIndex.getInputDOMNode(); + + if (configuration.begin_index === undefined || configuration.begin_index < 0) { + _beginIndex.value = '0'; + _onChange('begin_index')({ target: _beginIndex }); + } + + if (configuration.end_index === undefined || configuration.end_index < 0) { + _endIndex.value = '0'; + _onChange('end_index')({ target: _endIndex }); + } + + if (configuration.begin_index > configuration.end_index) { + _beginIndex.value = configuration.end_index; + _onChange('begin_index')({ target: _beginIndex }); + } + }; + + const _onTryClick = () => { + setTrying(true); + + _verifySubstringInputs(); + + if (configuration.begin_index === configuration.end_index) { + onExtractorPreviewLoad(''); + setTrying(false); + } else { + const promise = ToolsStore.testSubstring(configuration.begin_index, configuration.end_index, exampleMessage); + + promise.then((result) => { + if (!result.successful) { + UserNotification.warning('We were not able to run the substring extraction. Please check index boundaries.'); + + return; + } + + onExtractorPreviewLoad({result.cut}); + }); + + promise.finally(() => setTrying(false)); + } + }; + + const _isTryButtonDisabled = trying || configuration.begin_index === undefined || configuration.begin_index < 0 || configuration.end_index === undefined || configuration.end_index < 0 || !exampleMessage; + + const endIndexHelpMessage = ( + + Where to end extracting. (Exclusive){' '} + Example: 1,5 cuts love from the string ilovelogs. + + ); + + return ( +
    + { setBeginIndex(_beginIndex); }} + id="begin_index" + label="Begin index" + labelClassName="col-md-2" + wrapperClassName="col-md-10" + defaultValue={configuration.begin_index} + onChange={_onChange('begin_index')} + min="0" + required + help="Character position from where to start extracting. (Inclusive)" /> + + { setEndIndex(_endIndex); }} + id="end_index" + label="End index" + labelClassName="col-md-2" + wrapperClassName="col-md-10" + defaultValue={configuration.end_index} + onChange={_onChange('end_index')} + min="0" + required + help={endIndexHelpMessage} /> + + + + + + +
    + ); +}; + +export default SubstringExtractorConfiguration; diff --git a/graylog2-web-interface/src/components/inputs/InputThroughput.jsx b/graylog2-web-interface/src/components/inputs/InputThroughput.tsx similarity index 90% rename from graylog2-web-interface/src/components/inputs/InputThroughput.jsx rename to graylog2-web-interface/src/components/inputs/InputThroughput.tsx index d56c34b4270c..e457cc1f7f58 100644 --- a/graylog2-web-interface/src/components/inputs/InputThroughput.jsx +++ b/graylog2-web-interface/src/components/inputs/InputThroughput.tsx @@ -15,17 +15,16 @@ * . */ /* eslint-disable no-restricted-globals */ -import PropTypes from 'prop-types'; import React from 'react'; -// eslint-disable-next-line no-restricted-imports -import createReactClass from 'create-react-class'; -import Reflux from 'reflux'; import numeral from 'numeral'; import styled, { css } from 'styled-components'; import NumberUtils from 'util/NumberUtils'; import { Icon, LinkToNode, Spinner } from 'components/common'; +import type { ClusterMetric } from 'stores/metrics/MetricsStore'; import { MetricsActions, MetricsStore } from 'stores/metrics/MetricsStore'; +import type { Input } from 'components/messageloaders/Types'; +import connect from 'stores/connect'; const InputIO = styled.span(({ theme }) => css` .total { @@ -56,28 +55,30 @@ const InputIO = styled.span(({ theme }) => css` } `); -const InputThroughput = createReactClass({ - displayName: 'InputThroughput', +type Props = { + input: Input, + metrics: ClusterMetric, +} +type State = { + showDetails: boolean, +} - propTypes: { - input: PropTypes.object.isRequired, - }, +class InputThroughput extends React.Component { + constructor(props: Readonly) { + super(props); - mixins: [Reflux.connect(MetricsStore)], - - getInitialState() { - return { + this.state = { showDetails: false, }; - }, + } UNSAFE_componentWillMount() { this._metricNames().forEach((metricName) => MetricsActions.addGlobal(metricName)); - }, + } componentWillUnmount() { this._metricNames().forEach((metricName) => MetricsActions.removeGlobal(metricName)); - }, + } _metricNames() { return [ @@ -90,13 +91,13 @@ const InputThroughput = createReactClass({ this._prefix('read_bytes_1sec'), this._prefix('read_bytes_total'), ]; - }, + } _prefix(metric) { const { input } = this.props; return `${input.type}.${input.id}.${metric}`; - }, + } _getValueFromMetric(metric) { if (metric === null || metric === undefined) { @@ -113,7 +114,7 @@ const InputThroughput = createReactClass({ default: return undefined; } - }, + } _calculateMetrics(metrics) { const result = {}; @@ -124,10 +125,10 @@ const InputThroughput = createReactClass({ return previous; } - const value = this._getValueFromMetric(metrics[nodeId][metricName]); + const _value = this._getValueFromMetric(metrics[nodeId][metricName]); - if (value !== undefined) { - return isNaN(previous) ? value : previous + value; + if (_value !== undefined) { + return isNaN(previous) ? _value : previous + _value; } return previous; @@ -135,11 +136,11 @@ const InputThroughput = createReactClass({ }); return result; - }, + } _formatCount(count) { return numeral(count).format('0,0'); - }, + } _formatNetworkStats(writtenBytes1Sec, writtenBytesTotal, readBytes1Sec, readBytesTotal) { const network = ( @@ -167,7 +168,7 @@ const InputThroughput = createReactClass({ ); return network; - }, + } _formatConnections(openConnections, totalConnections) { return ( @@ -177,7 +178,7 @@ const InputThroughput = createReactClass({
    ); - }, + } _formatAllNodeDetails(metrics) { return ( @@ -186,7 +187,7 @@ const InputThroughput = createReactClass({ {Object.keys(metrics).map((nodeId) => this._formatNodeDetails(nodeId, metrics[nodeId]))} ); - }, + } _formatNodeDetails(nodeId, metrics) { const { input } = this.props; @@ -209,17 +210,18 @@ const InputThroughput = createReactClass({
    ); - }, + } _toggleShowDetails(evt) { evt.preventDefault(); const { showDetails } = this.state; this.setState({ showDetails: !showDetails }); - }, + } render() { - const { metrics, showDetails } = this.state; + const { metrics } = this.props; + const { showDetails } = this.state; const { input } = this.props; if (!metrics) { @@ -251,7 +253,10 @@ const InputThroughput = createReactClass({ ); - }, -}); + } +} -export default InputThroughput; +export default connect(InputThroughput, { metrics: MetricsStore }, (props) => ({ + ...props, + metrics: props.metrics?.metrics, +})); diff --git a/graylog2-web-interface/src/components/loggers/Constants.ts b/graylog2-web-interface/src/components/loggers/Constants.ts new file mode 100644 index 000000000000..aa21ba6e5945 --- /dev/null +++ b/graylog2-web-interface/src/components/loggers/Constants.ts @@ -0,0 +1,26 @@ +/* + * 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 + * . + */ +export const availableLoglevels = [ + 'fatal', + 'error', + 'warn', + 'info', + 'debug', + 'trace', +] as const; + +export type AvailableLogLevels = typeof availableLoglevels; diff --git a/graylog2-web-interface/src/components/loggers/LogLevelDropdown.jsx b/graylog2-web-interface/src/components/loggers/LogLevelDropdown.jsx deleted file mode 100644 index 8263756c83b6..000000000000 --- a/graylog2-web-interface/src/components/loggers/LogLevelDropdown.jsx +++ /dev/null @@ -1,81 +0,0 @@ -/* - * 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 PropTypes from 'prop-types'; -import React from 'react'; -// eslint-disable-next-line no-restricted-imports -import createReactClass from 'create-react-class'; -import Reflux from 'reflux'; -import capitalize from 'lodash/capitalize'; - -import { DropdownButton, MenuItem } from 'components/bootstrap'; -import { LoggersActions, LoggersStore } from 'stores/system/LoggersStore'; -import withTelemetry from 'logic/telemetry/withTelemetry'; -import { getPathnameWithoutId } from 'util/URLUtils'; -import { TELEMETRY_EVENT_TYPE } from 'logic/telemetry/Constants'; -import withLocation from 'routing/withLocation'; - -const LogLevelDropdown = createReactClass({ - // eslint-disable-next-line react/no-unused-class-component-methods - displayName: 'LogLevelDropdown', - - // eslint-disable-next-line react/no-unused-class-component-methods - propTypes: { - name: PropTypes.string.isRequired, - nodeId: PropTypes.string.isRequired, - subsystem: PropTypes.object.isRequired, - sendTelemetry: PropTypes.func.isRequired, - location: PropTypes.object.isRequired, - }, - - mixins: [Reflux.connect(LoggersStore)], - - _changeLoglevel(loglevel) { - LoggersActions.setSubsystemLoggerLevel(this.props.nodeId, this.props.name, loglevel); - }, - - _menuLevelClick(loglevel) { - return () => { - this._changeLoglevel(loglevel); - - this.props.sendTelemetry(TELEMETRY_EVENT_TYPE.LOGGING.LOG_LEVEL_EDITED, { - app_pathname: getPathnameWithoutId(this.props.location.pathname), - app_action_value: 'log-level-change', - event_details: { value: loglevel }, - }); - }; - }, - - render() { - const { subsystem, nodeId } = this.props; - const loglevels = this.state.availableLoglevels - .map((loglevel) => ( - - {capitalize(loglevel)} - - )); - - return ( - - {loglevels} - - ); - }, -}); - -export default withLocation(withTelemetry(LogLevelDropdown)); diff --git a/graylog2-web-interface/src/components/loggers/LogLevelDropdown.tsx b/graylog2-web-interface/src/components/loggers/LogLevelDropdown.tsx new file mode 100644 index 000000000000..609bdadd2a62 --- /dev/null +++ b/graylog2-web-interface/src/components/loggers/LogLevelDropdown.tsx @@ -0,0 +1,68 @@ +/* + * 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 React from 'react'; +import { useCallback } from 'react'; +import capitalize from 'lodash/capitalize'; + +import { DropdownButton, MenuItem } from 'components/bootstrap'; +import { getPathnameWithoutId } from 'util/URLUtils'; +import { TELEMETRY_EVENT_TYPE } from 'logic/telemetry/Constants'; +import { availableLoglevels } from 'components/loggers/Constants'; +import useSetSubsystemLoggerLevel from 'components/loggers/useSetSubsystemLoggerLevel'; +import useSendTelemetry from 'logic/telemetry/useSendTelemetry'; +import useLocation from 'routing/useLocation'; + +type Props = { + name: string, + nodeId: string, + subsystem: { level: string }, +} + +const LogLevelDropdown = ({ nodeId, name, subsystem }: Props) => { + const sendTelemetry = useSendTelemetry(); + const location = useLocation(); + const { setSubsystemLoggerLevel } = useSetSubsystemLoggerLevel(); + + const _changeLoglevel = useCallback((loglevel: string) => setSubsystemLoggerLevel(nodeId, name, loglevel), [name, nodeId, setSubsystemLoggerLevel]); + + const _menuLevelClick = useCallback((loglevel: string) => () => { + _changeLoglevel(loglevel); + + sendTelemetry(TELEMETRY_EVENT_TYPE.LOGGING.LOG_LEVEL_EDITED, { + app_pathname: getPathnameWithoutId(location.pathname), + app_action_value: 'log-level-change', + event_details: { value: loglevel }, + }); + }, [_changeLoglevel, location?.pathname, sendTelemetry]); + + const loglevels = availableLoglevels + .map((loglevel) => ( + + {capitalize(loglevel)} + + )); + + return ( + + {loglevels} + + ); +}; + +export default LogLevelDropdown; diff --git a/graylog2-web-interface/src/components/loggers/LogLevelMetrics.jsx b/graylog2-web-interface/src/components/loggers/LogLevelMetrics.jsx deleted file mode 100644 index fe84ff084620..000000000000 --- a/graylog2-web-interface/src/components/loggers/LogLevelMetrics.jsx +++ /dev/null @@ -1,83 +0,0 @@ -/* - * 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 PropTypes from 'prop-types'; -import React from 'react'; -import createReactClass from 'create-react-class'; -import Reflux from 'reflux'; -import capitalize from 'lodash/capitalize'; -import numeral from 'numeral'; - -import { Col } from 'components/bootstrap'; -import { Spinner } from 'components/common'; -import { MetricsActions, MetricsStore } from 'stores/metrics/MetricsStore'; - -const LogLevelMetrics = createReactClass({ - displayName: 'LogLevelMetrics', - - propTypes: { - nodeId: PropTypes.string.isRequired, - loglevel: PropTypes.string.isRequired, - }, - - mixins: [Reflux.connect(MetricsStore)], - - componentDidMount() { - MetricsActions.add(this.props.nodeId, this._metricName()); - }, - - componentWillUnmount() { - MetricsActions.remove(this.props.nodeId, this._metricName()); - }, - - _metricName() { - return `org.apache.logging.log4j.core.Appender.${this.props.loglevel}`; - }, - - render() { - const { loglevel, nodeId } = this.props; - const { metrics } = this.state; - let metricsDetails; - - if (!metrics || !metrics[nodeId] || !metrics[nodeId][this._metricName()]) { - metricsDetails = ; - } else { - const { metric } = metrics[nodeId][this._metricName()]; - - metricsDetails = ( -
    -
    Total written:
    -
    {metric.rate.total}
    -
    Mean rate:
    -
    {numeral(metric.rate.mean).format('0.00')} / second
    -
    1 min rate:
    -
    {numeral(metric.rate.one_minute).format('0.00')} / second
    -
    - ); - } - - return ( -
    - -

    Level: {capitalize(loglevel)}

    - {metricsDetails} - -
    - ); - }, -}); - -export default LogLevelMetrics; diff --git a/graylog2-web-interface/src/components/loggers/LogLevelMetrics.tsx b/graylog2-web-interface/src/components/loggers/LogLevelMetrics.tsx new file mode 100644 index 000000000000..2fbfcb0eb135 --- /dev/null +++ b/graylog2-web-interface/src/components/loggers/LogLevelMetrics.tsx @@ -0,0 +1,70 @@ +/* + * 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 React, { useEffect } from 'react'; +import capitalize from 'lodash/capitalize'; +import numeral from 'numeral'; + +import { Col } from 'components/bootstrap'; +import { Spinner } from 'components/common'; +import { MetricsActions, MetricsStore } from 'stores/metrics/MetricsStore'; +import { useStore } from 'stores/connect'; + +type Props = { + nodeId: string, + loglevel: string, +} + +const LogLevelMetrics = ({ nodeId, loglevel }: Props) => { + const { metrics } = useStore(MetricsStore); + const metricName = `org.apache.logging.log4j.core.Appender.${loglevel}`; + + useEffect(() => { + MetricsActions.add(nodeId, metricName); + + return () => { MetricsActions.remove(nodeId, metricName); }; + }, [metricName, nodeId]); + + let metricsDetails; + + if (!metrics?.[nodeId]?.[metricName]) { + metricsDetails = ; + } else { + const { metric } = metrics[nodeId][metricName]; + + metricsDetails = 'rate' in metric ? ( +
    +
    Total written:
    +
    {metric.rate.total}
    +
    Mean rate:
    +
    {numeral(metric.rate.mean).format('0.00')} / second
    +
    1 min rate:
    +
    {numeral(metric.rate.one_minute).format('0.00')} / second
    +
    + ) : null; + } + + return ( +
    + +

    Level: {capitalize(loglevel)}

    + {metricsDetails} + +
    + ); +}; + +export default LogLevelMetrics; diff --git a/graylog2-web-interface/src/components/loggers/LogLevelMetricsOverview.jsx b/graylog2-web-interface/src/components/loggers/LogLevelMetricsOverview.tsx similarity index 51% rename from graylog2-web-interface/src/components/loggers/LogLevelMetricsOverview.jsx rename to graylog2-web-interface/src/components/loggers/LogLevelMetricsOverview.tsx index d4c35ea8f445..83bfae3656b3 100644 --- a/graylog2-web-interface/src/components/loggers/LogLevelMetricsOverview.jsx +++ b/graylog2-web-interface/src/components/loggers/LogLevelMetricsOverview.tsx @@ -14,34 +14,28 @@ * along with this program. If not, see * . */ -import PropTypes from 'prop-types'; import React from 'react'; -import createReactClass from 'create-react-class'; -import Reflux from 'reflux'; import { LogLevelMetrics } from 'components/loggers'; -import { LoggersStore } from 'stores/system/LoggersStore'; +import { availableLoglevels } from 'components/loggers/Constants'; -const LogLevelMetricsOverview = createReactClass({ - displayName: 'LogLevelMetricsOverview', +type Props = { + nodeId: string, +} - propTypes: { - nodeId: PropTypes.string.isRequired, - }, +const LogLevelMetricsOverview = ({ nodeId }:Props) => { + const logLevelMetrics = availableLoglevels + .map((loglevel) => ( + + )); - mixins: [Reflux.connect(LoggersStore)], - - render() { - const { nodeId } = this.props; - const logLevelMetrics = this.state.availableLoglevels - .map((loglevel) => ); - - return ( -
    - {logLevelMetrics} -
    - ); - }, -}); + return ( +
    + {logLevelMetrics} +
    + ); +}; export default LogLevelMetricsOverview; diff --git a/graylog2-web-interface/src/components/loggers/LoggerOverview.jsx b/graylog2-web-interface/src/components/loggers/LoggerOverview.tsx similarity index 51% rename from graylog2-web-interface/src/components/loggers/LoggerOverview.jsx rename to graylog2-web-interface/src/components/loggers/LoggerOverview.tsx index a496e2169797..3318e2448a7d 100644 --- a/graylog2-web-interface/src/components/loggers/LoggerOverview.jsx +++ b/graylog2-web-interface/src/components/loggers/LoggerOverview.tsx @@ -15,36 +15,32 @@ * . */ import React from 'react'; -import createReactClass from 'create-react-class'; -import Reflux from 'reflux'; import { Spinner } from 'components/common'; import { NodeLoggers } from 'components/loggers'; -import { LoggersStore } from 'stores/system/LoggersStore'; +import useLoggers from 'components/loggers/useLoggers'; +import useSubsystems from 'components/loggers/useSubsystems'; -const LoggerOverview = createReactClass({ - displayName: 'LoggerOverview', - mixins: [Reflux.connect(LoggersStore)], +const LoggerOverview = () => { + const { data: loggers } = useLoggers(); + const { data: subsystems } = useSubsystems(); - render() { - if (!this.state.loggers || !this.state.subsystems) { - return ; - } + if (!loggers || !subsystems) { + return ; + } - const { subsystems } = this.state; - const nodeLoggers = Object.keys(this.state.loggers) - .map((nodeId) => ( - - )); + const nodeLoggers = Object.keys(loggers) + .map((nodeId) => ( + + )); - return ( - - {nodeLoggers} - - ); - }, -}); + return ( + + {nodeLoggers} + + ); +}; export default LoggerOverview; diff --git a/graylog2-web-interface/src/components/loggers/NodeLoggers.jsx b/graylog2-web-interface/src/components/loggers/NodeLoggers.jsx deleted file mode 100644 index a7f4d2e1c1a8..000000000000 --- a/graylog2-web-interface/src/components/loggers/NodeLoggers.jsx +++ /dev/null @@ -1,131 +0,0 @@ -/* - * 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 PropTypes from 'prop-types'; -import React from 'react'; -// eslint-disable-next-line no-restricted-imports -import createReactClass from 'create-react-class'; -import Reflux from 'reflux'; - -import { Col, Row, Button } from 'components/bootstrap'; -import { LinkToNode, IfPermitted, Icon } from 'components/common'; -import { LoggingSubsystem, LogLevelMetricsOverview } from 'components/loggers'; -import { MetricsActions, MetricsStore } from 'stores/metrics/MetricsStore'; -import withTelemetry from 'logic/telemetry/withTelemetry'; -import { getPathnameWithoutId } from 'util/URLUtils'; -import { TELEMETRY_EVENT_TYPE } from 'logic/telemetry/Constants'; -import withLocation from 'routing/withLocation'; - -const NodeLoggers = createReactClass({ - // eslint-disable-next-line react/no-unused-class-component-methods - displayName: 'NodeLoggers', - - // eslint-disable-next-line react/no-unused-class-component-methods - propTypes: { - nodeId: PropTypes.string.isRequired, - subsystems: PropTypes.object.isRequired, - sendTelemetry: PropTypes.func.isRequired, - location: PropTypes.object.isRequired, - }, - - mixins: [Reflux.connect(MetricsStore)], - - getInitialState() { - return { showDetails: false }; - }, - - componentDidMount() { - const { nodeId } = this.props; - - MetricsActions.add(nodeId, this.metric_name); - }, - - componentWillUnmount() { - const { nodeId } = this.props; - - MetricsActions.remove(nodeId, this.metric_name); - }, - - metric_name: 'org.apache.logging.log4j.core.Appender.all', - - _formatThroughput() { - const { metrics } = this.state; - const { nodeId } = this.props; - - if (metrics && metrics[nodeId] && metrics[nodeId][this.metric_name]) { - const { metric } = metrics[nodeId][this.metric_name]; - - return metric.rate.total; - } - - return 'n/a'; - }, - - render() { - const { nodeId, subsystems, sendTelemetry } = this.props; - const { showDetails } = this.state; - const subsystemKeys = Object.keys(subsystems) - .map((subsystem) => ( - - )); - - const logLevelMetrics = ; - - return ( - - - -
    -
    - -
    -

    - - - Has written a total of {this._formatThroughput()} internal log messages. - -

    -
    -
    - {subsystemKeys} -
    - {showDetails && logLevelMetrics} -
    - -
    - ); - }, -}); - -export default withLocation(withTelemetry(NodeLoggers)); diff --git a/graylog2-web-interface/src/components/loggers/NodeLoggers.tsx b/graylog2-web-interface/src/components/loggers/NodeLoggers.tsx new file mode 100644 index 000000000000..35d134d19442 --- /dev/null +++ b/graylog2-web-interface/src/components/loggers/NodeLoggers.tsx @@ -0,0 +1,110 @@ +/* + * 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 React from 'react'; +import { useEffect, useMemo, useState } from 'react'; + +import { MetricsActions, MetricsStore } from 'stores/metrics/MetricsStore'; +import useSendTelemetry from 'logic/telemetry/useSendTelemetry'; +import useLocation from 'routing/useLocation'; +import { useStore } from 'stores/connect'; +import LoggingSubsystem from 'components/loggers/LoggingSubsystem'; +import LogLevelMetricsOverview from 'components/loggers/LogLevelMetricsOverview'; +import { Col, Row, Button } from 'components/bootstrap'; +import { LinkToNode, IfPermitted, Icon } from 'components/common'; +import { getPathnameWithoutId } from 'util/URLUtils'; +import { TELEMETRY_EVENT_TYPE } from 'logic/telemetry/Constants'; + +type Props = { + nodeId: string, + subsystems: {}, +} +const metric_name = 'org.apache.logging.log4j.core.Appender.all'; + +const NodeLoggers = ({ nodeId, subsystems }: Props) => { + const sendTelemetry = useSendTelemetry(); + const location = useLocation(); + + const { metrics } = useStore(MetricsStore); + const [showDetails, setShowDetails] = useState(false); + + useEffect(() => { + MetricsActions.add(nodeId, metric_name); + + return () => { MetricsActions.remove(nodeId, metric_name); }; + }, [nodeId]); + + const _formattedThroughput = useMemo(() => { + if (metrics?.[nodeId]?.[metric_name]) { + const { metric } = metrics[nodeId][metric_name]; + + return 'rate' in metric ? metric.rate.total : 'n/a'; + } + + return 'n/a'; + }, [metrics, nodeId]); + + const subsystemKeys = Object.keys(subsystems) + .map((subsystem) => ( + + )); + + const logLevelMetrics = ; + + return ( + + + +
    +
    + +
    +

    + {' '} + + Has written a total of {_formattedThroughput} internal log messages. + +

    +
    +
    + {subsystemKeys} +
    + {showDetails && logLevelMetrics} +
    + +
    + ); +}; + +export default NodeLoggers; diff --git a/graylog2-web-interface/src/components/loggers/index.jsx b/graylog2-web-interface/src/components/loggers/index.tsx similarity index 96% rename from graylog2-web-interface/src/components/loggers/index.jsx rename to graylog2-web-interface/src/components/loggers/index.tsx index 12b767c2b3f8..09ad36e6f512 100644 --- a/graylog2-web-interface/src/components/loggers/index.jsx +++ b/graylog2-web-interface/src/components/loggers/index.tsx @@ -14,7 +14,7 @@ * along with this program. If not, see * . */ -/* eslint-disable import/no-cycle */ + export { default as LoggerOverview } from './LoggerOverview'; export { default as LoggingSubsystem } from './LoggingSubsystem'; export { default as LogLevelDropdown } from './LogLevelDropdown'; diff --git a/graylog2-web-interface/src/components/loggers/useLoggers.ts b/graylog2-web-interface/src/components/loggers/useLoggers.ts new file mode 100644 index 000000000000..c4cb105c3449 --- /dev/null +++ b/graylog2-web-interface/src/components/loggers/useLoggers.ts @@ -0,0 +1,22 @@ +/* + * 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 { ClusterSystemLoggers } from '@graylog/server-api'; + +const useLoggers = () => useQuery(['loggers', 'loggers'], ClusterSystemLoggers.loggers); +export default useLoggers; diff --git a/graylog2-web-interface/src/components/loggers/useSetSubsystemLoggerLevel.ts b/graylog2-web-interface/src/components/loggers/useSetSubsystemLoggerLevel.ts new file mode 100644 index 000000000000..d68465e4aa8d --- /dev/null +++ b/graylog2-web-interface/src/components/loggers/useSetSubsystemLoggerLevel.ts @@ -0,0 +1,36 @@ +/* + * 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 { useMutation, useQueryClient } from '@tanstack/react-query'; +import { useCallback } from 'react'; + +import { ClusterSystemLoggers } from '@graylog/server-api'; + +const _setSubsystemLoggerLevel = (args: { nodeId: string, name: string, level: string }) => ClusterSystemLoggers.setSubsystemLoggerLevel(args.nodeId, args.name, args.level); + +const useSetSubsystemLoggerLevel = () => { + const queryClient = useQueryClient(); + const { mutateAsync, isLoading } = useMutation(_setSubsystemLoggerLevel, { + onSuccess: () => { + queryClient.invalidateQueries(['loggers']); + }, + }); + const setSubsystemLoggerLevel = useCallback((nodeId: string, name: string, level: string) => mutateAsync({ nodeId, name, level }), [mutateAsync]); + + return { setSubsystemLoggerLevel, isLoading }; +}; + +export default useSetSubsystemLoggerLevel; diff --git a/graylog2-web-interface/src/components/loggers/useSubsystems.ts b/graylog2-web-interface/src/components/loggers/useSubsystems.ts new file mode 100644 index 000000000000..0e7dbc9b0466 --- /dev/null +++ b/graylog2-web-interface/src/components/loggers/useSubsystems.ts @@ -0,0 +1,22 @@ +/* + * 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 { ClusterSystemLoggers } from '@graylog/server-api'; + +const useSubsystems = () => useQuery(['loggers', 'subsystems'], ClusterSystemLoggers.subsystems); +export default useSubsystems; diff --git a/graylog2-web-interface/src/components/lookup-tables/CachesContainer.jsx b/graylog2-web-interface/src/components/lookup-tables/CachesContainer.tsx similarity index 52% rename from graylog2-web-interface/src/components/lookup-tables/CachesContainer.jsx rename to graylog2-web-interface/src/components/lookup-tables/CachesContainer.tsx index 66108af5c29c..e5701bafaf93 100644 --- a/graylog2-web-interface/src/components/lookup-tables/CachesContainer.jsx +++ b/graylog2-web-interface/src/components/lookup-tables/CachesContainer.tsx @@ -14,51 +14,34 @@ * along with this program. If not, see * . */ -import PropTypes from 'prop-types'; -import React from 'react'; -// eslint-disable-next-line -import createReactClass from 'create-react-class'; -import Reflux from 'reflux'; +import * as React from 'react'; +import { useEffect } from 'react'; import { Spinner } from 'components/common'; import { LookupTableCachesActions, LookupTableCachesStore } from 'stores/lookup-tables/LookupTableCachesStore'; +import { useStore } from 'stores/connect'; -const CachesContainer = createReactClass({ - // eslint-disable-next-line - displayName: 'CachesContainer', +type Props = { + children: React.ReactElement[], +}; - // eslint-disable-next-line - propTypes: { - children: PropTypes.oneOfType([ - PropTypes.arrayOf(PropTypes.node), - PropTypes.node, - ]), - }, +const CachesContainer = ({ children }: Props) => { + const { caches, pagination } = useStore(LookupTableCachesStore); - mixins: [Reflux.connect(LookupTableCachesStore)], - - getDefaultProps() { - return { - children: null, - }; - }, - - componentDidMount() { + useEffect(() => { // TODO the 10k items is bad. we need a searchable/scrollable long list select box LookupTableCachesActions.searchPaginated(1, 10000, null); - }, + }, []); - render() { - if (!this.state.caches) { - return ; - } + if (!caches) { + return ; + } - const childrenWithProps = React.Children.map(this.props.children, - (child) => React.cloneElement(child, - { caches: this.state.caches, pagination: this.state.pagination })); + const childrenWithProps = React.Children.map(children, + (child) => React.cloneElement(child, + { caches, pagination })); - return
    {childrenWithProps}
    ; - }, -}); + return
    {childrenWithProps}
    ; +}; export default CachesContainer; diff --git a/graylog2-web-interface/src/components/lookup-tables/DataAdaptersContainer.jsx b/graylog2-web-interface/src/components/lookup-tables/DataAdaptersContainer.tsx similarity index 54% rename from graylog2-web-interface/src/components/lookup-tables/DataAdaptersContainer.jsx rename to graylog2-web-interface/src/components/lookup-tables/DataAdaptersContainer.tsx index 8f48950da71b..7df92727d009 100644 --- a/graylog2-web-interface/src/components/lookup-tables/DataAdaptersContainer.jsx +++ b/graylog2-web-interface/src/components/lookup-tables/DataAdaptersContainer.tsx @@ -14,48 +14,34 @@ * along with this program. If not, see * . */ -import PropTypes from 'prop-types'; -import React from 'react'; -import createReactClass from 'create-react-class'; -import Reflux from 'reflux'; +import * as React from 'react'; +import { useEffect } from 'react'; import { Spinner } from 'components/common'; import { LookupTableDataAdaptersActions, LookupTableDataAdaptersStore } from 'stores/lookup-tables/LookupTableDataAdaptersStore'; +import { useStore } from 'stores/connect'; -const DataAdaptersContainer = createReactClass({ - displayName: 'DataAdaptersContainer', +type Props = { + children: React.ReactElement[], +}; - propTypes: { - children: PropTypes.oneOfType([ - PropTypes.arrayOf(PropTypes.node), - PropTypes.node, - ]), - }, +const DataAdaptersContainer = ({ children }: Props) => { + const { dataAdapters, pagination } = useStore(LookupTableDataAdaptersStore); - mixins: [Reflux.connect(LookupTableDataAdaptersStore)], - - getDefaultProps() { - return { - children: null, - }; - }, - - componentDidMount() { + useEffect(() => { // TODO the 10k items is bad. we need a searchable/scrollable long list select box LookupTableDataAdaptersActions.searchPaginated(1, 10000, null); - }, + }, []); - render() { - if (!this.state.dataAdapters) { - return ; - } + if (!dataAdapters) { + return ; + } - const childrenWithProps = React.Children.map(this.props.children, - (child) => React.cloneElement(child, - { dataAdapters: this.state.dataAdapters, pagination: this.state.pagination })); + const childrenWithProps = React.Children.map(children, + (child) => React.cloneElement(child, + { dataAdapters, pagination })); - return
    {childrenWithProps}
    ; - }, -}); + return
    {childrenWithProps}
    ; +}; export default DataAdaptersContainer; diff --git a/graylog2-web-interface/src/components/lookup-tables/adapters/DnsAdapterSummary.tsx b/graylog2-web-interface/src/components/lookup-tables/adapters/DnsAdapterSummary.tsx index cc6accabba3a..910376bc2eaa 100644 --- a/graylog2-web-interface/src/components/lookup-tables/adapters/DnsAdapterSummary.tsx +++ b/graylog2-web-interface/src/components/lookup-tables/adapters/DnsAdapterSummary.tsx @@ -17,6 +17,7 @@ import React from 'react'; import { TimeUnit } from 'components/common'; +import type { TimeUnit as TimeUnitString } from 'components/common/types'; type DnsAdapterSummaryProps = { dataAdapter: { @@ -25,8 +26,8 @@ type DnsAdapterSummaryProps = { request_timeout: number; server_ips: string; cache_ttl_override_enabled: boolean; - cache_ttl_override: string; - cache_ttl_override_unit: string; + cache_ttl_override: number; + cache_ttl_override_unit: TimeUnitString; }; }; }; diff --git a/graylog2-web-interface/src/components/metrics/MetricsMapper.jsx b/graylog2-web-interface/src/components/metrics/MetricsMapper.jsx deleted file mode 100644 index b8618ce14afd..000000000000 --- a/graylog2-web-interface/src/components/metrics/MetricsMapper.jsx +++ /dev/null @@ -1,92 +0,0 @@ -/* - * 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 PropTypes from 'prop-types'; -import React from 'react'; -import createReactClass from 'create-react-class'; -import Reflux from 'reflux'; - -import { MetricsActions, MetricsStore } from 'stores/metrics/MetricsStore'; - -const MetricsMapper = createReactClass({ - displayName: 'MetricsMapper', - - propTypes: { - map: PropTypes.object.isRequired, - computeValue: PropTypes.func.isRequired, - }, - - mixins: [Reflux.connect(MetricsStore)], - - getDefaultProps() { - return { - }; - }, - - getInitialState() { - return {}; - }, - - UNSAFE_componentWillMount() { - Object.keys(this.props.map).forEach((name) => MetricsActions.addGlobal(this.props.map[name])); - }, - - shouldComponentUpdate(_, nextState) { - // Only re-render this component if the metric data has changed - if (this.state.metricsUpdatedAt && nextState.metricsUpdatedAt) { - return nextState.metricsUpdatedAt > this.state.metricsUpdatedAt; - } - - return true; - }, - - componentWillUnmount() { - Object.keys(this.props.map).forEach((name) => MetricsActions.removeGlobal(this.props.map[name])); - }, - - render() { - if (!this.state.metrics) { - return null; - } - - const metricsMap = {}; - - Object.keys(this.state.metrics).forEach((nodeId) => { - Object.keys(this.props.map).forEach((key) => { - const metricName = this.props.map[key]; - - if (this.state.metrics[nodeId][metricName]) { - // Only create the node entry if we actually have data - if (!metricsMap[nodeId]) { - metricsMap[nodeId] = {}; - } - - metricsMap[nodeId][key] = this.state.metrics[nodeId][metricName]; - } - }); - }); - - const value = this.props.computeValue(metricsMap); - - return ( - - {value} - - ); - }, -}); - -export default MetricsMapper; diff --git a/graylog2-web-interface/src/components/metrics/MetricsMapper.tsx b/graylog2-web-interface/src/components/metrics/MetricsMapper.tsx new file mode 100644 index 000000000000..fd2bf739eb91 --- /dev/null +++ b/graylog2-web-interface/src/components/metrics/MetricsMapper.tsx @@ -0,0 +1,69 @@ +/* + * 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 { useEffect } from 'react'; + +import { MetricsActions, MetricsStore } from 'stores/metrics/MetricsStore'; +import { useStore } from 'stores/connect'; + +type Props = { + map: {}, + computeValue: (map: any) => string, +} + +const MetricsMapper = ({ map, computeValue }: Props) => { + const { metrics } = useStore(MetricsStore); + + useEffect(() => { + Object.keys(map).forEach((name) => MetricsActions.addGlobal(map[name])); + + return () => { + Object.keys(map).forEach((name) => MetricsActions.removeGlobal(map[name])); + }; + }); + + if (!metrics) { + return null; + } + + const metricsMap = {}; + + Object.keys(metrics).forEach((nodeId) => { + Object.keys(map).forEach((key) => { + const metricName = map[key]; + + if (metrics[nodeId][metricName]) { + // Only create the node entry if we actually have data + if (!metricsMap[nodeId]) { + metricsMap[nodeId] = {}; + } + + metricsMap[nodeId][key] = metrics[nodeId][metricName]; + } + }); + }); + + const value = computeValue(metricsMap); + + return ( + + {value} + + ); +}; + +export default MetricsMapper; diff --git a/graylog2-web-interface/src/components/nodes/JournalDetails.jsx b/graylog2-web-interface/src/components/nodes/JournalDetails.jsx deleted file mode 100644 index 79ee1f49deeb..000000000000 --- a/graylog2-web-interface/src/components/nodes/JournalDetails.jsx +++ /dev/null @@ -1,183 +0,0 @@ -/* - * 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 PropTypes from 'prop-types'; -import React from 'react'; -// eslint-disable-next-line no-restricted-imports -import createReactClass from 'create-react-class'; -import Reflux from 'reflux'; -import numeral from 'numeral'; -import moment from 'moment'; -import 'moment-duration-format'; -import styled from 'styled-components'; - -import { Link } from 'components/common/router'; -import { Row, Col, Alert } from 'components/bootstrap'; -import { Spinner, RelativeTime } from 'components/common'; -import ProgressBar, { Bar } from 'components/common/ProgressBar'; -import MetricsExtractor from 'logic/metrics/MetricsExtractor'; -import NumberUtils from 'util/NumberUtils'; -import Routes from 'routing/Routes'; -import { JournalStore } from 'stores/journal/JournalStore'; -import { MetricsActions, MetricsStore } from 'stores/metrics/MetricsStore'; - -const JournalUsageProgressBar = styled(ProgressBar)` - margin-bottom: 5px; - margin-top: 10px; - - ${Bar} { - min-width: 3em; - } -`; - -const JournalDetails = createReactClass({ - displayName: 'JournalDetails', - - propTypes: { - nodeId: PropTypes.string.isRequired, - }, - - mixins: [Reflux.connect(MetricsStore)], - - getInitialState() { - return { - journalInformation: undefined, - }; - }, - - componentDidMount() { - const { nodeId } = this.props; - - JournalStore.get(nodeId).then((journalInformation) => { - this.setState({ journalInformation: journalInformation }, this._listenToMetrics); - }); - }, - - componentWillUnmount() { - const { nodeId } = this.props; - - if (this.metricNames) { - Object.keys(this.metricNames).forEach((metricShortName) => MetricsActions.remove(nodeId, this.metricNames[metricShortName])); - } - }, - - _listenToMetrics() { - const { nodeId } = this.props; - const { journalInformation } = this.state; - - // only listen for updates if the journal is actually turned on - if (journalInformation.enabled) { - this.metricNames = { - append: 'org.graylog2.journal.append.1-sec-rate', - read: 'org.graylog2.journal.read.1-sec-rate', - segments: 'org.graylog2.journal.segments', - entriesUncommitted: 'org.graylog2.journal.entries-uncommitted', - utilizationRatio: 'org.graylog2.journal.utilization-ratio', - oldestSegment: 'org.graylog2.journal.oldest-segment', - }; - - Object.keys(this.metricNames).forEach((metricShortName) => MetricsActions.add(nodeId, this.metricNames[metricShortName])); - } - }, - - _isLoading() { - const { journalInformation, metrics } = this.state; - - return !(metrics && journalInformation); - }, - - render() { - if (this._isLoading()) { - return ; - } - - const { nodeId } = this.props; - const { metrics: metricsState } = this.state; - const nodeMetrics = metricsState[nodeId]; - const { journalInformation } = this.state; - - if (!journalInformation.enabled) { - return ( - - The disk journal is disabled on this node. - - ); - } - - const metrics = this.metricNames ? MetricsExtractor.getValuesForNode(nodeMetrics, this.metricNames) : {}; - - if (Object.keys(metrics).length === 0) { - return ( - - Journal metrics unavailable. - - ); - } - - const oldestSegment = moment(metrics.oldestSegment); - let overcommittedWarning; - - if (metrics.utilizationRatio >= 1) { - overcommittedWarning = ( - - Warning! The journal utilization is exceeding the maximum size defined. - {' '}Click here for more information.
    -
    - ); - } - - return ( - - -

    Configuration

    -
    -
    Path:
    -
    {journalInformation.journal_config.directory}
    -
    Earliest entry:
    -
    -
    Maximum size:
    -
    {NumberUtils.formatBytes(journalInformation.journal_config.max_size)}
    -
    Maximum age:
    -
    {moment.duration(journalInformation.journal_config.max_age).format('d [days] h [hours] m [minutes]')}
    -
    Flush policy:
    -
    - Every {numeral(journalInformation.journal_config.flush_interval).format('0,0')} messages - {' '}or {moment.duration(journalInformation.journal_config.flush_age).format('h [hours] m [minutes] s [seconds]')} -
    -
    - - -

    Utilization

    - - - - {overcommittedWarning} - - {numeral(metrics.entriesUncommitted).format('0,0')} unprocessed messages - {' '}are currently in the journal, in {metrics.segments} segments.
    - {numeral(metrics.append).format('0,0')} messages - {' '}have been appended in the last second,{' '} - {numeral(metrics.read).format('0,0')} messages have been read in the last second. - -
    - ); - }, -}); - -export default JournalDetails; diff --git a/graylog2-web-interface/src/components/nodes/JournalDetails.tsx b/graylog2-web-interface/src/components/nodes/JournalDetails.tsx new file mode 100644 index 000000000000..a9f86f93a5d8 --- /dev/null +++ b/graylog2-web-interface/src/components/nodes/JournalDetails.tsx @@ -0,0 +1,151 @@ +/* + * 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 { useEffect } from 'react'; +import numeral from 'numeral'; +import moment from 'moment'; +import 'moment-duration-format'; +import styled from 'styled-components'; +import { useQuery } from '@tanstack/react-query'; + +import { Link } from 'components/common/router'; +import { Row, Col, Alert } from 'components/bootstrap'; +import { Spinner, RelativeTime } from 'components/common'; +import ProgressBar, { Bar } from 'components/common/ProgressBar'; +import MetricsExtractor from 'logic/metrics/MetricsExtractor'; +import NumberUtils from 'util/NumberUtils'; +import Routes from 'routing/Routes'; +import { JournalStore } from 'stores/journal/JournalStore'; +import { MetricsActions, MetricsStore } from 'stores/metrics/MetricsStore'; +import { useStore } from 'stores/connect'; + +const JournalUsageProgressBar = styled(ProgressBar)` + margin-bottom: 5px; + margin-top: 10px; + + ${Bar} { + min-width: 3em; + } +`; + +type Props = { + nodeId: string, +} +const metricNames = { + append: 'org.graylog2.journal.append.1-sec-rate', + read: 'org.graylog2.journal.read.1-sec-rate', + segments: 'org.graylog2.journal.segments', + entriesUncommitted: 'org.graylog2.journal.entries-uncommitted', + utilizationRatio: 'org.graylog2.journal.utilization-ratio', + oldestSegment: 'org.graylog2.journal.oldest-segment', +}; + +const JournalDetails = ({ nodeId }: Props) => { + const { metrics: metricsState } = useStore(MetricsStore); + const { data: journalInformation } = useQuery(['journal', 'info', nodeId], () => JournalStore.get(nodeId)); + + useEffect(() => { + if (journalInformation?.enabled) { + Object.keys(metricNames).forEach((metricShortName) => MetricsActions.add(nodeId, metricNames[metricShortName])); + + return () => { + Object.keys(metricNames).forEach((metricShortName) => MetricsActions.remove(nodeId, metricNames[metricShortName])); + }; + } + + return () => {}; + }, [journalInformation?.enabled, nodeId]); + + const _isLoading = !(metricsState && journalInformation); + + if (_isLoading) { + return ; + } + + const nodeMetrics = metricsState[nodeId]; + + if (!journalInformation.enabled) { + return ( + + The disk journal is disabled on this node. + + ); + } + + const metrics = journalInformation.enabled ? MetricsExtractor.getValuesForNode(nodeMetrics, metricNames) : {}; + + if (Object.keys(metrics).length === 0) { + return ( + + Journal metrics unavailable. + + ); + } + + const oldestSegment = moment(metrics.oldestSegment); + let overcommittedWarning; + + if (metrics.utilizationRatio >= 1) { + overcommittedWarning = ( + + Warning! The journal utilization is exceeding the maximum size defined. + {' '}Click here for more information.
    +
    + ); + } + + return ( + + +

    Configuration

    +
    +
    Path:
    +
    {journalInformation.journal_config.directory}
    +
    Earliest entry:
    +
    +
    Maximum size:
    +
    {NumberUtils.formatBytes(journalInformation.journal_config.max_size)}
    +
    Maximum age:
    +
    {moment.duration(journalInformation.journal_config.max_age).format('d [days] h [hours] m [minutes]')}
    +
    Flush policy:
    +
    + Every {numeral(journalInformation.journal_config.flush_interval).format('0,0')} messages + {' '}or {moment.duration(journalInformation.journal_config.flush_age).format('h [hours] m [minutes] s [seconds]')} +
    +
    + + +

    Utilization

    + + + + {overcommittedWarning} + + {numeral(metrics.entriesUncommitted).format('0,0')} unprocessed messages + {' '}are currently in the journal, in {metrics.segments} segments.
    + {numeral(metrics.append).format('0,0')} messages + {' '}have been appended in the last second,{' '} + {numeral(metrics.read).format('0,0')} messages have been read in the last second. + +
    + ); +}; + +export default JournalDetails; diff --git a/graylog2-web-interface/src/components/nodes/JournalState.jsx b/graylog2-web-interface/src/components/nodes/JournalState.jsx deleted file mode 100644 index 200b9ac82199..000000000000 --- a/graylog2-web-interface/src/components/nodes/JournalState.jsx +++ /dev/null @@ -1,80 +0,0 @@ -/* - * 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 PropTypes from 'prop-types'; -import React from 'react'; -import createReactClass from 'create-react-class'; -import Reflux from 'reflux'; -import numeral from 'numeral'; - -import { Pluralize, Spinner } from 'components/common'; -import MetricsExtractor from 'logic/metrics/MetricsExtractor'; -import { MetricsActions, MetricsStore } from 'stores/metrics/MetricsStore'; - -const JournalState = createReactClass({ - displayName: 'JournalState', - - propTypes: { - nodeId: PropTypes.string.isRequired, - }, - - mixins: [Reflux.connect(MetricsStore)], - - UNSAFE_componentWillMount() { - this.metricNames = { - append: 'org.graylog2.journal.append.1-sec-rate', - read: 'org.graylog2.journal.read.1-sec-rate', - segments: 'org.graylog2.journal.segments', - entriesUncommitted: 'org.graylog2.journal.entries-uncommitted', - }; - - Object.keys(this.metricNames).forEach((metricShortName) => MetricsActions.add(this.props.nodeId, this.metricNames[metricShortName])); - }, - - componentWillUnmount() { - Object.keys(this.metricNames).forEach((metricShortName) => MetricsActions.remove(this.props.nodeId, this.metricNames[metricShortName])); - }, - - _isLoading() { - return !this.state.metrics; - }, - - render() { - if (this._isLoading()) { - return ; - } - - const { nodeId } = this.props; - const nodeMetrics = this.state.metrics[nodeId]; - const metrics = MetricsExtractor.getValuesForNode(nodeMetrics, this.metricNames); - - if (Object.keys(metrics).length === 0) { - return Journal metrics unavailable.; - } - - return ( - - The journal contains {numeral(metrics.entriesUncommitted).format('0,0')} unprocessed messages in {metrics.segments} - {' '}.{' '} - {numeral(metrics.append).format('0,0')} messages appended, - {numeral(metrics.read).format('0,0')} messages - read in the last second. - - ); - }, -}); - -export default JournalState; diff --git a/graylog2-web-interface/src/components/nodes/JournalState.tsx b/graylog2-web-interface/src/components/nodes/JournalState.tsx new file mode 100644 index 000000000000..42d32a2abbdd --- /dev/null +++ b/graylog2-web-interface/src/components/nodes/JournalState.tsx @@ -0,0 +1,70 @@ +/* + * 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 React, { useEffect } from 'react'; +import numeral from 'numeral'; + +import { Pluralize, Spinner } from 'components/common'; +import MetricsExtractor from 'logic/metrics/MetricsExtractor'; +import { MetricsActions, MetricsStore } from 'stores/metrics/MetricsStore'; +import { useStore } from 'stores/connect'; + +type Props = { + nodeId: string, +} +const metricNames = { + append: 'org.graylog2.journal.append.1-sec-rate', + read: 'org.graylog2.journal.read.1-sec-rate', + segments: 'org.graylog2.journal.segments', + entriesUncommitted: 'org.graylog2.journal.entries-uncommitted', +}; + +const JournalState = ({ nodeId }: Props) => { + const { metrics } = useStore(MetricsStore); + + useEffect(() => { + Object.keys(metricNames).forEach((metricShortName) => MetricsActions.add(nodeId, metricNames[metricShortName])); + + return () => { + Object.keys(metricNames).forEach((metricShortName) => MetricsActions.remove(nodeId, metricNames[metricShortName])); + }; + }, [nodeId]); + + const nodeMetrics = metrics?.[nodeId]; + const _isLoading = !nodeMetrics; + + if (_isLoading) { + return ; + } + + const _metrics = MetricsExtractor.getValuesForNode(nodeMetrics, metricNames); + + if (Object.keys(_metrics).length === 0) { + return Journal metrics unavailable.; + } + + return ( + + The journal contains {numeral(_metrics.entriesUncommitted).format('0,0')} unprocessed messages in {_metrics.segments} + {' '}.{' '} + {numeral(_metrics.append).format('0,0')} messages appended, + {numeral(_metrics.read).format('0,0')} messages + read in the last second. + + ); +}; + +export default JournalState; diff --git a/graylog2-web-interface/src/components/nodes/JvmHeapUsage.jsx b/graylog2-web-interface/src/components/nodes/JvmHeapUsage.jsx deleted file mode 100644 index 03afdf30b7e1..000000000000 --- a/graylog2-web-interface/src/components/nodes/JvmHeapUsage.jsx +++ /dev/null @@ -1,161 +0,0 @@ -/* - * 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 PropTypes from 'prop-types'; -import React from 'react'; -// eslint-disable-next-line no-restricted-imports -import createReactClass from 'create-react-class'; -import Reflux from 'reflux'; -import styled, { css } from 'styled-components'; - -import ProgressBar from 'components/common/ProgressBar'; -import { Spinner } from 'components/common'; -import NumberUtils from 'util/NumberUtils'; -import MetricsExtractor from 'logic/metrics/MetricsExtractor'; -import { MetricsActions, MetricsStore } from 'stores/metrics/MetricsStore'; - -const NodeHeap = styled.div` - margin-top: 10px; - - p { - margin-bottom: 0; - } -`; - -const Blob = styled.span(({ theme }) => css` - display: inline-block; - width: 9px; - height: 9px; - margin-left: 2px; - border: 1px solid; - - &.used-memory { - background-color: ${theme.colors.variant.primary}; - border-color: ${theme.colors.variant.dark.primary}; - } - - &.committed-memory { - background-color: ${theme.colors.variant.warning}; - border-color: ${theme.colors.variant.dark.warning}; - } - - &.max-memory { - background-color: ${theme.colors.global.background}; - border-color: ${theme.colors.gray[80]}; - } -`); - -const StyledProgressBar = styled(ProgressBar)` - height: 25px; - margin-bottom: 5px; -`; - -const JvmHeapUsage = createReactClass({ - // eslint-disable-next-line react/no-unused-class-component-methods - displayName: 'JvmHeapUsage', - - // eslint-disable-next-line react/no-unused-class-component-methods - propTypes: { - nodeId: PropTypes.string.isRequired, - }, - - mixins: [Reflux.connect(MetricsStore)], - - UNSAFE_componentWillMount() { - const { nodeId } = this.props; - - this.metricNames = { - usedMemory: 'jvm.memory.heap.used', - committedMemory: 'jvm.memory.heap.committed', - maxMemory: 'jvm.memory.heap.max', - }; - - Object.keys(this.metricNames).forEach((metricShortName) => MetricsActions.add(nodeId, this.metricNames[metricShortName])); - }, - - componentWillUnmount() { - const { nodeId } = this.props; - - Object.keys(this.metricNames).forEach((metricShortName) => MetricsActions.remove(nodeId, this.metricNames[metricShortName])); - }, - - _extractMetricValues() { - const { nodeId } = this.props; - const { metrics } = this.state; - - if (metrics && metrics[nodeId]) { - const extractedMetric = MetricsExtractor.getValuesForNode(metrics[nodeId], this.metricNames); - const { maxMemory, usedMemory, committedMemory } = extractedMetric; - - if (maxMemory) { - extractedMetric.usedPercentage = maxMemory === 0 ? 0 : Math.ceil((usedMemory / maxMemory) * 100); - extractedMetric.committedPercentage = maxMemory === 0 ? 0 : Math.ceil((committedMemory / maxMemory) * 100); - - return extractedMetric; - } - - return { - usedPercentage: 0, - committedPercentage: 0, - }; - } - - return {}; - }, - - render() { - const { nodeId } = this.props; - const extractedMetrics = this._extractMetricValues(); - const { usedPercentage, committedPercentage, usedMemory, committedMemory, maxMemory } = extractedMetrics; - let progressBarConfig = [{ value: 0 }]; - let detail =

    ; - - if (usedPercentage || committedPercentage) { - if (Object.keys(extractedMetrics).length === 0) { - detail =

    Heap information unavailable.

    ; - } else { - progressBarConfig = [ - { value: usedPercentage, bsStyle: 'primary' }, - { value: committedPercentage - usedPercentage, bsStyle: 'warning' }, - ]; - - detail = ( -

    - The JVM is using{' '} - - {NumberUtils.formatBytes(usedMemory)} - {' '}of{' '} - - {NumberUtils.formatBytes(committedMemory)} - {' '}heap space and will not attempt to use more than{' '} - - {NumberUtils.formatBytes(maxMemory)} -

    - ); - } - } - - return ( - - - - {detail} - - ); - }, -}); - -export default JvmHeapUsage; diff --git a/graylog2-web-interface/src/components/nodes/JvmHeapUsage.tsx b/graylog2-web-interface/src/components/nodes/JvmHeapUsage.tsx new file mode 100644 index 000000000000..13ea177b5b4c --- /dev/null +++ b/graylog2-web-interface/src/components/nodes/JvmHeapUsage.tsx @@ -0,0 +1,143 @@ +/* + * 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 { useEffect, useMemo } from 'react'; +import styled, { css } from 'styled-components'; + +import ProgressBar from 'components/common/ProgressBar'; +import { Spinner } from 'components/common'; +import NumberUtils from 'util/NumberUtils'; +import MetricsExtractor from 'logic/metrics/MetricsExtractor'; +import { MetricsActions, MetricsStore } from 'stores/metrics/MetricsStore'; +import { useStore } from 'stores/connect'; + +const NodeHeap = styled.div` + margin-top: 10px; + + p { + margin-bottom: 0; + } +`; + +const Blob = styled.span(({ theme }) => css` + display: inline-block; + width: 9px; + height: 9px; + margin-left: 2px; + border: 1px solid; + + &.used-memory { + background-color: ${theme.colors.variant.primary}; + border-color: ${theme.colors.variant.dark.primary}; + } + + &.committed-memory { + background-color: ${theme.colors.variant.warning}; + border-color: ${theme.colors.variant.dark.warning}; + } + + &.max-memory { + background-color: ${theme.colors.global.background}; + border-color: ${theme.colors.gray[80]}; + } +`); + +const StyledProgressBar = styled(ProgressBar)` + height: 25px; + margin-bottom: 5px; +`; + +type Props = { + nodeId: string, +} +const metricNames = { + usedMemory: 'jvm.memory.heap.used', + committedMemory: 'jvm.memory.heap.committed', + maxMemory: 'jvm.memory.heap.max', +}; + +const JvmHeapUsage = ({ nodeId }: Props) => { + const { metrics } = useStore(MetricsStore); + + useEffect(() => { + Object.keys(metricNames).forEach((metricShortName) => MetricsActions.add(nodeId, metricNames[metricShortName])); + + return () => { + Object.keys(metricNames).forEach((metricShortName) => MetricsActions.remove(nodeId, metricNames[metricShortName])); + }; + }, [nodeId]); + + const extractedMetrics = useMemo(() => { + if (metrics?.[nodeId]) { + const extractedMetric = MetricsExtractor.getValuesForNode(metrics[nodeId], metricNames); + const { maxMemory, usedMemory, committedMemory } = extractedMetric; + + if (maxMemory) { + extractedMetric.usedPercentage = maxMemory === 0 ? 0 : Math.ceil((usedMemory / maxMemory) * 100); + extractedMetric.committedPercentage = maxMemory === 0 ? 0 : Math.ceil((committedMemory / maxMemory) * 100); + + return extractedMetric; + } + + return { + usedPercentage: 0, + committedPercentage: 0, + }; + } + + return {}; + }, [metrics, nodeId]); + + const { usedPercentage, committedPercentage, usedMemory, committedMemory, maxMemory } = extractedMetrics; + let progressBarConfig: Array<{ value: number, bsStyle?: 'primary' | 'warning' }> = [{ value: 0 }]; + let detail =

    ; + + if (usedPercentage || committedPercentage) { + if (Object.keys(extractedMetrics).length === 0) { + detail =

    Heap information unavailable.

    ; + } else { + progressBarConfig = [ + { value: usedPercentage, bsStyle: 'primary' }, + { value: committedPercentage - usedPercentage, bsStyle: 'warning' }, + ]; + + detail = ( +

    + The JVM is using{' '} + + {NumberUtils.formatBytes(usedMemory)} + {' '}of{' '} + + {NumberUtils.formatBytes(committedMemory)} + {' '}heap space and will not attempt to use more than{' '} + + {NumberUtils.formatBytes(maxMemory)} +

    + ); + } + } + + return ( + + + + {detail} + + ); +}; + +export default JvmHeapUsage; diff --git a/graylog2-web-interface/src/components/nodes/NodeOverview.tsx b/graylog2-web-interface/src/components/nodes/NodeOverview.tsx index 1a6ec051f07f..a1c7a636a6d3 100644 --- a/graylog2-web-interface/src/components/nodes/NodeOverview.tsx +++ b/graylog2-web-interface/src/components/nodes/NodeOverview.tsx @@ -33,6 +33,7 @@ import type { NodeInfo } from 'stores/nodes/NodesStore'; import type { Plugin } from 'stores/system/SystemPluginsStore'; import type { Input } from 'components/messageloaders/Types'; import type { InputDescription } from 'stores/inputs/InputTypesStore'; +import type { SystemOverview } from 'stores/cluster/types'; type InputState = { detailed_message:string, @@ -57,29 +58,13 @@ type JvmInformation = { info: string } -type ClusterOverview = { - facility: string - codename: string - node_id: string - cluster_id: string - version: string - started_at: string - hostname: string - lifecycle: string - lb_status: string - timezone: string - operating_system: string - is_processing: boolean - is_leader: boolean -} - type Props = { node: NodeInfo, plugins?: Array inputStates?: Array - inputDescriptions?: Array + inputDescriptions?: { [type: string]: InputDescription }, jvmInformation?: JvmInformation - systemOverview: ClusterOverview, + systemOverview: SystemOverview, } const NodeOverview = ({ node, plugins, inputStates, inputDescriptions, jvmInformation, systemOverview }: Props) => { diff --git a/graylog2-web-interface/src/components/nodes/NodesList.jsx b/graylog2-web-interface/src/components/nodes/NodesList.jsx deleted file mode 100644 index 34ffad6a363f..000000000000 --- a/graylog2-web-interface/src/components/nodes/NodesList.jsx +++ /dev/null @@ -1,73 +0,0 @@ -/* - * 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 PropTypes from 'prop-types'; -import React from 'react'; -import createReactClass from 'create-react-class'; -import Reflux from 'reflux'; - -import { Row, Col } from 'components/bootstrap'; -import { Spinner, EntityList, Pluralize } from 'components/common'; -import { ClusterOverviewStore } from 'stores/cluster/ClusterOverviewStore'; - -import NodeListItem from './NodeListItem'; - -const NodesList = createReactClass({ - displayName: 'NodesList', - - propTypes: { - permissions: PropTypes.array.isRequired, - nodes: PropTypes.object, - }, - - mixins: [Reflux.connect(ClusterOverviewStore)], - - _isLoading() { - const { nodes } = this.props; - const { clusterOverview } = this.state; - - return !(nodes && clusterOverview); - }, - - _formatNodes(nodes, clusterOverview) { - const nodeIDs = Object.keys(nodes); - - return nodeIDs.map((nodeID) => ); - }, - - render() { - if (this._isLoading()) { - return ; - } - - const nodesNo = Object.keys(this.props.nodes).length; - - return ( - - -

    - There {nodesNo} active -

    - - -
    - ); - }, -}); - -export default NodesList; diff --git a/graylog2-web-interface/src/components/nodes/NodesList.tsx b/graylog2-web-interface/src/components/nodes/NodesList.tsx new file mode 100644 index 000000000000..31004eedfc27 --- /dev/null +++ b/graylog2-web-interface/src/components/nodes/NodesList.tsx @@ -0,0 +1,64 @@ +/* + * 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 { useMemo } from 'react'; + +import { Row, Col } from 'components/bootstrap'; +import { Spinner, EntityList, Pluralize } from 'components/common'; +import { ClusterOverviewStore } from 'stores/cluster/ClusterOverviewStore'; +import { useStore } from 'stores/connect'; + +import NodeListItem from './NodeListItem'; + +type Props = { + nodes?: {}, +} + +const NodesList = ({ nodes }: Props) => { + const { clusterOverview } = useStore(ClusterOverviewStore); + + const _isLoading = !nodes || !clusterOverview; + + const _formattedNodes = useMemo(() => { + if (_isLoading) { + return []; + } + + return Object.keys(nodes).map((nodeID) => ); + }, [clusterOverview, nodes]); + + if (_isLoading) { + return ; + } + + const nodesNo = Object.keys(nodes).length; + + return ( + + +

    + There {nodesNo} active +

    + + +
    + ); +}; + +export default NodesList; diff --git a/graylog2-web-interface/src/components/nodes/index.js b/graylog2-web-interface/src/components/nodes/index.ts similarity index 100% rename from graylog2-web-interface/src/components/nodes/index.js rename to graylog2-web-interface/src/components/nodes/index.ts diff --git a/graylog2-web-interface/src/components/outputs/OutputsComponent.jsx b/graylog2-web-interface/src/components/outputs/OutputsComponent.jsx deleted file mode 100644 index 7d425988e26d..000000000000 --- a/graylog2-web-interface/src/components/outputs/OutputsComponent.jsx +++ /dev/null @@ -1,220 +0,0 @@ -/* - * 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 React from 'react'; -import PropTypes from 'prop-types'; -// eslint-disable-next-line no-restricted-imports -import createReactClass from 'create-react-class'; - -import { Row, Col } from 'components/bootstrap'; -import UserNotification from 'util/UserNotification'; -import PermissionsMixin from 'util/PermissionsMixin'; -import Spinner from 'components/common/Spinner'; -import StreamsStore from 'stores/streams/StreamsStore'; -import { OutputsStore } from 'stores/outputs/OutputsStore'; -import withTelemetry from 'logic/telemetry/withTelemetry'; -import { getPathnameWithoutId } from 'util/URLUtils'; -import { TELEMETRY_EVENT_TYPE } from 'logic/telemetry/Constants'; -import withLocation from 'routing/withLocation'; - -import OutputList from './OutputList'; -import CreateOutputDropdown from './CreateOutputDropdown'; -import AssignOutputDropdown from './AssignOutputDropdown'; - -const OutputsComponent = createReactClass({ - // eslint-disable-next-line react/no-unused-class-component-methods - displayName: 'OutputsComponent', - - // eslint-disable-next-line react/no-unused-class-component-methods - propTypes: { - streamId: PropTypes.string.isRequired, - permissions: PropTypes.array.isRequired, - sendTelemetry: PropTypes.func.isRequired, - location: PropTypes.object.isRequired, - }, - - mixins: [PermissionsMixin], - - getInitialState() { - return {}; - }, - - componentDidMount() { - this.loadData(); - }, - - loadData() { - const callback = (resp) => { - this.setState({ - outputs: resp.outputs, - }); - - if (this.props.streamId) { - this._fetchAssignableOutputs(resp.outputs); - } - }; - - if (this.props.streamId) { - OutputsStore.loadForStreamId(this.props.streamId, callback); - } else { - OutputsStore.load(callback); - } - - OutputsStore.loadAvailableTypes((resp) => { - this.setState({ types: resp.types }); - }); - }, - - _handleUpdate() { - this.loadData(); - }, - - _handleCreateOutput(data) { - this.props.sendTelemetry(TELEMETRY_EVENT_TYPE.OUTPUTS.OUTPUT_CREATED, { - app_pathname: getPathnameWithoutId(this.props.location.pathname), - app_action_value: 'create-output', - }); - - OutputsStore.save(data, (result) => { - if (this.props.streamId) { - StreamsStore.addOutput(this.props.streamId, result.id, (response) => { - this._handleUpdate(); - - return response; - }); - } else { - this._handleUpdate(); - } - - return result; - }); - }, - - _fetchAssignableOutputs(outputs) { - OutputsStore.load((resp) => { - const streamOutputIds = outputs.map((output) => output.id); - const assignableOutputs = resp.outputs - .filter((output) => streamOutputIds.indexOf(output.id) === -1) - .sort((output1, output2) => output1.title.localeCompare(output2.title)); - - this.setState({ assignableOutputs: assignableOutputs }); - }); - }, - - _handleAssignOutput(outputId) { - this.props.sendTelemetry(TELEMETRY_EVENT_TYPE.OUTPUTS.OUTPUT_ASSIGNED, { - app_pathname: getPathnameWithoutId(this.props.location.pathname), - app_action_value: 'assign-output', - }); - - StreamsStore.addOutput(this.props.streamId, outputId, (response) => { - this._handleUpdate(); - - return response; - }); - }, - - _removeOutputGlobally(outputId) { - this.props.sendTelemetry(TELEMETRY_EVENT_TYPE.OUTPUTS.OUTPUT_GLOBALLY_REMOVED, { - app_pathname: getPathnameWithoutId(this.props.location.pathname), - app_action_value: 'globally-remove-output', - }); - - // eslint-disable-next-line no-alert - if (window.confirm('Do you really want to terminate this output?')) { - OutputsStore.remove(outputId, (response) => { - UserNotification.success('Output was terminated.', 'Success'); - this._handleUpdate(); - - return response; - }); - } - }, - - _removeOutputFromStream(outputId, streamId) { - this.props.sendTelemetry(TELEMETRY_EVENT_TYPE.OUTPUTS.OUTPUT_FROM_STREAM_REMOVED, { - app_pathname: getPathnameWithoutId(this.props.location.pathname), - app_action_value: 'remove-output-from-stream', - }); - - // eslint-disable-next-line no-alert - if (window.confirm('Do you really want to remove this output from the stream?')) { - StreamsStore.removeOutput(streamId, outputId, (response) => { - UserNotification.success('Output was removed from stream.', 'Success'); - this._handleUpdate(); - - return response; - }); - } - }, - - _handleOutputUpdate(output, deltas) { - this.props.sendTelemetry(TELEMETRY_EVENT_TYPE.OUTPUTS.OUTPUT_UPDATED, { - app_pathname: getPathnameWithoutId(this.props.location.pathname), - app_action_value: 'output-update', - }); - - OutputsStore.update(output, deltas, () => { - this._handleUpdate(); - }); - }, - - render() { - if (this.state.outputs && this.state.types && (!this.props.streamId || this.state.assignableOutputs)) { - const { permissions } = this.props; - const { streamId } = this.props; - const createOutputDropdown = (this.isPermitted(permissions, ['outputs:create']) - ? ( - - ) : null); - const assignOutputDropdown = (streamId - ? ( - - ) : null); - - return ( -
    - - - {createOutputDropdown} - - - {assignOutputDropdown} - - - - -
    - ); - } - - return ; - }, -}); - -export default withLocation(withTelemetry(OutputsComponent)); diff --git a/graylog2-web-interface/src/components/outputs/OutputsComponent.tsx b/graylog2-web-interface/src/components/outputs/OutputsComponent.tsx new file mode 100644 index 000000000000..867b22b82c38 --- /dev/null +++ b/graylog2-web-interface/src/components/outputs/OutputsComponent.tsx @@ -0,0 +1,202 @@ +/* + * 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 { useEffect, useState } from 'react'; +import type * as Immutable from 'immutable'; + +import { Row, Col } from 'components/bootstrap'; +import UserNotification from 'util/UserNotification'; +import Spinner from 'components/common/Spinner'; +import StreamsStore from 'stores/streams/StreamsStore'; +import { OutputsStore } from 'stores/outputs/OutputsStore'; +import { getPathnameWithoutId } from 'util/URLUtils'; +import { TELEMETRY_EVENT_TYPE } from 'logic/telemetry/Constants'; +import useLocation from 'routing/useLocation'; +import useSendTelemetry from 'logic/telemetry/useSendTelemetry'; +import useOutputTypes from 'components/outputs/useOutputTypes'; +import { isPermitted } from 'util/PermissionsMixin'; + +import OutputList from './OutputList'; +import CreateOutputDropdown from './CreateOutputDropdown'; +import AssignOutputDropdown from './AssignOutputDropdown'; + +type Props = { + streamId?: string, + permissions: Immutable.List, +} + +const OutputsComponent = ({ streamId, permissions }: Props) => { + const location = useLocation(); + const sendTelemetry = useSendTelemetry(); + const { types } = useOutputTypes(); + const [outputs, setOutputs] = useState(); + const [assignableOutputs, setAssignableOutputs] = useState(); + + const _fetchAssignableOutputs = (_outputs) => { + OutputsStore.load((resp) => { + const streamOutputIds = _outputs.map((output) => output.id); + const _assignableOutputs = resp.outputs + .filter((output) => streamOutputIds.indexOf(output.id) === -1) + .sort((output1, output2) => output1.title.localeCompare(output2.title)); + + setAssignableOutputs(_assignableOutputs); + }); + }; + + const loadData = () => { + const callback = (resp) => { + setOutputs(resp.outputs); + + if (streamId) { + _fetchAssignableOutputs(resp.outputs); + } + }; + + if (streamId) { + OutputsStore.loadForStreamId(streamId, callback); + } else { + OutputsStore.load(callback); + } + }; + + useEffect(() => { + loadData(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const _handleUpdate = () => { + loadData(); + }; + + const _handleCreateOutput = (data) => { + sendTelemetry(TELEMETRY_EVENT_TYPE.OUTPUTS.OUTPUT_CREATED, { + app_pathname: getPathnameWithoutId(location.pathname), + app_action_value: 'create-output', + }); + + OutputsStore.save(data, (result) => { + if (streamId) { + StreamsStore.addOutput(streamId, result.id, (response) => { + _handleUpdate(); + + return response; + }); + } else { + _handleUpdate(); + } + + return result; + }); + }; + + const _handleAssignOutput = (outputId) => { + sendTelemetry(TELEMETRY_EVENT_TYPE.OUTPUTS.OUTPUT_ASSIGNED, { + app_pathname: getPathnameWithoutId(location.pathname), + app_action_value: 'assign-output', + }); + + StreamsStore.addOutput(streamId, outputId, (response) => { + _handleUpdate(); + + return response; + }); + }; + + const _removeOutputGlobally = (outputId) => { + sendTelemetry(TELEMETRY_EVENT_TYPE.OUTPUTS.OUTPUT_GLOBALLY_REMOVED, { + app_pathname: getPathnameWithoutId(location.pathname), + app_action_value: 'globally-remove-output', + }); + + // eslint-disable-next-line no-alert + if (window.confirm('Do you really want to terminate this output?')) { + OutputsStore.remove(outputId, (response) => { + UserNotification.success('Output was terminated.', 'Success'); + _handleUpdate(); + + return response; + }); + } + }; + + const _removeOutputFromStream = (outputId: string, _streamId: string) => { + sendTelemetry(TELEMETRY_EVENT_TYPE.OUTPUTS.OUTPUT_FROM_STREAM_REMOVED, { + app_pathname: getPathnameWithoutId(location.pathname), + app_action_value: 'remove-output-from-stream', + }); + + // eslint-disable-next-line no-alert + if (window.confirm('Do you really want to remove this output from the stream?')) { + StreamsStore.removeOutput(_streamId, outputId, (response) => { + UserNotification.success('Output was removed from stream.', 'Success'); + _handleUpdate(); + + return response; + }); + } + }; + + const _handleOutputUpdate = (output, deltas) => { + sendTelemetry(TELEMETRY_EVENT_TYPE.OUTPUTS.OUTPUT_UPDATED, { + app_pathname: getPathnameWithoutId(location.pathname), + app_action_value: 'output-update', + }); + + OutputsStore.update(output, deltas, () => { + _handleUpdate(); + }); + }; + + if (outputs && types && (!streamId || assignableOutputs)) { + const createOutputDropdown = (isPermitted(permissions, ['outputs:create']) + ? ( + + ) : null); + const assignOutputDropdown = (streamId + ? ( + + ) : null); + + return ( +
    + + + {createOutputDropdown} + + + {assignOutputDropdown} + + + + +
    + ); + } + + return ; +}; + +export default OutputsComponent; diff --git a/graylog2-web-interface/src/components/outputs/useOutputTypes.ts b/graylog2-web-interface/src/components/outputs/useOutputTypes.ts new file mode 100644 index 000000000000..bdc47afbf9ff --- /dev/null +++ b/graylog2-web-interface/src/components/outputs/useOutputTypes.ts @@ -0,0 +1,27 @@ +/* + * 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 { SystemOutputs } from '@graylog/server-api'; + +const useOutputTypes = () => { + const { data, isInitialLoading } = useQuery(['outputs', 'types'], () => SystemOutputs.available()); + + return { types: data?.types, isLoading: isInitialLoading }; +}; + +export default useOutputTypes; diff --git a/graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministration.tsx b/graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministration.tsx index 360ae5d48be5..b89f0b039ec9 100644 --- a/graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministration.tsx +++ b/graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministration.tsx @@ -119,7 +119,7 @@ type Props = { perPage: number; }, onPageChange: (currentPage: number, pageSize: number) => void, - onFilter: (collectorIds?: string[], callback?: () => void) => void, + onFilter: (name?: string, value?: string) => void, onQueryChange: (query?: string, callback?: () => void) => void, onConfigurationChange: (pairs: SidecarCollectorPairType[], configs: Configuration[], callback: () => void) => void, onProcessAction: (action: string, collectorDict: { [sidecarId: string]: string[] }, callback: () => void) => void, diff --git a/graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministrationContainer.jsx b/graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministrationContainer.jsx deleted file mode 100644 index 88fbf3e935ca..000000000000 --- a/graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministrationContainer.jsx +++ /dev/null @@ -1,179 +0,0 @@ -/* - * 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 React from 'react'; -// eslint-disable-next-line no-restricted-imports -import createReactClass from 'create-react-class'; -import PropTypes from 'prop-types'; -import Reflux from 'reflux'; -import cloneDeep from 'lodash/cloneDeep'; -import find from 'lodash/find'; -import isEmpty from 'lodash/isEmpty'; - -import { naturalSortIgnoreCase } from 'util/SortUtils'; -import { Spinner } from 'components/common'; -import withPaginationQueryParameter from 'components/common/withPaginationQueryParameter'; -import { CollectorConfigurationsActions, CollectorConfigurationsStore } from 'stores/sidecars/CollectorConfigurationsStore'; -import { CollectorsActions, CollectorsStore } from 'stores/sidecars/CollectorsStore'; -import { SidecarsActions } from 'stores/sidecars/SidecarsStore'; -import { SidecarsAdministrationActions, SidecarsAdministrationStore } from 'stores/sidecars/SidecarsAdministrationStore'; - -import CollectorsAdministration, { PAGE_SIZES } from './CollectorsAdministration'; - -const CollectorsAdministrationContainer = createReactClass({ - // eslint-disable-next-line react/no-unused-class-component-methods - propTypes: { - nodeId: PropTypes.string, - paginationQueryParameter: PropTypes.object.isRequired, - }, - - mixins: [Reflux.connect(CollectorsStore, 'collectors'), Reflux.connect(SidecarsAdministrationStore, 'sidecars'), Reflux.connect(CollectorConfigurationsStore, 'configurations')], - - getDefaultProps() { - return { - nodeId: undefined, - }; - }, - - componentDidMount() { - this.loadData(this.props.nodeId); - this.interval = setInterval(this.reloadSidecars, 5000); - }, - - componentDidUpdate(prevProps) { - if (prevProps.nodeId !== this.props.nodeId) { - // This means the user changed the URL, so we don't need to keep the previous state. - this.loadData(this.props.nodeId); - } - }, - - componentWillUnmount() { - if (this.interval) { - clearInterval(this.interval); - } - }, - - handlePageChange(page, pageSize) { - const { filters, query } = this.state.sidecars; - - SidecarsAdministrationActions.list({ query, filters, page, pageSize }); - }, - - handleFilter(property, value) { - const { resetPage, pageSize } = this.props.paginationQueryParameter; - const { filters, query } = this.state.sidecars; - let newFilters; - - if (property) { - newFilters = cloneDeep(filters); - newFilters[property] = value; - } else { - newFilters = {}; - } - - resetPage(); - - SidecarsAdministrationActions.list({ query, filters: newFilters, pageSize, page: 1 }); - }, - - handleQueryChange(query = '', callback = () => {}) { - const { resetPage, pageSize } = this.props.paginationQueryParameter; - const { filters } = this.state.sidecars; - - resetPage(); - - SidecarsAdministrationActions.list({ query, filters, pageSize, page: 1 }).finally(callback); - }, - - handleConfigurationChange(selectedSidecars, selectedConfigurations, doneCallback) { - SidecarsActions.assignConfigurations(selectedSidecars, selectedConfigurations).then((response) => { - doneCallback(); - const { query, filters } = this.state.sidecars; - const { page, pageSize } = this.props.paginationQueryParameter; - - SidecarsAdministrationActions.list({ query, filters, pageSize, page }); - - return response; - }); - }, - - handleProcessAction(action, selectedCollectors, doneCallback) { - SidecarsAdministrationActions.setAction(action, selectedCollectors).then((response) => { - doneCallback(); - - return response; - }); - }, - - reloadSidecars() { - if (this.state.sidecars) { - SidecarsAdministrationActions.refreshList(); - } - }, - - loadData(nodeId) { - const { page, pageSize } = this.props.paginationQueryParameter; - const query = nodeId ? `node_id:${nodeId}` : ''; - - CollectorsActions.all(); - SidecarsAdministrationActions.list({ query, page, pageSize }); - CollectorConfigurationsActions.all(); - }, - - render() { - const { collectors, configurations, sidecars } = this.state; - - if (!collectors || !collectors.collectors || !sidecars || !sidecars.sidecars || !configurations || !configurations.configurations) { - return ; - } - - const sidecarCollectors = []; - - sidecars.sidecars - .sort((s1, s2) => naturalSortIgnoreCase(s1.node_name, s2.node_name)) - .forEach((sidecar) => { - const compatibleCollectorIds = sidecar.collectors; - - if (isEmpty(compatibleCollectorIds)) { - sidecarCollectors.push({ collector: {}, sidecar: sidecar }); - - return; - } - - compatibleCollectorIds - .map((id) => find(collectors.collectors, { id: id })) - .forEach((compatibleCollector) => { - sidecarCollectors.push({ collector: compatibleCollector, sidecar: sidecar }); - }); - }); - - return ( - - ); - }, -}); - -export default withPaginationQueryParameter(CollectorsAdministrationContainer, { pageSizes: PAGE_SIZES }); diff --git a/graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministrationContainer.tsx b/graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministrationContainer.tsx new file mode 100644 index 000000000000..2cefa5f16bce --- /dev/null +++ b/graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministrationContainer.tsx @@ -0,0 +1,162 @@ +/* + * 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 { useEffect } from 'react'; +import cloneDeep from 'lodash/cloneDeep'; +import find from 'lodash/find'; +import isEmpty from 'lodash/isEmpty'; + +import { naturalSortIgnoreCase } from 'util/SortUtils'; +import { Spinner } from 'components/common'; +import withPaginationQueryParameter from 'components/common/withPaginationQueryParameter'; +import { CollectorConfigurationsActions, CollectorConfigurationsStore } from 'stores/sidecars/CollectorConfigurationsStore'; +import { CollectorsActions, CollectorsStore } from 'stores/sidecars/CollectorsStore'; +import { SidecarsActions } from 'stores/sidecars/SidecarsStore'; +import { SidecarsAdministrationActions, SidecarsAdministrationStore } from 'stores/sidecars/SidecarsAdministrationStore'; +import type { PaginationQueryParameterResult } from 'hooks/usePaginationQueryParameter'; +import { useStore } from 'stores/connect'; +import type { SidecarCollectorPairType, Configuration } from 'components/sidecars/types'; + +import CollectorsAdministration, { PAGE_SIZES } from './CollectorsAdministration'; + +type Props = { + nodeId?: string, + paginationQueryParameter: PaginationQueryParameterResult, +} + +const CollectorsAdministrationContainer = (props: Props) => { + const collectors = useStore(CollectorsStore); + const sidecars = useStore(SidecarsAdministrationStore); + const configurations = useStore(CollectorConfigurationsStore); + + const reloadSidecars = () => { + if (sidecars) { + SidecarsAdministrationActions.refreshList(); + } + }; + + const loadData = (nodeId: string) => { + const { page, pageSize } = props.paginationQueryParameter; + const query = nodeId ? `node_id:${nodeId}` : ''; + + CollectorsActions.all(); + SidecarsAdministrationActions.list({ query, page, pageSize }); + CollectorConfigurationsActions.all(); + }; + + useEffect(() => { + loadData(props.nodeId); + }, [props?.nodeId]); + + useEffect(() => { + const interval = setInterval(reloadSidecars, 5000); + + return () => clearInterval(interval); + }, []); + + const handlePageChange = (page: number, pageSize: number) => { + const { filters, query } = sidecars; + + SidecarsAdministrationActions.list({ query, filters, page, pageSize }); + }; + + const handleFilter = (property: string, value: string) => { + const { resetPage, pageSize } = props.paginationQueryParameter; + const { filters, query } = sidecars; + let newFilters; + + if (property) { + newFilters = cloneDeep(filters); + newFilters[property] = value; + } else { + newFilters = {}; + } + + resetPage(); + + SidecarsAdministrationActions.list({ query, filters: newFilters, pageSize, page: 1 }); + }; + + const handleQueryChange = (query = '', callback = () => {}) => { + const { resetPage, pageSize } = props.paginationQueryParameter; + const { filters } = sidecars; + + resetPage(); + + SidecarsAdministrationActions.list({ query, filters, pageSize, page: 1 }).finally(callback); + }; + + const handleConfigurationChange = (selectedSidecars: SidecarCollectorPairType[], selectedConfigurations: Configuration[], doneCallback: () => void) => { + SidecarsActions.assignConfigurations(selectedSidecars, selectedConfigurations).then((response) => { + doneCallback(); + const { query, filters } = sidecars; + const { page, pageSize } = props.paginationQueryParameter; + + SidecarsAdministrationActions.list({ query, filters, pageSize, page }); + + return response; + }); + }; + + const handleProcessAction = (action: string, selectedCollectors: { [sidecarId: string]: string[] }, doneCallback: () => void) => { + SidecarsAdministrationActions.setAction(action, selectedCollectors).then((response) => { + doneCallback(); + + return response; + }); + }; + + if (!collectors?.collectors || !sidecars?.sidecars || !configurations?.configurations) { + return ; + } + + const sidecarCollectors = []; + + sidecars.sidecars + .sort((s1, s2) => naturalSortIgnoreCase(s1.node_name, s2.node_name)) + .forEach((sidecar) => { + const compatibleCollectorIds = sidecar.collectors; + + if (isEmpty(compatibleCollectorIds)) { + sidecarCollectors.push({ collector: {}, sidecar: sidecar }); + + return; + } + + compatibleCollectorIds + .map((id) => find(collectors.collectors, { id: id })) + .forEach((compatibleCollector) => { + sidecarCollectors.push({ collector: compatibleCollector, sidecar: sidecar }); + }); + }); + + return ( + + ); +}; + +export default withPaginationQueryParameter(CollectorsAdministrationContainer, { pageSizes: PAGE_SIZES }); diff --git a/graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministrationFilters.jsx b/graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministrationFilters.tsx similarity index 72% rename from graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministrationFilters.jsx rename to graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministrationFilters.tsx index 31044468596c..a8aabc55d587 100644 --- a/graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministrationFilters.jsx +++ b/graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministrationFilters.tsx @@ -15,8 +15,6 @@ * . */ import React from 'react'; -import createReactClass from 'create-react-class'; -import PropTypes from 'prop-types'; import find from 'lodash/find'; import uniq from 'lodash/uniq'; import upperFirst from 'lodash/upperFirst'; @@ -27,42 +25,53 @@ import { naturalSortIgnoreCase } from 'util/SortUtils'; import CollectorIndicator from 'components/sidecars/common/CollectorIndicator'; import ColorLabel from 'components/sidecars/common/ColorLabel'; import SidecarStatusEnum from 'logic/sidecar/SidecarStatusEnum'; - -const CollectorsAdministrationFilters = createReactClass({ - propTypes: { - collectors: PropTypes.array.isRequired, - configurations: PropTypes.array.isRequired, - filters: PropTypes.object.isRequired, - filter: PropTypes.func.isRequired, +import type { Collector } from 'components/sidecars/types'; + +type Configuration = { + id: string, + name: string, + color: string, +} +type Props = { + collectors: Collector[], + configurations: Configuration[], + filters: { + collector?: string, + configuration?: string, + os?: string, + status?: string, }, + filter: (name?: string, value?: string) => void, +} - onFilterChange(name, value, callback) { - const { filter } = this.props; +const CollectorsAdministrationFilters = (props: Props) => { + const onFilterChange = (name: string, value: string, callback: () => void) => { + const { filter } = props; filter(name, value); callback(); - }, + }; - getCollectorsFilter() { - const { collectors, filters } = this.props; - const collectorMapper = (collector) => `${collector.id};${collector.name}`; + const getCollectorsFilter = () => { + const { collectors, filters } = props; + const collectorMapper = (collector: Collector) => `${collector.id};${collector.name}`; const collectorItems = collectors .sort((c1, c2) => naturalSortIgnoreCase(c1.name, c2.name)) // TODO: Hack to be able to filter in SelectPopover. We should change that to avoid this hack. .map(collectorMapper); - const collectorFormatter = (collectorId) => { + const collectorFormatter = (collectorId: string) => { const [id] = collectorId.split(';'); const collector = find(collectors, { id: id }); return ; }; - const filter = ([collectorId], callback) => { + const filter = ([collectorId]: Array, callback: () => void) => { const [id] = collectorId ? collectorId.split(';') : []; - this.onFilterChange('collector', id, callback); + onFilterChange('collector', id, callback); }; let collectorFilter; @@ -83,28 +92,28 @@ const CollectorsAdministrationFilters = createReactClass({ selectedItems={collectorFilter ? [collectorFilter] : []} filterPlaceholder="Filter by collector" /> ); - }, + }; - getConfigurationFilter() { - const { configurations, filters } = this.props; + const getConfigurationFilter = () => { + const { configurations, filters } = props; - const configurationMapper = (configuration) => `${configuration.id};${configuration.name}`; + const configurationMapper = (configuration: Configuration) => `${configuration.id};${configuration.name}`; const configurationItems = configurations .sort((c1, c2) => naturalSortIgnoreCase(c1.name, c2.name)) // TODO: Hack to be able to filter in SelectPopover. We should change that to avoid this hack. .map(configurationMapper); - const configurationFormatter = (configurationId) => { + const configurationFormatter = (configurationId: string) => { const [id] = configurationId.split(';'); const configuration = find(configurations, { id: id }); return {configuration.name}; }; - const filter = ([configurationId], callback) => { + const filter = ([configurationId]: Array, callback: () => void) => { const [id] = configurationId ? configurationId.split(';') : []; - this.onFilterChange('configuration', id, callback); + onFilterChange('configuration', id, callback); }; let configurationFilter; @@ -125,15 +134,15 @@ const CollectorsAdministrationFilters = createReactClass({ selectedItems={configurationFilter ? [configurationFilter] : []} filterPlaceholder="Filter by configuration" /> ); - }, + }; - getOSFilter() { - const { collectors, filters } = this.props; + const getOSFilter = () => { + const { collectors, filters } = props; const operatingSystems = uniq(collectors.map((collector) => upperFirst(collector.node_operating_system))) .sort(naturalSortIgnoreCase); - const filter = ([os], callback) => this.onFilterChange('os', os, callback); + const filter = ([os]: Array, callback: () => void) => onFilterChange('os', os, callback); const osFilter = filters.os; @@ -146,15 +155,15 @@ const CollectorsAdministrationFilters = createReactClass({ selectedItems={osFilter ? [osFilter] : []} filterPlaceholder="Filter by OS" /> ); - }, + }; - getStatusFilter() { - const { filters } = this.props; + const getStatusFilter = () => { + const { filters } = props; const status = Object.keys(SidecarStatusEnum.properties).map((key) => String(key)); - const filter = ([statusCode], callback) => this.onFilterChange('status', statusCode, callback); + const filter = ([statusCode]: Array, callback: () => void) => onFilterChange('status', statusCode, callback); const statusFilter = filters.status; - const statusFormatter = (statusCode) => upperFirst(SidecarStatusEnum.toString(statusCode)); + const statusFormatter = (statusCode: string) => upperFirst(SidecarStatusEnum.toString(statusCode)); return ( ); - }, - - render() { - return ( - - {this.getCollectorsFilter()} - {this.getConfigurationFilter()} - {this.getStatusFilter()} - {this.getOSFilter()} - - ); - }, -}); + }; + + return ( + + {getCollectorsFilter()} + {getConfigurationFilter()} + {getStatusFilter()} + {getOSFilter()} + + ); +}; export default CollectorsAdministrationFilters; diff --git a/graylog2-web-interface/src/components/sidecars/common/CollectorIndicator.jsx b/graylog2-web-interface/src/components/sidecars/common/CollectorIndicator.tsx similarity index 57% rename from graylog2-web-interface/src/components/sidecars/common/CollectorIndicator.jsx rename to graylog2-web-interface/src/components/sidecars/common/CollectorIndicator.tsx index 31a683e30d59..2f471df89b58 100644 --- a/graylog2-web-interface/src/components/sidecars/common/CollectorIndicator.jsx +++ b/graylog2-web-interface/src/components/sidecars/common/CollectorIndicator.tsx @@ -15,34 +15,20 @@ * . */ import React from 'react'; -import createReactClass from 'create-react-class'; -import PropTypes from 'prop-types'; import upperFirst from 'lodash/upperFirst'; import OperatingSystemIcon from './OperatingSystemIcon'; -const CollectorIndicator = createReactClass({ - propTypes: { - collector: PropTypes.string.isRequired, - operatingSystem: PropTypes.string, - }, +type Props = { + collector: React.ReactNode, + operatingSystem?: string, +} - getDefaultProps() { - return { - operatingSystem: undefined, - }; - }, - - render() { - const { collector, operatingSystem } = this.props; - - return ( - - {collector} - {operatingSystem && on {upperFirst(operatingSystem)}} - - ); - }, -}); +const CollectorIndicator = ({ collector, operatingSystem }: Props) => ( + + {collector} + {operatingSystem && on {upperFirst(operatingSystem)}} + +); export default CollectorIndicator; diff --git a/graylog2-web-interface/src/components/sidecars/configuration-forms/ConfigurationForm.tsx b/graylog2-web-interface/src/components/sidecars/configuration-forms/ConfigurationForm.tsx index fb40e50dc5ee..676b08eb4e4d 100644 --- a/graylog2-web-interface/src/components/sidecars/configuration-forms/ConfigurationForm.tsx +++ b/graylog2-web-interface/src/components/sidecars/configuration-forms/ConfigurationForm.tsx @@ -85,6 +85,7 @@ const ConfigurationForm = ({ const nextValidation = clone(validation); if (checkForRequiredFields && !_isTemplateSet(nextFormData.template)) { + // @ts-expect-error nextValidation.errors.template = ['Please fill out the configuration field.']; nextValidation.failed = true; } @@ -162,12 +163,12 @@ const ConfigurationForm = ({ _formDataUpdate('tags')(nextTagsArray); }; - const _getCollectorDefaultTemplate = (collectorId: string | number) => { + const _getCollectorDefaultTemplate = (collectorId: string) => { const storedTemplate = defaultTemplates.current[collectorId]; if (storedTemplate !== undefined) { // eslint-disable-next-line no-promise-executor-return - return new Promise((resolve) => resolve(storedTemplate)); + return new Promise((resolve) => resolve(storedTemplate)); } return CollectorsActions.getCollector(collectorId).then((collector) => { diff --git a/graylog2-web-interface/src/components/sidecars/configurations/CollectorList.tsx b/graylog2-web-interface/src/components/sidecars/configurations/CollectorList.tsx index 275bf486f33b..9dd0f927e4b2 100644 --- a/graylog2-web-interface/src/components/sidecars/configurations/CollectorList.tsx +++ b/graylog2-web-interface/src/components/sidecars/configurations/CollectorList.tsx @@ -38,7 +38,7 @@ type CollectorListProps = { onDelete: (...args: any[]) => void, onPageChange: (...args: any[]) => void, onQueryChange: (...args: any[]) => void, - validateCollector: (...args: any[]) => void, + validateCollector: (collector: Collector) => Promise<{ errors: { name: string[] } }>, } class CollectorList extends React.Component { diff --git a/graylog2-web-interface/src/components/sidecars/configurations/CollectorListContainer.tsx b/graylog2-web-interface/src/components/sidecars/configurations/CollectorListContainer.tsx index 8336f1d33f5b..ddc89a0c8774 100644 --- a/graylog2-web-interface/src/components/sidecars/configurations/CollectorListContainer.tsx +++ b/graylog2-web-interface/src/components/sidecars/configurations/CollectorListContainer.tsx @@ -21,10 +21,11 @@ import { Spinner } from 'components/common'; import connect from 'stores/connect'; import withTelemetry from 'logic/telemetry/withTelemetry'; import { TELEMETRY_EVENT_TYPE } from 'logic/telemetry/Constants'; +import type { Collector } from 'components/sidecars/types'; import CollectorList from './CollectorList'; -const validateCollector = (collector) => CollectorsActions.validate(collector); +const validateCollector = (collector: Collector) => CollectorsActions.validate(collector); const loadCollectors = () => { CollectorsActions.list({}); diff --git a/graylog2-web-interface/src/components/sidecars/configurations/CollectorRow.jsx b/graylog2-web-interface/src/components/sidecars/configurations/CollectorRow.jsx deleted file mode 100644 index def57e22211a..000000000000 --- a/graylog2-web-interface/src/components/sidecars/configurations/CollectorRow.jsx +++ /dev/null @@ -1,79 +0,0 @@ -/* - * 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 React from 'react'; -// eslint-disable-next-line no-restricted-imports -import createReactClass from 'create-react-class'; -import PropTypes from 'prop-types'; -import upperFirst from 'lodash/upperFirst'; - -import { LinkContainer } from 'components/common/router'; -import { ButtonToolbar, MenuItem, Button } from 'components/bootstrap'; -import Routes from 'routing/Routes'; -import OperatingSystemIcon from 'components/sidecars/common/OperatingSystemIcon'; -import { MoreActions } from 'components/common/EntityDataTable'; - -import CopyCollectorModal from './CopyCollectorModal'; - -const CollectorRow = createReactClass({ - // eslint-disable-next-line react/no-unused-class-component-methods - propTypes: { - collector: PropTypes.object.isRequired, - onClone: PropTypes.func.isRequired, - onDelete: PropTypes.func.isRequired, - validateCollector: PropTypes.func.isRequired, - }, - - handleDelete() { - const { onDelete, collector } = this.props; - - // eslint-disable-next-line no-alert - if (window.confirm(`You are about to delete collector "${collector.name}". Are you sure?`)) { - onDelete(collector); - } - }, - - render() { - const { collector, validateCollector, onClone } = this.props; - - return ( - - - {collector.name} - - - {upperFirst(collector.node_operating_system)} - - - - - - - - - - Delete - - - - - ); - }, -}); - -export default CollectorRow; diff --git a/graylog2-web-interface/src/components/sidecars/configurations/CollectorRow.tsx b/graylog2-web-interface/src/components/sidecars/configurations/CollectorRow.tsx new file mode 100644 index 000000000000..7366f568df73 --- /dev/null +++ b/graylog2-web-interface/src/components/sidecars/configurations/CollectorRow.tsx @@ -0,0 +1,71 @@ +/* + * 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 { useCallback } from 'react'; +import upperFirst from 'lodash/upperFirst'; + +import { LinkContainer } from 'components/common/router'; +import { ButtonToolbar, MenuItem, Button } from 'components/bootstrap'; +import Routes from 'routing/Routes'; +import OperatingSystemIcon from 'components/sidecars/common/OperatingSystemIcon'; +import { MoreActions } from 'components/common/EntityDataTable'; +import type { Collector } from 'components/sidecars/types'; + +import CopyCollectorModal from './CopyCollectorModal'; + +type Props = { + collector: Collector, + onClone: () => void, + onDelete: (collector: Collector) => void, + validateCollector: (collector: Collector) => Promise<{ errors: { name: string[] } }>, +} + +const CollectorRow = ({ collector, onClone, onDelete, validateCollector }: Props) => { + const handleDelete = useCallback(() => { + // eslint-disable-next-line no-alert + if (window.confirm(`You are about to delete collector "${collector.name}". Are you sure?`)) { + onDelete(collector); + } + }, [collector, onDelete]); + + return ( + + + {collector.name} + + + {upperFirst(collector.node_operating_system)} + + + + + + + + + + Delete + + + + + ); +}; + +export default CollectorRow; diff --git a/graylog2-web-interface/src/components/sidecars/sidecars/SidecarList.tsx b/graylog2-web-interface/src/components/sidecars/sidecars/SidecarList.tsx index e4a6735d29dd..08f8ea86662c 100644 --- a/graylog2-web-interface/src/components/sidecars/sidecars/SidecarList.tsx +++ b/graylog2-web-interface/src/components/sidecars/sidecars/SidecarList.tsx @@ -36,7 +36,7 @@ export const PAGE_SIZES = [10, 25, 50, 100]; type SidecarListProps = { sidecars: any[]; - onlyActive: boolean; + onlyActive: boolean | string; pagination: any; query: string; sort: any; @@ -49,7 +49,7 @@ type SidecarListProps = { class SidecarList extends React.Component { - formatSidecarList = (sidecars) => { + formatSidecarList = (sidecars: React.ReactElement[]) => { const { onSortChange, sort } = this.props; const sidecarCollection = { node_name: 'Name', diff --git a/graylog2-web-interface/src/components/sidecars/sidecars/SidecarListContainer.jsx b/graylog2-web-interface/src/components/sidecars/sidecars/SidecarListContainer.jsx deleted file mode 100644 index a4278d9bb7c4..000000000000 --- a/graylog2-web-interface/src/components/sidecars/sidecars/SidecarListContainer.jsx +++ /dev/null @@ -1,135 +0,0 @@ -/* - * 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 React from 'react'; -import PropTypes from 'prop-types'; -// eslint-disable-next-line no-restricted-imports -import createReactClass from 'create-react-class'; -import Reflux from 'reflux'; - -import { Spinner } from 'components/common'; -import withPaginationQueryParameter from 'components/common/withPaginationQueryParameter'; -import { SidecarsActions, SidecarsStore } from 'stores/sidecars/SidecarsStore'; - -import SidecarList, { PAGE_SIZES } from './SidecarList'; - -const SidecarListContainer = createReactClass({ - // eslint-disable-next-line react/no-unused-class-component-methods - propTypes: { - paginationQueryParameter: PropTypes.object.isRequired, - }, - - mixins: [Reflux.connect(SidecarsStore)], - - componentDidMount() { - this._reloadSidecars({}); - this.interval = setInterval(() => this._reloadSidecars({}), this.SIDECAR_DATA_REFRESH); - }, - - componentWillUnmount() { - if (this.interval) { - clearInterval(this.interval); - } - }, - - handleSortChange(field) { - return () => { - this._reloadSidecars({ - sortField: field, - // eslint-disable-next-line no-nested-ternary - order: (this.state.sort.field === field ? (this.state.sort.order === 'asc' ? 'desc' : 'asc') : 'asc'), - }); - }; - }, - - handlePageChange(page, pageSize) { - this._reloadSidecars({ page: page, pageSize: pageSize }); - }, - - handleQueryChange(query = '', callback = () => {}) { - const { resetPage } = this.props.paginationQueryParameter; - - resetPage(); - - this._reloadSidecars({ query: query }).finally(callback); - }, - - toggleShowInactive() { - const { resetPage } = this.props.paginationQueryParameter; - - resetPage(); - - this._reloadSidecars({ onlyActive: !this.state.onlyActive }); - }, - - _reloadSidecars({ query, page, pageSize, onlyActive, sortField, order }) { - const effectiveQuery = query === undefined ? this.state.query : query; - - const options = { - query: effectiveQuery, - onlyActive: 'true', - }; - - if (this.state.sort) { - options.sortField = sortField || this.state.sort.field; - options.order = order || this.state.sort.order; - } - - const { paginationQueryParameter } = this.props; - - options.pageSize = pageSize || paginationQueryParameter.pageSize; - options.onlyActive = onlyActive === undefined ? this.state.onlyActive : onlyActive; // Avoid || to handle false values - - const shouldKeepPage = options.pageSize === paginationQueryParameter.pageSize - && options.onlyActive === this.state.onlyActive - && options.query === this.state.query; // Only keep page number when other parameters don't change - let effectivePage = 1; - - if (shouldKeepPage) { - effectivePage = page || paginationQueryParameter.page; - } - - options.page = effectivePage; - - return SidecarsActions.listPaginated(options); - }, - - SIDECAR_DATA_REFRESH: 5 * 1000, - - render() { - const { sidecars, onlyActive, pagination, query, sort } = this.state; - - const isLoading = !sidecars; - - if (isLoading) { - return ; - } - - return ( - - ); - }, -}); - -export default withPaginationQueryParameter(SidecarListContainer, { pageSizes: PAGE_SIZES }); diff --git a/graylog2-web-interface/src/components/sidecars/sidecars/SidecarListContainer.tsx b/graylog2-web-interface/src/components/sidecars/sidecars/SidecarListContainer.tsx new file mode 100644 index 000000000000..80035840e0cd --- /dev/null +++ b/graylog2-web-interface/src/components/sidecars/sidecars/SidecarListContainer.tsx @@ -0,0 +1,123 @@ +/* + * 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 { useCallback, useEffect } from 'react'; + +import { Spinner } from 'components/common'; +import withPaginationQueryParameter from 'components/common/withPaginationQueryParameter'; +import type { PaginationOptions } from 'stores/sidecars/SidecarsStore'; +import { SidecarsActions, SidecarsStore } from 'stores/sidecars/SidecarsStore'; +import { useStore } from 'stores/connect'; +import type { PaginationQueryParameterResult } from 'hooks/usePaginationQueryParameter'; + +import SidecarList, { PAGE_SIZES } from './SidecarList'; + +type Props = { + paginationQueryParameter: PaginationQueryParameterResult, +} +const SIDECAR_DATA_REFRESH = 5 * 1000; + +const SidecarListContainer = ({ paginationQueryParameter }: Props) => { + const sidecars = useStore(SidecarsStore); + + const _reloadSidecars = useCallback(({ query, page, pageSize, onlyActive, sortField, order }: Partial = {}) => { + const effectiveQuery = query === undefined ? sidecars.query : query; + + const options: Partial = { + query: effectiveQuery, + onlyActive: 'true', + }; + + if (sidecars.sort) { + options.sortField = sortField || sidecars.sort.field; + options.order = order || sidecars.sort.order; + } + + options.pageSize = pageSize || paginationQueryParameter.pageSize; + options.onlyActive = onlyActive === undefined ? sidecars.onlyActive : onlyActive; // Avoid || to handle false values + + const shouldKeepPage = options.pageSize === paginationQueryParameter.pageSize + && options.onlyActive === sidecars.onlyActive + && options.query === sidecars.query; // Only keep page number when other parameters don't change + let effectivePage = 1; + + if (shouldKeepPage) { + effectivePage = page || paginationQueryParameter.page; + } + + options.page = effectivePage; + + return SidecarsActions.listPaginated(options); + }, [paginationQueryParameter?.page, paginationQueryParameter?.pageSize]); + + useEffect(() => { + _reloadSidecars(); + const interval = setInterval(() => _reloadSidecars({}), SIDECAR_DATA_REFRESH); + + return () => clearInterval(interval); + }, []); + + const handleSortChange = useCallback((field) => () => { + _reloadSidecars({ + sortField: field, + // eslint-disable-next-line no-nested-ternary + order: (sidecars.sort.field === field ? (sidecars.sort.order === 'asc' ? 'desc' : 'asc') : 'asc'), + }); + }, []); + + const handlePageChange = useCallback((page, pageSize) => { + _reloadSidecars({ page: page, pageSize: pageSize }); + }, []); + + const handleQueryChange = useCallback((query = '', callback = () => {}) => { + const { resetPage } = paginationQueryParameter; + + resetPage(); + + _reloadSidecars({ query: query }).finally(callback); + }, []); + + const toggleShowInactive = useCallback(() => { + const { resetPage } = paginationQueryParameter; + + resetPage(); + + _reloadSidecars({ onlyActive: !sidecars.onlyActive }); + }, []); + + const { sidecars: sidecarsList, onlyActive, pagination, query, sort } = sidecars; + + const isLoading = !sidecars; + + if (isLoading) { + return ; + } + + return ( + + ); +}; + +export default withPaginationQueryParameter(SidecarListContainer, { pageSizes: PAGE_SIZES }); diff --git a/graylog2-web-interface/src/components/sidecars/sidecars/SidecarStatus.jsx b/graylog2-web-interface/src/components/sidecars/sidecars/SidecarStatus.jsx deleted file mode 100644 index 4445df86ad62..000000000000 --- a/graylog2-web-interface/src/components/sidecars/sidecars/SidecarStatus.jsx +++ /dev/null @@ -1,191 +0,0 @@ -/* - * 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 React from 'react'; -// eslint-disable-next-line no-restricted-imports -import createReactClass from 'create-react-class'; -import PropTypes from 'prop-types'; -import defaultTo from 'lodash/defaultTo'; -import isNumber from 'lodash/isNumber'; - -import { Col, Row, Button } from 'components/bootstrap'; -import { Icon } from 'components/common'; -import SidecarStatusEnum from 'logic/sidecar/SidecarStatusEnum'; -import commonStyles from 'components/sidecars/common/CommonSidecarStyles.css'; - -import SidecarStatusFileList from './SidecarStatusFileList'; -import VerboseMessageModal from './VerboseMessageModal'; - -const SidecarStatus = createReactClass({ - // eslint-disable-next-line react/no-unused-class-component-methods - propTypes: { - sidecar: PropTypes.object.isRequired, - collectors: PropTypes.array.isRequired, - }, - - getInitialState() { - return { collectorName: '', collectorVerbose: '', showVerboseModal: false }; - }, - - // eslint-disable-next-line react/no-unstable-nested-components - formatNodeDetails(details) { - if (!details) { - return

    Node details are currently unavailable. Please wait a moment and ensure the sidecar is correctly connected to the server.

    ; - } - - const metrics = details.metrics || {}; - - return ( -
    -
    IP Address
    -
    {defaultTo(details.ip, 'Not available')}
    -
    Operating System
    -
    {defaultTo(details.operating_system, 'Not available')}
    -
    CPU Idle
    -
    {isNumber(metrics.cpu_idle) ? `${metrics.cpu_idle}%` : 'Not available'}
    -
    Load
    -
    {defaultTo(metrics.load_1, 'Not available')}
    -
    Volumes > 75% full
    - {metrics.disks_75 === undefined - ?
    Not available
    - :
    {metrics.disks_75.length > 0 ? metrics.disks_75.join(', ') : 'None'}
    } -
    - ); - }, - - // eslint-disable-next-line react/no-unstable-nested-components - formatCollectorStatus(details, collectors) { - if (!details || !collectors) { - return

    Collectors status are currently unavailable. Please wait a moment and ensure the sidecar is correctly connected to the server.

    ; - } - - if (!details.status) { - return

    Did not receive collectors status, set the option send_status: true in the sidecar configuration to see this information.

    ; - } - - const collectorStatuses = details.status.collectors; - - if (collectorStatuses.length === 0) { - return

    There are no collectors configured in this sidecar.

    ; - } - - const statuses = []; - - collectorStatuses.forEach((status) => { - const collector = collectors.find((c) => c.id === status.collector_id); - - let statusMessage; - let statusBadge; - let statusClass; - let verboseButton; - - switch (status.status) { - case SidecarStatusEnum.RUNNING: - statusMessage = 'Collector is running.'; - statusClass = 'text-success'; - statusBadge = ; - break; - case SidecarStatusEnum.FAILING: - statusMessage = status.message; - statusClass = 'text-danger'; - statusBadge = ; - - if (status.verbose_message) { - verboseButton = ( - - ); - } - - break; - case SidecarStatusEnum.STOPPED: - statusMessage = status.message; - statusClass = 'text-danger'; - statusBadge = ; - break; - default: - statusMessage = 'Collector status is currently unknown.'; - statusClass = 'text-info'; - statusBadge = ; - } - - if (collector) { - statuses.push( -
    {collector.name}
    , -
    {statusBadge} {statusMessage} {verboseButton}
    , - ); - } - }); - - return ( -
    - {statuses} -
    - ); - }, - - _onShowVerbose(name, verbose) { - this.setState({ collectorName: name, collectorVerbose: verbose, showVerboseModal: true }); - }, - - _onHideVerbose() { - this.setState({ showVerboseModal: false }); - }, - - render() { - const { sidecar } = this.props; - - const logFileList = sidecar.node_details.log_file_list || []; - - return ( -
    - - -

    Node details

    - {this.formatNodeDetails(sidecar.node_details)} - -
    - - -

    Collectors status

    -
    - {this.formatCollectorStatus(sidecar.node_details, this.props.collectors)} -
    - -
    - - -
    - ); - }, - -}); - -export default SidecarStatus; diff --git a/graylog2-web-interface/src/components/sidecars/sidecars/SidecarStatus.tsx b/graylog2-web-interface/src/components/sidecars/sidecars/SidecarStatus.tsx new file mode 100644 index 000000000000..77b43d3815f2 --- /dev/null +++ b/graylog2-web-interface/src/components/sidecars/sidecars/SidecarStatus.tsx @@ -0,0 +1,184 @@ +/* + * 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 { useState } from 'react'; +import defaultTo from 'lodash/defaultTo'; +import isNumber from 'lodash/isNumber'; + +import { Col, Row, Button } from 'components/bootstrap'; +import { Icon } from 'components/common'; +import SidecarStatusEnum from 'logic/sidecar/SidecarStatusEnum'; +import commonStyles from 'components/sidecars/common/CommonSidecarStyles.css'; +import type { SidecarSummary, Collector, NodeDetails } from 'components/sidecars/types'; + +import SidecarStatusFileList from './SidecarStatusFileList'; +import VerboseMessageModal from './VerboseMessageModal'; + +type Props = { + sidecar: SidecarSummary, + collectors: Array, +} + +const formatNodeDetails = (details: NodeDetails) => { + if (!details) { + return

    Node details are currently unavailable. Please wait a moment and ensure the sidecar is correctly connected to the server.

    ; + } + + const { metrics } = details; + + return ( +
    +
    IP Address
    +
    {defaultTo(details.ip, 'Not available')}
    +
    Operating System
    +
    {defaultTo(details.operating_system, 'Not available')}
    +
    CPU Idle
    +
    {isNumber(metrics?.cpu_idle) ? `${metrics?.cpu_idle}%` : 'Not available'}
    +
    Load
    +
    {defaultTo(metrics?.load_1, 'Not available')}
    +
    Volumes > 75% full
    + {metrics?.disks_75 === undefined + ?
    Not available
    + :
    {metrics?.disks_75.length > 0 ? metrics?.disks_75.join(', ') : 'None'}
    } +
    + ); +}; + +const formatCollectorStatus = (details: NodeDetails, collectors: Array, _onShowVerbose: (name: string, verbose: string) => void) => { + if (!details || !collectors) { + return

    Collectors status are currently unavailable. Please wait a moment and ensure the sidecar is correctly connected to the server.

    ; + } + + if (!details.status) { + return

    Did not receive collectors status, set the option send_status: true in the sidecar configuration to see this information.

    ; + } + + const collectorStatuses = details.status.collectors; + + if (collectorStatuses.length === 0) { + return

    There are no collectors configured in this sidecar.

    ; + } + + const statuses = []; + + collectorStatuses.forEach((status) => { + const collector = collectors.find((c) => c.id === status.collector_id); + + let statusMessage; + let statusBadge; + let statusClass; + let verboseButton; + + switch (status.status) { + case SidecarStatusEnum.RUNNING: + statusMessage = 'Collector is running.'; + statusClass = 'text-success'; + statusBadge = ; + break; + case SidecarStatusEnum.FAILING: + statusMessage = status.message; + statusClass = 'text-danger'; + statusBadge = ; + + if (status.verbose_message) { + verboseButton = ( + + ); + } + + break; + case SidecarStatusEnum.STOPPED: + statusMessage = status.message; + statusClass = 'text-danger'; + statusBadge = ; + break; + default: + statusMessage = 'Collector status is currently unknown.'; + statusClass = 'text-info'; + statusBadge = ; + } + + if (collector) { + statuses.push( +
    {collector.name}
    , +
    {statusBadge} {statusMessage} {verboseButton}
    , + ); + } + }); + + return ( +
    + {statuses} +
    + ); +}; + +const SidecarStatus = ({ sidecar, collectors }: Props) => { + const [showVerboseModal, setShowVerboseModal] = useState(false); + const [collectorName, setCollectorName] = useState(''); + const [collectorVerbose, setCollectorVerbose] = useState(''); + + const _onShowVerbose = (name: string, verbose: string) => { + setCollectorName(name); + setCollectorVerbose(verbose); + setShowVerboseModal(true); + }; + + const _onHideVerbose = () => { + setShowVerboseModal(false); + }; + + const logFileList = sidecar.node_details.log_file_list || []; + + return ( +
    + + +

    Node details

    + {formatNodeDetails(sidecar.node_details)} + +
    + + +

    Collectors status

    +
    + {formatCollectorStatus(sidecar.node_details, collectors, _onShowVerbose)} +
    + +
    + + +
    + ); +}; + +export default SidecarStatus; diff --git a/graylog2-web-interface/src/components/simulator/SimulationChanges.jsx b/graylog2-web-interface/src/components/simulator/SimulationChanges.tsx similarity index 63% rename from graylog2-web-interface/src/components/simulator/SimulationChanges.jsx rename to graylog2-web-interface/src/components/simulator/SimulationChanges.tsx index cb7a3f6109aa..3e395a5f04c5 100644 --- a/graylog2-web-interface/src/components/simulator/SimulationChanges.jsx +++ b/graylog2-web-interface/src/components/simulator/SimulationChanges.tsx @@ -14,11 +14,8 @@ * along with this program. If not, see * . */ -import PropTypes from 'prop-types'; -import React from 'react'; +import * as React from 'react'; import styled, { css } from 'styled-components'; -// eslint-disable-next-line no-restricted-imports -import createReactClass from 'create-react-class'; import { Col, Row } from 'components/bootstrap'; import { Pluralize } from 'components/common'; @@ -57,7 +54,7 @@ const OriginalChanges = styled.div` margin-top: 10px; `; -const FieldResultWrap = styled.div(({ resultType, theme }) => { +const FieldResultWrap = styled.div<{ resultType: string }>(({ resultType, theme }) => { const { success, danger, info } = theme.colors.variant.light; const types = { added: success, @@ -74,7 +71,7 @@ const FieldResultWrap = styled.div(({ resultType, theme }) => { `; }); -const FieldValue = styled.dd(({ removed, theme }) => css` +const FieldValue = styled.dd<{ removed: boolean }>(({ removed, theme }) => css` font-family: ${theme.fonts.family.monospace}; ${removed && css` @@ -83,27 +80,23 @@ const FieldValue = styled.dd(({ removed, theme }) => css` `} `); -const SimulationChanges = createReactClass({ - displayName: 'SimulationChanges', - - propTypes: { - originalMessage: PropTypes.object.isRequired, - simulationResults: PropTypes.object.isRequired, +type Props = { + originalMessage: { + id: string, }, - - _isOriginalMessageRemoved(originalMessage, processedMessages) { - return !processedMessages.find((message) => message.id === originalMessage.id); + simulationResults: { + messages: any[] }, +} - _formatFieldTitle(field) { - return
    {field}
    ; - }, +const SimulationChanges = (props: Props) => { + const _isOriginalMessageRemoved = (originalMessage, processedMessages) => !processedMessages.find((message) => message.id === originalMessage.id); - _formatFieldValue(field, value, isRemoved = false) { - return {String(value)}; - }, + const _formatFieldTitle = (field) =>
    {field}
    ; + + const _formatFieldValue = (field, value, isRemoved = false) => {String(value)}; - _formatAddedFields(addedFields) { + const _formatAddedFields = (addedFields) => { const keys = Object.keys(addedFields); if (keys.length === 0) { @@ -113,8 +106,8 @@ const SimulationChanges = createReactClass({ const formattedFields = []; keys.sort().forEach((field) => { - formattedFields.push(this._formatFieldTitle(field)); - formattedFields.push(this._formatFieldValue(field, addedFields[field])); + formattedFields.push(_formatFieldTitle(field)); + formattedFields.push(_formatFieldValue(field, addedFields[field])); }); return ( @@ -125,9 +118,9 @@ const SimulationChanges = createReactClass({ ); - }, + }; - _formatRemovedFields(removedFields) { + const _formatRemovedFields = (removedFields) => { const keys = Object.keys(removedFields); if (keys.length === 0) { @@ -137,8 +130,8 @@ const SimulationChanges = createReactClass({ const formattedFields = []; keys.sort().forEach((field) => { - formattedFields.push(this._formatFieldTitle(field)); - formattedFields.push(this._formatFieldValue(field, removedFields[field])); + formattedFields.push(_formatFieldTitle(field)); + formattedFields.push(_formatFieldValue(field, removedFields[field])); }); return ( @@ -149,9 +142,9 @@ const SimulationChanges = createReactClass({ ); - }, + }; - _formatMutatedFields(mutatedFields) { + const _formatMutatedFields = (mutatedFields) => { const keys = Object.keys(mutatedFields); if (keys.length === 0) { @@ -161,9 +154,9 @@ const SimulationChanges = createReactClass({ const formattedFields = []; keys.sort().forEach((field) => { - formattedFields.push(this._formatFieldTitle(field)); - formattedFields.push(this._formatFieldValue(`${field}-original`, mutatedFields[field].before, true)); - formattedFields.push(this._formatFieldValue(field, mutatedFields[field].after)); + formattedFields.push(_formatFieldTitle(field)); + formattedFields.push(_formatFieldValue(`${field}-original`, mutatedFields[field].before, true)); + formattedFields.push(_formatFieldValue(field, mutatedFields[field].after)); }); return ( @@ -174,21 +167,21 @@ const SimulationChanges = createReactClass({ ); - }, + }; - _getOriginalMessageChanges() { - const { originalMessage, simulationResults } = this.props; + const _getOriginalMessageChanges = () => { + const { originalMessage, simulationResults } = props; const processedMessages = simulationResults.messages; - if (this._isOriginalMessageRemoved(originalMessage, processedMessages)) { + if (_isOriginalMessageRemoved(originalMessage, processedMessages)) { return

    Original message would be dropped during processing.

    ; } const processedMessage = processedMessages.find((message) => message.id === originalMessage.id); - const formattedAddedFields = this._formatAddedFields(processedMessage.decoration_stats.added_fields); - const formattedRemovedFields = this._formatRemovedFields(processedMessage.decoration_stats.removed_fields); - const formattedMutatedFields = this._formatMutatedFields(processedMessage.decoration_stats.changed_fields); + const formattedAddedFields = _formatAddedFields(processedMessage.decoration_stats.added_fields); + const formattedRemovedFields = _formatRemovedFields(processedMessage.decoration_stats.removed_fields); + const formattedMutatedFields = _formatMutatedFields(processedMessage.decoration_stats.changed_fields); if (!formattedAddedFields && !formattedRemovedFields && !formattedMutatedFields) { return

    Original message would be not be modified during processing.

    ; @@ -201,10 +194,10 @@ const SimulationChanges = createReactClass({ {formattedMutatedFields} ); - }, + }; - _formatOriginalMessageChanges() { - const { originalMessage } = this.props; + const _formatOriginalMessageChanges = () => { + const { originalMessage } = props; return ( @@ -213,14 +206,14 @@ const SimulationChanges = createReactClass({ Changes in original message{' '} {originalMessage.id} - {this._getOriginalMessageChanges()} + {_getOriginalMessageChanges()} ); - }, + }; - _formatOtherChanges() { - const { originalMessage, simulationResults } = this.props; + const _formatOtherChanges = () => { + const { originalMessage, simulationResults } = props; const createdMessages = simulationResults.messages.filter((message) => message.id !== originalMessage.id); @@ -241,16 +234,14 @@ const SimulationChanges = createReactClass({ ); - }, + }; - render() { - return ( - - {this._formatOriginalMessageChanges()} - {this._formatOtherChanges()} - - ); - }, -}); + return ( + + {_formatOriginalMessageChanges()} + {_formatOtherChanges()} + + ); +}; export default SimulationChanges; diff --git a/graylog2-web-interface/src/components/simulator/SimulationTrace.jsx b/graylog2-web-interface/src/components/simulator/SimulationTrace.jsx deleted file mode 100644 index 1dad0d5ca53c..000000000000 --- a/graylog2-web-interface/src/components/simulator/SimulationTrace.jsx +++ /dev/null @@ -1,63 +0,0 @@ -/* - * 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 PropTypes from 'prop-types'; -import React from 'react'; -// eslint-disable-next-line no-restricted-imports -import createReactClass from 'create-react-class'; - -import NumberUtils from 'util/NumberUtils'; - -import style from './SimulationTrace.lazy.css'; - -const SimulationTrace = createReactClass({ - displayName: 'SimulationTrace', - - propTypes: { - simulationResults: PropTypes.object.isRequired, - }, - - componentDidMount() { - style.use(); - }, - - componentWillUnmount() { - style.unuse(); - }, - - render() { - const { simulationResults } = this.props; - - const simulationTrace = simulationResults.simulation_trace; - - const traceEntries = []; - - simulationTrace.forEach((trace, idx) => { - // eslint-disable-next-line react/no-array-index-key - traceEntries.push(
    {NumberUtils.formatNumber(trace.time)} μs
    ); - // eslint-disable-next-line react/no-array-index-key - traceEntries.push(
    {trace.message}
    ); - }); - - return ( -
    - {traceEntries} -
    - ); - }, -}); - -export default SimulationTrace; diff --git a/graylog2-web-interface/src/components/simulator/SimulationTrace.tsx b/graylog2-web-interface/src/components/simulator/SimulationTrace.tsx new file mode 100644 index 000000000000..2ce110007664 --- /dev/null +++ b/graylog2-web-interface/src/components/simulator/SimulationTrace.tsx @@ -0,0 +1,53 @@ +/* + * 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 { useEffect } from 'react'; + +import NumberUtils from 'util/NumberUtils'; + +import style from './SimulationTrace.lazy.css'; + +type Props = { + simulationResults: { simulation_trace: Array<{ time: string, message: string }> }, +} + +const SimulationTrace = ({ simulationResults }: Props) => { + useEffect(() => { + style.use(); + + return () => style.unuse(); + }, []); + + const simulationTrace = simulationResults.simulation_trace; + + const traceEntries = []; + + simulationTrace.forEach((trace, idx) => { + // eslint-disable-next-line react/no-array-index-key + traceEntries.push(
    {NumberUtils.formatNumber(trace.time)} μs
    ); + // eslint-disable-next-line react/no-array-index-key + traceEntries.push(
    {trace.message}
    ); + }); + + return ( +
    + {traceEntries} +
    + ); +}; + +export default SimulationTrace; diff --git a/graylog2-web-interface/src/components/throughput/NodeThroughput.jsx b/graylog2-web-interface/src/components/throughput/NodeThroughput.jsx deleted file mode 100644 index 8da749197ac3..000000000000 --- a/graylog2-web-interface/src/components/throughput/NodeThroughput.jsx +++ /dev/null @@ -1,96 +0,0 @@ -/* - * 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 PropTypes from 'prop-types'; -import React from 'react'; -import createReactClass from 'create-react-class'; -import Reflux from 'reflux'; -import numeral from 'numeral'; - -import { Spinner } from 'components/common'; -import MetricsExtractor from 'logic/metrics/MetricsExtractor'; -import { MetricsActions, MetricsStore } from 'stores/metrics/MetricsStore'; - -// TODO this is a copy of GlobalTroughput, it just renders differently and only targets a single node. -const NodeThroughput = createReactClass({ - displayName: 'NodeThroughput', - - propTypes: { - nodeId: PropTypes.string.isRequired, - longFormat: PropTypes.bool, - }, - - mixins: [Reflux.connect(MetricsStore)], - - getDefaultProps() { - return { - longFormat: false, - }; - }, - - UNSAFE_componentWillMount() { - this.metricNames = { - totalIn: 'org.graylog2.throughput.input.1-sec-rate', - totalOut: 'org.graylog2.throughput.output.1-sec-rate', - }; - - Object.keys(this.metricNames).forEach((metricShortName) => MetricsActions.add(this.props.nodeId, this.metricNames[metricShortName])); - }, - - componentWillUnmount() { - Object.keys(this.metricNames).forEach((metricShortName) => MetricsActions.remove(this.props.nodeId, this.metricNames[metricShortName])); - }, - - _isLoading() { - return !this.state.metrics; - }, - - _formatThroughput(metrics) { - if (this.props.longFormat) { - return ( - - Processing {numeral(metrics.totalIn).format('0,0')} incoming and - {numeral(metrics.totalOut).format('0,0')} - outgoing msg/s. - - ); - } - - return ( - - In {numeral(metrics.totalIn).format('0,0')} / Out {numeral(metrics.totalOut).format('0,0')} msg/s. - - ); - }, - - render() { - if (this._isLoading()) { - return ; - } - - const { nodeId } = this.props; - const nodeMetrics = this.state.metrics[nodeId]; - const metrics = MetricsExtractor.getValuesForNode(nodeMetrics, this.metricNames); - - if (Object.keys(metrics).length === 0) { - return (Unable to load throughput.); - } - - return this._formatThroughput(metrics); - }, -}); - -export default NodeThroughput; diff --git a/graylog2-web-interface/src/components/throughput/NodeThroughput.tsx b/graylog2-web-interface/src/components/throughput/NodeThroughput.tsx new file mode 100644 index 000000000000..4b8d6ec74f22 --- /dev/null +++ b/graylog2-web-interface/src/components/throughput/NodeThroughput.tsx @@ -0,0 +1,77 @@ +/* + * 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 { useEffect } from 'react'; +import numeral from 'numeral'; + +import { Spinner } from 'components/common'; +import MetricsExtractor from 'logic/metrics/MetricsExtractor'; +import { MetricsActions, MetricsStore } from 'stores/metrics/MetricsStore'; +import { useStore } from 'stores/connect'; + +type Props = { + nodeId: string, + longFormat?: boolean, +} +const metricNames = { + totalIn: 'org.graylog2.throughput.input.1-sec-rate', + totalOut: 'org.graylog2.throughput.output.1-sec-rate', +}; + +// TODO this is a copy of GlobalThroughput, it just renders differently and only targets a single node. +const NodeThroughput = ({ nodeId, longFormat = false }: Props) => { + const { metrics: _metrics } = useStore(MetricsStore); + + useEffect(() => { + Object.keys(metricNames).forEach((metricShortName) => MetricsActions.add(nodeId, metricNames[metricShortName])); + + return () => { + Object.keys(metricNames).forEach((metricShortName) => MetricsActions.remove(nodeId, metricNames[metricShortName])); + }; + }, [nodeId]); + + const _isLoading = !_metrics; + + if (_isLoading) { + return ; + } + + const nodeMetrics = _metrics[nodeId]; + const metrics = MetricsExtractor.getValuesForNode(nodeMetrics, metricNames); + + if (Object.keys(metrics).length === 0) { + return (Unable to load throughput.); + } + + if (longFormat) { + return ( + + Processing {numeral(metrics.totalIn).format('0,0')} incoming and + {numeral(metrics.totalOut).format('0,0')} + outgoing msg/s. + + ); + } + + return ( + + In {numeral(metrics.totalIn).format('0,0')} / Out {numeral(metrics.totalOut).format('0,0')} msg/s. + + ); +}; + +export default NodeThroughput; diff --git a/graylog2-web-interface/src/components/times/TimesList.jsx b/graylog2-web-interface/src/components/times/TimesList.jsx deleted file mode 100644 index 0a52ec0414fe..000000000000 --- a/graylog2-web-interface/src/components/times/TimesList.jsx +++ /dev/null @@ -1,79 +0,0 @@ -/* - * 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 React from 'react'; -// eslint-disable-next-line no-restricted-imports -import createReactClass from 'create-react-class'; -import Reflux from 'reflux'; -import moment from 'moment'; - -import { Col, Row } from 'components/bootstrap'; -import { Spinner, Timestamp, BrowserTime } from 'components/common'; -import { CurrentUserStore } from 'stores/users/CurrentUserStore'; -import { SystemStore } from 'stores/system/SystemStore'; - -const TimesList = createReactClass({ - // eslint-disable-next-line react/no-unused-class-component-methods - displayName: 'TimesList', - mixins: [Reflux.connect(CurrentUserStore), Reflux.connect(SystemStore)], - - getInitialState() { - return { time: moment() }; - }, - - componentDidMount() { - this.interval = setInterval(() => this.setState(this.getInitialState()), 1000); - }, - - componentWillUnmount() { - clearInterval(this.interval); - }, - - render() { - if (!this.state.system) { - return ; - } - - const { time } = this.state; - const timeFormat = 'withTz'; - const { currentUser } = this.state; - const serverTimezone = this.state.system.timezone; - - return ( - - -

    Time configuration

    - -

    - Dealing with timezones can be confusing. Here you can see the timezone applied to different components of your system. - You can check timezone settings of specific graylog-server nodes on their respective detail page. -

    - -
    -
    User {currentUser.username}:
    -
    -
    Your web browser:
    -
    -
    Graylog server:
    -
    -
    - -
    - ); - }, -}); - -export default TimesList; diff --git a/graylog2-web-interface/src/components/times/TimesList.tsx b/graylog2-web-interface/src/components/times/TimesList.tsx new file mode 100644 index 000000000000..833ca3f9f465 --- /dev/null +++ b/graylog2-web-interface/src/components/times/TimesList.tsx @@ -0,0 +1,68 @@ +/* + * 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 React from 'react'; +import { useEffect, useState } from 'react'; +import moment from 'moment'; + +import { Col, Row } from 'components/bootstrap'; +import { Spinner, Timestamp, BrowserTime } from 'components/common'; +import { SystemStore } from 'stores/system/SystemStore'; +import useCurrentUser from 'hooks/useCurrentUser'; +import { useStore } from 'stores/connect'; + +const TimesList = () => { + const [time, setTime] = useState(moment()); + const currentUser = useCurrentUser(); + const { system } = useStore(SystemStore); + + useEffect(() => { + const interval = setInterval(() => setTime(moment()), 1000); + + return () => clearInterval(interval); + }, []); + + if (!system) { + return ; + } + + const timeFormat = 'withTz'; + const serverTimezone = system.timezone; + + return ( + + +

    Time configuration

    + +

    + Dealing with timezones can be confusing. Here you can see the timezone applied to different components of your system. + You can check timezone settings of specific graylog-server nodes on their respective detail page. +

    + +
    +
    User {currentUser.username}:
    +
    +
    Your web browser:
    +
    +
    Graylog server:
    +
    +
    + +
    + ); +}; + +export default TimesList; diff --git a/graylog2-web-interface/src/hooks/usePluginList.ts b/graylog2-web-interface/src/hooks/usePluginList.ts new file mode 100644 index 000000000000..5df46da52c62 --- /dev/null +++ b/graylog2-web-interface/src/hooks/usePluginList.ts @@ -0,0 +1,27 @@ +/* + * 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 { ClusterPlugins } from '@graylog/server-api'; + +const usePluginList = (nodeId: string) => { + const { data, isInitialLoading } = useQuery(['plugins', 'list', nodeId], () => ClusterPlugins.list(nodeId)); + + return { pluginList: data, isLoading: isInitialLoading }; +}; + +export default usePluginList; diff --git a/graylog2-web-interface/src/logic/content-packs/ContentPackRevisions.js b/graylog2-web-interface/src/logic/content-packs/ContentPackRevisions.ts similarity index 98% rename from graylog2-web-interface/src/logic/content-packs/ContentPackRevisions.js rename to graylog2-web-interface/src/logic/content-packs/ContentPackRevisions.ts index 85ce111c870c..e85da46c44f4 100644 --- a/graylog2-web-interface/src/logic/content-packs/ContentPackRevisions.js +++ b/graylog2-web-interface/src/logic/content-packs/ContentPackRevisions.ts @@ -19,6 +19,8 @@ import max from 'lodash/max'; import ContentPack from 'logic/content-packs/ContentPack'; export default class ContentPackRevisions { + private _value = {}; + constructor(contentPackRevision) { this._value = Object.keys(contentPackRevision).reduce((acc, rev) => { const contentPack = contentPackRevision[rev]; diff --git a/graylog2-web-interface/src/logic/content-packs/EntityIndex.js b/graylog2-web-interface/src/logic/content-packs/EntityIndex.ts similarity index 94% rename from graylog2-web-interface/src/logic/content-packs/EntityIndex.js rename to graylog2-web-interface/src/logic/content-packs/EntityIndex.ts index 22039ec368d4..f0866de4996a 100644 --- a/graylog2-web-interface/src/logic/content-packs/EntityIndex.js +++ b/graylog2-web-interface/src/logic/content-packs/EntityIndex.ts @@ -17,6 +17,12 @@ import Immutable from 'immutable'; export default class EntityIndex { + private _value: { + id: string, + title: string, + type: string, + }; + constructor(id, title, type) { this._value = { id, title, type }; } @@ -36,7 +42,6 @@ export default class EntityIndex { toBuilder() { const { id, title, type } = this._value; - // eslint-disable-next-line no-use-before-define return new Builder(Immutable.Map({ id, title, type })); } @@ -75,6 +80,8 @@ export default class EntityIndex { } class Builder { + private readonly value: Immutable.Map; + constructor(value = Immutable.Map()) { this.value = value; } diff --git a/graylog2-web-interface/src/pages/ContentPacksPage.tsx b/graylog2-web-interface/src/pages/ContentPacksPage.tsx index 383611e8c84b..8324cf9c33f5 100644 --- a/graylog2-web-interface/src/pages/ContentPacksPage.tsx +++ b/graylog2-web-interface/src/pages/ContentPacksPage.tsx @@ -27,7 +27,6 @@ import ContentPacksList from 'components/content-packs/ContentPacksList'; import ContentPackUploadControls from 'components/content-packs/ContentPackUploadControls'; import { ContentPacksActions, ContentPacksStore } from 'stores/content-packs/ContentPacksStore'; import { useStore } from 'stores/connect'; -import type { ContentPackInstallation, ContentPackMetadata } from 'components/content-packs/Types'; const ConfigurationBundles = styled.div(({ theme }) => css` font-size: ${theme.fonts.size.body}; @@ -65,7 +64,7 @@ const _installContentPack = (contentPackId: string, contentPackRev: string, para }; const ContentPacksPage = () => { - const { contentPacks, contentPackMetadata } = useStore<{ contentPacks: Array, contentPackMetadata: ContentPackMetadata }>(ContentPacksStore); + const { contentPacks, contentPackMetadata } = useStore(ContentPacksStore); useEffect(() => { ContentPacksActions.list(); diff --git a/graylog2-web-interface/src/pages/CreateContentPackPage.tsx b/graylog2-web-interface/src/pages/CreateContentPackPage.tsx index 906a6312e887..6e2393aa24c8 100644 --- a/graylog2-web-interface/src/pages/CreateContentPackPage.tsx +++ b/graylog2-web-interface/src/pages/CreateContentPackPage.tsx @@ -31,7 +31,7 @@ import { useStore } from 'stores/connect'; const CreateContentPackPage = () => { const history = useHistory(); - const { entityIndex } = useStore<{ entityIndex: string | undefined }>(CatalogStore); + const { entityIndex } = useStore(CatalogStore); const [contentPackState, setContentPackState] = useState({ contentPack: ContentPack.builder().build(), appliedParameter: {}, @@ -76,7 +76,7 @@ const CreateContentPackPage = () => { ); }; - const _getEntities = (selectedEntities: unknown) => { + const _getEntities = (selectedEntities) => { const { contentPack } = contentPackState; CatalogActions.getSelectedEntities(selectedEntities).then((result) => { diff --git a/graylog2-web-interface/src/pages/EditContentPackPage.jsx b/graylog2-web-interface/src/pages/EditContentPackPage.jsx deleted file mode 100644 index 2a5d0e6faaba..000000000000 --- a/graylog2-web-interface/src/pages/EditContentPackPage.jsx +++ /dev/null @@ -1,225 +0,0 @@ -/* - * 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 React from 'react'; -import PropTypes from 'prop-types'; -import Reflux from 'reflux'; -// eslint-disable-next-line no-restricted-imports -import createReactClass from 'create-react-class'; -import cloneDeep from 'lodash/cloneDeep'; -import groupBy from 'lodash/groupBy'; - -import withHistory from 'routing/withHistory'; -import { LinkContainer } from 'components/common/router'; -import Routes from 'routing/Routes'; -import { Button } from 'components/bootstrap'; -import UserNotification from 'util/UserNotification'; -import { DocumentTitle, PageHeader } from 'components/common'; -import ValueReferenceData from 'util/ValueReferenceData'; -import ContentPackEdit from 'components/content-packs/ContentPackEdit'; -import Entity from 'logic/content-packs/Entity'; -import withParams from 'routing/withParams'; -import { CatalogActions, CatalogStore } from 'stores/content-packs/CatalogStore'; -import { ContentPacksActions, ContentPacksStore } from 'stores/content-packs/ContentPacksStore'; - -const EditContentPackPage = createReactClass({ - // eslint-disable-next-line react/no-unused-class-component-methods - displayName: 'EditContentPackPage', - - // eslint-disable-next-line react/no-unused-class-component-methods - propTypes: { - history: PropTypes.object.isRequired, - params: PropTypes.object.isRequired, - }, - - mixins: [Reflux.connect(CatalogStore), Reflux.connect(ContentPacksStore)], - - getInitialState() { - return { - selectedEntities: {}, - contentPackEntities: undefined, - appliedParameter: {}, - entityCatalog: {}, - }; - }, - - componentDidMount() { - const { params } = this.props; - - ContentPacksActions.get(params.contentPackId).then((result) => { - const { contentPackRevisions } = result; - const originContentPackRev = params.contentPackRev; - const newContentPack = contentPackRevisions.createNewVersionFromRev(originContentPackRev); - - this.setState({ contentPack: newContentPack, contentPackEntities: cloneDeep(newContentPack.entities) }); - - CatalogActions.showEntityIndex().then(() => { - this._createEntityCatalog(); - this._getSelectedEntities(); - this._getAppliedParameter(); - }); - }); - }, - - _createEntityCatalog() { - const { contentPack, contentPackEntities, entityIndex } = this.state; - - if (!contentPack || !entityIndex) { - return; - } - - const groupedContentPackEntities = groupBy(contentPackEntities, 'type.name'); - const entityCatalog = Object.keys(entityIndex) - .reduce((result, entityType) => { - /* eslint-disable-next-line no-param-reassign */ - result[entityType] = entityIndex[entityType].concat(groupedContentPackEntities[entityType] || []); - - return result; - }, {}); - - this.setState({ entityCatalog }); - }, - - _getSelectedEntities() { - const { contentPack, entityCatalog, entityIndex } = this.state; - - if (!contentPack || !entityIndex) { - return; - } - - const selectedEntities = contentPack.entities.reduce((result, entity) => { - if (entityCatalog[entity.type.name] - && entityCatalog[entity.type.name].findIndex((fetchedEntity) => fetchedEntity.id === entity.id) >= 0) { - const newResult = result; - - newResult[entity.type.name] = result[entity.type.name] || []; - newResult[entity.type.name].push(entity); - - return newResult; - } - - return result; - }, {}); - - this.setState({ selectedEntities: selectedEntities }); - }, - - _getAppliedParameter() { - const { contentPack } = this.state; - - const appliedParameter = contentPack.entities.reduce((result, entity) => { - const entityData = new ValueReferenceData(entity.data); - const configPaths = entityData.getPaths(); - - const paramMap = Object.keys(configPaths).filter((path) => configPaths[path].isValueParameter()).map((path) => ({ configKey: path, paramName: configPaths[path].getValue(), readOnly: true })); - const newResult = result; - - if (paramMap.length > 0) { - newResult[entity.id] = paramMap; - } - - return newResult; - }, {}); - - this.setState({ appliedParameter: appliedParameter }); - }, - - _onStateChanged(newState) { - const { contentPack, selectedEntities, appliedParameter } = this.state; - - this.setState({ - contentPack: newState.contentPack || contentPack, - selectedEntities: newState.selectedEntities || selectedEntities, - appliedParameter: newState.appliedParameter || appliedParameter, - }); - }, - - _onSave() { - const { contentPack } = this.state; - const { history } = this.props; - - ContentPacksActions.create.triggerPromise(contentPack.toJSON()) - .then( - () => { - UserNotification.success('Content pack imported successfully', 'Success!'); - history.push(Routes.SYSTEM.CONTENTPACKS.LIST); - }, - (response) => { - const message = 'Error importing content pack, please ensure it is a valid JSON file. Check your ' - + 'Graylog logs for more information.'; - const title = 'Could not import content pack'; - let smallMessage = ''; - - if (response.additional && response.additional.body && response.additional.body.message) { - smallMessage = `
    ${response.additional.body.message}`; - } - - UserNotification.error(message + smallMessage, title); - }, - ); - }, - - _getEntities(selectedEntities) { - const { contentPack } = this.state; - - CatalogActions.getSelectedEntities(selectedEntities).then((result) => { - const contentPackEntities = Object.keys(selectedEntities) - .reduce((acc, entityType) => acc.concat(selectedEntities[entityType]), []).filter((e) => e instanceof Entity); - /* Mark entities from server */ - const entities = contentPackEntities.concat(result.entities.map((e) => Entity.fromJSON(e, true))); - const builtContentPack = contentPack.toBuilder() - .entities(entities) - .build(); - - this.setState({ contentPack: builtContentPack, fetchedEntities: builtContentPack.entities }); - }); - }, - - render() { - const { contentPack, fetchedEntities, selectedEntities, entityCatalog, appliedParameter } = this.state; - - return ( - - - - - - )}> - - Content packs accelerate the set up process for a specific data source. A content pack can include inputs/extractors, streams, and dashboards. -
    - Find more content packs in {' '} - the Graylog Marketplace. -
    -
    - -
    -
    - ); - }, -}); - -export default withHistory(withParams(EditContentPackPage)); diff --git a/graylog2-web-interface/src/pages/EditContentPackPage.tsx b/graylog2-web-interface/src/pages/EditContentPackPage.tsx new file mode 100644 index 000000000000..b05a881088d9 --- /dev/null +++ b/graylog2-web-interface/src/pages/EditContentPackPage.tsx @@ -0,0 +1,194 @@ +/* + * 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 { useEffect, useMemo, useState } from 'react'; +import cloneDeep from 'lodash/cloneDeep'; +import groupBy from 'lodash/groupBy'; + +import { LinkContainer } from 'components/common/router'; +import Routes from 'routing/Routes'; +import { Button } from 'components/bootstrap'; +import UserNotification from 'util/UserNotification'; +import { DocumentTitle, PageHeader } from 'components/common'; +import ValueReferenceData from 'util/ValueReferenceData'; +import ContentPackEdit from 'components/content-packs/ContentPackEdit'; +import Entity from 'logic/content-packs/Entity'; +import { CatalogActions, CatalogStore } from 'stores/content-packs/CatalogStore'; +import { ContentPacksActions, ContentPacksStore } from 'stores/content-packs/ContentPacksStore'; +import useParams from 'routing/useParams'; +import useHistory from 'routing/useHistory'; +import { useStore } from 'stores/connect'; + +const EditContentPackPage = () => { + const { entityIndex } = useStore(CatalogStore); + const {} = useStore(ContentPacksStore); + const { contentPackId, contentPackRev } = useParams<{ contentPackId: string, contentPackRev: string }>(); + const history = useHistory(); + const [selectedEntities, setSelectedEntities] = useState({}); + const [appliedParameter, setAppliedParameter] = useState({}); + const [contentPack, setContentPack] = useState(undefined); + const [contentPackEntities, setContentPackEntities] = useState(undefined); + const [fetchedEntities, setFetchedEntities] = useState(undefined); + + useEffect(() => { + ContentPacksActions.get(contentPackId).then((result) => { + const { contentPackRevisions } = result; + const originContentPackRev = contentPackRev; + const newContentPack = contentPackRevisions.createNewVersionFromRev(originContentPackRev); + + setContentPack(newContentPack); + setContentPackEntities(cloneDeep(newContentPack.entities)); + + CatalogActions.showEntityIndex(); + }); + }, []); + + const entityCatalog = useMemo(() => { + if (!contentPack || !entityIndex) { + return {}; + } + + const groupedContentPackEntities = groupBy(contentPackEntities, 'type.name'); + const newEntityCatalog = Object.keys(entityIndex) + .reduce((result, entityType) => { + /* eslint-disable-next-line no-param-reassign */ + result[entityType] = entityIndex[entityType].concat(groupedContentPackEntities[entityType] || []); + + return result; + }, {}); + + return newEntityCatalog; + }, [contentPack, entityIndex, contentPackEntities]); + + useEffect(() => { + if (!contentPack || !entityIndex) { + return; + } + + const newSelectedEntities = contentPack.entities.reduce((result, entity) => { + if (entityCatalog[entity.type.name] + && entityCatalog[entity.type.name].findIndex((fetchedEntity) => fetchedEntity.id === entity.id) >= 0) { + const newResult = result; + + newResult[entity.type.name] = result[entity.type.name] || []; + newResult[entity.type.name].push(entity); + + return newResult; + } + + return result; + }, {}); + + setSelectedEntities(newSelectedEntities); + }, [contentPack, entityIndex]); + + useEffect(() => { + if (!contentPack) { + return; + } + + const newAppliedParameter = contentPack.entities.reduce((result, entity) => { + const entityData = new ValueReferenceData(entity.data); + const configPaths = entityData.getPaths(); + + const paramMap = Object.keys(configPaths).filter((path) => configPaths[path].isValueParameter()).map((path) => ({ configKey: path, paramName: configPaths[path].getValue(), readOnly: true })); + const newResult = result; + + if (paramMap.length > 0) { + newResult[entity.id] = paramMap; + } + + return newResult; + }, {}); + + setAppliedParameter(newAppliedParameter); + }, [contentPack]); + + const _onStateChanged = (newState) => { + setContentPack(newState.contentPack || contentPack); + setSelectedEntities(newState.selectedEntities || selectedEntities); + setAppliedParameter(newState.appliedParameter || appliedParameter); + }; + + const _onSave = () => { + ContentPacksActions.create(contentPack.toJSON()) + .then( + () => { + UserNotification.success('Content pack imported successfully', 'Success!'); + history.push(Routes.SYSTEM.CONTENTPACKS.LIST); + }, + (response) => { + const message = 'Error importing content pack, please ensure it is a valid JSON file. Check your ' + + 'Graylog logs for more information.'; + const title = 'Could not import content pack'; + let smallMessage = ''; + + if (response.additional && response.additional.body && response.additional.body.message) { + smallMessage = `
    ${response.additional.body.message}`; + } + + UserNotification.error(message + smallMessage, title); + }, + ); + }; + + const _getEntities = (selectedEntities) => { + CatalogActions.getSelectedEntities(selectedEntities).then((result) => { + const contentPackEntities = Object.keys(selectedEntities) + .reduce((acc, entityType) => acc.concat(selectedEntities[entityType]), []).filter((e) => e instanceof Entity); + /* Mark entities from server */ + const entities = contentPackEntities.concat(result.entities.map((e) => Entity.fromJSON(e, true))); + const builtContentPack = contentPack.toBuilder() + .entities(entities) + .build(); + + setContentPack(builtContentPack); + setFetchedEntities(builtContentPack.entities); + }); + }; + + return ( + + + + + + )}> + + Content packs accelerate the set up process for a specific data source. A content pack can include inputs/extractors, streams, and dashboards. +
    + Find more content packs in {' '} + the Graylog Marketplace. +
    +
    + +
    +
    + ); +}; + +export default EditContentPackPage; diff --git a/graylog2-web-interface/src/pages/EditExtractorsPage.jsx b/graylog2-web-interface/src/pages/EditExtractorsPage.jsx deleted file mode 100644 index a431c990a4d1..000000000000 --- a/graylog2-web-interface/src/pages/EditExtractorsPage.jsx +++ /dev/null @@ -1,113 +0,0 @@ -/* - * 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 PropTypes from 'prop-types'; -import React from 'react'; -// eslint-disable-next-line no-restricted-imports -import createReactClass from 'create-react-class'; -import Reflux from 'reflux'; - -import { DocumentTitle, PageHeader, Spinner } from 'components/common'; -import EditExtractor from 'components/extractors/EditExtractor'; -import DocsHelper from 'util/DocsHelper'; -import Routes from 'routing/Routes'; -import withParams from 'routing/withParams'; -import { ExtractorsActions, ExtractorsStore } from 'stores/extractors/ExtractorsStore'; -import { InputsActions, InputsStore } from 'stores/inputs/InputsStore'; -import { UniversalSearchStore } from 'stores/search/UniversalSearchStore'; -import withHistory from 'routing/withHistory'; - -const EditExtractorsPage = createReactClass({ - // eslint-disable-next-line react/no-unused-class-component-methods - displayName: 'EditExtractorsPage', - - // eslint-disable-next-line react/no-unused-class-component-methods - propTypes: { - history: PropTypes.object.isRequired, - params: PropTypes.object.isRequired, - }, - - mixins: [Reflux.connect(ExtractorsStore), Reflux.connect(InputsStore)], - - componentDidMount() { - const { params } = this.props; - - InputsActions.get.triggerPromise(params.inputId); - ExtractorsActions.get.triggerPromise(params.inputId, params.extractorId); - - UniversalSearchStore.search('relative', `gl2_source_input:${params.inputId} OR gl2_source_radio_input:${params.inputId}`, { relative: 3600 }, undefined, 1) - .then((response) => { - if (response.total_results > 0) { - this.setState({ exampleMessage: response.messages[0] }); - } else { - this.setState({ exampleMessage: {} }); - } - }); - }, - - _isLoading() { - return !(this.state.input && this.state.extractor && this.state.exampleMessage); - }, - - _extractorSaved() { - let url; - const { input } = this.state; - const { params } = this.props; - - if (input.global) { - url = Routes.global_input_extractors(params.inputId); - } else { - url = Routes.local_input_extractors(params.nodeId, params.inputId); - } - - const { history } = this.props; - history.push(url); - }, - - render() { - // TODO: - // - Redirect when extractor or input were deleted - - if (this._isLoading()) { - return ; - } - - const { extractor, exampleMessage, input } = this.state; - - return ( - - Edit extractor {extractor.title} for input {input.title}} - documentationLink={{ - title: 'Extractors documentation', - path: DocsHelper.PAGES.EXTRACTORS, - }}> - - Extractors are applied on every message that is received by an input. Use them to extract and transform{' '} - any text data into fields that allow you easy filtering and analysis later on. - - - - - - ); - }, -}); - -export default withHistory(withParams(EditExtractorsPage)); diff --git a/graylog2-web-interface/src/pages/EditExtractorsPage.tsx b/graylog2-web-interface/src/pages/EditExtractorsPage.tsx new file mode 100644 index 000000000000..206bbeb9410a --- /dev/null +++ b/graylog2-web-interface/src/pages/EditExtractorsPage.tsx @@ -0,0 +1,95 @@ +/* + * 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 { useEffect, useState } from 'react'; + +import { DocumentTitle, PageHeader, Spinner } from 'components/common'; +import EditExtractor from 'components/extractors/EditExtractor'; +import DocsHelper from 'util/DocsHelper'; +import Routes from 'routing/Routes'; +import { ExtractorsActions, ExtractorsStore } from 'stores/extractors/ExtractorsStore'; +import { InputsActions, InputsStore } from 'stores/inputs/InputsStore'; +import { UniversalSearchStore } from 'stores/search/UniversalSearchStore'; +import { useStore } from 'stores/connect'; +import useParams from 'routing/useParams'; +import useHistory from 'routing/useHistory'; + +const EditExtractorsPage = () => { + const history = useHistory(); + const { extractor } = useStore(ExtractorsStore); + const { input } = useStore(InputsStore); + const { inputId, extractorId, nodeId } = useParams<{ inputId: string, extractorId: string, nodeId: string }>(); + const [exampleMessage, setExampleMessage] = useState<{ fields?: { [key: string]: any }}>({}); + + useEffect(() => { + InputsActions.get(inputId); + ExtractorsActions.get(inputId, extractorId); + + UniversalSearchStore.search('relative', `gl2_source_input:${inputId} OR gl2_source_radio_input:${inputId}`, { relative: 3600 }, undefined, 1) + .then((response) => { + if (response.total_results > 0) { + setExampleMessage(response.messages[0]); + } else { + setExampleMessage({}); + } + }); + }, [extractorId, inputId]); + + const _isLoading = !(input && extractor && exampleMessage); + + const _extractorSaved = () => { + let url; + + if (input.global) { + url = Routes.global_input_extractors(inputId); + } else { + url = Routes.local_input_extractors(nodeId, inputId); + } + + history.push(url); + }; + + // TODO: + // - Redirect when extractor or input were deleted + + if (_isLoading) { + return ; + } + + return ( + + Edit extractor {extractor.title} for input {input.title}} + documentationLink={{ + title: 'Extractors documentation', + path: DocsHelper.PAGES.EXTRACTORS, + }}> + + Extractors are applied on every message that is received by an input. Use them to extract and transform{' '} + any text data into fields that allow you easy filtering and analysis later on. + + + + + + ); +}; + +export default EditExtractorsPage; diff --git a/graylog2-web-interface/src/pages/ExtractorsPage.jsx b/graylog2-web-interface/src/pages/ExtractorsPage.jsx deleted file mode 100644 index 044b979c422f..000000000000 --- a/graylog2-web-interface/src/pages/ExtractorsPage.jsx +++ /dev/null @@ -1,110 +0,0 @@ -/* - * 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 - * . - */ -// eslint-disable-next-line no-restricted-imports -import createReactClass from 'create-react-class'; -import PropTypes from 'prop-types'; -import React from 'react'; -import Reflux from 'reflux'; - -import { DocumentTitle, Spinner } from 'components/common'; -import PageHeader from 'components/common/PageHeader'; -import ExtractorsList from 'components/extractors/ExtractorsList'; -import { DropdownButton, MenuItem } from 'components/bootstrap'; -import Routes from 'routing/Routes'; -import DocsHelper from 'util/DocsHelper'; -import withParams from 'routing/withParams'; -import { InputsActions } from 'stores/inputs/InputsStore'; -import { NodesActions, NodesStore } from 'stores/nodes/NodesStore'; - -const ExtractorsPage = createReactClass({ - // eslint-disable-next-line react/no-unused-class-component-methods - displayName: 'ExtractorsPage', - - // eslint-disable-next-line react/no-unused-class-component-methods - propTypes: { - params: PropTypes.object.isRequired, - }, - - mixins: [Reflux.listenTo(NodesStore, 'onNodesChange')], - - getInitialState() { - return { - node: undefined, - }; - }, - - componentDidMount() { - const { params } = this.props; - - InputsActions.get(params.inputId).then((input) => this.setState({ input })); - NodesActions.list(); - }, - - // eslint-disable-next-line react/no-unused-class-component-methods - onNodesChange(nodes) { - const { params } = this.props; - const newNode = params.nodeId ? nodes.nodes[params.nodeId] : Object.values(nodes.nodes).filter((node) => node.is_leader)[0]; - - const { node } = this.state; - - if (!node || node.node_id !== newNode.node_id) { - this.setState({ node: newNode }); - } - }, - - _isLoading() { - const { node, input } = this.state; - - return !(input && node); - }, - - render() { - if (this._isLoading()) { - return ; - } - - const { node, input } = this.state; - - return ( - -
    - Extractors of {input.title}} - actions={( - - Import extractors - Export extractors - - )} - documentationLink={{ - title: 'Extractors documentation', - path: DocsHelper.PAGES.EXTRACTORS, - }}> - - Extractors are applied on every message that is received by this input. Use them to extract and transform{' '} - any text data into fields that allow you easy filtering and analysis later on.{' '} - Example: Extract the HTTP response code from a log message, transform it to a numeric field and attach it{' '} - as http_response_code to the message. - - - -
    -
    - ); - }, -}); - -export default withParams(ExtractorsPage); diff --git a/graylog2-web-interface/src/pages/ExtractorsPage.tsx b/graylog2-web-interface/src/pages/ExtractorsPage.tsx new file mode 100644 index 000000000000..8321debd5e39 --- /dev/null +++ b/graylog2-web-interface/src/pages/ExtractorsPage.tsx @@ -0,0 +1,77 @@ +/* + * 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 { useEffect } from 'react'; +import { useQuery } from '@tanstack/react-query'; + +import { DocumentTitle, Spinner } from 'components/common'; +import PageHeader from 'components/common/PageHeader'; +import ExtractorsList from 'components/extractors/ExtractorsList'; +import { DropdownButton, MenuItem } from 'components/bootstrap'; +import Routes from 'routing/Routes'; +import DocsHelper from 'util/DocsHelper'; +import withParams from 'routing/withParams'; +import { InputsActions } from 'stores/inputs/InputsStore'; +import { NodesActions, NodesStore } from 'stores/nodes/NodesStore'; +import { useStore } from 'stores/connect'; +import useParams from 'routing/useParams'; + +const ExtractorsPage = () => { + const params = useParams<{ inputId: string, nodeId: string }>(); + const node = useStore(NodesStore, (nodes) => (params.nodeId + ? nodes.nodes?.[params.nodeId] + : Object.values(nodes.nodes).filter((_node) => _node.is_leader)[0])); + const { data: input } = useQuery(['input', params.inputId], () => InputsActions.get(params.inputId)); + + useEffect(() => { + NodesActions.list(); + }, []); + + const _isLoading = !(input && node); + + if (_isLoading) { + return ; + } + + return ( + +
    + Extractors of {input.title}} + actions={( + + Import extractors + Export extractors + + )} + documentationLink={{ + title: 'Extractors documentation', + path: DocsHelper.PAGES.EXTRACTORS, + }}> + + Extractors are applied on every message that is received by this input. Use them to extract and transform{' '} + any text data into fields that allow you easy filtering and analysis later on.{' '} + Example: Extract the HTTP response code from a log message, transform it to a numeric field and attach it{' '} + as http_response_code to the message. + + + +
    +
    + ); +}; + +export default withParams(ExtractorsPage); diff --git a/graylog2-web-interface/src/pages/ImportExtractorsPage.jsx b/graylog2-web-interface/src/pages/ImportExtractorsPage.jsx deleted file mode 100644 index 7b67f7b310da..000000000000 --- a/graylog2-web-interface/src/pages/ImportExtractorsPage.jsx +++ /dev/null @@ -1,72 +0,0 @@ -/* - * 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 PropTypes from 'prop-types'; -import React from 'react'; -import createReactClass from 'create-react-class'; -import Reflux from 'reflux'; - -import { DocumentTitle, PageHeader, Spinner } from 'components/common'; -import ImportExtractors from 'components/extractors/ImportExtractors'; -import withParams from 'routing/withParams'; -import { InputsActions, InputsStore } from 'stores/inputs/InputsStore'; - -const ImportExtractorsPage = createReactClass({ - displayName: 'ImportExtractorsPage', - - propTypes: { - params: PropTypes.object.isRequired, - }, - - mixins: [Reflux.connect(InputsStore)], - - componentDidMount() { - const { params } = this.props; - - InputsActions.get.triggerPromise(params.inputId).then((input) => this.setState({ input: input })); - }, - - _isLoading() { - return !this.state.input; - }, - - render() { - if (this._isLoading()) { - return ; - } - - const { input } = this.state; - - return ( - -
    - Import extractors to {input.title}}> - - Exported extractors can be imported to an input. All you need is the JSON export of extractors from any - other Graylog setup or from{' '} - - the Graylog Marketplace - . - - - -
    -
    - ); - }, -}); - -export default withParams(ImportExtractorsPage); diff --git a/graylog2-web-interface/src/pages/ImportExtractorsPage.tsx b/graylog2-web-interface/src/pages/ImportExtractorsPage.tsx new file mode 100644 index 000000000000..f100b729bfd2 --- /dev/null +++ b/graylog2-web-interface/src/pages/ImportExtractorsPage.tsx @@ -0,0 +1,60 @@ +/* + * 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 React from 'react'; +import { useEffect, useState } from 'react'; + +import { DocumentTitle, PageHeader, Spinner } from 'components/common'; +import ImportExtractors from 'components/extractors/ImportExtractors'; +import type { ParamsContext } from 'routing/withParams'; +import withParams from 'routing/withParams'; +import { InputsActions } from 'stores/inputs/InputsStore'; +import type { Input } from 'components/messageloaders/Types'; + +type Props = ParamsContext; + +const ImportExtractorsPage = ({ params }: Props) => { + const [input, setInput] = useState(); + + useEffect(() => { + InputsActions.get(params.inputId).then((_input) => setInput(_input)); + }, []); + + const _isLoading = !input; + + if (_isLoading) { + return ; + } + + return ( + +
    + Import extractors to {input.title}}> + + Exported extractors can be imported to an input. All you need is the JSON export of extractors from any + other Graylog setup or from{' '} + + the Graylog Marketplace + . + + + +
    +
    + ); +}; + +export default withParams(ImportExtractorsPage); diff --git a/graylog2-web-interface/src/pages/NodesPage.tsx b/graylog2-web-interface/src/pages/NodesPage.tsx index f6a818c9f15a..91a1d90e61ab 100644 --- a/graylog2-web-interface/src/pages/NodesPage.tsx +++ b/graylog2-web-interface/src/pages/NodesPage.tsx @@ -24,8 +24,6 @@ import type { NodeInfo } from 'stores/nodes/NodesStore'; import { NodesStore } from 'stores/nodes/NodesStore'; import { useStore } from 'stores/connect'; -import useCurrentUser from '../hooks/useCurrentUser'; - const GLOBAL_API_BROWSER_URL = '/api-browser/global/index.html'; const hasExternalURI = (nodes: { [nodeId: string]: NodeInfo }) => { @@ -52,7 +50,6 @@ const GlobalAPIButton = ({ nodes }: { nodes: { [nodeId: string]: NodeInfo } }) = }; const NodesPage = () => { - const currentUser = useCurrentUser(); const { nodes } = useStore(NodesStore); return ( @@ -66,7 +63,7 @@ const NodesPage = () => { will be persisted to disk, even when processing is disabled. - + ); diff --git a/graylog2-web-interface/src/pages/PipelineDetailsPage.jsx b/graylog2-web-interface/src/pages/PipelineDetailsPage.jsx deleted file mode 100644 index de119642ed3b..000000000000 --- a/graylog2-web-interface/src/pages/PipelineDetailsPage.jsx +++ /dev/null @@ -1,191 +0,0 @@ -/* - * 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 PropTypes from 'prop-types'; -import React from 'react'; -// eslint-disable-next-line no-restricted-imports -import createReactClass from 'create-react-class'; -import Reflux from 'reflux'; - -import { Col, Row } from 'components/bootstrap'; -import { DocumentTitle, PageHeader, Spinner } from 'components/common'; -import Pipeline from 'components/pipelines/Pipeline'; -import NewPipeline from 'components/pipelines/NewPipeline'; -import SourceGenerator from 'logic/pipelines/SourceGenerator'; -import ObjectUtils from 'util/ObjectUtils'; -import withParams from 'routing/withParams'; -import { StreamsStore } from 'stores/streams/StreamsStore'; -import { PipelineConnectionsStore, PipelineConnectionsActions } from 'stores/pipelines/PipelineConnectionsStore'; -import { PipelinesStore, PipelinesActions } from 'stores/pipelines/PipelinesStore'; -import { RulesStore } from 'stores/rules/RulesStore'; -import DocsHelper from 'util/DocsHelper'; - -import PipelinesPageNavigation from '../components/pipelines/PipelinesPageNavigation'; - -function filterPipeline(state) { - return state.pipelines ? state.pipelines.filter((p) => p.id === this.props.params.pipelineId)[0] : undefined; -} - -function filterConnections(state) { - if (!state.connections) { - return undefined; - } - - return state.connections.filter((c) => c.pipeline_ids && c.pipeline_ids.includes(this.props.params.pipelineId)); -} - -const PipelineDetailsPage = createReactClass({ - // eslint-disable-next-line react/no-unused-class-component-methods - displayName: 'PipelineDetailsPage', - - // eslint-disable-next-line react/no-unused-class-component-methods - propTypes: { - params: PropTypes.object.isRequired, - }, - - mixins: [Reflux.connectFilter(PipelinesStore, 'pipeline', filterPipeline), Reflux.connectFilter(PipelineConnectionsStore, 'connections', filterConnections)], - - componentDidMount() { - const { params } = this.props; - - if (!this._isNewPipeline(params.pipelineId)) { - PipelinesActions.get(params.pipelineId); - } - - RulesStore.list(); - PipelineConnectionsActions.list(); - - StreamsStore.listStreams().then((streams) => { - const filteredStreams = streams.filter((s) => s.is_editable); - - this.setState({ streams: filteredStreams }); - }); - }, - - UNSAFE_componentWillReceiveProps(nextProps) { - if (!this._isNewPipeline(nextProps.params.pipelineId)) { - PipelinesActions.get(nextProps.params.pipelineId); - } - }, - - _onConnectionsChange(updatedConnections, callback) { - PipelineConnectionsActions.connectToPipeline(updatedConnections); - callback(); - }, - - _onStagesChange(newStages, callback) { - const { pipeline } = this.state; - const newPipeline = ObjectUtils.clone(pipeline); - - newPipeline.stages = newStages; - newPipeline.source = SourceGenerator.generatePipeline(newPipeline); - PipelinesActions.update(newPipeline); - - if (typeof callback === 'function') { - callback(); - } - }, - - _savePipeline(pipeline, callback) { - const requestPipeline = ObjectUtils.clone(pipeline); - - requestPipeline.source = SourceGenerator.generatePipeline(pipeline); - let promise; - - if (requestPipeline.id) { - promise = PipelinesActions.update(requestPipeline); - } else { - promise = PipelinesActions.save(requestPipeline); - } - - promise.then((p) => callback(p)); - }, - - _isNewPipeline(pipelineId) { - return pipelineId === 'new'; - }, - - _isLoading() { - const { params } = this.props; - const { connections, streams, pipeline } = this.state; - - return !this._isNewPipeline(params.pipelineId) && (!pipeline || !connections || !streams); - }, - - render() { - if (this._isLoading()) { - return ; - } - - const { params } = this.props; - const { connections, streams, pipeline, rules } = this.state; - - let title; - - if (this._isNewPipeline(params.pipelineId)) { - title = 'New pipeline'; - } else { - title = Pipeline {pipeline.title}; - } - - let content; - - if (this._isNewPipeline(params.pipelineId)) { - content = ; - } else { - content = ( - - ); - } - - const pageTitle = (this._isNewPipeline(params.pipelineId) ? 'New pipeline' : `Pipeline ${pipeline.title}`); - - return ( - -
    - - - - Pipelines let you transform and process messages coming from streams. Pipelines consist of stages where - rules are evaluated and applied. Messages can go through one or more stages. -
    - After each stage is completed, you can decide if messages matching all or one of the rules continue to - the next stage. -
    -
    - - - - {content} - - -
    -
    - ); - }, -}); - -export default withParams(PipelineDetailsPage); diff --git a/graylog2-web-interface/src/pages/PipelineDetailsPage.tsx b/graylog2-web-interface/src/pages/PipelineDetailsPage.tsx new file mode 100644 index 000000000000..611e614fe5a4 --- /dev/null +++ b/graylog2-web-interface/src/pages/PipelineDetailsPage.tsx @@ -0,0 +1,145 @@ +/* + * 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 { useEffect, useState } from 'react'; + +import { Col, Row } from 'components/bootstrap'; +import { DocumentTitle, PageHeader, Spinner } from 'components/common'; +import Pipeline from 'components/pipelines/Pipeline'; +import NewPipeline from 'components/pipelines/NewPipeline'; +import SourceGenerator from 'logic/pipelines/SourceGenerator'; +import { StreamsStore } from 'stores/streams/StreamsStore'; +import { PipelineConnectionsStore, PipelineConnectionsActions } from 'stores/pipelines/PipelineConnectionsStore'; +import { PipelinesStore, PipelinesActions } from 'stores/pipelines/PipelinesStore'; +import DocsHelper from 'util/DocsHelper'; +import { useStore } from 'stores/connect'; +import useParams from 'routing/useParams'; +import { RulesActions } from 'stores/rules/RulesStore'; + +import PipelinesPageNavigation from '../components/pipelines/PipelinesPageNavigation'; + +const _isNewPipeline = (pipelineId: string) => pipelineId === 'new'; + +const PipelineDetailsPage = () => { + const params = useParams<{ pipelineId: string }>(); + const pipeline = useStore(PipelinesStore, (state) => state.pipelines?.filter((p) => p.id === params.pipelineId)?.[0]); + const connections = useStore(PipelineConnectionsStore, (state) => state.connections?.filter( + (c) => c.pipeline_ids && c.pipeline_ids.includes(params.pipelineId), + )); + const [streams, setStreams] = useState(); + + useEffect(() => { + if (!_isNewPipeline(params.pipelineId)) { + PipelinesActions.get(params.pipelineId); + } + }, [params.pipelineId]); + + useEffect(() => { + RulesActions.list(); + PipelineConnectionsActions.list(); + + StreamsStore.listStreams().then((_streams) => { + const filteredStreams = _streams.filter((s) => s.is_editable); + + setStreams(filteredStreams); + }); + }, []); + + const _onConnectionsChange = (updatedConnections, callback) => { + PipelineConnectionsActions.connectToPipeline(updatedConnections); + callback(); + }; + + const _onStagesChange = (newStages, callback) => { + const newPipeline = { + ...pipeline, + stages: newStages, + source: SourceGenerator.generatePipeline(pipeline), + }; + + PipelinesActions.update(newPipeline); + + if (typeof callback === 'function') { + callback(); + } + }; + + const _savePipeline = (_pipeline, callback) => { + const requestPipeline = { + ..._pipeline, + source: SourceGenerator.generatePipeline(pipeline), + }; + + const promise = requestPipeline.id + ? PipelinesActions.update(requestPipeline) + : PipelinesActions.save(requestPipeline); + + promise.then((p) => callback(p)); + }; + + const _isLoading = !_isNewPipeline(params.pipelineId) && (!pipeline || !connections || !streams); + + if (_isLoading) { + return ; + } + + const title = _isNewPipeline(params.pipelineId) + ? 'New pipeline' + : Pipeline {pipeline.title}; + + const content = _isNewPipeline(params.pipelineId) + ? + : ( + + ); + + const pageTitle = _isNewPipeline(params.pipelineId) ? 'New pipeline' : `Pipeline ${pipeline.title}`; + + return ( + +
    + + + + Pipelines let you transform and process messages coming from streams. Pipelines consist of stages where + rules are evaluated and applied. Messages can go through one or more stages. +
    + After each stage is completed, you can decide if messages matching all or one of the rules continue to + the next stage. +
    +
    + + + + {content} + + +
    +
    + ); +}; + +export default PipelineDetailsPage; diff --git a/graylog2-web-interface/src/pages/ProcessBufferDumpPage.jsx b/graylog2-web-interface/src/pages/ProcessBufferDumpPage.jsx deleted file mode 100644 index 7ea43b6497bb..000000000000 --- a/graylog2-web-interface/src/pages/ProcessBufferDumpPage.jsx +++ /dev/null @@ -1,88 +0,0 @@ -/* - * 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 PropTypes from 'prop-types'; -import React from 'react'; -// eslint-disable-next-line no-restricted-imports -import createReactClass from 'create-react-class'; -import Reflux from 'reflux'; - -import { Row, Col } from 'components/bootstrap'; -import { DocumentTitle, PageHeader, Spinner, Timestamp } from 'components/common'; -import withParams from 'routing/withParams'; -import { ClusterOverviewStore } from 'stores/cluster/ClusterOverviewStore'; -import { CurrentUserStore } from 'stores/users/CurrentUserStore'; -import { NodesStore } from 'stores/nodes/NodesStore'; - -function nodeFilter(state) { - return state.nodes ? state.nodes[this.props.params.nodeId] : state.nodes; -} - -const ProcessBufferDumpPage = createReactClass({ - // eslint-disable-next-line react/no-unused-class-component-methods - displayName: 'ProcessBufferDumpPage', - - // eslint-disable-next-line react/no-unused-class-component-methods - propTypes: { - params: PropTypes.object.isRequired, - }, - - mixins: [Reflux.connect(CurrentUserStore), Reflux.connectFilter(NodesStore, 'node', nodeFilter)], - - componentDidMount() { - const { params } = this.props; - - ClusterOverviewStore.processbufferDump(params.nodeId) - .then((processbufferDump) => this.setState({ processbufferDump: processbufferDump })); - }, - - _isLoading() { - return !this.state.node; - }, - - render() { - if (this._isLoading()) { - return ; - } - - const { node, processbufferDump } = this.state; - - const title = ( - - Process-buffer dump of node {node.short_node_id} / {node.hostname} -   - Taken at - - ); - - const content = processbufferDump ?
    {JSON.stringify(processbufferDump, null, 2)}
    : ; - - return ( - -
    - - - - {content} - - -
    -
    - ); - }, -}); - -export default withParams(ProcessBufferDumpPage); diff --git a/graylog2-web-interface/src/pages/ProcessBufferDumpPage.tsx b/graylog2-web-interface/src/pages/ProcessBufferDumpPage.tsx new file mode 100644 index 000000000000..84397ba1a36e --- /dev/null +++ b/graylog2-web-interface/src/pages/ProcessBufferDumpPage.tsx @@ -0,0 +1,62 @@ +/* + * 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 React from 'react'; +import { useQuery } from '@tanstack/react-query'; + +import { Row, Col } from 'components/bootstrap'; +import { DocumentTitle, PageHeader, Spinner, Timestamp } from 'components/common'; +import { ClusterOverviewStore } from 'stores/cluster/ClusterOverviewStore'; +import { NodesStore } from 'stores/nodes/NodesStore'; +import useParams from 'routing/useParams'; +import { useStore } from 'stores/connect'; + +const ProcessBufferDumpPage = () => { + const { nodeId } = useParams<{ nodeId: string }>(); + const { nodes } = useStore(NodesStore); + const { data: processbufferDump } = useQuery(['processBufferDump', nodeId], () => ClusterOverviewStore.processbufferDump(nodeId)); + + const node = nodes?.[nodeId]; + + if (!node) { + return ; + } + + const title = ( + + Process-buffer dump of node {node.short_node_id} / {node.hostname} +   + Taken at + + ); + + const content = processbufferDump ?
    {JSON.stringify(processbufferDump, null, 2)}
    : ; + + return ( + +
    + + + + {content} + + +
    +
    + ); +}; + +export default ProcessBufferDumpPage; diff --git a/graylog2-web-interface/src/pages/ShowContentPackPage.jsx b/graylog2-web-interface/src/pages/ShowContentPackPage.jsx deleted file mode 100644 index 671212a893b8..000000000000 --- a/graylog2-web-interface/src/pages/ShowContentPackPage.jsx +++ /dev/null @@ -1,217 +0,0 @@ -/* - * 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 React from 'react'; -import Reflux from 'reflux'; -// eslint-disable-next-line no-restricted-imports -import createReactClass from 'create-react-class'; -import PropTypes from 'prop-types'; - -import { LinkContainer } from 'components/common/router'; -import { Row, Col, Button, ButtonToolbar, BootstrapModalConfirm } from 'components/bootstrap'; -import Spinner from 'components/common/Spinner'; -import Routes from 'routing/Routes'; -import UserNotification from 'util/UserNotification'; -import { DocumentTitle, PageHeader } from 'components/common'; -import ContentPackDetails from 'components/content-packs/ContentPackDetails'; -import ContentPackVersions from 'components/content-packs/ContentPackVersions'; -import ContentPackInstallations from 'components/content-packs/ContentPackInstallations'; -import ContentPackInstallEntityList from 'components/content-packs/ContentPackInstallEntityList'; -import withParams from 'routing/withParams'; -import { ContentPacksActions, ContentPacksStore } from 'stores/content-packs/ContentPacksStore'; -import withHistory from 'routing/withHistory'; - -import ShowContentPackStyle from './ShowContentPackPage.css'; - -const ShowContentPackPage = createReactClass({ - // eslint-disable-next-line react/no-unused-class-component-methods - displayName: 'ShowContentPackPage', - - // eslint-disable-next-line react/no-unused-class-component-methods - propTypes: { - history: PropTypes.object.isRequired, - params: PropTypes.object.isRequired, - }, - - mixins: [Reflux.connect(ContentPacksStore)], - - getInitialState() { - return { - showModal: false, - selectedVersion: undefined, - uninstallEntities: undefined, - uninstallContentPackId: undefined, - uninstallInstallId: undefined, - }; - }, - - componentDidMount() { - ContentPacksActions.get(this.props.params.contentPackId).catch((error) => { - if (error.status === 404) { - UserNotification.error( - `Cannot find Content Pack with the id ${this.props.params.contentPackId} and may have been deleted.`, - ); - } else { - UserNotification.error('An internal server error occurred. Please check your logfiles for more information'); - } - - const { history } = this.props; - history.push(Routes.SYSTEM.CONTENTPACKS.LIST); - }); - - ContentPacksActions.installList(this.props.params.contentPackId); - }, - - _onVersionChanged(newVersion) { - this.setState({ selectedVersion: newVersion }); - }, - - _deleteContentPackRev(contentPackId, revision) { - /* eslint-disable-next-line no-alert */ - if (window.confirm('You are about to delete this content pack revision, are you sure?')) { - ContentPacksActions.deleteRev(contentPackId, revision).then(() => { - UserNotification.success('Content pack revision deleted successfully.', 'Success'); - - ContentPacksActions.get(contentPackId).catch((error) => { - if (error.status !== 404) { - UserNotification.error('An internal server error occurred. Please check your logfiles for more information'); - } - - const { history } = this.props; - history.push(Routes.SYSTEM.CONTENTPACKS.LIST); - }); - }, (error) => { - let errMessage = error.message; - - if (error.responseMessage) { - errMessage = error.responseMessage; - } - - UserNotification.error(`Deleting content pack failed: ${errMessage}`, 'Error'); - }); - } - }, - - _onUninstallContentPackRev(contentPackId, installId) { - ContentPacksActions.uninstallDetails(contentPackId, installId).then((result) => { - this.setState({ uninstallEntities: result.entities }); - }); - - this.setState({ - showModal: true, - uninstallContentPackId: contentPackId, - uninstallInstallId: installId, - }); - }, - - _clearUninstall() { - this.setState({ - showModal: false, - uninstallContentPackId: undefined, - uninstallInstallId: undefined, - uninstallEntities: undefined, - }); - }, - - _uninstallContentPackRev() { - const contentPackId = this.state.uninstallContentPackId; - - ContentPacksActions.uninstall(this.state.uninstallContentPackId, this.state.uninstallInstallId).then(() => { - UserNotification.success('Content Pack uninstalled successfully.', 'Success'); - ContentPacksActions.installList(contentPackId); - this._clearUninstall(); - }, () => { - UserNotification.error('Uninstall content pack failed, please check your logs for more information.', 'Error'); - }); - }, - - _installContentPack(contentPackId, contentPackRev, parameters) { - ContentPacksActions.install(contentPackId, contentPackRev, parameters).then(() => { - UserNotification.success('Content Pack installed successfully.', 'Success'); - ContentPacksActions.installList(contentPackId); - }, (error) => { - UserNotification.error(`Installing content pack failed with status: ${error}. - Could not install content pack with ID: ${contentPackId}`); - }); - }, - - render() { - if (!this.state.contentPackRevisions) { - return (); - } - - const { contentPackRevisions, selectedVersion, constraints } = this.state; - - return ( - - - - - - - - )}> - - Content packs accelerate the set up process for a specific data source. A content pack can include inputs/extractors, streams, and dashboards. -
    - Find more content packs in {' '} - the Graylog Marketplace. -
    -
    - - - -
    - - -

    Versions

    - - -
    - - -

    Installations

    - - -
    -
    - - - - -
    -
    - - - -
    - ); - }, -}); - -export default withHistory(withParams(ShowContentPackPage)); diff --git a/graylog2-web-interface/src/pages/ShowContentPackPage.tsx b/graylog2-web-interface/src/pages/ShowContentPackPage.tsx new file mode 100644 index 000000000000..14603a69ff96 --- /dev/null +++ b/graylog2-web-interface/src/pages/ShowContentPackPage.tsx @@ -0,0 +1,195 @@ +/* + * 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 { useEffect, useState } from 'react'; + +import { LinkContainer } from 'components/common/router'; +import { Row, Col, Button, ButtonToolbar, BootstrapModalConfirm } from 'components/bootstrap'; +import Spinner from 'components/common/Spinner'; +import Routes from 'routing/Routes'; +import UserNotification from 'util/UserNotification'; +import { DocumentTitle, PageHeader } from 'components/common'; +import ContentPackDetails from 'components/content-packs/ContentPackDetails'; +import ContentPackVersions from 'components/content-packs/ContentPackVersions'; +import ContentPackInstallations from 'components/content-packs/ContentPackInstallations'; +import ContentPackInstallEntityList from 'components/content-packs/ContentPackInstallEntityList'; +import { ContentPacksActions, ContentPacksStore } from 'stores/content-packs/ContentPacksStore'; +import { useStore } from 'stores/connect'; +import useHistory from 'routing/useHistory'; +import useParams from 'routing/useParams'; + +import ShowContentPackStyle from './ShowContentPackPage.css'; + +const ShowContentPackPage = () => { + const { contentPackRevisions, installations, constraints } = useStore(ContentPacksStore); + const history = useHistory(); + const params = useParams<{ contentPackId: string }>(); + + const [showModal, setShowModal] = useState(false); + const [selectedVersion, setSelectedVersion] = useState(undefined); + const [uninstallEntities, setUninstallEntities] = useState(undefined); + const [uninstallContentPackId, setUninstallContentPackId] = useState(undefined); + const [uninstallInstallId, setUninstallInstallId] = useState(undefined); + + useEffect(() => { + ContentPacksActions.get(params.contentPackId).catch((error) => { + if (error.status === 404) { + UserNotification.error( + `Cannot find Content Pack with the id ${params.contentPackId} and may have been deleted.`, + ); + } else { + UserNotification.error('An internal server error occurred. Please check your logfiles for more information'); + } + + history.push(Routes.SYSTEM.CONTENTPACKS.LIST); + }); + + ContentPacksActions.installList(params.contentPackId); + }, []); + + const _onVersionChanged = (newVersion) => { + setSelectedVersion(newVersion); + }; + + const _deleteContentPackRev = (contentPackId: string, revision?: number) => { + /* eslint-disable-next-line no-alert */ + if (window.confirm('You are about to delete this content pack revision, are you sure?')) { + ContentPacksActions.deleteRev(contentPackId, revision).then(() => { + UserNotification.success('Content pack revision deleted successfully.', 'Success'); + + ContentPacksActions.get(contentPackId).catch((error) => { + if (error.status !== 404) { + UserNotification.error('An internal server error occurred. Please check your logfiles for more information'); + } + + history.push(Routes.SYSTEM.CONTENTPACKS.LIST); + }); + }, (error) => { + let errMessage = error.message; + + if (error.responseMessage) { + errMessage = error.responseMessage; + } + + UserNotification.error(`Deleting content pack failed: ${errMessage}`, 'Error'); + }); + } + }; + + const _onUninstallContentPackRev = (contentPackId: string, installId: string) => { + ContentPacksActions.uninstallDetails(contentPackId, installId).then((result: { entities: unknown }) => { + setUninstallEntities(result.entities); + }); + + setSelectedVersion(true); + setUninstallContentPackId(contentPackId); + setUninstallInstallId(installId); + }; + + const _clearUninstall = () => { + setShowModal(false); + setUninstallContentPackId(undefined); + setUninstallInstallId(undefined); + setUninstallEntities(undefined); + }; + + const _uninstallContentPackRev = () => { + const contentPackId = uninstallContentPackId; + + ContentPacksActions.uninstall(uninstallContentPackId, uninstallInstallId).then(() => { + UserNotification.success('Content Pack uninstalled successfully.', 'Success'); + ContentPacksActions.installList(contentPackId); + _clearUninstall(); + }, () => { + UserNotification.error('Uninstall content pack failed, please check your logs for more information.', 'Error'); + }); + }; + + const _installContentPack = (contentPackId: string, contentPackRev: string, parameters) => { + ContentPacksActions.install(contentPackId, contentPackRev, parameters).then(() => { + UserNotification.success('Content Pack installed successfully.', 'Success'); + ContentPacksActions.installList(contentPackId); + }, (error) => { + UserNotification.error(`Installing content pack failed with status: ${error}. + Could not install content pack with ID: ${contentPackId}`); + }); + }; + + if (!contentPackRevisions) { + return (); + } + + return ( + + + + + + + + )}> + + Content packs accelerate the set up process for a specific data source. A content pack can include inputs/extractors, streams, and dashboards. +
    + Find more content packs in {' '} + the Graylog Marketplace. +
    +
    + + + +
    + + +

    Versions

    + + +
    + + +

    Installations

    + + +
    +
    + + + {/* @ts-ignore */} + + +
    +
    + + + +
    + ); +}; + +export default ShowContentPackPage; diff --git a/graylog2-web-interface/src/pages/ShowNodePage.jsx b/graylog2-web-interface/src/pages/ShowNodePage.jsx deleted file mode 100644 index e5782a589609..000000000000 --- a/graylog2-web-interface/src/pages/ShowNodePage.jsx +++ /dev/null @@ -1,123 +0,0 @@ -/* - * 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 PropTypes from 'prop-types'; -import React from 'react'; -// eslint-disable-next-line no-restricted-imports -import createReactClass from 'create-react-class'; -import Reflux from 'reflux'; - -import { NodeMaintenanceDropdown, NodeOverview } from 'components/nodes'; -import { DocumentTitle, PageErrorOverview, PageHeader, Spinner } from 'components/common'; -import withParams from 'routing/withParams'; -import { ClusterOverviewStore } from 'stores/cluster/ClusterOverviewStore'; -import { InputStatesStore } from 'stores/inputs/InputStatesStore'; -import { InputTypesStore } from 'stores/inputs/InputTypesStore'; -import { NodesStore } from 'stores/nodes/NodesStore'; - -import { PluginsStore } from '../stores/plugins/PluginsStore'; - -function nodeFilter(state) { - return state.nodes ? state.nodes[this.props.params.nodeId] : state.nodes; -} - -function clusterOverviewFilter(state) { - return state.clusterOverview ? state.clusterOverview[this.props.params.nodeId] : undefined; -} - -const ShowNodePage = createReactClass({ - // eslint-disable-next-line react/no-unused-class-component-methods - displayName: 'ShowNodePage', - - // eslint-disable-next-line react/no-unused-class-component-methods - propTypes: { - params: PropTypes.object.isRequired, - }, - - mixins: [ - Reflux.connectFilter(NodesStore, 'node', nodeFilter), - Reflux.connectFilter(ClusterOverviewStore, 'systemOverview', clusterOverviewFilter), - Reflux.connect(InputTypesStore), - ], - - getInitialState() { - return { - jvmInformation: undefined, - plugins: undefined, - }; - }, - - UNSAFE_componentWillMount() { - Promise.all([ - ClusterOverviewStore.jvm(this.props.params.nodeId) - .then((jvmInformation) => this.setState({ jvmInformation: jvmInformation })), - PluginsStore.list(this.props.params.nodeId).then((plugins) => this.setState({ plugins: plugins })), - InputStatesStore.list().then((inputStates) => { - // We only want the input states for the current node - const inputIds = Object.keys(inputStates); - const filteredInputStates = []; - - inputIds.forEach((inputId) => { - const inputObject = inputStates[inputId][this.props.params.nodeId]; - - if (inputObject) { - filteredInputStates.push(inputObject); - } - }); - - this.setState({ inputStates: filteredInputStates }); - }), - ]).then(() => {}, (errors) => this.setState({ errors: errors })); - }, - - _isLoading() { - return !(this.state.node && this.state.systemOverview); - }, - - render() { - if (this.state.errors) { - return ; - } - - if (this._isLoading()) { - return ; - } - - const { node } = this.state; - const title = Node {node.short_node_id} / {node.hostname}; - - return ( - -
    - }> - - This page shows details of a Graylog server node that is active and reachable in your cluster.
    - {node.is_leader ? This is the leader node. : This is not the leader node.} -
    -
    - -
    -
    - ); - }, -}); - -export default withParams(ShowNodePage); diff --git a/graylog2-web-interface/src/pages/ShowNodePage.tsx b/graylog2-web-interface/src/pages/ShowNodePage.tsx new file mode 100644 index 000000000000..41a951c6155a --- /dev/null +++ b/graylog2-web-interface/src/pages/ShowNodePage.tsx @@ -0,0 +1,83 @@ +/* + * 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 React from 'react'; +import { useQuery } from '@tanstack/react-query'; + +import { NodeMaintenanceDropdown, NodeOverview } from 'components/nodes'; +import { DocumentTitle, PageHeader, Spinner } from 'components/common'; +import { ClusterOverviewStore } from 'stores/cluster/ClusterOverviewStore'; +import { InputStatesStore } from 'stores/inputs/InputStatesStore'; +import { InputTypesStore } from 'stores/inputs/InputTypesStore'; +import { NodesStore } from 'stores/nodes/NodesStore'; +import useParams from 'routing/useParams'; +import { useStore } from 'stores/connect'; +import usePluginList from 'hooks/usePluginList'; + +const ShowNodePage = () => { + const { nodeId } = useParams<{ nodeId: string }>(); + const { inputDescriptions } = useStore(InputTypesStore); + const { nodes } = useStore(NodesStore); + const { clusterOverview } = useStore(ClusterOverviewStore); + const { pluginList, isLoading: isLoadingPlugins } = usePluginList(nodeId); + const { data: jvmInformation } = useQuery(['jvm', nodeId], () => ClusterOverviewStore.jvm(nodeId)); + const { data: inputStates } = useQuery(['inputs', 'states', nodeId], () => InputStatesStore.list().then((inputStates) => { + // We only want the input states for the current node + const inputIds = Object.keys(inputStates); + const filteredInputStates = []; + + inputIds.forEach((inputId) => { + const inputObject = inputStates[inputId][nodeId]; + + if (inputObject) { + filteredInputStates.push(inputObject); + } + }); + + return filteredInputStates; + })); + + const systemOverview = clusterOverview?.[nodeId]; + const node = nodes?.[nodeId]; + const _isLoading = !node || !systemOverview || isLoadingPlugins; + + if (_isLoading) { + return ; + } + + const title = Node {node.short_node_id} / {node.hostname}; + + return ( + +
    + }> + + This page shows details of a Graylog server node that is active and reachable in your cluster.
    + {node.is_leader ? This is the leader node. : This is not the leader node.} +
    +
    + +
    +
    + ); +}; + +export default ShowNodePage; diff --git a/graylog2-web-interface/src/pages/SidecarAdministrationPage.tsx b/graylog2-web-interface/src/pages/SidecarAdministrationPage.tsx index df7a19de6407..e63d9a170255 100644 --- a/graylog2-web-interface/src/pages/SidecarAdministrationPage.tsx +++ b/graylog2-web-interface/src/pages/SidecarAdministrationPage.tsx @@ -41,7 +41,7 @@ const SidecarAdministrationPage = () => { - + diff --git a/graylog2-web-interface/src/pages/SidecarEditCollectorPage.tsx b/graylog2-web-interface/src/pages/SidecarEditCollectorPage.tsx index fcf30232b163..01dcc83e006f 100644 --- a/graylog2-web-interface/src/pages/SidecarEditCollectorPage.tsx +++ b/graylog2-web-interface/src/pages/SidecarEditCollectorPage.tsx @@ -25,11 +25,12 @@ import SidecarsPageNavigation from 'components/sidecars/common/SidecarsPageNavig import DocsHelper from 'util/DocsHelper'; import useParams from 'routing/useParams'; import useHistory from 'routing/useHistory'; +import type { Collector } from 'components/sidecars/types'; const SidecarEditCollectorPage = () => { const history = useHistory(); const { collectorId } = useParams(); - const [collector, setCollector] = useState(); + const [collector, setCollector] = useState(); const _reloadCollector = useCallback(() => { CollectorsActions.getCollector(collectorId).then( diff --git a/graylog2-web-interface/src/stores/cluster/ClusterOverviewStore.js b/graylog2-web-interface/src/stores/cluster/ClusterOverviewStore.ts similarity index 96% rename from graylog2-web-interface/src/stores/cluster/ClusterOverviewStore.js rename to graylog2-web-interface/src/stores/cluster/ClusterOverviewStore.ts index 13017dc5b35e..1ee5adc3e595 100644 --- a/graylog2-web-interface/src/stores/cluster/ClusterOverviewStore.js +++ b/graylog2-web-interface/src/stores/cluster/ClusterOverviewStore.ts @@ -23,11 +23,12 @@ import { singletonStore } from 'logic/singleton'; import { NodesStore } from 'stores/nodes/NodesStore'; import { SystemLoadBalancerStore } from 'stores/load-balancer/SystemLoadBalancerStore'; import { SystemProcessingStore } from 'stores/system-processing/SystemProcessingStore'; +import type { SystemOverview } from 'stores/cluster/types'; // eslint-disable-next-line import/prefer-default-export export const ClusterOverviewStore = singletonStore( 'core.ClusterOverview', - () => Reflux.createStore({ + () => Reflux.createStore<{ clusterOverview: { [nodeId: string]: SystemOverview } }>({ sourceUrl: '/cluster', clusterOverview: undefined, diff --git a/graylog2-web-interface/src/stores/cluster/types.ts b/graylog2-web-interface/src/stores/cluster/types.ts new file mode 100644 index 000000000000..c3902209948e --- /dev/null +++ b/graylog2-web-interface/src/stores/cluster/types.ts @@ -0,0 +1,31 @@ +/* + * 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 + * . + */ +export type SystemOverview = { + facility: string + codename: string + node_id: string + cluster_id: string + version: string + started_at: string + hostname: string + lifecycle: string + lb_status: string + timezone: string + operating_system: string + is_processing: boolean + is_leader: boolean +} diff --git a/graylog2-web-interface/src/stores/content-packs/CatalogStore.js b/graylog2-web-interface/src/stores/content-packs/CatalogStore.ts similarity index 84% rename from graylog2-web-interface/src/stores/content-packs/CatalogStore.js rename to graylog2-web-interface/src/stores/content-packs/CatalogStore.ts index 8248afa50141..a88115e7c45f 100644 --- a/graylog2-web-interface/src/stores/content-packs/CatalogStore.js +++ b/graylog2-web-interface/src/stores/content-packs/CatalogStore.ts @@ -23,17 +23,25 @@ import fetch from 'logic/rest/FetchProvider'; import EntityIndex from 'logic/content-packs/EntityIndex'; import { singletonStore, singletonActions } from 'logic/singleton'; +type RequestedEntities = { entities: Array<{ id: string, type: string }> }; +type Actions = { + showEntityIndex: () => Promise, + getSelectedEntities: (requestedEntities: RequestedEntities) => Promise<{ entities: Array }>, +} export const CatalogActions = singletonActions( 'core.Catalog', - () => Reflux.createActions({ + () => Reflux.createActions({ showEntityIndex: { asyncResult: true }, getSelectedEntities: { asyncResult: true }, }), ); +type StoreState = { + entityIndex: { [category: string]: Array }, +} export const CatalogStore = singletonStore( 'core.Catalog', - () => Reflux.createStore({ + () => Reflux.createStore({ listenables: [CatalogActions], getInitialState() { diff --git a/graylog2-web-interface/src/stores/content-packs/ContentPacksStore.js b/graylog2-web-interface/src/stores/content-packs/ContentPacksStore.ts similarity index 81% rename from graylog2-web-interface/src/stores/content-packs/ContentPacksStore.js rename to graylog2-web-interface/src/stores/content-packs/ContentPacksStore.ts index 9a49193752a2..06ea6bddb9de 100644 --- a/graylog2-web-interface/src/stores/content-packs/ContentPacksStore.js +++ b/graylog2-web-interface/src/stores/content-packs/ContentPacksStore.ts @@ -21,10 +21,23 @@ import ApiRoutes from 'routing/ApiRoutes'; import fetch from 'logic/rest/FetchProvider'; import ContentPackRevisions from 'logic/content-packs/ContentPackRevisions'; import { singletonStore, singletonActions } from 'logic/singleton'; - +import type { ContentPackMetadata, ContentPackInstallation } from 'components/content-packs/Types'; + +type Actions = { + create: (pack: string) => Promise, + list: () => Promise, + get: (id: string) => Promise<{ contentPackRevisions: ContentPackRevisions }>, + getRev: () => Promise, + delete: (id: string) => Promise, + deleteRev: (id: string, revision: number) => Promise, + install: (contentPackId: string, contentPackRev: string, parameters: {}) => Promise, + installList: (id: string) => Promise, + uninstall: (uninstallContentPackId: string, uninstallInstallId: string) => Promise, + uninstallDetails: (id: string, installId: string) => Promise, +}; export const ContentPacksActions = singletonActions( 'core.ContentPacks', - () => Reflux.createActions({ + () => Reflux.createActions({ create: { asyncResult: true }, list: { asyncResult: true }, get: { asyncResult: true }, @@ -38,9 +51,19 @@ export const ContentPacksActions = singletonActions( }), ); +type StoreState = { + contentPack: unknown, + contentPackMetadata: ContentPackMetadata, + contentPacks: Array, + installations: Array, + uninstallEntities: unknown, + contentPackRevisions: ContentPackRevisions, + selectedVersion: unknown, + constraints: unknown, +} export const ContentPacksStore = singletonStore( 'core.ContentPacks', - () => Reflux.createStore({ + () => Reflux.createStore({ listenables: [ContentPacksActions], getInitialState() { diff --git a/graylog2-web-interface/src/stores/lookup-tables/LookupTableCachesStore.js b/graylog2-web-interface/src/stores/lookup-tables/LookupTableCachesStore.ts similarity index 88% rename from graylog2-web-interface/src/stores/lookup-tables/LookupTableCachesStore.js rename to graylog2-web-interface/src/stores/lookup-tables/LookupTableCachesStore.ts index 31d47b5db102..4625b08df1e7 100644 --- a/graylog2-web-interface/src/stores/lookup-tables/LookupTableCachesStore.js +++ b/graylog2-web-interface/src/stores/lookup-tables/LookupTableCachesStore.ts @@ -20,10 +20,21 @@ import UserNotification from 'util/UserNotification'; import * as URLUtils from 'util/URLUtils'; import fetch from 'logic/rest/FetchProvider'; import { singletonStore, singletonActions } from 'logic/singleton'; - +import type { LookupTableCache } from 'logic/lookup-tables/types'; + +type Actions = { + searchPaginated: (page: number, perPage: number, query?: string) => Promise, + reloadPage: () => Promise, + get: (idOrName: string) => Promise, + create: (cache: LookupTableCache) => Promise, + update: (cache: LookupTableCache) => Promise, + getTypes: () => Promise, + delete: (idOrName: string) => Promise, + validate: (cache: LookupTableCache) => Promise, +} export const LookupTableCachesActions = singletonActions( 'core.LookupTableCaches', - () => Reflux.createActions({ + () => Reflux.createActions({ searchPaginated: { asyncResult: true }, reloadPage: { asyncResult: true }, get: { asyncResult: true }, @@ -35,9 +46,19 @@ export const LookupTableCachesActions = singletonActions( }), ); +type StoreState = { + caches: LookupTableCache[], + pagination: { + page: number, + per_page: number, + total: number, + count: number, + query: string | null + } +} export const LookupTableCachesStore = singletonStore( 'core.LookupTableCaches', - () => Reflux.createStore({ + () => Reflux.createStore({ listenables: [LookupTableCachesActions], cache: null, caches: null, diff --git a/graylog2-web-interface/src/stores/lookup-tables/LookupTableDataAdaptersStore.js b/graylog2-web-interface/src/stores/lookup-tables/LookupTableDataAdaptersStore.ts similarity index 88% rename from graylog2-web-interface/src/stores/lookup-tables/LookupTableDataAdaptersStore.js rename to graylog2-web-interface/src/stores/lookup-tables/LookupTableDataAdaptersStore.ts index 2aa51e0c10db..1a0064481542 100644 --- a/graylog2-web-interface/src/stores/lookup-tables/LookupTableDataAdaptersStore.js +++ b/graylog2-web-interface/src/stores/lookup-tables/LookupTableDataAdaptersStore.ts @@ -20,10 +20,22 @@ import UserNotification from 'util/UserNotification'; import * as URLUtils from 'util/URLUtils'; import fetch from 'logic/rest/FetchProvider'; import { singletonStore, singletonActions } from 'logic/singleton'; - +import type { LookupTableAdapter } from 'logic/lookup-tables/types'; + +type Actions = { + create: (dataAdapter: LookupTableAdapter) => Promise, + delete: (idOrName: string) => Promise, + get: (idOrName: string) => Promise, + getTypes: () => Promise, + lookup: (idOrName: string, key: string) => Promise, + reloadPage: () => Promise, + searchPaginated: (page: number, perPage: number, query?: string) => Promise, + update: (dataAdapter: LookupTableAdapter) => Promise, + validate: (dataAdapter: LookupTableAdapter) => Promise, +} export const LookupTableDataAdaptersActions = singletonActions( 'core.LookupTableDataAdapters', - () => Reflux.createActions({ + () => Reflux.createActions({ searchPaginated: { asyncResult: true }, reloadPage: { asyncResult: true }, get: { asyncResult: true }, @@ -36,9 +48,20 @@ export const LookupTableDataAdaptersActions = singletonActions( }), ); +type StoreState = { + dataAdapters: LookupTableAdapter[], + dataAdapter: LookupTableAdapter, + pagination: { + page: number, + per_page: number, + total: number, + count: number, + query: string | null + } +} export const LookupTableDataAdaptersStore = singletonStore( 'core.LookupTableDataAdapters', - () => Reflux.createStore({ + () => Reflux.createStore({ listenables: [LookupTableDataAdaptersActions], dataAdapter: null, dataAdapters: undefined, diff --git a/graylog2-web-interface/src/stores/pipelines/PipelineConnectionsStore.ts b/graylog2-web-interface/src/stores/pipelines/PipelineConnectionsStore.ts index f09616d46759..acf925214c03 100644 --- a/graylog2-web-interface/src/stores/pipelines/PipelineConnectionsStore.ts +++ b/graylog2-web-interface/src/stores/pipelines/PipelineConnectionsStore.ts @@ -25,7 +25,7 @@ import { singletonStore, singletonActions } from 'logic/singleton'; type PipelineConnectionsActionsType = { list: () => Promise, connectToStream: () => Promise, - connectToPipeline: () => Promise, + connectToPipeline: (connections: PipelineConnectionsType) => Promise, } export const PipelineConnectionsActions = singletonActions( 'core.PipelineConnections', @@ -48,7 +48,7 @@ type PipelineReverseConnectionsType = { }; type PipelineConnectionsStoreState = { - connections: any, + connections: PipelineConnectionsType[], } export const PipelineConnectionsStore = singletonStore( 'core.PipelineConnections', diff --git a/graylog2-web-interface/src/stores/pipelines/PipelinesStore.ts b/graylog2-web-interface/src/stores/pipelines/PipelinesStore.ts index 92e1f9d54b78..3b5e047d46f1 100644 --- a/graylog2-web-interface/src/stores/pipelines/PipelinesStore.ts +++ b/graylog2-web-interface/src/stores/pipelines/PipelinesStore.ts @@ -29,9 +29,9 @@ type PipelinesActionsType = { delete: (id: string) => Promise, list: () => Promise, listPaginated: (pagination: Pagination) => Promise, - get: () => Promise, - save: () => Promise, - update: () => Promise, + get: (id: string) => Promise, + save: (pipeline: PipelineType) => Promise, + update: (pipeline: PipelineType) => Promise, parse: () => Promise, } export const PipelinesActions = singletonActions( @@ -81,9 +81,12 @@ const listFailCallback = (error: Error) => { 'Could not retrieve processing pipelines'); }; +type StoreState = { + pipelines: Array, +} export const PipelinesStore = singletonStore( 'core.Pipelines', - () => Reflux.createStore({ + () => Reflux.createStore({ listenables: [PipelinesActions], pipelines: undefined, diff --git a/graylog2-web-interface/src/stores/sidecars/CollectorConfigurationsStore.js b/graylog2-web-interface/src/stores/sidecars/CollectorConfigurationsStore.ts similarity index 89% rename from graylog2-web-interface/src/stores/sidecars/CollectorConfigurationsStore.js rename to graylog2-web-interface/src/stores/sidecars/CollectorConfigurationsStore.ts index 48ad04dc0297..5e6a478b10e0 100644 --- a/graylog2-web-interface/src/stores/sidecars/CollectorConfigurationsStore.js +++ b/graylog2-web-interface/src/stores/sidecars/CollectorConfigurationsStore.ts @@ -22,10 +22,23 @@ import * as URLUtils from 'util/URLUtils'; import UserNotification from 'util/UserNotification'; import fetch from 'logic/rest/FetchProvider'; import { singletonStore, singletonActions } from 'logic/singleton'; - +import type { Configuration, ConfigurationSidecarsResponse } from 'components/sidecars/types'; + +type Actions = { + all: () => Promise, + list: (opts: { query?: string, page?: number, pageSize?: number }) => Promise, + getConfiguration: (id: string) => Promise, + getConfigurationSidecars: (id: string) => Promise, + createConfiguration: (config: Configuration) => Promise, + updateConfiguration: (config: Configuration) => Promise, + renderPreview: (template: string) => Promise<{ preview: string }>, + copyConfiguration: (id: string, name: string) => Promise, + delete: (config: Configuration) => Promise, + validate: (config: Partial) => Promise<{ errors: { name: string[] }, failed: boolean }>, +} export const CollectorConfigurationsActions = singletonActions( 'core.CollectorConfigurations', - () => Reflux.createActions({ + () => Reflux.createActions({ all: { asyncResult: true }, list: { asyncResult: true }, getConfiguration: { asyncResult: true }, @@ -39,9 +52,19 @@ export const CollectorConfigurationsActions = singletonActions( }), ); +type StoreState = { + configurations: Configuration[], + pagination: { + page: number, + pageSize: number, + total: number, + }, + total: number, + query: string | undefined, +} export const CollectorConfigurationsStore = singletonStore( 'core.CollectorConfigurations', - () => Reflux.createStore({ + () => Reflux.createStore({ listenables: [CollectorConfigurationsActions], sourceUrl: '/sidecar', configurations: undefined, diff --git a/graylog2-web-interface/src/stores/sidecars/CollectorsStore.js b/graylog2-web-interface/src/stores/sidecars/CollectorsStore.ts similarity index 89% rename from graylog2-web-interface/src/stores/sidecars/CollectorsStore.js rename to graylog2-web-interface/src/stores/sidecars/CollectorsStore.ts index 3352c8ab8145..f0699a36f12c 100644 --- a/graylog2-web-interface/src/stores/sidecars/CollectorsStore.js +++ b/graylog2-web-interface/src/stores/sidecars/CollectorsStore.ts @@ -22,10 +22,21 @@ import * as URLUtils from 'util/URLUtils'; import fetch from 'logic/rest/FetchProvider'; import UserNotification from 'util/UserNotification'; import { singletonStore, singletonActions } from 'logic/singleton'; - +import type { Collector } from 'components/sidecars/types'; + +type Actions = { + getCollector: (id: string) => Promise, + all: () => Promise<{ collectors: Array }>, + list: (opts: { query?: string, page?: number, pageSize?: number }) => Promise, + create: (collector: Collector) => Promise, + update: (collector: Collector) => Promise, + delete: (collector: Collector) => Promise, + copy: (id: string, name: string) => Promise, + validate: (collector: Collector) => Promise<{ errors: { name: string[] }, failed: boolean }>, +} export const CollectorsActions = singletonActions( 'core.Collectors', - () => Reflux.createActions({ + () => Reflux.createActions({ getCollector: { asyncResult: true }, all: { asyncResult: true }, list: { asyncResult: true }, @@ -37,9 +48,19 @@ export const CollectorsActions = singletonActions( }), ); +type StoreState = { + query: string | undefined, + collectors: Array, + pagination: { + page: number, + pageSize: number, + total: number, + }, + total: number, +} export const CollectorsStore = singletonStore( 'core.Collectors', - () => Reflux.createStore({ + () => Reflux.createStore({ listenables: [CollectorsActions], sourceUrl: '/sidecar', collectors: undefined, @@ -230,9 +251,9 @@ export const CollectorsStore = singletonStore( CollectorsActions.copy.promise(promise); }, - validate(collector) { + validate(collector: Collector) { // set minimum api defaults for faster validation feedback - const payload = { + const payload: Partial = { id: ' ', service_type: 'exec', executable_path: ' ', diff --git a/graylog2-web-interface/src/stores/sidecars/SidecarsAdministrationStore.js b/graylog2-web-interface/src/stores/sidecars/SidecarsAdministrationStore.ts similarity index 82% rename from graylog2-web-interface/src/stores/sidecars/SidecarsAdministrationStore.js rename to graylog2-web-interface/src/stores/sidecars/SidecarsAdministrationStore.ts index 2ec16a3a73a7..7557e1e8ce36 100644 --- a/graylog2-web-interface/src/stores/sidecars/SidecarsAdministrationStore.js +++ b/graylog2-web-interface/src/stores/sidecars/SidecarsAdministrationStore.ts @@ -21,19 +21,48 @@ import * as URLUtils from 'util/URLUtils'; import UserNotification from 'util/UserNotification'; import { fetchPeriodically } from 'logic/rest/FetchProvider'; import { singletonStore, singletonActions } from 'logic/singleton'; +import type { SidecarSummary } from 'components/sidecars/types'; +type Actions = { + list: (opts: { query: string, page: number, pageSize: number, filters?: {} }) => Promise, + refreshList: () => Promise, + setAction: (action: string, selectedCollectors: { [sidecarId: string]: string[] }) => Promise, +} export const SidecarsAdministrationActions = singletonActions( 'core.SidecarsAdministration', - () => Reflux.createActions({ + () => Reflux.createActions({ list: { asyncResult: true }, refreshList: { asyncResult: true }, setAction: { asyncResult: true }, }), ); +type StoreState = { + pagination: { + count: number, + page: number, + pageSize: number, + total: number, + perPage: number + }, + sidecars: Array, + filters: {}, + query: string, +} +type Response = { + sidecars: Array, + query: string, + filters: {}, + pagination: { + total: number, + count: number, + page: number, + per_page: number, + } +} export const SidecarsAdministrationStore = singletonStore( 'core.SidecarsAdministration', - () => Reflux.createStore({ + () => Reflux.createStore({ listenables: [SidecarsAdministrationActions], sourceUrl: '/sidecar', sidecars: undefined, @@ -75,7 +104,7 @@ export const SidecarsAdministrationStore = singletonStore( const promise = fetchPeriodically('POST', URLUtils.qualifyUrl(`${this.sourceUrl}/administration`), body); promise.then( - (response) => { + (response: Response) => { this.sidecars = response.sidecars; this.query = response.query; this.filters = response.filters; diff --git a/graylog2-web-interface/src/stores/sidecars/SidecarsStore.js b/graylog2-web-interface/src/stores/sidecars/SidecarsStore.ts similarity index 81% rename from graylog2-web-interface/src/stores/sidecars/SidecarsStore.js rename to graylog2-web-interface/src/stores/sidecars/SidecarsStore.ts index 0c790dc0c13d..817ebd31fe30 100644 --- a/graylog2-web-interface/src/stores/sidecars/SidecarsStore.js +++ b/graylog2-web-interface/src/stores/sidecars/SidecarsStore.ts @@ -21,10 +21,26 @@ import * as URLUtils from 'util/URLUtils'; import UserNotification from 'util/UserNotification'; import fetch, { fetchPeriodically } from 'logic/rest/FetchProvider'; import { singletonStore, singletonActions } from 'logic/singleton'; - +import type { SidecarSummary, SidecarCollectorPairType, Configuration } from 'components/sidecars/types'; + +export type PaginationOptions = { + query: string, + sortField?: string, + order?: string, + pageSize: number, + page: number, + onlyActive: string | boolean, +} +type Actions = { + listPaginated: (options: Partial) => Promise, + getSidecar: (id: string) => Promise, + getSidecarActions: () => Promise, + restartCollector: () => Promise, + assignConfigurations: (selectedSidecars: SidecarCollectorPairType[], selectedConfigurations: Configuration[]) => Promise, +} export const SidecarsActions = singletonActions( 'core.Sidecars', - () => Reflux.createActions({ + () => Reflux.createActions({ listPaginated: { asyncResult: true }, getSidecar: { asyncResult: true }, getSidecarActions: { asyncResult: true }, @@ -33,9 +49,37 @@ export const SidecarsActions = singletonActions( }), ); +type StoreState = { + sidecars: SidecarSummary[], + onlyActive: string, + pagination: { + count: number, + page: number, + pageSize: number, + total: undefined, + }, + query: string | undefined, + sort: { + field: string, + order: string, + } +} +type Response = { + sidecars: SidecarSummary[], + query: string, + only_active: boolean, + pagination: { + total: number, + count: number, + page: number, + per_page: number, + }, + sort: string, + order: string, +} export const SidecarsStore = singletonStore( 'core.Sidecars', - () => Reflux.createStore({ + () => Reflux.createStore({ listenables: [SidecarsActions], sourceUrl: '/sidecars', sidecars: undefined, @@ -80,7 +124,7 @@ export const SidecarsStore = singletonStore( const promise = fetchPeriodically('GET', URLUtils.qualifyUrl(uri)); promise.then( - (response) => { + (response: Response) => { this.sidecars = response.sidecars; this.query = response.query; this.onlyActive = response.only_active; @@ -127,11 +171,10 @@ export const SidecarsStore = singletonStore( }, restartCollector(sidecarId, collector) { - const action = {}; - - action.collector = collector; - action.properties = {}; - action.properties.restart = true; + const action = { + collector, + properties: { restart: true }, + }; const promise = fetch('PUT', URLUtils.qualifyUrl(`${this.sourceUrl}/${sidecarId}/action`), [action]); promise diff --git a/graylog2-web-interface/src/stores/system/LoggersStore.js b/graylog2-web-interface/src/stores/system/LoggersStore.js deleted file mode 100644 index 3657dd61910f..000000000000 --- a/graylog2-web-interface/src/stores/system/LoggersStore.js +++ /dev/null @@ -1,87 +0,0 @@ -/* - * 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 Reflux from 'reflux'; - -import * as URLUtils from 'util/URLUtils'; -import ApiRoutes from 'routing/ApiRoutes'; -import fetch from 'logic/rest/FetchProvider'; -import { singletonStore, singletonActions } from 'logic/singleton'; - -export const LoggersActions = singletonActions( - 'core.Loggers', - () => Reflux.createActions({ - loggers: { asyncResult: true }, - subsystems: { asyncResult: true }, - setSubsystemLoggerLevel: { asyncResult: true }, - }), -); - -export const LoggersStore = singletonStore( - 'core.Loggers', - () => Reflux.createStore({ - listenables: [LoggersActions], - state: { - availableLoglevels: [ - 'fatal', - 'error', - 'warn', - 'info', - 'debug', - 'trace', - ], - }, - init() { - this.loggers(); - this.subsystems(); - }, - getInitialState() { - return this.state; - }, - loggers() { - const url = URLUtils.qualifyUrl(ApiRoutes.ClusterLoggersResource.loggers().url); - const promise = fetch('GET', url).then((response) => { - this.state.loggers = response; - this.trigger(this.state); - - return response; - }); - - LoggersActions.loggers.promise(promise); - }, - subsystems() { - const url = URLUtils.qualifyUrl(ApiRoutes.ClusterLoggersResource.subsystems().url); - const promise = fetch('GET', url).then((response) => { - this.state.subsystems = response; - this.trigger(this.state); - - return response; - }); - - LoggersActions.loggers.promise(promise); - }, - setSubsystemLoggerLevel(nodeId, subsystem, level) { - const url = URLUtils.qualifyUrl(ApiRoutes.ClusterLoggersResource.setSubsystemLoggerLevel(nodeId, subsystem, level).url); - const promise = fetch('PUT', url); - - promise.then(() => { - this.init(); - }); - - LoggersActions.setSubsystemLoggerLevel.promise(promise); - }, - }), -); diff --git a/graylog2-web-interface/src/stores/system/SystemStore.ts b/graylog2-web-interface/src/stores/system/SystemStore.ts index da50fd21e343..5ab631da87c0 100644 --- a/graylog2-web-interface/src/stores/system/SystemStore.ts +++ b/graylog2-web-interface/src/stores/system/SystemStore.ts @@ -26,7 +26,21 @@ export type Locales = { display_name: string, } type SystemStoreState = { - system: {}, + system: { + cluster_id: string, + codename: string, + facility: string, + hostname: string, + is_leader: boolean, + is_processing: boolean, + lb_status: string, + lifecycle: string, + node_id: string, + operating_system: string, + started_at: string, + timezone: string, + version: string, + }, locales: Array, } export const SystemStore = singletonStore( diff --git a/graylog2-web-interface/src/stores/tools/ToolsStore.ts b/graylog2-web-interface/src/stores/tools/ToolsStore.ts index 2f3d8038d422..78ba36f909da 100644 --- a/graylog2-web-interface/src/stores/tools/ToolsStore.ts +++ b/graylog2-web-interface/src/stores/tools/ToolsStore.ts @@ -79,6 +79,7 @@ const ToolsStore = { key_whitespace_replacement: string, key_prefix: string, string: string, + matches: { [key: string]: string } }> { const { url } = ApiRoutes.ToolsApiController.jsonTest(); const payload = { @@ -221,6 +222,8 @@ const ToolsStore = { start: number, end: number, string: string, + successful: boolean, + cut: string, }> { const { url } = ApiRoutes.ToolsApiController.substringTest(); const payload = { diff --git a/graylog2-web-interface/src/threatintel/bindings.jsx b/graylog2-web-interface/src/threatintel/bindings.tsx similarity index 95% rename from graylog2-web-interface/src/threatintel/bindings.jsx rename to graylog2-web-interface/src/threatintel/bindings.tsx index 743275bd8119..f989e52f5d0e 100644 --- a/graylog2-web-interface/src/threatintel/bindings.jsx +++ b/graylog2-web-interface/src/threatintel/bindings.tsx @@ -14,8 +14,8 @@ * along with this program. If not, see * . */ -// eslint-disable-next-line no-unused-vars, no-unused-vars -import { PluginManifest, PluginStore } from 'graylog-web-plugin/plugin'; + +import type { PluginExports } from 'graylog-web-plugin/plugin'; import ThreatIntelPluginConfig from './components/ThreatIntelPluginConfig'; import { @@ -40,7 +40,7 @@ import { } from './components/adapters/abusech/index'; import { OTXAdapterDocumentation, OTXAdapterFieldSet, OTXAdapterSummary } from './components/adapters/otx'; -const bindings = { +const bindings: PluginExports = { systemConfigurations: [ { component: ThreatIntelPluginConfig, diff --git a/graylog2-web-interface/src/threatintel/components/ThreatIntelPluginConfig.jsx b/graylog2-web-interface/src/threatintel/components/ThreatIntelPluginConfig.jsx deleted file mode 100644 index 4f38ef3b37ed..000000000000 --- a/graylog2-web-interface/src/threatintel/components/ThreatIntelPluginConfig.jsx +++ /dev/null @@ -1,172 +0,0 @@ -/* - * 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 PropTypes from 'prop-types'; -import React from 'react'; -// eslint-disable-next-line no-restricted-imports -import createReactClass from 'create-react-class'; - -import { Button, BootstrapModalForm, Input } from 'components/bootstrap'; -import { IfPermitted } from 'components/common'; -import ObjectUtils from 'util/ObjectUtils'; -import withTelemetry from 'logic/telemetry/withTelemetry'; -import { TELEMETRY_EVENT_TYPE } from 'logic/telemetry/Constants'; - -const ThreatIntelPluginConfig = createReactClass({ - // eslint-disable-next-line react/no-unused-class-component-methods - displayName: 'ThreatIntelPluginConfig', - - // eslint-disable-next-line react/no-unused-class-component-methods - propTypes: { - config: PropTypes.object, - updateConfig: PropTypes.func.isRequired, - sendTelemetry: PropTypes.func.isRequired, - }, - - getDefaultProps() { - return { - config: { - tor_enabled: false, - spamhaus_enabled: false, - abusech_ransom_enabled: false, - }, - }; - }, - - getInitialState() { - const { config } = this.props; - - return { - config: ObjectUtils.clone(config), - threatintelConfigModal: false, - }; - }, - - UNSAFE_componentWillReceiveProps(newProps) { - this.setState({ config: ObjectUtils.clone(newProps.config) }); - }, - - _updateConfigField(field, value) { - const { config } = this.state; - const update = ObjectUtils.clone(config); - update[field] = value; - this.setState({ config: update }); - }, - - _onCheckboxClick(field, ref) { - return () => { - this._updateConfigField(field, this[ref].getChecked()); - }; - }, - - // eslint-disable-next-line react/no-unused-class-component-methods - _onSelect(field) { - return (selection) => { - this._updateConfigField(field, selection); - }; - }, - - // eslint-disable-next-line react/no-unused-class-component-methods - _onUpdate(field) { - return (e) => { - this._updateConfigField(field, e.target.value); - }; - }, - - _openModal() { - this.setState({ threatintelConfigModal: true }); - }, - - _closeModal() { - this.setState({ threatintelConfigModal: false }); - }, - - _resetConfig() { - // Reset to initial state when the modal is closed without saving. - this.setState(this.getInitialState()); - }, - - _saveConfig() { - const { updateConfig, sendTelemetry } = this.props; - - sendTelemetry(TELEMETRY_EVENT_TYPE.CONFIGURATIONS.THREATINTEL_CONFIGURATION_UPDATED, { - app_pathname: 'configurations', - app_section: 'threat-intel', - }); - - updateConfig(this.state.config).then(() => { - this._closeModal(); - }); - }, - - render() { - return ( -
    -

    Threat Intelligence Lookup Configuration

    - -

    - Configuration for threat intelligence lookup plugin. -

    - -
    -
    Tor exit nodes:
    -
    {this.state.config.tor_enabled === true ? 'Enabled' : 'Disabled'}
    - -
    Spamhaus:
    -
    {this.state.config.spamhaus_enabled === true ? 'Enabled' : 'Disabled'}
    -
    - - - - - - -
    - { - // eslint-disable-next-line react/no-unused-class-component-methods - this.torEnabled = elem; - }} - label="Allow Tor exit node lookups?" - help="Enable to include Tor exit node lookup in global pipeline function, disabling also stops refreshing the data." - name="tor_enabled" - checked={this.state.config.tor_enabled} - onChange={this._onCheckboxClick('tor_enabled', 'torEnabled')} /> - - { - // eslint-disable-next-line react/no-unused-class-component-methods - this.spamhausEnabled = elem; - }} - label="Allow Spamhaus DROP/EDROP lookups?" - help="Enable to include Spamhaus lookup in global pipeline function, disabling also stops refreshing the data." - name="tor_enabled" - checked={this.state.config.spamhaus_enabled} - onChange={this._onCheckboxClick('spamhaus_enabled', 'spamhausEnabled')} /> -
    -
    -
    - ); - }, -}); - -export default withTelemetry(ThreatIntelPluginConfig); diff --git a/graylog2-web-interface/src/threatintel/components/ThreatIntelPluginConfig.tsx b/graylog2-web-interface/src/threatintel/components/ThreatIntelPluginConfig.tsx new file mode 100644 index 000000000000..f09cd30fd9b9 --- /dev/null +++ b/graylog2-web-interface/src/threatintel/components/ThreatIntelPluginConfig.tsx @@ -0,0 +1,126 @@ +/* + * 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 { useState } from 'react'; + +import { Button, BootstrapModalForm, Input } from 'components/bootstrap'; +import { IfPermitted } from 'components/common'; +import ObjectUtils from 'util/ObjectUtils'; +import { TELEMETRY_EVENT_TYPE } from 'logic/telemetry/Constants'; +import type { SystemConfigurationComponentProps } from 'views/types'; +import useSendTelemetry from 'logic/telemetry/useSendTelemetry'; + +type Config = { + tor_enabled: boolean, + spamhaus_enabled: boolean, +}; +type Props = SystemConfigurationComponentProps; + +const defaultConfig = { + tor_enabled: false, + spamhaus_enabled: false, +}; + +const ThreatIntelPluginConfig = ({ config: initialConfig = defaultConfig, updateConfig }: Props) => { + const [showModal, setShowModal] = useState(false); + const [config, setConfig] = useState(ObjectUtils.clone(initialConfig)); + const sendTelemetry = useSendTelemetry(); + + const _updateConfigField = (field: string, value: boolean) => { + const newConfig = { + ...config, + [field]: value, + }; + setConfig(newConfig); + }; + + const _onCheckboxClick = (e: React.ChangeEvent) => { + _updateConfigField(e.target.name, e.target.checked); + }; + + const _openModal = () => { + setShowModal(true); + }; + + const _closeModal = () => { + setShowModal(false); + }; + + const _resetConfig = () => { + // Reset to initial state when the modal is closed without saving. + setConfig(initialConfig); + }; + + const _saveConfig = () => { + sendTelemetry(TELEMETRY_EVENT_TYPE.CONFIGURATIONS.THREATINTEL_CONFIGURATION_UPDATED, { + app_pathname: 'configurations', + app_section: 'threat-intel', + }); + + updateConfig(config).then(() => { + _closeModal(); + }); + }; + + return ( +
    +

    Threat Intelligence Lookup Configuration

    + +

    + Configuration for threat intelligence lookup plugin. +

    + +
    +
    Tor exit nodes:
    +
    {config.tor_enabled === true ? 'Enabled' : 'Disabled'}
    + +
    Spamhaus:
    +
    {config.spamhaus_enabled === true ? 'Enabled' : 'Disabled'}
    +
    + + + + + + +
    + + + +
    +
    +
    + ); +}; + +export default ThreatIntelPluginConfig; diff --git a/graylog2-web-interface/src/views/types.ts b/graylog2-web-interface/src/views/types.ts index 6a7632374af3..9c1720fd57b2 100644 --- a/graylog2-web-interface/src/views/types.ts +++ b/graylog2-web-interface/src/views/types.ts @@ -195,9 +195,9 @@ export interface ExportFormat { formatSpecificFileDownloader?: (format: string, widget: Widget, view: View, executionState: SearchExecutionState, currentUser: User, currentQuery: Query, exportPayload: ExportPayload,) => Promise } -export interface SystemConfigurationComponentProps { - config: any, - updateConfig: (newConfig: any) => any, +export interface SystemConfigurationComponentProps { + config: T, + updateConfig: (newConfig: T) => any, } export interface SystemConfiguration {