Skip to content

Commit

Permalink
Timeline chart and table components
Browse files Browse the repository at this point in the history
  • Loading branch information
MelsHyrule committed Jan 27, 2023
1 parent ff10537 commit c5073e5
Show file tree
Hide file tree
Showing 19 changed files with 8,286 additions and 6,101 deletions.
51 changes: 51 additions & 0 deletions app/javascript/components/timeline-options/timeline-chart.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import React, { useEffect, useRef } from 'react';
import PropTypes from 'prop-types';
import { LineChart } from '@carbon/charts-react';
import { timelineUiOptions } from './timeline-helper';

const TimelineChart = ({ data, title, buildTableData }) => {
const chartRef = useRef(null);
const chartOnClick = ({ detail }) => {
buildTableData(detail.datum);
};

useEffect(() => {
chartRef.current.chart.services.events.addEventListener(
'scatter-click',
chartOnClick
);
}, [chartRef]);

// Unmount
useEffect(
() => () => {
if (chartRef.current) {
chartRef.current.chart.services.events.removeEventListener(
'scatter-click',
chartOnClick
);
}
},
[]
);

return (
<div>
<LineChart className="line_charts" data={data} options={timelineUiOptions(title)} ref={chartRef} />
</div>
);
};

TimelineChart.propTypes = {
data: PropTypes.arrayOf(PropTypes.any).isRequired,
title: PropTypes.string,
buildTableData: PropTypes.func.isRequired,
};

TimelineChart.defaultProps = {
data: [],
title: '',
buildTableData: PropTypes.func.isRequired,
};

export default TimelineChart;
114 changes: 114 additions & 0 deletions app/javascript/components/timeline-options/timeline-helper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
export const timelineUiOptions = (title) => ({
title,
axes: {
bottom: {
title: __('Date'),
mapsTo: 'date',
scaleType: 'time',
},
left: {
title: __('# of Events'),
mapsTo: 'value',
scaleType: 'linear',
},
},
legend: {
clickable: true,
},
tooltip: {
totalLabel: __('Total Events'),
},
points: {
radius: 3,
fillOpacity: 1,
filled: true,
enabled: true,
},
zoomBar: {
top: {
className: 'zoom-bar',
enabled: true,
type: 'graph_view',
},
},
height: '400px',
});

const smartAndOrStatements = (group, array) => {
let shortUrl = '';
if (array.length === 1) {
shortUrl += `&filter[]=${group}=${array[0]}`;
} else {
shortUrl += `&filter[]=${group}=[${array.toString()}]`;
}
return shortUrl;
};

/** Function to build the URL to get all events that fall under the user selected parameters. */
export const buildUrl = (values) => {
// TODO: verify that everyone needs / uses the host and ems on the filters
let url = `/api/event_streams?limit=5000&offset=0&expand=resources&attributes=group,group_level,group_name,id,event_type,message,ems_id,type,timestamp,created_on,host,source,ems_id,ext_management_system`;

url += `&filter[]=type=${values.type}`;
url += smartAndOrStatements('group', values.group);
url += smartAndOrStatements('group_level', values.group_level);

if (values.start_date) {
url += `&filter[]=timestamp%3E${values.start_date[0].toISOString()}`;
}
if (values.end_date) {
url += `&filter[]=timestamp%3C${values.end_date[0].toISOString()}`;
}
return url;
};

/** Function to format the raw data such that it can be used by the Timeline Chart component */
export const buildChartDataObject = (rawData) => {
// https://codesandbox.io/s/tender-river-9t3vu5?file=/src/index.js
// Link to how a dataset object should look like
const datasets = [];
rawData.resources.forEach((event) => {
const idx = datasets.findIndex((element) => {
if (element.date === event.timestamp && element.group === event.group_name) {
return true;
}
return false;
});
if (datasets[idx]) {
datasets[idx].value += 1;
datasets[idx].eventsObj.push(event);
} else { // idx comes back as -1 when its undefined
const obj = {
group: event.group_name,
date: event.timestamp ? event.timestamp : event.created_on,
value: 1,
eventsObj: [event],
};
datasets.push(obj);
}
});
return datasets;
};

