Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Groundwork for events and investigations widget export #19597

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions changelog/unreleased/issue-19580.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
type = "Added"
message = "Add export feature to events and investigations widget"

issues = ["19580"]
pulls = ["19597", "graylog-plugin-enterprise#7587"]
2 changes: 1 addition & 1 deletion graylog2-web-interface/src/util/FileDownloadUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@
* <http://www.mongodb.com/licensing/server-side-public-license>.
*/

import UserNotification from 'util/UserNotification';
import { fetchBlobFile, fetchFile } from 'logic/rest/FetchProvider';
import UserNotification from 'preflight/util/UserNotification';

export const createLinkAndDownload = (href: string, fileName: string) => {
const a = document.createElement('a');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ import { TELEMETRY_EVENT_TYPE } from 'logic/telemetry/Constants';
import useSendTelemetry from 'logic/telemetry/useSendTelemetry';
import { getPathnameWithoutId } from 'util/URLUtils';
import useLocation from 'routing/useLocation';
import useCurrentUser from 'hooks/useCurrentUser';
import useCurrentQuery from 'views/logic/queries/useCurrentQuery';

import ExportSettings from './ExportSettings';
import ExportStrategy from './ExportStrategy';
Expand Down Expand Up @@ -89,6 +91,8 @@ const ExportModal = ({ closeModal, view, directExportWidgetId }: Props) => {
downloadFile,
} = ExportStrategy.createExportStrategy(view.type);
const exportableWidgets = viewStates.map((state) => state.widgets.filter((widget) => widget.isExportable).toList()).toList().flatten(true) as List<Widget>;
const currentUser = useCurrentUser();
const currentQuery = useCurrentQuery();

const [loading, setLoading] = useState(false);
const initialSelectedWidget = initialWidget(exportableWidgets, directExportWidgetId);
Expand All @@ -104,7 +108,7 @@ const ExportModal = ({ closeModal, view, directExportWidgetId }: Props) => {

setLoading(true);

return startDownload(format, downloadFile, view, executionState, selectedWidget, selectedFields, limit, customSettings)
return startDownload(format, downloadFile, view, executionState, selectedWidget, selectedFields, limit, customSettings, currentUser, currentQuery)
.then(closeModal)
.finally(() => setLoading(false));
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ const _initialSearchWidget = (widgets, directExportWidgetId) => {
return null;
};

const formatDefinition = (format: string) => {
export const formatDefinition = (format: string) => {
const formats = PluginStore.exports('views.export.formats');

const definition = formats.find(({ type }) => (type === format));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ import ViewTypeLabel from 'views/components/ViewTypeLabel';
import type SearchExecutionState from 'views/logic/search/SearchExecutionState';
import type { SearchType } from 'views/logic/queries/SearchType';
import type { ExportSettings } from 'views/components/ExportSettingsContext';
import { formatDefinition } from 'views/components/export/ExportStrategy';
import type User from 'logic/users/User';

const getFilename = (view, selectedWidget) => {
let filename = 'search-result';
Expand All @@ -51,7 +53,11 @@ const startDownload = (
selectedFields: { field: string }[],
limit: number | undefined | null,
customSettings: ExportSettings,
currentUser?: User | undefined,
currentQuery?: Query | undefined,
) => {
const { formatSpecificFileDownloader } = formatDefinition(format);

const payload: ExportPayload = {
execution_state: executionState,
fields_in_order: selectedFields.map((field) => field.field),
Expand All @@ -61,6 +67,10 @@ const startDownload = (
const searchType: SearchType | undefined | null = selectedWidget ? view.getSearchTypeByWidgetId(selectedWidget.id) : undefined;
const filename = getFilename(view, selectedWidget);

if (typeof formatSpecificFileDownloader === 'function') {
return formatSpecificFileDownloader(format, selectedWidget, view, executionState, currentUser, currentQuery, payload);
}

return downloadFile(format, payload, view.search.queries, searchType, view.search.id, filename);
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,12 @@ import AggregationWidget from 'views/logic/aggregationbuilder/AggregationWidget'
import type Widget from 'views/logic/widgets/Widget';
import type { WidgetActionType } from 'views/components/widgets/Types';
import ExportWidgetActionDelegate from 'views/components/widgets/ExportWidgetAction/ExportWidgetActionDelegate';
import EventsWidget from 'views/logic/widgets/events/EventsWidget';

const ExportWidgetAction: WidgetActionType = {
type: 'export-widget-action',
position: 'menu',
isHidden: (w: Widget) => (w.type !== AggregationWidget.type),
isHidden: (w: Widget) => ![AggregationWidget.type, EventsWidget.type].includes(w.type),
component: ExportWidgetActionDelegate,
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import ExportWidgetPlug from 'views/components/widgets/ExportWidgetAction/Export
import useWidgetExportActionComponent from 'views/components/widgets/useWidgetExportActionComponent';

const ExportWidgetActionDelegate = ({ widget, contexts, disabled }: WidgetMenuActionComponentProps) => {
const ExportActionComponent = useWidgetExportActionComponent();
const ExportActionComponent = useWidgetExportActionComponent(widget);
if (!ExportActionComponent) return <ExportWidgetPlug />;

return <ExportActionComponent widget={widget} contexts={contexts} disabled={disabled} />;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
* <http://www.mongodb.com/licensing/server-side-public-license>.
*/
import * as React from 'react';
import { useCallback, useState } from 'react';
import { useCallback, useContext, useEffect, useState } from 'react';
import styled from 'styled-components';

import type { WidgetComponentProps } from 'views/types';
Expand All @@ -29,6 +29,7 @@ import type EventsWidgetSortConfig from 'views/logic/widgets/events/EventsWidget
import useOnSearchExecution from 'views/hooks/useOnSearchExecution';
import useAutoRefresh from 'views/hooks/useAutoRefresh';
import ErrorWidget from 'views/components/widgets/ErrorWidget';
import RenderCompletionCallback from 'views/components/widgets/RenderCompletionCallback';

import EventsTable from './EventsTable';

Expand Down Expand Up @@ -102,6 +103,11 @@ const EventsList = ({ data, config, onConfigChange, setLoadingState }: WidgetCom

return onConfigChange(newConfig);
}, [config, onConfigChange]);
const onRenderComplete = useContext(RenderCompletionCallback);

useEffect(() => {
onRenderComplete();
}, [onRenderComplete]);

return (
<Container>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,14 @@
* along with this program. If not, see
* <http://www.mongodb.com/licensing/server-side-public-license>.
*/
import React from 'react';
import React, { useContext, useEffect } from 'react';
import styled from 'styled-components';

import type { WidgetComponentProps } from 'views/types';
import AutoFontSizer from 'views/components/visualizations/number/AutoFontSizer';
import { ElementDimensions } from 'components/common';
import type EventsWidgetConfig from 'views/logic/widgets/events/EventsWidgetConfig';
import RenderCompletionCallback from 'views/components/widgets/RenderCompletionCallback';

import type { EventsListResult } from './types';

Expand All @@ -30,16 +31,24 @@ const NumberBox = styled(ElementDimensions)`
padding-bottom: 10px;
`;

const EventsNumber = ({ data } : WidgetComponentProps<EventsWidgetConfig, EventsListResult>) => (
<NumberBox resizeDelay={20}>
{({ height, width }) => (
<AutoFontSizer height={height} width={width} center>
<div>
{data.totalResults}
</div>
</AutoFontSizer>
)}
</NumberBox>
);
const EventsNumber = ({ data } : WidgetComponentProps<EventsWidgetConfig, EventsListResult>) => {
const onRenderComplete = useContext(RenderCompletionCallback);

useEffect(() => {
onRenderComplete();
}, [onRenderComplete]);

return (
<NumberBox resizeDelay={20}>
{({ height, width }) => (
<AutoFontSizer height={height} width={width} center>
<div>
{data.totalResults}
</div>
</AutoFontSizer>
)}
</NumberBox>
);
};

export default EventsNumber;
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,18 @@
* <http://www.mongodb.com/licensing/server-side-public-license>.
*/
import usePluginEntities from 'hooks/usePluginEntities';
import { type WidgetActionType } from 'views/components/widgets/Types';
import type Widget from 'views/logic/widgets/Widget';

const useWidgetExportActionComponent = () => {
const exportAction = usePluginEntities('views.components.widgets.exportAction')?.[0];
const useWidgetExportActionComponent = (widget: Widget) => {
const exportActions = usePluginEntities('views.widgets.exportAction');

return exportAction && exportAction();
const widgetExportAction = exportActions && exportActions
// eslint-disable-next-line react-hooks/rules-of-hooks
.filter(({ action, useCondition }) => (typeof useCondition === 'function' && useCondition()) && action && (typeof action.isHidden !== 'function' || !action.isHidden(widget)))
.map(({ action }: { action: WidgetActionType }) => action);

return widgetExportAction?.[0]?.component;
};

export default useWidgetExportActionComponent;
8 changes: 5 additions & 3 deletions graylog2-web-interface/src/views/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import type { Reducer, AnyAction } from '@reduxjs/toolkit';
import type Widget from 'views/logic/widgets/Widget';
import type { ActionDefinition } from 'views/components/actions/ActionHandler';
import type { VisualizationComponent } from 'views/components/aggregationbuilder/AggregationBuilder';
import type { WidgetActionType, WidgetMenuActionComponentProps } from 'views/components/widgets/Types';
import type { WidgetActionType } from 'views/components/widgets/Types';
import type { Creator } from 'views/components/sidebar/create/AddWidgetButton';
import type { ViewHook } from 'views/logic/hooks/ViewHook';
import type WidgetConfig from 'views/logic/widgets/WidgetConfig';
Expand Down Expand Up @@ -58,6 +58,7 @@ import type { UndoRedoState } from 'views/logic/slices/undoRedoSlice';
import type { SearchExecutors } from 'views/logic/slices/searchExecutionSlice';
import type { JobIds } from 'views/stores/SearchJobs';
import type { FilterComponents, Attributes } from 'views/components/widgets/overview-configuration/filters/types';
import type { ExportPayload } from 'util/MessagesExportUtils';

export type ArrayElement<ArrayType extends readonly unknown[]> =
ArrayType extends readonly (infer ElementType)[] ? ElementType : never;
Expand Down Expand Up @@ -182,12 +183,13 @@ interface SearchType<T, R> {
defaults: {};
}

interface ExportFormat {
export interface ExportFormat {
type: string;
displayName: () => string;
disabled?: () => boolean;
mimeType: string;
fileExtension: string;
formatSpecificFileDownloader?: (format: string, widget: Widget, view: View, executionState: SearchExecutionState, currentUser: User, currentQuery: Query, exportPayload: ExportPayload,) => Promise<void>
}

export interface SystemConfigurationComponentProps {
Expand Down Expand Up @@ -474,7 +476,6 @@ declare module 'graylog-web-plugin/plugin' {
key: string,
}>;
'views.components.widgets.events.actions'?: Array<EventWidgetAction>;
'views.components.widgets.exportAction'?: Array<() => React.ComponentType<WidgetMenuActionComponentProps> | null>;
'views.components.searchActions'?: Array<SearchAction>;
'views.components.searchBar'?: Array<() => SearchBarControl | null>;
'views.components.saveViewForm'?: Array<() => SaveViewControls | null>;
Expand All @@ -492,6 +493,7 @@ declare module 'graylog-web-plugin/plugin' {
'views.hooks.removingWidget'?: Array<RemovingWidgetHook>;
'views.overrides.widgetEdit'?: Array<React.ComponentType<OverrideProps>>;
'views.widgets.actions'?: Array<WidgetActionType>;
'views.widgets.exportAction'?: Array<{ action: WidgetActionType, useCondition: () => boolean }>;
'views.reducers'?: Array<ViewsReducer>;
'views.requires.provided'?: Array<string>;
'views.queryInput.commands'?: Array<CustomCommand>;
Expand Down
Loading