Skip to content

Commit

Permalink
Create a new dashboards page that makes use of embeddable dashboards …
Browse files Browse the repository at this point in the history
…by reference (#6817)

* First iteration of poc

* first iteration of poc

* Working on how to load dashboards

* Logic to check if the dashboard exists and if not, create it

* borrar metodo existsOrCreateDashboard en deshuso al reformular logica

* logic for creating dashboards

* show dashboard in vulnerabilities

* dashboard component for vuls

* Adding the use of the dashboard component in the vulnerabilities module

* fixes in the two use cases

* change in the visualization

* update dashboard and visualization creation logic

* update the method name to create index pattern

* maintain visualization creation logic

* update request

* Using the endpoint to import and consume files from the backend

* organizing code

* complete logic in operation

* complete logic in operation

* delete folders and organize code

* rename file

* correct import

* clean code

* delete file

* Code restructure

* Code restructure

* Code restructure

* Code restructure

---------

Co-authored-by: Federico Rodriguez <[email protected]>
  • Loading branch information
chantal-kelm and asteriscos authored Aug 23, 2024
1 parent 0cf78ba commit f466e22
Show file tree
Hide file tree
Showing 8 changed files with 493 additions and 2 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
import React from 'react';
import { compose } from 'redux';
import { connect } from 'react-redux';
import { withGuardAsync } from '.';
import { getSavedObjects } from '../../../kibana-services';
import { SavedObject } from '../../../react-services';
import { NOT_TIME_FIELD_NAME_INDEX_PATTERN } from '../../../../common/constants';
import { EuiButton, EuiEmptyPrompt, EuiLink } from '@elastic/eui';
import { webDocumentationLink } from '../../../../common/services/web_documentation';
import { vulnerabilityDetection } from '../../../utils/applications';
import { LoadingSpinnerDataSource } from '../loading/loading-spinner-data-source';
import NavigationService from '../../../react-services/navigation-service';

const INDEX_PATTERN_CREATION_NO_INDEX = 'INDEX_PATTERN_CREATION_NO_INDEX';

async function checkExistenceIndexPattern(indexPatternID) {
return await getSavedObjects().client.get('index-pattern', indexPatternID);
}

async function checkExistenceIndices(indexPatternId) {
try {
const fields = await SavedObject.getIndicesFields(indexPatternId);
return { exist: true, fields };
} catch (error) {
return { exist: false };
}
}

async function createIndexPattern(indexPattern, fields) {
try {
await SavedObject.createSavedObject(
'index-pattern',
indexPattern,
{
attributes: {
title: indexPattern,
timeFieldName: NOT_TIME_FIELD_NAME_INDEX_PATTERN,
},
},
fields,
);
await SavedObject.validateIndexPatternSavedObjectCanBeFound([indexPattern]);
} catch (error) {
return { error: error.message };
}
}

export async function createDashboard() {
try {
// Create the dashboard
const result = await SavedObject.createSavedObjectDashboard();

let targetDashboard = result?.data?.successResults?.find(
dashboard => dashboard.id === '94febc80-55a2-11ef-a580-5b5ba88681be',
);

if (result) {
return targetDashboard;
} else {
console.error('Failed to create dashboard.');
return null;
}
} catch (error) {
console.error('Error creating dashboard:', error);
return null;
}
}

export async function validateVulnerabilitiesStateDataSources({
vulnerabilitiesStatesindexPatternID: indexPatternID,
}) {
try {
// Check the existence of related index pattern
const existIndexPattern = await checkExistenceIndexPattern(indexPatternID);
let indexPattern = existIndexPattern;

// If the index pattern does not exist, then check the existence of index
if (existIndexPattern?.error?.statusCode === 404) {
// Check the existence of indices
const { exist, fields } = await checkExistenceIndices(indexPatternID);

if (!exist) {
return {
ok: true,
data: {
error: {
title:
'Vulnerability detection seems to be disabled or has a problem',
type: INDEX_PATTERN_CREATION_NO_INDEX,
},
},
};
}
// If some index matches the index pattern, then create the index pattern
const resultCreateIndexPattern = await createIndexPattern(
indexPatternID,
fields,
);
if (resultCreateIndexPattern?.error) {
return {
ok: true,
data: {
error: {
title: 'There was a problem creating the index pattern',
message: resultCreateIndexPattern?.error,
},
},
};
}
/* WORKAROUND: Redirect to the root of Vulnerabilities Detection application that should
redirects to the Dashboard tab. We want to redirect to this view, because we need the
component is visible (visualizations) to ensure the process that defines the filters for the
Events tab is run when the Dashboard component is unmounted. This workaround solves a
problem in the Events tabs related there are no implicit filters when accessing if the HOC
that protect the view is passed.
*/
NavigationService.getInstance().navigateToApp(vulnerabilityDetection.id);
}
return {
ok: false,
data: { indexPattern },
};
} catch (error) {
return {
ok: true,
data: {
error: { title: 'There was a problem', message: error.message },
},
};
}
}

const errorPromptBody = {
INDEX_PATTERN_CREATION_NO_INDEX: (
<p>
Please check the cluster status. Also, you can check the{' '}
<EuiLink
href={webDocumentationLink(
'user-manual/capabilities/vulnerability-detection/index.html',
)}
target='_blank'
rel='noopener noreferrer'
external
>
vulnerability detection documentation.
</EuiLink>
</p>
),
};

export const PromptCheckIndex = props => {
const { refresh } = props;
const { title, message } = props?.error;
const body = errorPromptBody?.[props?.error?.type] || <p>{message}</p>;

return (
<EuiEmptyPrompt
iconType='alert'
title={<h2>{title}</h2>}
body={body}
actions={
<EuiButton color='primary' fill onClick={refresh}>
Refresh
</EuiButton>
}
/>
);
};

const mapStateToProps = state => ({
vulnerabilitiesStatesindexPatternID:
state.appConfig.data['vulnerabilities.pattern'],
});

export const withVulnerabilitiesStateDataSource = compose(
connect(mapStateToProps),
withGuardAsync(
validateVulnerabilitiesStateDataSources,
({ error, check }) => <PromptCheckIndex error={error} refresh={check} />,
() => <LoadingSpinnerDataSource />,
),
);
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { ComplianceTable } from '../../overview/compliance-table';
import { ButtonModuleGenerateReport } from '../modules/buttons';
import { OfficePanel } from '../../overview/office/panel';
import { GitHubPanel } from '../../overview/github/panel';
import { DashboardPOCByReference } from '../../overview/poc/dashboards/overview/dashboard';
import { withModuleNotForAgent } from '../hocs';
import {
WazuhDiscover,
Expand Down Expand Up @@ -89,7 +90,6 @@ const renderDiscoverTab = (props: WazuhDiscoverProps) => {
component: () => <WazuhDiscover {...props} />,
};
};

export const ModulesDefaults = {
general: {
init: 'events',
Expand Down Expand Up @@ -248,6 +248,13 @@ export const ModulesDefaults = {
vuls: {
init: 'dashboard',
tabs: [
{
id: 'dashboardByReference',
name: 'Dashboard by reference',
component: DashboardPOCByReference,
/* For ButtonExploreAgent to insert correctly according to the module's index pattern, the moduleIndexPatternTitle parameter is added. By default it applies the index pattern wazuh-alerts-* */
buttons: [],
},
{
id: 'dashboard',
name: 'Dashboard',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
import React, { useState, useEffect } from 'react';
import { getPlugins } from '../../../../../kibana-services';
import { ViewMode } from '../../../../../../../../src/plugins/embeddable/public';
import { withErrorBoundary } from '../../../../common/hocs';
import { compose } from 'redux';
import {
withVulnerabilitiesStateDataSource,
createDashboard,
} from '../../../../common/hocs/validate-states-index-pattern-and-dashboards';
import { SavedObject } from '../../../../../react-services';
import { EuiButton } from '@elastic/eui';

const DashboardByRenderer =
getPlugins().dashboard.DashboardContainerByValueRenderer;

const transformPanelsJSON = ({ panelsJSON, references }) =>
Object.fromEntries(
JSON.parse(panelsJSON).map(({ gridData, panelIndex, panelRefName }) => [
panelIndex,
{
gridData,
type: 'visualization',
explicitInput: {
id: panelIndex,
savedObjectId: references.find(({ name }) => name === panelRefName)
.id,
},
},
]),
);

const transform = spec => {
const options = JSON.parse(spec.attributes.optionsJSON);
return {
title: spec.attributes.title,
panels: transformPanelsJSON({
panelsJSON: spec.attributes.panelsJSON,
references: spec.references,
}),
useMargins: options.useMargins,
hidePanelTitles: options.hidePanelTitles,
description: spec.attributes.description,
id: spec.id,
};
};

export const DashboardSavedObject = ({ savedObjectId }) => {
const [dashboardSpecForComponent, setDashboardSpecForComponent] =
useState(null);

useEffect(() => {
(async () => {
try {
const { data } = await SavedObject.getDashboardById(savedObjectId);
const dashboardSpecRenderer = transform(data);
setDashboardSpecForComponent(dashboardSpecRenderer);
} catch (error) {
console.error('Error fetching dashboard:', error);
}
})();
}, [savedObjectId]);

return dashboardSpecForComponent ? (
<DashboardByRenderer
input={{
...dashboardSpecForComponent,
viewMode: ViewMode.VIEW,
isFullScreenMode: false,
filters: [],
query: '',
refreshConfig: {
pause: false,
value: 15,
},
}}
/>
) : (
<p>Loading dashboard...</p>
);
};

const DashboardComponent = () => {
const [idDashboard, setIdDashboard] = useState(null);
const [isLoading, setIsLoading] = useState(true);
const [previousDashboardState, setPreviousDashboardState] = useState(null);

useEffect(() => {
(async () => {
try {
const dashboards = await SavedObject.getAllDashboards();
let targetDashboard = dashboards.data.saved_objects.find(
dashboard => dashboard.id === '94febc80-55a2-11ef-a580-5b5ba88681be',
);

if (!targetDashboard) {
const newDashboardId = await createDashboard();
if (newDashboardId) {
targetDashboard = { id: newDashboardId.id };
setIdDashboard(targetDashboard.id);
}
} else {
setIdDashboard(targetDashboard.id);
setPreviousDashboardState(targetDashboard);
}
} catch (error) {
console.error('Error processing dashboards:', error);
} finally {
setIsLoading(false);
}
})();
}, [idDashboard]);

const handleRestart = async () => {
try {
const savedObjectId = '94febc80-55a2-11ef-a580-5b5ba88681be';
const dashboardChanged = await SavedObject.getDashboardById(
savedObjectId,
);

const changed =
JSON.stringify(dashboardChanged) !==
JSON.stringify(previousDashboardState);

if (changed) {
if (
window.confirm(
'The dashboard has changed. Do you want to revert to the previous version?',
)
) {
await createDashboard(); // Restore the dashboard to its previous state
setIdDashboard(null); // Force re-rendering
setTimeout(() => setIdDashboard(savedObjectId), 0); // Reassign ID to render the restored dashboard
}
} else {
alert('No changes detected in the dashboard.');
}
} catch (error) {
console.error('Error processing dashboard changes:', error);
}
};

return (
<>
{idDashboard ? (
<>
<EuiButton onClick={handleRestart}>Restart</EuiButton>
<DashboardSavedObject key={idDashboard} savedObjectId={idDashboard} />
</>
) : isLoading ? (
<p>Loading dashboard...</p>
) : (
<p>No matching dashboard found.</p>
)}
</>
);
};

export const DashboardPOCByReference = compose(
withErrorBoundary,
withVulnerabilitiesStateDataSource,
)(DashboardComponent);
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ async function checkExistenceIndices(indexPatternId: string) {

async function createIndexPattern(indexPattern, fields: any) {
try {
await SavedObject.createSavedObject(
await SavedObject.createSavedObjectIndexPattern(
'index-pattern',
indexPattern,
{
Expand Down
Loading

0 comments on commit f466e22

Please sign in to comment.