/** Function to go through all the data pulled by the form submit and finds the right objects */
export const buildDataTableObject = (pointChoice) => {
const tableData = [];
pointChoice.eventsObj.forEach((event) => {
const eventHost = (event.host === null) ? '' : event.host.name;
const eventObj = {
event_type: event.event_type,
source: event.source,
group_level: event.group_level,
provider: '', // TODO hyperlink
provider_username: '',
message: '',
host: eventHost, // TODO hyperlink
source_vm: '', // TODO hyperlink
source_vm_location: '',
timestamp: event.timestamp, // is Zulu time, should copnvert to user's timezone
id: event.id,
};
tableData.push(eventObj);
});
return tableData;
};
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import { componentTypes, validatorTypes } from '@@ddf';

const getOneWeekAgo = () => {
let oneWeekAgo = new Date();
oneWeekAgo.setDate(oneWeekAgo.getDate() - 7);
return [oneWeekAgo];
}

const createSchemaSimple = (
timelineEvents, managementGroupNames, managementGroupLevels, policyGroupNames, policyGroupLevels
) => ({
Expand Down Expand Up @@ -141,12 +147,14 @@ const createSchemaSimple = (
id: 'startDate',
name: 'startDate',
label: __('Start Date'),
initialValue: getOneWeekAgo(),
},
{
component: 'date-picker',
id: 'endDate',
name: 'endDate',
label: __('End Date'),
initialValue: [new Date()],
},
],
},
Expand Down
24 changes: 11 additions & 13 deletions app/javascript/components/timeline-options/timeline-options.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import PropTypes from 'prop-types';
import createSchemaSimple from './timeline-options-simple.schema';
import mapper from '../../forms/mappers/componentMapper';

