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 Feb 10, 2023
1 parent eace6c5 commit 82d1310
Show file tree
Hide file tree
Showing 24 changed files with 3,445 additions and 6,332 deletions.
1 change: 0 additions & 1 deletion app/controllers/application_controller/timelines.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ def tl_chooser
page << javascript_prologue
page.replace("flash_msg_div", :partial => "layouts/flash_msg")
page << "miqScrollTop();" if @flash_array.present?
page.replace("tl_div", :partial => "layouts/tl_detail")
page << "ManageIQ.calendar.calDateFrom = new Date(#{@tl_options.date.start});" unless @tl_options.date.start.nil?
page << "ManageIQ.calendar.calDateTo = new Date(#{@tl_options.date.end});" unless @tl_options.date.end.nil?
page << 'miqBuildCalendar();'
Expand Down
56 changes: 56 additions & 0 deletions app/javascript/components/timeline-options/timeline-chart.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
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
);
}
},
[]
);

const LengthWarning = (
<label className="bx--label">
{__('*Only 5000 events shown. Limit date range to avoid \"missing\" events.')}
</label>
);

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

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

TimelineChart.defaultProps = {
data: [],
title: '',
buildTableData: () => {},
};

export default TimelineChart;
111 changes: 111 additions & 0 deletions app/javascript/components/timeline-options/timeline-helper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
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) => {
const item = (array.length === 1) ? array[0] : `[${array.toString()}]`;
return `&filter[]=${group}=${item}`;
};

/** Function to build the URL to get all events that fall under the user selected parameters. */
export const buildUrl = (values) => {
// TODO: Different timeline show different data, ensure all necesary data is pulled
let url = `/api/event_streams?limit=5000&offset=0&expand=resources`;
url += `&attributes=group,group_level,group_name,id,event_type,ems_id,type,timestamp,created_on,host.name,source`;

// User set values
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) => {
// TODO: Different timeline show different data, ensure all necesary data is included
const eventObj = {
event_type: event.event_type,
source: event.source,
group_level: event.group_level,
provider: '', // TODO hyperlink
provider_username: '',
message: '',
host: (event.host === null) ? '' : event.host.name, // TODO hyperlink
source_vm: '', // TODO hyperlink
source_vm_location: '',
timestamp: event.timestamp, // is Zulu time, should convert 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 = () => {
const 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
25 changes: 11 additions & 14 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 @@ -59,18 +59,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 +85,11 @@ const TimelineOptions = ({ url }) => {
};

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

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

export default TimelineOptions;
57 changes: 57 additions & 0 deletions app/javascript/components/timeline-options/timeline-page.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
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 = () => {
const [{
timelineChartData, timelineTableData, submitWasPressed,
}, setState] = useState({
timelineFormChoices: {},
timelineResources: [],
timelineChartData: [],
timelineTableData: [],
submitWasPressed: false,
});

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

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

const timelineComponent = submitWasPressed ? 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;
77 changes: 77 additions & 0 deletions app/javascript/components/timeline-options/timeline-table.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import React from 'react';
import PropTypes from 'prop-types';
import MiqDataTable from '../miq-data-table';

const TimelineTable = ({ data }) => {
// TODO Commented out columns exist in certain pages but not other
// we need to figure out how to distinguish when which columns are used
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),
};

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

export default TimelineTable;

0 comments on commit 82d1310

Please sign in to comment.