const TimelineOptions = ({ url }) => {
const TimelineOptions = ({ submitChosenFormOptions }) => {
const [{
isLoading, timelineEvents, managementGroupNames, managementGroupLevels, policyGroupNames, policyGroupLevels,
}, setState] = useState({
Expand All @@ -25,7 +25,7 @@ const TimelineOptions = ({ url }) => {
// Management Events
Object.entries(data.EmsEvent.group_names).forEach((entry) => {
const [key, value] = entry;
managementGroupNames.push({ label: value, value });
managementGroupNames.push({ label: value, value: key });
});
Object.entries(data.EmsEvent.group_levels).forEach((entry) => {
const [key, value] = entry;
Expand Down Expand Up @@ -60,17 +60,15 @@ const TimelineOptions = ({ url }) => {

const onSubmit = (values) => {
miqSparkleOn();
const show = values.timelineEvents === 'EmsEvent' ? 'timeline' : 'policy_timeline';
const categories = values.timelineEvents === 'EmsEvent' ? values.managementGroupNames : values.policyGroupNames;
const vmData = {
tl_show: show,
tl_categories: categories,
tl_levels: values.managementGroupLevels ? values.managementGroupLevels : [],
tl_result: values.policyGroupLevels ? values.policyGroupLevels : 'success',
const newData = {
type: values.timelineEvents,
group: categories,
group_level: values.managementGroupLevels ? values.managementGroupLevels : [values.policyGroupLevels],
start_date: values.startDate,
end_date: values.endDate,
};
window.ManageIQ.calendar.calDateFrom = values.startDate;
window.ManageIQ.calendar.calDateTo = values.endDate;
window.miqJqueryRequest(url, { beforeSend: true, data: vmData });
submitChosenFormOptions(newData);
};

return !isLoading && (
Expand All @@ -88,11 +86,11 @@ const TimelineOptions = ({ url }) => {
};

TimelineOptions.propTypes = {
url: PropTypes.string,
submitChosenFormOptions: PropTypes.func.isRequired,
};

TimelineOptions.defaultProps = {
url: '',
submitChosenFormOptions: undefined,
};

export default TimelineOptions;
54 changes: 54 additions & 0 deletions app/javascript/components/timeline-options/timeline-page.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import React, { useState } from 'react';
import TimelineOptions from './timeline-options';
import TimelineChart from './timeline-chart';
import TimelineTable from './timeline-table';
import NoRecordsFound from '../no-records-found';
import { buildUrl, buildChartDataObject, buildDataTableObject } from './timeline-helper';

const TimelinePage = ({ id }) => {
const [{
timelineFormChoices, timelineResources, timelineChartData, timelineTableData,
}, setState] = useState({
timelineFormChoices: {},
timelineResources: [],
timelineChartData: [],
timelineTableData: [],
});

const submitChosenFormOptions = (formChoices) => {
API.get(buildUrl(formChoices)).then((chartValues) => {
miqSparkleOff();
setState((state) => ({
...state,
timelineFormChoices: formChoices,
timelineResources: chartValues.resources,
timelineChartData: buildChartDataObject(chartValues),
timelineTableData: [],
}));
});
};

const buildTableData = (pointChosen) => {
setState((state) => ({
...state,
timelineTableData: buildDataTableObject(pointChosen),
}));
};

// TODO: Have it be such that on page initial load it's empty, but after pressing submit it would be NoRecordFound
const timelineComponent = timelineChartData.length === 0 ? <NoRecordsFound />
: <TimelineChart data={timelineChartData} title={__('Timeline Data')} buildTableData={buildTableData} />;

const timelineTableComponent = timelineTableData.length === 0 ? <></>
: <TimelineTable data={timelineTableData} />;

return (
<>
<TimelineOptions submitChosenFormOptions={submitChosenFormOptions} />
{timelineComponent}
{timelineTableComponent}
</>
);
};

export default TimelinePage;
66 changes: 66 additions & 0 deletions app/javascript/components/timeline-options/timeline-table.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import React from 'react';
import PropTypes from 'prop-types';
import MiqDataTable from '../miq-data-table';

const TimelineTable = ({ data }) => {
const headers = [
{
key: 'event_type',
header: __('Event Type'),
}, {
key: 'source',
header: __('Event Source'),
}, {
key: 'group_level',
header: __('Group Level'),
}, {
key: 'provider',
header: __('Provider'),
}, {
key: 'provider_username',
header: __('Provider User Name'),
}, {
key: 'message',
header: __('Message'),
}, {
key: 'host',
header: __('Source Host'),
}, {
key: 'source_vm',
header: __('Source VM'),
}, {
key: 'source_vm_location',
header: __('Source VM Location'),
}, {
key: 'timestamp',
header: __('Date Time'),
},
];
/**
* NOTE: In the original tables, the information displayed was different
* depending from where you accessed the timeline page from
*
* Ex. 'Source VM Location' would now appear on the Host page, but it would
* for the VM & Templates page
*/

return (
<div className="timeline-data-table">
<MiqDataTable
rows={data}
headers={headers}
/>
</div>

);
};

TimelineTable.propTypes = {
data: PropTypes.arrayOf(PropTypes.any).isRequired,
};

TimelineTable.defaultProps = {
data: [],
};

export default TimelineTable;
6 changes: 6 additions & 0 deletions app/javascript/packs/component-definitions-common.js
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,10 @@ import TableListViewWrapper from '../react/table_list_view_wrapper';
import TaggingWrapperConnected from '../components/taggingWrapper';
import { TagView } from '../tagging';
import TenantQuotaForm from '../components/tenant-quota-form';
import TimelineChart from '../components/timeline-options/timeline-chart';
import TimelineOptions from '../components/timeline-options/timeline-options';
import TimelinePage from '../components/timeline-options/timeline-page';
import TimelineTable from '../components/timeline-options/timeline-table';
import ToastList from '../components/toast-list/toast-list';
import VmEditForm from '../components/vm-edit-form';
import TextualSummaryWrapper from '../react/textual_summary_wrapper';
Expand Down Expand Up @@ -259,7 +262,10 @@ ManageIQ.component.addReact('TagView', TagView);
ManageIQ.component.addReact('TaggingWrapperConnected', TaggingWrapperConnected);
ManageIQ.component.addReact('TenantQuotaForm', TenantQuotaForm);
ManageIQ.component.addReact('TextualSummaryWrapper', TextualSummaryWrapper);
ManageIQ.component.addReact('TimelineChart', TimelineChart);
ManageIQ.component.addReact('TimelineOptions', TimelineOptions);
ManageIQ.component.addReact('TimelinePage', TimelinePage);
ManageIQ.component.addReact('TimelineTable', TimelineTable);
ManageIQ.component.addReact('TimeProfileReportsTable', TimeProfileReportsTable);
ManageIQ.component.addReact('TimeProfileTable', TimeProfileTable);
ManageIQ.component.addReact('ToastList', ToastList);
Expand Down

0 comments on commit c5073e5

Please sign in to comment.