(litmus-portal): Graph pop out mode, state-less prometheus queries for chaos events, mongoDB resync updates removed, optimization of analytics code for monitoring, reducing heap-size usage, time range / refresh rate selection and chaos metrics on graphs with selection module. (#2593)

* Added clean up feature for disabled workflow events.

Signed-off-by: ishangupta-ds <ishan@chaosnative.com>

* temp-no-push

Signed-off-by: ishangupta-ds <ishan@chaosnative.com>

* optimizations and state-less chaos events

Signed-off-by: ishangupta-ds <ishan@chaosnative.com>

* Updating prometheus query sync logic

Signed-off-by: ishangupta-ds <ishan@chaosnative.com>

* expand button working

Signed-off-by: Ritik Srivastava <ritik.srivastava@mayadata.io>

* styling for grids

Signed-off-by: Ritik Srivastava <ritik.srivastava@mayadata.io>

* translations added for tooltip

Signed-off-by: Ritik Srivastava <ritik.srivastava@mayadata.io>

* config reset

Signed-off-by: Ritik Srivastava <ritik.srivastava@mayadata.io>

* Adding refresh rate selector and basic time range selector with some state management fixes.

Signed-off-by: ishangupta-ds <ishan@chaosnative.com>

* minor fix

Signed-off-by: ishangupta-ds <ishan@chaosnative.com>

* styling fixes for card background in dashboard graphs

Signed-off-by: Ritik Srivastava <ritik.srivastava@mayadata.io>

* reset config

Signed-off-by: Ritik Srivastava <ritik.srivastava@mayadata.io>

* Adding initial event mapping with refresh rate and time range selection fixes.

Signed-off-by: ishangupta-ds <ishan@chaosnative.com>

* minor fix

Signed-off-by: ishangupta-ds <ishan@chaosnative.com>

* added filtering for chaos metrics.

Signed-off-by: ishangupta-ds <ishan@chaosnative.com>

* minor fixes

Signed-off-by: ishangupta-ds <ishan@chaosnative.com>

* minor fix

Signed-off-by: ishangupta-ds <ishan@chaosnative.com>

* enabled real time updates.

Signed-off-by: ishangupta-ds <ishan@chaosnative.com>

* minor fix

Signed-off-by: ishangupta-ds <ishan@chaosnative.com>

* minor fix

Signed-off-by: ishangupta-ds <ishan@chaosnative.com>

* fixes

Signed-off-by: ishangupta-ds <ishan@chaosnative.com>

* Fixes for events.

Signed-off-by: ishangupta-ds <ishan@chaosnative.com>

* linting fixes.

Signed-off-by: ishangupta-ds <ishan@chaosnative.com>

* minor fix.

Signed-off-by: ishangupta-ds <ishan@chaosnative.com>

* minor fix.

Signed-off-by: ishangupta-ds <ishan@chaosnative.com>

* minor fix.

Signed-off-by: ishangupta-ds <ishan@chaosnative.com>

* fixes

Signed-off-by: ishangupta-ds <ishan@chaosnative.com>

* minor fix

Signed-off-by: ishangupta-ds <ishan@chaosnative.com>

* color fixes.

Signed-off-by: ishangupta-ds <ishan@chaosnative.com>

* reverting to old package lock file.

Signed-off-by: ishangupta-ds <ishan@chaosnative.com>

* fixes after review.

Signed-off-by: ishangupta-ds <ishan@chaosnative.com>

* adding constants also removed uneccesary style classes.

Signed-off-by: ishangupta-ds <ishan@chaosnative.com>

* divided index into modules for the page.

Signed-off-by: ishangupta-ds <ishan@chaosnative.com>

* fixes after review.

Signed-off-by: ishangupta-ds <ishan@chaosnative.com>

* moving component overrides to tsx files from styles.

Signed-off-by: ishangupta-ds <ishan@chaosnative.com>

* minor NaN% fix.

Signed-off-by: ishangupta-ds <ishan@chaosnative.com>

* removed an unused variable.

Signed-off-by: ishangupta-ds <ishan@chaosnative.com>

* fix after review.

Signed-off-by: ishangupta-ds <ishan@chaosnative.com>

Co-authored-by: Ritik Srivastava <ritik.srivastava@mayadata.io>
This commit is contained in:
Ishan Gupta 2021-04-06 19:05:07 +05:30 committed by GitHub
parent b5fc3f0e6a
commit d5173bf1c1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
47 changed files with 3016 additions and 1330 deletions

View File

@ -0,0 +1,3 @@
<svg width="74" height="74" viewBox="0 0 74 74" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M66.0508 47.9562C62.4191 47.9562 59.3509 50.4076 58.4045 53.7437H50.7305V45.2715H58.5659C67.0764 45.2715 74 38.3404 74 29.8207C74 22.9899 69.4744 16.9891 63.0497 15.0347C61.1077 7.24987 54.0881 1.66651 45.9583 1.66651C42.8068 1.66651 39.7879 2.48009 37.1236 4.03447C34.3019 1.45122 30.602 0 26.7146 0C18.3761 0 11.5609 6.65434 11.2889 14.9366C4.68946 16.7832 0 22.8322 0 29.8207C0 38.3404 6.92363 45.2715 15.4341 45.2715H23.2695V53.7437H15.5955C14.6493 50.4076 11.5809 47.9562 7.94922 47.9562C3.56602 47.9562 0 51.5261 0 55.914C0 60.302 3.56602 63.8718 7.94922 63.8718C11.5809 63.8718 14.6491 61.4204 15.5955 58.0843H25.4375C26.6348 58.0843 27.6055 57.1126 27.6055 55.914V45.2715H34.832V58.3876C31.4996 59.3349 29.0508 62.4066 29.0508 66.0422C29.0508 70.4301 32.6168 74 37 74C41.3832 74 44.9492 70.4301 44.9492 66.0422C44.9492 62.4066 42.5004 59.335 39.168 58.3876V45.2715H46.3945V55.914C46.3945 57.1126 47.3652 58.0843 48.5625 58.0843H58.4045C59.3507 61.4204 62.4191 63.8718 66.0508 63.8718C70.434 63.8718 74 60.302 74 55.914C74 51.5261 70.434 47.9562 66.0508 47.9562ZM7.94922 59.5312C5.95686 59.5312 4.33594 57.9085 4.33594 55.914C4.33594 53.9195 5.95686 52.2968 7.94922 52.2968C9.94158 52.2968 11.5625 53.9195 11.5625 55.914C11.5625 57.9085 9.94158 59.5312 7.94922 59.5312ZM40.6133 66.0422C40.6133 68.0367 38.9924 69.6594 37 69.6594C35.0076 69.6594 33.3867 68.0367 33.3867 66.0422C33.3867 64.0476 35.0076 62.425 37 62.425C38.9924 62.425 40.6133 64.0476 40.6133 66.0422ZM15.4341 40.9309C9.31461 40.9309 4.33594 35.947 4.33594 29.8207C4.33594 24.3453 8.41273 19.6194 13.8186 18.8279C14.9594 18.661 15.7699 17.6293 15.6636 16.4802C15.6322 16.1412 15.6163 15.7948 15.6163 15.4506C15.6163 9.32469 20.595 4.34078 26.7144 4.34078C29.9794 4.34078 33.0583 5.76161 35.1616 8.23896C35.9 9.10868 37.185 9.25713 38.1021 8.57883C40.3758 6.8964 43.0924 6.00715 45.9582 6.00715C52.5529 6.00715 58.0371 10.8512 59.0623 17.1524C59.2072 18.0434 59.8881 18.7515 60.7719 18.9307C65.9243 19.9749 69.6639 24.5547 69.6639 29.8207C69.6639 35.947 64.6852 40.9309 58.5658 40.9309H15.4341ZM66.0508 59.5312C64.0584 59.5312 62.4375 57.9085 62.4375 55.914C62.4375 53.9195 64.0584 52.2968 66.0508 52.2968C68.0431 52.2968 69.6641 53.9195 69.6641 55.914C69.6641 57.9085 68.0431 59.5312 66.0508 59.5312Z" fill="#F6B92B"/>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@ -0,0 +1,12 @@
<svg width="13" height="13" viewBox="0 0 13 13" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0)">
<path d="M1.3 8.96875L0 12.9988L4.03 11.6988L1.3 8.96875Z" fill="#5B44BA"/>
<path d="M8.55602 1.69643L2.21338 8.03906L4.97105 10.7967L11.3137 4.4541L8.55602 1.69643Z" fill="#5B44BA"/>
<path d="M12.8052 2.015L10.9852 0.195C10.7252 -0.065 10.3352 -0.065 10.0752 0.195L9.49023 0.78L12.2202 3.51L12.8052 2.925C13.0652 2.665 13.0652 2.275 12.8052 2.015Z" fill="#5B44BA"/>
</g>
<defs>
<clipPath id="clip0">
<rect width="13" height="13" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 596 B

View File

@ -191,8 +191,35 @@ analyticsDashboard:
overview: Overview
monitoringDashboardPage:
dataSourceError: Reconfigure this dashboard with a different data source or update data source information.
dataSourceIs: Data source is
reConfigureDashboard: Re-configure dashboard
updateDataSource: Update data source
headerInfoText: "View the chaos events and metrics in a given \n time interval by selecting a time interval."
rangeSelector:
to: to
selectPeriod: Select Period
refresh:
heading: Refresh
off: Off
every5Seconds: Every 5 seconds
every10Seconds: Every 10 seconds
every30Seconds: Every 30 seconds
every1Minute: Every 1 minute
every5Minutes: Every 5 minutes
every15Minutes: Every 15 minutes
every30Minutes: Every 30 minutes
every1Hour: Every 1 hour
every2Hours: Every 2 hours
every1Day: Every 1 day
chaosTable:
tableHead1: Legend
tableHead2: Workflow
tableHead3: Experiment
tableHead4: "Target \n ( namespace / pod )"
tableHead5: Result
noRecords: No chaos happened during the interval
showTable: Show Chaos during this interval
hideTable: Hide Chaos during this interval
workflowClusterDashboard:
title: Dashboard
tab1: Hourly
@ -273,6 +300,11 @@ analyticsDashboard:
day: day
days: days
few: few
toolTip:
viewChaosMetric: View Chaos Metric Info
hideChaosMetric: Hide Chaos Metric Info
editPanel: Edit
popout: Popout
error:
whoops: Whoops!
@ -973,4 +1005,4 @@ customWorkflow:
addEnv: Add Key
addExp: Add experiment
viewYAML:
view: View the YAML here
view: View the YAML here

View File

@ -17,6 +17,7 @@ interface DateRangeSelectorProps {
isOpen: boolean;
onClose: () => void;
callbackToSetRange: RangeCallBackType;
className?: string;
}
const DateRangeSelector: React.FC<DateRangeSelectorProps> = ({
@ -24,6 +25,7 @@ const DateRangeSelector: React.FC<DateRangeSelectorProps> = ({
isOpen,
onClose,
callbackToSetRange,
className,
}) => {
const classes = useStyles();
const { palette } = useTheme();
@ -54,6 +56,7 @@ const DateRangeSelector: React.FC<DateRangeSelectorProps> = ({
classes={{
paper: classes.popoverDateRangeSelector,
}}
className={className}
>
<div className={classes.dateRangeSelectorContainer}>
<DateRangePicker

View File

@ -0,0 +1,9 @@
import { createStyles, TableCell, withStyles } from '@material-ui/core';
export const StyledTableCell = withStyles((theme) =>
createStyles({
root: {
borderBottom: `1px solid ${theme.palette.border.main}`,
},
})
)(TableCell);

View File

@ -14,13 +14,13 @@ const useStyles = makeStyles((theme: Theme) => ({
textSection: {
height: 'fit-content',
width: '60%',
margin: '0 3rem',
margin: theme.spacing(0, 6),
alignSelf: 'center',
minWidth: '20rem',
},
mainHeading: {
fontSize: '1.4rem',
lineHeight: '1rem',
width: '95%',
display: 'inline-block',
marginBottom: theme.spacing(2),

View File

@ -44,6 +44,7 @@ export const WORKFLOW_LIST_DETAILS = gql`
query workflowListDetails($projectID: String!, $workflowIDs: [ID]) {
ListWorkflow(project_id: $projectID, workflow_ids: $workflowIDs) {
workflow_id
workflow_manifest
cronSyntax
cluster_name
workflow_name
@ -58,6 +59,7 @@ export const WORKFLOW_LIST_DETAILS = gql`
project_id
cluster_id
cluster_type
isRemoved
workflow_runs {
execution_data
workflow_run_id

View File

@ -1,4 +1,7 @@
import { PanelGroupResponse } from './graphql/dashboardsDetails';
import { GraphMetric } from 'litmus-ui';
import { PanelGroupResponse, PanelResponse } from './graphql/dashboardsDetails';
import { promQueryInput } from './graphql/prometheus';
import { ChaosData } from './graphql/workflowListData';
export interface PanelGroupMap {
groupName: string;
@ -34,4 +37,78 @@ export interface ChaosResultNamesAndNamespacesMap {
resultNamespace: string;
workflowName: string;
experimentName: string;
selectedWorkflowIds: string[];
}
export interface RunWiseChaosMetrics {
runIndex: number;
runID: string;
lastUpdatedTimeStamp: number;
probeSuccessPercentage: string;
experimentStatus: string;
experimentVerdict: string;
resilienceScore: string;
workflowStatus: string;
}
export interface WorkflowAndExperimentMetaDataMap {
workflowID: string;
workflowName: string;
experimentName: string;
targetApp: string;
targetNamespace: string;
runWiseChaosMetrics: RunWiseChaosMetrics[];
}
export interface ExperimentNameAndChaosDataMap {
experimentName: string;
chaosData: ChaosData;
}
export interface WorkflowRunWiseDetails {
idsOfWorkflowRuns: string[];
resilienceScoreForWorkflowRuns: number[];
statusOfWorkflowRuns: string[];
experimentNameWiseChaosDataOfWorkflowRuns: ExperimentNameAndChaosDataMap[][];
}
export interface ChaosEventDetails {
id: string;
legend: string;
workflow: string;
experiment: string;
target: string;
result: string;
chaosMetrics: WorkflowAndExperimentMetaDataMap;
showOnTable: Boolean;
}
export interface ChaosInformation {
promQueries: promQueryInput[];
chaosQueryIDs: string[];
chaosEventList: ChaosEventDetails[];
numberOfWorkflowsUnderConsideration: number;
}
export interface ChaosDataUpdates {
queryIDs: string[];
chaosData: Array<EventMetric>;
reGenerate: Boolean;
latestEventResult: string[];
}
export interface EventMetric extends GraphMetric {
subData?: Array<{
subDataName: string;
value: string;
}>;
}
export interface GraphPanelProps extends PanelResponse {
className?: string;
chaos_data?: Array<EventMetric>;
}
export interface GraphPanelGroupProps extends PanelGroupResponse {
chaos_data?: Array<EventMetric>;
}

View File

@ -17,7 +17,7 @@ export interface PrometheusQueryInput {
url: string;
start: string;
end: string;
queries?: promQueryInput[];
queries: promQueryInput[];
}
export interface timeStampValue {

View File

@ -33,6 +33,7 @@ export interface Nodes {
}
export interface ExecutionData {
resiliency_score?: number;
event_type: string;
uid: string;
namespace: string;
@ -52,6 +53,7 @@ export interface WorkflowRun {
export interface Workflow {
workflow_id: string;
workflow_manifest: string;
cronSyntax: string;
cluster_name: string;
workflow_name: string;
@ -63,6 +65,7 @@ export interface Workflow {
project_id: string;
cluster_id: string;
cluster_type: string;
isRemoved: Boolean;
workflow_runs: WorkflowRun[];
}

View File

@ -3,6 +3,11 @@ export interface PanelGroupMap {
panels: string[];
}
export interface RangeType {
startDate: string;
endDate: string;
}
export interface DashboardData {
selectedDashboardID: string;
selectedDashboardName?: string;
@ -13,6 +18,8 @@ export interface DashboardData {
selectedAgentID?: string;
selectedAgentName?: string;
refreshRate?: number;
range: RangeType;
forceUpdate: Boolean;
}
export enum DashboardSelectionActions {

View File

@ -0,0 +1,17 @@
export const PROMETHEUS_ERROR_QUERY_RESOLUTION_LIMIT_REACHED: string = `bad_data: exceeded maximum resolution of 11,000 points per timeseries. Try decreasing the query resolution (?step=XX)`;
export const DEFAULT_CHAOS_EVENT_PROMETHEUS_QUERY_RESOLUTION: string = '1/2';
export const CHAOS_EXPERIMENT_VERDICT_PASS: string = 'Pass';
export const CHAOS_EXPERIMENT_VERDICT_FAIL: string = 'Fail';
export const STATUS_RUNNING: string = 'Running';
export const PROMETHEUS_QUERY_RESOLUTION_LIMIT: number = 11000;
export const MAX_REFRESH_RATE: number = 2147483647;
export const DEFAULT_REFRESH_RATE: number = 10000;
export const ACTIVE: string = 'Active';
export const MINIMUM_TOLERANCE_LIMIT: number = 4;
export const DEFAULT_TOLERANCE_LIMIT: number = 14;
export const INVALID_RESILIENCE_SCORE_STRING: string = 'NaN';
export const DEFAULT_METRIC_SERIES_NAME: string = 'metric';
export const DEFAULT_CHAOS_EVENT_NAME: string = 'chaos';
export const DEFAULT_RELATIVE_TIME_RANGE: number = 1800;
export const DASHBOARD_TYPE_1: string = 'Kubernetes Platform';
export const DASHBOARD_TYPE_2: string = 'Sock Shop';

View File

@ -1,14 +1,39 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { useQuery } from '@apollo/client';
import { IconButton, Menu, MenuItem, Typography } from '@material-ui/core';
import { ApolloError, useQuery } from '@apollo/client';
import {
FormControl,
IconButton,
InputLabel,
Menu,
MenuItem,
OutlinedInput,
Select,
Typography,
useTheme,
} from '@material-ui/core';
import AutorenewOutlinedIcon from '@material-ui/icons/AutorenewOutlined';
import KeyboardArrowDownIcon from '@material-ui/icons/KeyboardArrowDown';
import { ButtonFilled, Modal } from 'litmus-ui';
import WatchLaterRoundedIcon from '@material-ui/icons/WatchLaterRounded';
import { ButtonOutlined, GraphMetric } from 'litmus-ui';
import moment from 'moment';
import React, { useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';
import BackButton from '../../components/Button/BackButton';
import DateRangeSelector from '../../components/DateRangeSelector';
import Scaffold from '../../containers/layouts/Scaffold';
import { LIST_DASHBOARD, LIST_DATASOURCE } from '../../graphql';
import {
LIST_DASHBOARD,
LIST_DATASOURCE,
PROM_QUERY,
WORKFLOW_LIST_DETAILS,
} from '../../graphql';
import {
ChaosDataUpdates,
ChaosEventDetails,
ChaosInformation,
EventMetric,
} from '../../models/dashboardsData';
import {
DashboardList,
ListDashboardResponse,
@ -20,15 +45,38 @@ import {
ListDataSourceResponse,
ListDataSourceVars,
} from '../../models/graphql/dataSourceDetails';
import {
PrometheusQueryInput,
PrometheusQueryVars,
PrometheusResponse,
} from '../../models/graphql/prometheus';
import {
WorkflowList,
WorkflowListDataVars,
} from '../../models/graphql/workflowListData';
import useActions from '../../redux/actions';
import * as DashboardActions from '../../redux/actions/dashboards';
import * as DataSourceActions from '../../redux/actions/dataSource';
import { history } from '../../redux/configureStore';
import { RootState } from '../../redux/reducers';
import { ReactComponent as CrossMarkIcon } from '../../svg/crossmark.svg';
import { getProjectID, getProjectRole } from '../../utils/getSearchParams';
import DashboardPanelGroup from '../../views/AnalyticsDashboard/MonitoringDashboardPage/DashboardPanelGroup';
import useStyles from './styles';
import { getProjectID } from '../../utils/getSearchParams';
import {
chaosEventDataParserForPrometheus,
getChaosQueryPromInputAndID,
} from '../../utils/promUtils';
import ChaosAccordion from '../../views/AnalyticsDashboard/MonitoringDashboardPage/ChaosAccordion';
import DataSourceInactiveModal from '../../views/AnalyticsDashboard/MonitoringDashboardPage/DataSourceInactiveModal';
import DashboardPanelGroup from '../../views/AnalyticsDashboard/MonitoringDashboardPage/Panel/DashboardPanelGroup';
import {
ACTIVE,
DEFAULT_REFRESH_RATE,
DEFAULT_RELATIVE_TIME_RANGE,
DEFAULT_TOLERANCE_LIMIT,
MAX_REFRESH_RATE,
MINIMUM_TOLERANCE_LIMIT,
PROMETHEUS_ERROR_QUERY_RESOLUTION_LIMIT_REACHED,
} from './constants';
import refreshData from './refreshData';
import useStyles, { useOutlinedInputStyles } from './styles';
interface SelectedDashboardInformation {
id: string;
@ -39,25 +87,51 @@ interface SelectedDashboardInformation {
dashboardListForAgent: ListDashboardResponse[];
metaData: ListDashboardResponse[];
dashboardKey: string;
selectionOverride: Boolean;
}
interface PrometheusQueryDataInterface {
promInput: PrometheusQueryInput;
chaosInput: string[];
numOfWorkflows: number;
firstLoad: Boolean;
chaosEvents: ChaosEventDetails[];
chaosEventsToBeShown: ChaosEventDetails[];
}
interface EventsToShowInterface {
eventsToShow: string[];
selectEvents: Boolean;
}
interface RefreshObjectType {
label: string;
value: number;
}
interface ChaosDataSet {
queryIDs: string[];
chaosData: Array<EventMetric>;
visibleChaos: Array<EventMetric>;
latestEventResult: string[];
}
const DashboardPage: React.FC = () => {
const classes = useStyles();
const outlinedInputClasses = useOutlinedInputStyles();
const { t } = useTranslation();
const ACTIVE: string = 'Active';
const { palette } = useTheme();
const areaGraph: string[] = palette.graph.area;
const dataSource = useActions(DataSourceActions);
const dashboard = useActions(DashboardActions);
// get ProjectID
const projectID = getProjectID();
const projectRole = getProjectRole();
const selectedDashboard = useSelector(
(state: RootState) => state.selectDashboard
);
const selectedDataSource = useSelector(
(state: RootState) => state.selectDataSource
);
const [
selectedDashboardInformation,
setSelectedDashboardInformation,
@ -70,19 +144,129 @@ const DashboardPage: React.FC = () => {
dashboardListForAgent: [],
metaData: [],
dashboardKey: 'Default',
selectionOverride: false,
});
const [
prometheusQueryData,
setPrometheusQueryData,
] = React.useState<PrometheusQueryDataInterface>({
promInput: {
url: '',
start: '',
end: '',
queries: [],
},
chaosInput: [],
chaosEvents: [],
chaosEventsToBeShown: [],
numOfWorkflows: 0,
firstLoad: true,
});
const [eventsToShow, setEventsToShow] = React.useState<EventsToShowInterface>(
{
eventsToShow: [],
selectEvents: false,
}
);
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
const [refreshRate, setRefreshRate] = React.useState<number>(10000);
const [refreshRate, setRefreshRate] = React.useState<number>(
selectedDashboard.refreshRate && !prometheusQueryData.firstLoad
? selectedDashboard.refreshRate
: 0
);
const [dataSourceStatus, setDataSourceStatus] = React.useState<string>(
'ACTIVE'
);
const open = Boolean(anchorEl);
const [chaosDataSet, setChaosDataSet] = React.useState<ChaosDataSet>({
queryIDs: [],
chaosData: [],
visibleChaos: [],
latestEventResult: [],
});
const handleClick = (event: React.MouseEvent<HTMLElement>) => {
setAnchorEl(event.currentTarget);
};
const handleClose = () => {
setAnchorEl(null);
};
const dateRangeSelectorRef = React.useRef<HTMLButtonElement>(null);
const [
isDateRangeSelectorPopoverOpen,
setDateRangeSelectorPopoverOpen,
] = React.useState(false);
const clearTimeOuts = async () => {
let id = window.setTimeout(() => {}, 0);
while (id--) {
window.clearTimeout(id);
}
return Promise.resolve(id === 0);
};
const CallbackFromRangeSelector = (
selectedStartDate: string,
selectedEndDate: string
) => {
const startDateFormatted: string = moment(selectedStartDate).format();
const endDateFormatted: string = moment(selectedEndDate)
.add(23, 'hours')
.add(59, 'minutes')
.add(59, 'seconds')
.format();
dashboard.selectDashboard({
range: { startDate: startDateFormatted, endDate: endDateFormatted },
});
const endDate: number =
new Date(moment(endDateFormatted).format()).getTime() / 1000;
const now: number = Math.round(new Date().getTime() / 1000);
const diff: number = Math.abs(now - endDate);
const maxLim: number =
(selectedDashboard.refreshRate ?? DEFAULT_REFRESH_RATE) / 1000 !== 0
? (selectedDashboard.refreshRate ?? DEFAULT_REFRESH_RATE) / 1000 +
MINIMUM_TOLERANCE_LIMIT
: DEFAULT_TOLERANCE_LIMIT;
if (
!(diff >= 0 && diff <= maxLim) &&
selectedDashboard.refreshRate !== MAX_REFRESH_RATE
) {
clearTimeOuts().then(() => {
setPrometheusQueryData({
...prometheusQueryData,
firstLoad: true,
});
setRefreshRate(MAX_REFRESH_RATE);
});
} else if (!(diff >= 0 && diff <= maxLim)) {
clearTimeOuts().then(() => {
setPrometheusQueryData({
...prometheusQueryData,
firstLoad: true,
});
});
} else if (
diff >= 0 &&
diff <= maxLim &&
selectedDashboard.refreshRate === MAX_REFRESH_RATE
) {
clearTimeOuts().then(() => {
setPrometheusQueryData({
...prometheusQueryData,
firstLoad: true,
});
});
}
// If none of the above conditions match, then user has selected a relative time range.
};
const [openRefresh, setOpenRefresh] = React.useState(false);
const handleCloseRefresh = () => {
setOpenRefresh(false);
};
const handleOpenRefresh = () => {
setOpenRefresh(true);
};
// Apollo query to get the dashboards data
const { data: dashboards } = useQuery<DashboardList, ListDashboardVars>(
@ -102,6 +286,105 @@ const DashboardPage: React.FC = () => {
}
);
// Apollo query to get the scheduled workflow data
const { data: analyticsData, refetch } = useQuery<
WorkflowList,
WorkflowListDataVars
>(WORKFLOW_LIST_DETAILS, {
variables: { projectID, workflowIDs: [] },
notifyOnNetworkStatusChange: true,
fetchPolicy: 'no-cache',
});
// Apollo query to get the prometheus data
useQuery<PrometheusResponse, PrometheusQueryVars>(PROM_QUERY, {
variables: {
prometheusInput: prometheusQueryData?.promInput ?? {
url: '',
start: '',
end: '',
queries: [],
},
},
fetchPolicy: 'no-cache',
skip: prometheusQueryData?.promInput.url === '',
onCompleted: (eventData) => {
let chaosDataUpdates: ChaosDataUpdates = {
queryIDs: [],
chaosData: [],
reGenerate: false,
latestEventResult: [],
};
if (eventData && analyticsData) {
const selectedEndTime: number =
new Date(moment(selectedDashboard.range.endDate).format()).getTime() /
1000;
const selectedStartTime: number =
new Date(
moment(selectedDashboard.range.startDate).format()
).getTime() / 1000;
chaosDataUpdates = chaosEventDataParserForPrometheus(
prometheusQueryData?.numOfWorkflows,
analyticsData,
eventData,
prometheusQueryData?.chaosEvents,
selectedStartTime,
selectedEndTime
);
if (
chaosDataUpdates.reGenerate &&
!prometheusQueryData.firstLoad &&
!selectedDashboard.forceUpdate
) {
clearTimeOuts().then(() => {
dashboard.selectDashboard({
forceUpdate: true,
});
});
}
if (!chaosDataUpdates.reGenerate) {
if (selectedDashboard.forceUpdate) {
dashboard.selectDashboard({
forceUpdate: false,
});
}
if (!selectedDashboardInformation.selectionOverride) {
setChaosDataSet({
...chaosDataSet,
queryIDs: chaosDataUpdates.queryIDs,
chaosData: chaosDataUpdates.chaosData,
visibleChaos: chaosDataUpdates.chaosData,
latestEventResult: chaosDataUpdates.latestEventResult,
});
} else {
setChaosDataSet({
...chaosDataSet,
queryIDs: chaosDataUpdates.queryIDs,
chaosData: chaosDataUpdates.chaosData,
latestEventResult: chaosDataUpdates.latestEventResult,
});
}
}
chaosDataUpdates = {
queryIDs: [],
chaosData: [],
reGenerate: false,
latestEventResult: [],
};
}
},
onError: (error: ApolloError) => {
if (error.message === PROMETHEUS_ERROR_QUERY_RESOLUTION_LIMIT_REACHED) {
if (selectedDashboard.refreshRate !== MAX_REFRESH_RATE) {
dashboard.selectDashboard({
refreshRate: MAX_REFRESH_RATE,
});
}
setPrometheusQueryData({ ...prometheusQueryData, firstLoad: true });
}
},
});
useEffect(() => {
if (dashboards && dashboards.ListDashboard.length) {
if (
@ -118,6 +401,12 @@ const DashboardPage: React.FC = () => {
return data.db_id === selectedDashboardInformation.id;
}
)[0];
dashboard.selectDashboard({
selectedDashboardID: selectedDashboardInformation.id,
selectedDashboardName: selectedDashboard.db_name,
selectedDashboardTemplateName: selectedDashboard.db_type,
refreshRate: 0,
});
setSelectedDashboardInformation({
...selectedDashboardInformation,
dashboardListForAgent: availableDashboards,
@ -130,10 +419,10 @@ const DashboardPage: React.FC = () => {
useEffect(() => {
if (dataSources && dataSources.ListDataSource.length) {
dashboard.selectDashboard({
refreshRate: 0,
});
if (selectedDataSource.selectedDataSourceID === '') {
dashboard.selectDashboard({
refreshRate,
});
if (
selectedDashboardInformation.metaData &&
selectedDashboardInformation.metaData[0] &&
@ -152,6 +441,10 @@ const DashboardPage: React.FC = () => {
selectedDataSourceID: selectedDataSource.ds_id,
selectedDataSourceName: selectedDataSource.ds_name,
});
setSelectedDashboardInformation({
...selectedDashboardInformation,
selectionOverride: false,
});
}
if (
selectedDataSource &&
@ -164,6 +457,239 @@ const DashboardPage: React.FC = () => {
}
}, [selectedDashboardInformation.dashboardKey, dataSources]);
const generateChaosQueries = () => {
let chaosInformation: ChaosInformation = {
promQueries: prometheusQueryData.promInput.queries,
chaosQueryIDs: prometheusQueryData.chaosInput,
chaosEventList: prometheusQueryData.chaosEvents,
numberOfWorkflowsUnderConsideration: prometheusQueryData.numOfWorkflows,
};
if (prometheusQueryData.firstLoad && analyticsData?.ListWorkflow) {
const selectedEndTime: number =
new Date(moment(selectedDashboard.range.endDate).format()).getTime() /
1000;
const selectedStartTime: number =
new Date(moment(selectedDashboard.range.startDate).format()).getTime() /
1000;
const timeRangeDiff: number = selectedEndTime - selectedStartTime;
chaosInformation = getChaosQueryPromInputAndID(
analyticsData,
selectedDashboardInformation.agentID,
areaGraph,
timeRangeDiff,
selectedStartTime,
selectedEndTime,
prometheusQueryData.chaosEvents
);
}
setPrometheusQueryData({
...prometheusQueryData,
promInput: {
url: selectedDataSource.selectedDataSourceURL,
start: `${
selectedDashboard.range
? new Date(
moment(selectedDashboard.range.startDate).format()
).getTime() / 1000
: Math.round(new Date().getTime() / 1000) -
DEFAULT_RELATIVE_TIME_RANGE
}`,
end: `${
selectedDashboard.range
? new Date(
moment(selectedDashboard.range.endDate).format()
).getTime() / 1000
: Math.round(new Date().getTime() / 1000)
}`,
queries: chaosInformation.promQueries,
},
chaosInput: chaosInformation.chaosQueryIDs,
chaosEvents: chaosInformation.chaosEventList,
chaosEventsToBeShown: chaosInformation.chaosEventList.filter(
(event) => event.showOnTable || chaosDataSet.queryIDs.includes(event.id)
),
numOfWorkflows: chaosInformation.numberOfWorkflowsUnderConsideration,
firstLoad: !analyticsData?.ListWorkflow,
});
const existingEventIDs: string[] = prometheusQueryData.chaosEvents.map(
({ id }) => id
);
const newEventIDs: string[] = chaosInformation.chaosEventList
.map(({ id }) => id)
.filter((id: string) => !existingEventIDs.includes(id));
if (newEventIDs.length) {
setEventsToShow({
eventsToShow: selectedDashboardInformation.selectionOverride
? eventsToShow.eventsToShow
: eventsToShow.eventsToShow.concat(newEventIDs),
selectEvents: true,
});
}
chaosInformation = {
promQueries: [],
chaosQueryIDs: [],
chaosEventList: [],
numberOfWorkflowsUnderConsideration: 0,
};
};
const postEventSelectionRoutine = (selectedEvents: string[]) => {
setEventsToShow({
selectEvents: true,
eventsToShow: selectedEvents,
});
if (!selectedDashboardInformation.selectionOverride) {
setSelectedDashboardInformation({
...selectedDashboardInformation,
selectionOverride: true,
});
}
};
useEffect(() => {
if (prometheusQueryData.firstLoad) {
refetch();
generateChaosQueries();
if (selectedDashboard.refreshRate !== MAX_REFRESH_RATE) {
dashboard.selectDashboard({
range: {
startDate: moment
.unix(
Math.round(new Date().getTime() / 1000) -
DEFAULT_RELATIVE_TIME_RANGE
)
.format(),
endDate: moment
.unix(Math.round(new Date().getTime() / 1000))
.format(),
},
});
}
}
if (!prometheusQueryData.firstLoad) {
// check and update range for the default relative time selection
// for user selections / check with entire range of relative time differences
// for absolute time no updates to the range therefore no data refresh / feature disabled
const endDate: number =
new Date(moment(selectedDashboard.range.endDate).format()).getTime() /
1000;
const now: number = Math.round(new Date().getTime() / 1000);
const diff: number = Math.abs(now - endDate);
const maxLim: number =
(selectedDashboard.refreshRate ?? DEFAULT_REFRESH_RATE) / 1000 !== 0
? (selectedDashboard.refreshRate ?? DEFAULT_REFRESH_RATE) / 1000 +
MINIMUM_TOLERANCE_LIMIT
: DEFAULT_TOLERANCE_LIMIT;
if (
diff >= 0 &&
diff <= maxLim &&
selectedDashboard.refreshRate !== MAX_REFRESH_RATE
) {
const startDate: number =
new Date(
moment(selectedDashboard.range.startDate).format()
).getTime() / 1000;
const interval: number = endDate - startDate;
dashboard.selectDashboard({
range: {
startDate: moment
.unix(Math.round(new Date().getTime() / 1000) - interval)
.format(),
endDate: moment
.unix(Math.round(new Date().getTime() / 1000))
.format(),
},
});
}
setTimeout(
() => {
refetch();
generateChaosQueries();
},
selectedDashboard.refreshRate !== 0
? selectedDashboard.refreshRate
: DEFAULT_REFRESH_RATE
);
}
}, [prometheusQueryData]);
useEffect(() => {
if (
chaosDataSet.chaosData.length <
prometheusQueryData?.chaosEventsToBeShown.length
) {
clearTimeOuts().then(() => {
dashboard.selectDashboard({
forceUpdate: true,
});
});
} else {
let matchingEventsFound: ChaosEventDetails[] = [];
chaosDataSet.queryIDs.forEach((chaosQueryID: string, index: number) => {
matchingEventsFound = prometheusQueryData?.chaosEventsToBeShown.filter(
(event: ChaosEventDetails) => event.id === chaosQueryID
);
if (matchingEventsFound?.length === 0) {
clearTimeOuts().then(() => {
dashboard.selectDashboard({
forceUpdate: true,
});
});
} else if (
matchingEventsFound[0].result !==
chaosDataSet.latestEventResult[index]
) {
clearTimeOuts().then(() => {
dashboard.selectDashboard({
forceUpdate: true,
});
});
}
});
}
}, [chaosDataSet]);
useEffect(() => {
if (selectedDashboard.forceUpdate) {
refetch();
setPrometheusQueryData({ ...prometheusQueryData, firstLoad: true });
}
}, [selectedDashboard.forceUpdate]);
useEffect(() => {
if (eventsToShow.selectEvents) {
const filteredChaosData: Array<GraphMetric> = chaosDataSet.chaosData.filter(
(data, index) =>
eventsToShow.eventsToShow.includes(chaosDataSet.queryIDs[index])
);
setEventsToShow({ ...eventsToShow, selectEvents: false });
setChaosDataSet({ ...chaosDataSet, visibleChaos: filteredChaosData });
}
}, [eventsToShow.selectEvents]);
const getRefreshRateStatus = () => {
if (selectedDashboard.range) {
const endDate: number =
new Date(moment(selectedDashboard.range.endDate).format()).getTime() /
1000;
const now: number = Math.round(new Date().getTime() / 1000);
const diff: number = Math.abs(now - endDate);
const maxLim: number =
(selectedDashboard.refreshRate ?? DEFAULT_REFRESH_RATE) / 1000 !== 0
? (selectedDashboard.refreshRate ?? DEFAULT_REFRESH_RATE) / 1000 +
MINIMUM_TOLERANCE_LIMIT
: DEFAULT_TOLERANCE_LIMIT;
if (!(diff >= 0 && diff <= maxLim)) {
// A non relative time range has been selected.
// Refresh rate switch is not acknowledged and it's state is locked (Off).
// Select a relative time range or select a different refresh rate to unlock again.
return true;
}
}
// For relative time ranges.
return false;
};
return (
<Scaffold>
<div className={classes.rootContainer}>
@ -171,10 +697,10 @@ const DashboardPage: React.FC = () => {
<div className={classes.button}>
<BackButton />
</div>
<Typography variant="h3" className={classes.weightedFont}>
<Typography variant="h4" className={classes.weightedFont}>
{selectedDashboardInformation.agentName} /{' '}
<Typography
variant="h3"
variant="h4"
display="inline"
className={classes.italic}
>
@ -215,15 +741,20 @@ const DashboardPage: React.FC = () => {
selectedDataSourceID: '',
selectedDataSourceName: '',
});
setRefreshRate(0);
setAnchorEl(null);
}}
className={classes.menuItem}
className={
data.db_id === selectedDashboardInformation.id
? classes.menuItemSelected
: classes.menuItem
}
>
<div className={classes.expDiv}>
<Typography
data-cy="switchDashboard"
className={classes.btnText}
variant="h6"
className={`${classes.btnText} ${classes.italic}`}
variant="h5"
>
{data.db_name}
</Typography>
@ -235,17 +766,164 @@ const DashboardPage: React.FC = () => {
</Menu>
</Typography>
<div className={classes.headerDiv}>
<Typography
variant="h5"
className={`${classes.weightedFont} ${classes.dashboardType}`}
>
{selectedDashboardInformation.type}
<Typography className={classes.headerInfoText}>
{t('analyticsDashboard.monitoringDashboardPage.headerInfoText')}
</Typography>
<div className={classes.controls}>
<ButtonOutlined
className={classes.selectDate}
onClick={() => setDateRangeSelectorPopoverOpen(true)}
ref={dateRangeSelectorRef}
aria-label="time range"
aria-haspopup="true"
>
<Typography className={classes.displayDate}>
<IconButton className={classes.rangeSelectorClockIcon}>
<WatchLaterRoundedIcon />
</IconButton>
{!selectedDashboard.range ||
selectedDashboard.range.startDate === ' '
? `${t(
'analyticsDashboard.monitoringDashboardPage.rangeSelector.selectPeriod'
)}`
: `${selectedDashboard.range.startDate.split('-')[0]}-${
selectedDashboard.range.startDate.split('-')[1]
}-${selectedDashboard.range.startDate.substring(
selectedDashboard.range.startDate.lastIndexOf('-') + 1,
selectedDashboard.range.startDate.lastIndexOf('T')
)}
${selectedDashboard.range.startDate.substring(
selectedDashboard.range.startDate.lastIndexOf('T') + 1,
selectedDashboard.range.startDate.lastIndexOf('+')
)}
${t(
'analyticsDashboard.monitoringDashboardPage.rangeSelector.to'
)}
${selectedDashboard.range.endDate.split('-')[0]}-${
selectedDashboard.range.endDate.split('-')[1]
}-${selectedDashboard.range.endDate.substring(
selectedDashboard.range.endDate.lastIndexOf('-') + 1,
selectedDashboard.range.endDate.lastIndexOf('T')
)}
${selectedDashboard.range.endDate.substring(
selectedDashboard.range.endDate.lastIndexOf('T') + 1,
selectedDashboard.range.endDate.lastIndexOf('+')
)}`}
<IconButton className={classes.rangeSelectorIcon}>
<KeyboardArrowDownIcon />
</IconButton>
</Typography>
</ButtonOutlined>
<DateRangeSelector
anchorEl={dateRangeSelectorRef.current as HTMLElement}
isOpen={isDateRangeSelectorPopoverOpen}
onClose={() => {
setDateRangeSelectorPopoverOpen(false);
}}
callbackToSetRange={CallbackFromRangeSelector}
className={classes.rangeSelectorPopover}
/>
<FormControl className={classes.formControl} variant="outlined">
<InputLabel
id="refresh-controlled-open-select-label"
className={classes.inputLabel}
>
<AutorenewOutlinedIcon className={classes.refreshIcon} />
<Typography className={classes.refreshText}>
{t(
'analyticsDashboard.monitoringDashboardPage.refresh.heading'
)}
</Typography>
</InputLabel>
<Select
labelId="refresh-controlled-open-select-label"
id="refresh-controlled-open-select"
open={openRefresh}
disabled={getRefreshRateStatus()}
onClose={handleCloseRefresh}
onOpen={handleOpenRefresh}
value={refreshRate !== 0 ? refreshRate : null}
onChange={(event: React.ChangeEvent<{ value: unknown }>) => {
// When viewing data for non-relative time range, refresh should be Off ideally.
// UI can auto detect if it is not Off and switches it to Off.
// Now the user can try to view the non-relative time range data again.
if (selectedDashboard.refreshRate !== MAX_REFRESH_RATE) {
dashboard.selectDashboard({
refreshRate: event.target.value as number,
});
setRefreshRate(event.target.value as number);
} else {
dashboard.selectDashboard({
refreshRate: event.target.value as number,
});
setRefreshRate(event.target.value as number);
dashboard.selectDashboard({
forceUpdate: true,
});
setPrometheusQueryData({
...prometheusQueryData,
firstLoad: true,
});
}
}}
input={<OutlinedInput classes={outlinedInputClasses} />}
IconComponent={KeyboardArrowDownIcon}
MenuProps={{
anchorOrigin: {
vertical: 'bottom',
horizontal: 'left',
},
transformOrigin: {
vertical: 'top',
horizontal: 'left',
},
getContentAnchorEl: null,
}}
>
<MenuItem
key="Off-refresh-option"
value={MAX_REFRESH_RATE}
className={
refreshRate === MAX_REFRESH_RATE
? classes.menuListItemSelected
: classes.menuListItem
}
>
{t(
'analyticsDashboard.monitoringDashboardPage.refresh.off'
)}
</MenuItem>
{refreshData.map((data: RefreshObjectType) => (
<MenuItem
key={`${data.label}-refresh-option`}
value={data.value}
className={
refreshRate === data.value
? classes.menuListItemSelected
: classes.menuListItem
}
>
{t(data.label)}
</MenuItem>
))}
</Select>
</FormControl>
</div>
</div>
<div
className={classes.analyticsDiv}
key={selectedDashboardInformation.dashboardKey}
>
<div className={classes.chaosTableSection}>
<ChaosAccordion
dashboardKey={selectedDashboardInformation.dashboardKey}
chaosEventsToBeShown={prometheusQueryData?.chaosEventsToBeShown}
postEventSelectionRoutine={postEventSelectionRoutine}
/>
</div>
{selectedDashboardInformation.metaData[0] &&
selectedDashboardInformation.metaData[0].panel_groups.map(
(panelGroup: PanelGroupResponse) => (
@ -258,6 +936,7 @@ const DashboardPage: React.FC = () => {
panel_group_id={panelGroup.panel_group_id}
panel_group_name={panelGroup.panel_group_name}
panels={panelGroup.panels}
chaos_data={chaosDataSet.visibleChaos}
/>
</div>
)
@ -266,70 +945,12 @@ const DashboardPage: React.FC = () => {
</div>
</div>
{dataSourceStatus !== 'ACTIVE' ? (
<Modal open onClose={() => {}} width="60%">
<div className={classes.modal}>
<Typography align="center">
<CrossMarkIcon className={classes.icon} />
</Typography>
<Typography
className={classes.modalHeading}
align="center"
variant="h3"
>
Data source is {dataSourceStatus}
</Typography>
<Typography
align="center"
variant="body1"
className={classes.modalBody}
>
{t('analyticsDashboard.monitoringDashboardPage.dataSourceError')}
</Typography>
<div className={classes.flexButtons}>
<ButtonFilled
variant="success"
onClick={() => {
let dashboardTemplateID: number = -1;
if (
selectedDashboardInformation.type === 'Kubernetes Platform'
) {
dashboardTemplateID = 0;
} else if (
selectedDashboardInformation.type === 'Sock Shop'
) {
dashboardTemplateID = 1;
}
dashboard.selectDashboard({
selectedDashboardID: selectedDashboardInformation.id,
selectedDashboardName: selectedDashboardInformation.name,
selectedDashboardTemplateID: dashboardTemplateID,
});
history.push({
pathname: '/analytics/dashboard/configure',
search: `?projectID=${projectID}&projectRole=${projectRole}`,
});
}}
>
{t(
'analyticsDashboard.monitoringDashboardPage.reConfigureDashboard'
)}
</ButtonFilled>
<ButtonFilled
variant="success"
onClick={() => {
history.push({
pathname: '/analytics/datasource/configure',
search: `?projectID=${projectID}&projectRole=${projectRole}`,
});
}}
>
{t(
'analyticsDashboard.monitoringDashboardPage.updateDataSource'
)}
</ButtonFilled>
</div>
</div>
</Modal>
<DataSourceInactiveModal
dataSourceStatus={dataSourceStatus}
dashboardType={selectedDashboardInformation.type}
dashboardID={selectedDashboardInformation.id}
dashboardName={selectedDashboardInformation.name}
/>
) : (
<div />
)}

View File

@ -0,0 +1,42 @@
export default [
{
label: 'analyticsDashboard.monitoringDashboardPage.refresh.every5Seconds',
value: 5000,
},
{
label: 'analyticsDashboard.monitoringDashboardPage.refresh.every10Seconds',
value: 10000,
},
{
label: 'analyticsDashboard.monitoringDashboardPage.refresh.every30Seconds',
value: 30000,
},
{
label: 'analyticsDashboard.monitoringDashboardPage.refresh.every1Minute',
value: 60000,
},
{
label: 'analyticsDashboard.monitoringDashboardPage.refresh.every5Minutes',
value: 300000,
},
{
label: 'analyticsDashboard.monitoringDashboardPage.refresh.every15Minutes',
value: 900000,
},
{
label: 'analyticsDashboard.monitoringDashboardPage.refresh.every30Minutes',
value: 1800000,
},
{
label: 'analyticsDashboard.monitoringDashboardPage.refresh.every1Hour',
value: 3600000,
},
{
label: 'analyticsDashboard.monitoringDashboardPage.refresh.every2Hours',
value: 7200000,
},
{
label: 'analyticsDashboard.monitoringDashboardPage.refresh.every1Day',
value: 86400000,
},
];

View File

@ -1,4 +1,4 @@
import { makeStyles } from '@material-ui/core/styles';
import { fade, makeStyles, Theme } from '@material-ui/core/styles';
// Component styles
const useStyles = makeStyles((theme) => ({
@ -10,15 +10,15 @@ const useStyles = makeStyles((theme) => ({
},
root: {
marginTop: theme.spacing(5),
marginLeft: theme.spacing(6),
marginBottom: theme.spacing(8),
marginLeft: theme.spacing(1),
marginBottom: theme.spacing(2.5),
},
headerDiv: {
display: 'flex',
flexDirection: 'row',
justifyContent: 'space-between',
marginTop: theme.spacing(5),
paddingTop: theme.spacing(2.15),
backgroundColor: theme.palette.disabledBackground,
minHeight: '5rem',
},
@ -27,19 +27,8 @@ const useStyles = makeStyles((theme) => ({
fontStyle: 'italic',
},
dashboardType: {
marginLeft: theme.spacing(5),
marginTop: theme.spacing(3),
fontSize: '1.5rem',
},
loaderText: {
textAlign: 'center',
marginBottom: '3%',
},
analyticsDiv: {
paddingTop: theme.spacing(2),
padding: theme.spacing(1, 0, 2),
backgroundColor: theme.palette.background.paper,
minHeight: '26rem',
},
@ -58,6 +47,7 @@ const useStyles = makeStyles((theme) => ({
dashboardSwitchIcon: {
height: '2rem',
width: '2rem',
color: theme.palette.text.primary,
},
weightedFont: {
@ -66,9 +56,15 @@ const useStyles = makeStyles((theme) => ({
// Menu option
menuItem: {
minWidth: '10rem',
height: '2.5rem',
},
menuItemSelected: {
background: theme.palette.primary.light,
color: theme.palette.secondary.contrastText,
'&:hover': {
background: theme.palette.primary.light,
color: theme.palette.secondary.contrastText,
},
minWidth: '10rem',
height: '2.5rem',
@ -76,40 +72,142 @@ const useStyles = makeStyles((theme) => ({
expDiv: {
display: 'flex',
flexDirection: 'row',
},
btnText: {
fontWeight: 500,
},
icon: {
width: '6rem',
height: '6rem',
},
modalHeading: {
marginTop: theme.spacing(3.5),
fontSize: '2.25rem',
marginBottom: theme.spacing(4.5),
},
modalBody: {
marginBottom: theme.spacing(4.5),
},
flexButtons: {
selectDate: {
display: 'flex',
flexDirection: 'row',
justifyContent: 'space-evenly',
height: '2.9rem',
minWidth: '9rem',
marginLeft: theme.spacing(3.75),
border: `0.1px solid ${theme.palette.border.main}`,
background: theme.palette.background.paper,
borderRadius: 4,
textTransform: 'none',
},
loader: {
padding: '22%',
displayDate: {
width: '100%',
color: theme.palette.text.primary,
},
modal: {
padding: theme.spacing(15, 0),
rangeSelectorIcon: {
width: '0.625rem',
height: '0.625rem',
marginLeft: theme.spacing(1),
marginRight: theme.spacing(-1),
},
rangeSelectorClockIcon: {
width: '0.825rem',
height: '0.825rem',
marginLeft: theme.spacing(-1),
marginRight: theme.spacing(1),
},
rangeSelectorPopover: {
marginTop: theme.spacing(37.5),
},
formControl: {
width: '9rem',
marginLeft: theme.spacing(1.5),
[theme.breakpoints.down('sm')]: {
marginLeft: theme.spacing(3.75),
},
'& .MuiSelect-outlined': {
padding: '0.925rem',
'&:focus': {
borderColor: theme.palette.primary.main,
},
'& .MuiInputLabel-root': {
color: `${theme.palette.text.hint} !important`,
marginTop: `${theme.spacing(2)} !important`,
},
},
},
inputLabel: {
color: theme.palette.text.hint,
marginTop: theme.spacing(-1),
'&.MuiInputLabel-shrink': {
marginTop: theme.spacing(1),
},
},
menuListItem: {
color: theme.palette.text.hint,
height: '2.4rem',
},
menuListItemSelected: {
height: '2.4rem',
background: `${theme.palette.primary.light} !important`,
color: theme.palette.secondary.contrastText,
'&:hover': {
background: `${theme.palette.primary.light} !important`,
},
},
refreshIcon: {
marginRight: theme.spacing(1),
},
refreshText: {
marginTop: theme.spacing(-2.85),
marginLeft: theme.spacing(3.5),
},
headerInfoText: {
fontWeight: 500,
fontSize: '1rem',
lineHeight: '150%',
letterSpacing: '0.02em',
width: '35%',
marginLeft: theme.spacing(2.5),
},
controls: {
marginRight: theme.spacing(2.25),
display: 'flex',
[theme.breakpoints.down('sm')]: {
flexDirection: 'column',
marginRight: 0,
},
},
chaosTableSection: {
padding: theme.spacing(2.5, 2, 0),
},
}));
export const useOutlinedInputStyles = makeStyles((theme: Theme) => ({
root: {
'& $notchedOutline': {
borderColor: theme.palette.border.main,
},
'&:hover $notchedOutline': {
borderColor: theme.palette.primary.main,
boxShadow: `0px 4px 5px -2px ${fade(
theme.palette.highlight,
0.2
)},0px 7px 10px 1px ${fade(
theme.palette.highlight,
0.14
)},0px 2px 16px 1px ${fade(theme.palette.highlight, 0.12)}`,
},
'&$focused $notchedOutline': {
borderColor: theme.palette.primary.main,
},
'& .MuiInputLabel-root': {
color: `${theme.palette.text.hint} !important`,
marginTop: `${theme.spacing(2)} !important`,
},
color: theme.palette.text.hint,
background: theme.palette.background.paper,
},
}));

View File

@ -1,27 +1,12 @@
/* eslint-disable no-unused-expressions */
/* eslint-disable prefer-destructuring */
import { useMutation, useQuery } from '@apollo/client';
import { useMutation } from '@apollo/client';
import { Typography } from '@material-ui/core';
import { ButtonFilled, ButtonOutlined, Modal } from 'litmus-ui';
import React, { useEffect, useState } from 'react';
import { useSelector } from 'react-redux';
import { v4 as uuidv4 } from 'uuid';
import YAML from 'yaml';
import DashboardList from '../../components/PreconfiguredDashboards/data';
import Scaffold from '../../containers/layouts/Scaffold';
import { SCHEDULE_DETAILS } from '../../graphql';
import { CREATE_DASHBOARD, UPDATE_DASHBOARD } from '../../graphql/mutations';
import {
Artifact,
CronWorkflowYaml,
Parameter,
Template,
WorkflowYaml,
} from '../../models/chaosWorkflowYaml';
import {
ChaosResultNamesAndNamespacesMap,
DashboardDetails,
} from '../../models/dashboardsData';
import { DashboardDetails } from '../../models/dashboardsData';
import {
CreateDashboardInput,
Panel,
@ -30,20 +15,10 @@ import {
UpdateDashboardInput,
updatePanelGroupInput,
} from '../../models/graphql/dashboardsDetails';
import {
ScheduleDataVars,
Schedules,
ScheduleWorkflow,
} from '../../models/graphql/scheduleData';
import { history } from '../../redux/configureStore';
import { RootState } from '../../redux/reducers';
import { ReactComponent as CrossMarkIcon } from '../../svg/crossmark.svg';
import { getProjectID, getProjectRole } from '../../utils/getSearchParams';
import { validateWorkflowParameter } from '../../utils/validate';
import {
generateChaosQuery,
getWorkflowParameter,
} from '../../utils/yamlUtils';
import ConfigureDashboard from '../../views/AnalyticsDashboard/KubernetesDashboards/Form';
import useStyles from './styles';
@ -84,15 +59,6 @@ const DashboardConfigurePage: React.FC<DashboardConfigurePageProps> = ({
const [mutate, setMutate] = React.useState(false);
const [success, setSuccess] = React.useState(false);
// Apollo query to get the scheduled data
const { data: workflowSchedules } = useQuery<Schedules, ScheduleDataVars>(
SCHEDULE_DETAILS,
{
variables: { projectID },
fetchPolicy: 'cache-and-network',
}
);
const [createDashboard] = useMutation<CreateDashboardInput>(
CREATE_DASHBOARD,
{
@ -124,118 +90,12 @@ const DashboardConfigurePage: React.FC<DashboardConfigurePageProps> = ({
const getPanelGroups = () => {
if (configure === false) {
const chaosResultNamesAndNamespacesMap: ChaosResultNamesAndNamespacesMap[] = [];
workflowSchedules?.getScheduledWorkflows.forEach(
(schedule: ScheduleWorkflow) => {
if (
schedule.cluster_id === dashboardVars.agentID &&
!schedule.isRemoved
) {
let workflowYaml: WorkflowYaml | CronWorkflowYaml;
let parametersMap: Parameter[];
let workflowYamlCheck: boolean = true;
try {
workflowYaml = JSON.parse(schedule.workflow_manifest);
parametersMap = (workflowYaml as WorkflowYaml).spec.arguments
.parameters;
} catch (err) {
workflowYaml = JSON.parse(schedule.workflow_manifest);
parametersMap = (workflowYaml as CronWorkflowYaml).spec
.workflowSpec.arguments.parameters;
workflowYamlCheck = false;
}
(workflowYamlCheck
? (workflowYaml as WorkflowYaml).spec.templates
: (workflowYaml as CronWorkflowYaml).spec.workflowSpec.templates
).forEach((template: Template) => {
if (template.inputs && template.inputs.artifacts) {
template.inputs.artifacts.forEach((artifact: Artifact) => {
const parsedEmbeddedYaml = YAML.parse(artifact.raw.data);
if (parsedEmbeddedYaml.kind === 'ChaosEngine') {
let engineNamespace: string = '';
if (
typeof parsedEmbeddedYaml.metadata.namespace === 'string'
) {
engineNamespace = (parsedEmbeddedYaml.metadata
.namespace as string).substring(
1,
(parsedEmbeddedYaml.metadata.namespace as string)
.length - 1
);
} else {
engineNamespace = Object.keys(
parsedEmbeddedYaml.metadata.namespace
)[0];
}
if (validateWorkflowParameter(engineNamespace)) {
engineNamespace = getWorkflowParameter(engineNamespace);
parametersMap.forEach((parameterKeyValue: Parameter) => {
if (parameterKeyValue.name === engineNamespace) {
engineNamespace = parameterKeyValue.value;
}
});
} else {
engineNamespace = parsedEmbeddedYaml.metadata.namespace;
}
let matchIndex: number = -1;
const check: number = chaosResultNamesAndNamespacesMap.filter(
(data, index) => {
if (
data.resultName.includes(
parsedEmbeddedYaml.metadata.name
) &&
data.resultNamespace === engineNamespace
) {
matchIndex = index;
return true;
}
return false;
}
).length;
if (check === 0) {
chaosResultNamesAndNamespacesMap.push({
resultName: `${parsedEmbeddedYaml.metadata.name}-${parsedEmbeddedYaml.spec.experiments[0].name}`,
resultNamespace: engineNamespace,
workflowName: workflowYaml.metadata.name,
experimentName:
parsedEmbeddedYaml.spec.experiments[0].name,
});
} else {
chaosResultNamesAndNamespacesMap[
matchIndex
].workflowName = `${chaosResultNamesAndNamespacesMap[matchIndex].workflowName}, \n${workflowYaml.metadata.name}`;
}
}
});
}
});
}
}
);
const panelGroups: PanelGroup[] = [];
DashboardList[DashboardTemplateID ?? 0].panelGroups.forEach(
(panelGroup) => {
const selectedPanels: Panel[] = [];
panelGroup.panels.forEach((panel) => {
const selectedPanel: Panel = panel;
chaosResultNamesAndNamespacesMap.forEach((keyValue) => {
selectedPanel.prom_queries.push({
queryid: uuidv4(),
prom_query_name: generateChaosQuery(
DashboardList[DashboardTemplateID ?? 0]
.chaosEventQueryTemplate,
keyValue.resultName,
keyValue.resultNamespace
),
legend: `${keyValue.workflowName} / \n${keyValue.experimentName}`,
resolution: '1/1',
minstep: '1',
line: false,
close_area: true,
});
});
selectedPanels.push(selectedPanel);
selectedPanels.push(panel);
});
panelGroups.push({
panel_group_name: panelGroup.panel_group_name,

View File

@ -9,6 +9,11 @@ import createReducer from './createReducer';
const initialState: DashboardData = {
selectedDashboardID: '',
refreshRate: 0,
range: {
startDate: '',
endDate: '',
},
forceUpdate: false,
};
export const selectDashboard = createReducer<DashboardData>(initialState, {

View File

@ -0,0 +1,13 @@
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0)">
<path d="M0.360848 1.13672H11.6401C11.8383 1.13672 12.0005 1.29888 12.0005 1.49708C12.0005 1.69528 11.8383 1.85744 11.6401 1.85744H0.360848C0.16265 1.85744 0.000488281 1.69528 0.000488281 1.49708C0.000488281 1.29888 0.16265 1.13672 0.360848 1.13672Z" fill="black" fill-opacity="0.6"/>
<path d="M0.361031 4.125H8.25292C8.45112 4.125 8.61328 4.28716 8.61328 4.48536C8.61328 4.68356 8.45112 4.84572 8.25292 4.84572H0.361031C0.162832 4.84572 0.000670433 4.68356 0.000670433 4.48536C0.000670433 4.28716 0.162832 4.125 0.361031 4.125Z" fill="black" fill-opacity="0.6"/>
<path d="M0.360848 7.15234H11.6401C11.8383 7.15234 12.0005 7.3145 12.0005 7.5127C12.0005 7.71089 11.8383 7.87305 11.6401 7.87305H0.360848C0.16265 7.87305 0.000488281 7.71089 0.000488281 7.5127C0.000488281 7.3145 0.16265 7.15234 0.360848 7.15234Z" fill="black" fill-opacity="0.6"/>
<path d="M0.360804 10.1445H5.44189C5.64008 10.1445 5.80225 10.3067 5.80225 10.5049C5.80225 10.7031 5.64008 10.8653 5.44189 10.8653H0.360804C0.162605 10.8653 0.000442982 10.7031 0.000442982 10.5049C0.000442982 10.3067 0.162605 10.1445 0.360804 10.1445Z" fill="black" fill-opacity="0.6"/>
</g>
<defs>
<clipPath id="clip0">
<rect width="12" height="12" fill="white" transform="matrix(-1 0 0 1 12.0005 0)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,7 @@
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0.36036 1.13672H11.6396C11.8378 1.13672 12 1.29888 12 1.49708C12 1.69528 11.8378 1.85744 11.6396 1.85744H0.36036C0.162162 1.85744 0 1.69528 0 1.49708C0 1.29888 0.162162 1.13672 0.36036 1.13672Z" fill="#858CDD"/>
<path d="M0.360542 4.125H8.25243C8.45063 4.125 8.61279 4.28716 8.61279 4.48536C8.61279 4.68356 8.45063 4.84572 8.25243 4.84572H0.360542C0.162344 4.84572 0.000182152 4.68356 0.000182152 4.48536C0.000182152 4.28716 0.162344 4.125 0.360542 4.125Z" fill="#858CDD"/>
<path d="M0.36036 7.15234H11.6396C11.8378 7.15234 12 7.3145 12 7.5127C12 7.71089 11.8378 7.87305 11.6396 7.87305H0.36036C0.162162 7.87305 0 7.71089 0 7.5127C0 7.3145 0.162162 7.15234 0.36036 7.15234Z" fill="#858CDD"/>
<path d="M0.360315 10.1445H5.4414C5.6396 10.1445 5.80176 10.3067 5.80176 10.5049C5.80176 10.7031 5.6396 10.8653 5.4414 10.8653H0.360315C0.162117 10.8653 -4.52995e-05 10.7031 -4.52995e-05 10.5049C-4.52995e-05 10.3067 0.162117 10.1445 0.360315 10.1445Z" fill="#858CDD"/>
<rect x="-0.141421" width="15.2184" height="0.890909" rx="0.445455" transform="matrix(-0.707107 -0.707107 -0.707107 0.707107 11.4324 10.8023)" fill="#858CDD" stroke="white" stroke-width="0.2"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1,10 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.5 3H13V5.5" stroke="black" stroke-opacity="0.6" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M9.5 6.5L13 3" stroke="black" stroke-opacity="0.6" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M5.5 13H3V10.5" stroke="black" stroke-opacity="0.6" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M6.5 9.5L3 13" stroke="black" stroke-opacity="0.6" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M13 10.5V13H10.5" stroke="black" stroke-opacity="0.6" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M9.5 9.5L13 13" stroke="black" stroke-opacity="0.6" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M3 5.5V3H5.5" stroke="black" stroke-opacity="0.6" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M6.5 6.5L3 3" stroke="black" stroke-opacity="0.6" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 979 B

View File

@ -0,0 +1,12 @@
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0)">
<path d="M1.2 8.28125L0 12.0013L3.72 10.8013L1.2 8.28125Z" fill="black" fill-opacity="0.6"/>
<path d="M7.89771 1.56323L2.04297 7.41797L4.58851 9.96351L10.4433 4.10877L7.89771 1.56323Z" fill="black" fill-opacity="0.6"/>
<path d="M11.8203 1.86L10.1403 0.18C9.90025 -0.06 9.54025 -0.06 9.30025 0.18L8.76025 0.72L11.2803 3.24L11.8203 2.7C12.0603 2.46 12.0603 2.1 11.8203 1.86Z" fill="black" fill-opacity="0.6"/>
</g>
<defs>
<clipPath id="clip0">
<rect width="12" height="12" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 637 B

View File

@ -1,6 +1,55 @@
import { ChaosResultNamesAndNamespacesMap } from '../models/dashboardsData';
/* eslint-disable no-unused-expressions */
/* eslint-disable prefer-destructuring */
/* eslint-disable no-loop-func */
/* eslint-disable no-console */
/* eslint-disable max-len */
import { GraphMetric } from 'litmus-ui';
import { v4 as uuidv4 } from 'uuid';
import YAML from 'yaml';
import DashboardListData from '../components/PreconfiguredDashboards/data';
import {
Artifact,
CronWorkflowYaml,
Parameter,
Template,
WorkflowYaml,
} from '../models/chaosWorkflowYaml';
import {
ChaosDataUpdates,
ChaosEventDetails,
ChaosInformation,
ChaosResultNamesAndNamespacesMap,
ExperimentNameAndChaosDataMap,
RunWiseChaosMetrics,
WorkflowAndExperimentMetaDataMap,
WorkflowRunWiseDetails,
} from '../models/dashboardsData';
import { PromQuery } from '../models/graphql/dashboardsDetails';
import {
PrometheusResponse,
promQueryInput,
} from '../models/graphql/prometheus';
import {
ChaosData,
ExecutionData,
Workflow,
WorkflowList,
WorkflowRun,
} from '../models/graphql/workflowListData';
import {
CHAOS_EXPERIMENT_VERDICT_FAIL,
CHAOS_EXPERIMENT_VERDICT_PASS,
DEFAULT_CHAOS_EVENT_NAME,
DEFAULT_CHAOS_EVENT_PROMETHEUS_QUERY_RESOLUTION,
DEFAULT_METRIC_SERIES_NAME,
INVALID_RESILIENCE_SCORE_STRING,
PROMETHEUS_QUERY_RESOLUTION_LIMIT,
STATUS_RUNNING,
} from '../pages/MonitoringDashboardPage/constants';
import { validateWorkflowParameter } from './validate';
import { generateChaosQuery, getWorkflowParameter } from './yamlUtils';
const getResultNameAndNamespace = (chaosQueryString: string) => {
export const getResultNameAndNamespace = (chaosQueryString: string) => {
const parsedChaosInfoMap: ChaosResultNamesAndNamespacesMap = {
resultName: chaosQueryString
.split(',')[0]
@ -14,8 +63,597 @@ const getResultNameAndNamespace = (chaosQueryString: string) => {
.slice(1, -1),
workflowName: '',
experimentName: '',
selectedWorkflowIds: [],
};
return parsedChaosInfoMap;
};
export default getResultNameAndNamespace;
export const getWorkflowRunWiseDetails = (schedule: Workflow) => {
const workflowRunWiseDetailsForSchedule: WorkflowRunWiseDetails = {
idsOfWorkflowRuns: [],
resilienceScoreForWorkflowRuns: [],
statusOfWorkflowRuns: [],
experimentNameWiseChaosDataOfWorkflowRuns: [],
};
if (schedule.workflow_runs) {
schedule.workflow_runs.forEach((data: WorkflowRun, runIndex) => {
try {
const executionData: ExecutionData = JSON.parse(data.execution_data);
workflowRunWiseDetailsForSchedule.idsOfWorkflowRuns[runIndex] =
data.workflow_run_id;
workflowRunWiseDetailsForSchedule.statusOfWorkflowRuns[runIndex] =
executionData.finishedAt.length === 0
? STATUS_RUNNING
: executionData.phase;
const { nodes } = executionData;
for (const key of Object.keys(nodes)) {
const node = nodes[key];
if (node.chaosData) {
const { chaosData } = node;
if (
!workflowRunWiseDetailsForSchedule
.experimentNameWiseChaosDataOfWorkflowRuns[runIndex]
) {
workflowRunWiseDetailsForSchedule.experimentNameWiseChaosDataOfWorkflowRuns[
runIndex
] = [];
}
workflowRunWiseDetailsForSchedule.experimentNameWiseChaosDataOfWorkflowRuns[
runIndex
].push({
experimentName: chaosData.experimentName,
chaosData,
});
}
}
if (executionData.event_type === 'UPDATE') {
workflowRunWiseDetailsForSchedule.resilienceScoreForWorkflowRuns[
runIndex
] = executionData.resiliency_score ?? NaN;
} else if (
executionData.finishedAt.length === 0 ||
executionData.phase === STATUS_RUNNING
) {
workflowRunWiseDetailsForSchedule.resilienceScoreForWorkflowRuns[
runIndex
] = NaN;
}
} catch (error) {
console.error(error);
}
});
}
return workflowRunWiseDetailsForSchedule;
};
const getRunWiseChaosMetrics = (
idsOfWorkflowRuns: string[],
experimentNameWiseChaosDataOfWorkflowRuns: ExperimentNameAndChaosDataMap[][],
experimentNameFromYaml: string,
resilienceScoreForWorkflowRuns: number[],
statusOfWorkflowRuns: string[]
) => {
const workflowRunMetricsPerExperiment: RunWiseChaosMetrics[] = [];
experimentNameWiseChaosDataOfWorkflowRuns.forEach(
(dataArray: ExperimentNameAndChaosDataMap[], runIndex) => {
try {
let selectedChaosData: ChaosData = {
engineName: '',
engineUID: '',
experimentName: '',
experimentPod: '',
experimentStatus: '',
experimentVerdict: '',
failStep: '',
lastUpdatedAt: '',
namespace: '',
probeSuccessPercentage: '',
runnerPod: '',
};
dataArray.forEach(
(experimentNameAndChaosData: ExperimentNameAndChaosDataMap) => {
if (
experimentNameAndChaosData.experimentName ===
experimentNameFromYaml
) {
selectedChaosData = experimentNameAndChaosData.chaosData;
}
}
);
workflowRunMetricsPerExperiment.push({
runIndex,
runID: idsOfWorkflowRuns[runIndex],
lastUpdatedTimeStamp: parseInt(selectedChaosData.lastUpdatedAt, 10),
probeSuccessPercentage: `${selectedChaosData.probeSuccessPercentage}%`,
experimentStatus: selectedChaosData.experimentStatus,
experimentVerdict: selectedChaosData.experimentVerdict,
resilienceScore: `${
resilienceScoreForWorkflowRuns[runIndex] +
(Number.isNaN(resilienceScoreForWorkflowRuns[runIndex]) ? '' : '%')
}`,
workflowStatus: statusOfWorkflowRuns[runIndex],
});
} catch (error) {
console.error(error);
}
}
);
return workflowRunMetricsPerExperiment;
};
export const getChaosQueryPromInputAndID = (
analyticsData: WorkflowList,
agentID: string,
areaGraph: string[],
timeRangeDiff: number,
selectedStartTime: number,
selectedEndTime: number,
existingEvents: ChaosEventDetails[]
) => {
const chaosInformation: ChaosInformation = {
promQueries: [],
chaosQueryIDs: [],
chaosEventList: [],
numberOfWorkflowsUnderConsideration: 0,
};
const chaosResultNamesAndNamespacesMap: ChaosResultNamesAndNamespacesMap[] = [];
const workflowAndExperimentMetaDataMap: WorkflowAndExperimentMetaDataMap[] = [];
analyticsData?.ListWorkflow.forEach((schedule: Workflow) => {
if (schedule.cluster_id === agentID && !schedule.isRemoved) {
const workflowRunWiseDetails: WorkflowRunWiseDetails = getWorkflowRunWiseDetails(
schedule
);
let workflowYaml: WorkflowYaml | CronWorkflowYaml;
let parametersMap: Parameter[];
let workflowYamlCheck: boolean = true;
try {
workflowYaml = JSON.parse(schedule.workflow_manifest);
parametersMap = (workflowYaml as WorkflowYaml).spec.arguments
.parameters;
} catch (err) {
workflowYaml = JSON.parse(schedule.workflow_manifest);
parametersMap = (workflowYaml as CronWorkflowYaml).spec.workflowSpec
.arguments.parameters;
workflowYamlCheck = false;
}
(workflowYamlCheck
? (workflowYaml as WorkflowYaml).spec.templates
: (workflowYaml as CronWorkflowYaml).spec.workflowSpec.templates
).forEach((template: Template) => {
if (template.inputs && template.inputs.artifacts) {
template.inputs.artifacts.forEach((artifact: Artifact) => {
const parsedEmbeddedYaml = YAML.parse(artifact.raw.data);
if (parsedEmbeddedYaml.kind === 'ChaosEngine') {
let engineNamespace: string = '';
if (typeof parsedEmbeddedYaml.metadata.namespace === 'string') {
engineNamespace = (parsedEmbeddedYaml.metadata
.namespace as string).substring(
1,
(parsedEmbeddedYaml.metadata.namespace as string).length - 1
);
} else {
engineNamespace = Object.keys(
parsedEmbeddedYaml.metadata.namespace
)[0];
}
if (validateWorkflowParameter(engineNamespace)) {
engineNamespace = getWorkflowParameter(engineNamespace);
parametersMap.forEach((parameterKeyValue: Parameter) => {
if (parameterKeyValue.name === engineNamespace) {
engineNamespace = parameterKeyValue.value;
}
});
} else {
engineNamespace = parsedEmbeddedYaml.metadata.namespace;
}
let matchIndex: number = -1;
const check: number = chaosResultNamesAndNamespacesMap.filter(
(data, index) => {
if (
data.resultName.includes(
parsedEmbeddedYaml.metadata.name
) &&
data.resultNamespace === engineNamespace
) {
matchIndex = index;
return true;
}
return false;
}
).length;
if (check === 0) {
chaosResultNamesAndNamespacesMap.push({
resultName: `${parsedEmbeddedYaml.metadata.name}-${parsedEmbeddedYaml.spec.experiments[0].name}`,
resultNamespace: engineNamespace,
workflowName: workflowYaml.metadata.name,
experimentName: parsedEmbeddedYaml.spec.experiments[0].name,
selectedWorkflowIds: [schedule.workflow_id],
});
workflowAndExperimentMetaDataMap.push({
workflowID: schedule.workflow_id,
workflowName: workflowYaml.metadata.name,
experimentName: parsedEmbeddedYaml.spec.experiments[0].name,
targetApp: parsedEmbeddedYaml.spec.appinfo.applabel.split(
'='
)[1],
targetNamespace: parsedEmbeddedYaml.spec.appinfo.appns,
runWiseChaosMetrics: getRunWiseChaosMetrics(
workflowRunWiseDetails.idsOfWorkflowRuns,
workflowRunWiseDetails.experimentNameWiseChaosDataOfWorkflowRuns,
parsedEmbeddedYaml.spec.experiments[0].name,
workflowRunWiseDetails.resilienceScoreForWorkflowRuns,
workflowRunWiseDetails.statusOfWorkflowRuns
),
});
} else {
chaosResultNamesAndNamespacesMap[
matchIndex
].workflowName = `${chaosResultNamesAndNamespacesMap[matchIndex].workflowName}, \n${workflowYaml.metadata.name}`;
chaosResultNamesAndNamespacesMap[
matchIndex
].selectedWorkflowIds.push(schedule.workflow_id);
workflowAndExperimentMetaDataMap.push({
workflowID: schedule.workflow_id,
workflowName: workflowYaml.metadata.name,
experimentName:
chaosResultNamesAndNamespacesMap[matchIndex].experimentName,
targetApp: parsedEmbeddedYaml.spec.appinfo.applabel.split(
'='
)[1],
targetNamespace: parsedEmbeddedYaml.spec.appinfo.appns,
runWiseChaosMetrics: getRunWiseChaosMetrics(
workflowRunWiseDetails.idsOfWorkflowRuns,
workflowRunWiseDetails.experimentNameWiseChaosDataOfWorkflowRuns,
chaosResultNamesAndNamespacesMap[matchIndex].experimentName,
workflowRunWiseDetails.resilienceScoreForWorkflowRuns,
workflowRunWiseDetails.statusOfWorkflowRuns
),
});
}
}
});
}
});
}
});
chaosResultNamesAndNamespacesMap.forEach((keyValue, index) => {
let queryID: string = uuidv4();
const matchingEvent: ChaosEventDetails[] = existingEvents.filter(
(event: ChaosEventDetails) =>
event.workflow === keyValue.workflowName &&
event.experiment === keyValue.experimentName
);
if (matchingEvent.length) {
queryID = matchingEvent[0].id;
}
const chaosEventMetrics: WorkflowAndExperimentMetaDataMap = workflowAndExperimentMetaDataMap.filter(
(data) =>
keyValue.selectedWorkflowIds.includes(data.workflowID) &&
data.experimentName === keyValue.experimentName
)[0];
const availableRunMetrics: RunWiseChaosMetrics[] = chaosEventMetrics.runWiseChaosMetrics.filter(
(eventMetric) =>
eventMetric.lastUpdatedTimeStamp >= selectedStartTime &&
eventMetric.lastUpdatedTimeStamp <= selectedEndTime
);
const latestResult: string = availableRunMetrics.length
? availableRunMetrics[availableRunMetrics.length - 1].experimentVerdict
: '--';
chaosInformation.promQueries.push({
queryid: queryID,
query: generateChaosQuery(
DashboardListData[0].chaosEventQueryTemplate,
keyValue.resultName,
keyValue.resultNamespace
),
legend: `${keyValue.workflowName} / \n${keyValue.experimentName}`,
resolution: DEFAULT_CHAOS_EVENT_PROMETHEUS_QUERY_RESOLUTION,
minstep:
timeRangeDiff * chaosResultNamesAndNamespacesMap.length <
PROMETHEUS_QUERY_RESOLUTION_LIMIT - 1
? 1
: Math.floor(
(timeRangeDiff * chaosResultNamesAndNamespacesMap.length) /
(PROMETHEUS_QUERY_RESOLUTION_LIMIT + 1)
),
});
chaosInformation.chaosQueryIDs.push(queryID);
chaosInformation.chaosEventList.push({
id: queryID,
legend: areaGraph[index % areaGraph.length],
workflow: keyValue.workflowName,
experiment: keyValue.experimentName,
target: `${chaosEventMetrics.targetNamespace} / ${chaosEventMetrics.targetApp}`,
result: latestResult,
chaosMetrics: chaosEventMetrics,
showOnTable: availableRunMetrics.length > 0,
});
});
chaosInformation.numberOfWorkflowsUnderConsideration =
analyticsData?.ListWorkflow.length;
return chaosInformation;
};
export const chaosEventDataParserForPrometheus = (
numOfWorkflows: number,
workflowAnalyticsData: WorkflowList,
eventData: PrometheusResponse,
chaosEventList: ChaosEventDetails[],
selectedStartTime: number,
selectedEndTime: number
) => {
const chaosDataUpdates: ChaosDataUpdates = {
queryIDs: [],
chaosData: [],
reGenerate: false,
latestEventResult: [],
};
if (workflowAnalyticsData.ListWorkflow) {
if (numOfWorkflows !== workflowAnalyticsData.ListWorkflow.length) {
chaosDataUpdates.reGenerate = true;
}
}
const workflowCheckList: string[] = [];
eventData?.GetPromQuery.forEach((queryResponse) => {
if (
queryResponse.legends &&
queryResponse.legends[0] &&
parseInt(queryResponse.tsvs[0][0].timestamp ?? '0', 10) >=
selectedStartTime &&
parseInt(queryResponse.tsvs[0][0].timestamp ?? '0', 10) <= selectedEndTime
) {
const chaosEventDetails: ChaosEventDetails = chaosEventList.filter(
(e) => queryResponse.queryid === e.id
)[0];
let latestRunMetric: RunWiseChaosMetrics | undefined;
if (chaosEventDetails && workflowAnalyticsData.ListWorkflow) {
const workflowAndExperiments: WorkflowAndExperimentMetaDataMap =
chaosEventDetails.chaosMetrics;
const updatedWorkflowDetails: Workflow = workflowAnalyticsData.ListWorkflow.filter(
(workflow: Workflow) =>
workflow.workflow_id === workflowAndExperiments.workflowID
)[0];
const updatedWorkflowRunWiseDetailsFromAnalytics: WorkflowRunWiseDetails = getWorkflowRunWiseDetails(
updatedWorkflowDetails
);
if (!workflowCheckList.includes(workflowAndExperiments.workflowID)) {
workflowCheckList.push(workflowAndExperiments.workflowID);
updatedWorkflowRunWiseDetailsFromAnalytics.experimentNameWiseChaosDataOfWorkflowRuns.forEach(
(mapList: ExperimentNameAndChaosDataMap[], index) => {
const workflowRunID: string =
updatedWorkflowRunWiseDetailsFromAnalytics.idsOfWorkflowRuns[
index
];
const filteredChaosEventDetails: ChaosEventDetails[] = chaosEventList.filter(
(e) =>
workflowAndExperiments.workflowID ===
e.chaosMetrics.workflowID
);
filteredChaosEventDetails.forEach((event: ChaosEventDetails) => {
const experimentInWorkflowRunEvent: RunWiseChaosMetrics[] = event.chaosMetrics.runWiseChaosMetrics.filter(
(runWiseMetric) => runWiseMetric.runID === workflowRunID
);
if (
experimentInWorkflowRunEvent.length === 0 &&
!event.showOnTable
) {
chaosDataUpdates.reGenerate = true;
}
});
}
);
}
const updatedRunWiseMetricsPerExperiment: RunWiseChaosMetrics[] = getRunWiseChaosMetrics(
updatedWorkflowRunWiseDetailsFromAnalytics.idsOfWorkflowRuns,
updatedWorkflowRunWiseDetailsFromAnalytics.experimentNameWiseChaosDataOfWorkflowRuns,
workflowAndExperiments.experimentName,
updatedWorkflowRunWiseDetailsFromAnalytics.resilienceScoreForWorkflowRuns,
updatedWorkflowRunWiseDetailsFromAnalytics.statusOfWorkflowRuns
);
if (
updatedWorkflowRunWiseDetailsFromAnalytics.idsOfWorkflowRuns
.length !== workflowAndExperiments.runWiseChaosMetrics.length &&
updatedRunWiseMetricsPerExperiment.length !== 0
) {
chaosDataUpdates.reGenerate = true;
}
const updatedMetrics: RunWiseChaosMetrics[] = [];
workflowAndExperiments.runWiseChaosMetrics.forEach(
(metric: RunWiseChaosMetrics) => {
const changeIndex: number = updatedWorkflowRunWiseDetailsFromAnalytics.idsOfWorkflowRuns.indexOf(
metric.runID
);
updatedMetrics.push({
runIndex: metric.runIndex,
runID: metric.runID,
lastUpdatedTimeStamp:
updatedRunWiseMetricsPerExperiment[changeIndex]
.lastUpdatedTimeStamp,
probeSuccessPercentage:
updatedRunWiseMetricsPerExperiment[changeIndex]
.probeSuccessPercentage,
experimentStatus:
updatedRunWiseMetricsPerExperiment[changeIndex]
.experimentStatus,
experimentVerdict:
updatedRunWiseMetricsPerExperiment[changeIndex]
.experimentVerdict,
resilienceScore:
updatedRunWiseMetricsPerExperiment[changeIndex].resilienceScore,
workflowStatus:
updatedRunWiseMetricsPerExperiment[changeIndex].workflowStatus,
});
}
);
const availableRunMetrics: RunWiseChaosMetrics[] = updatedMetrics.filter(
(eventMetric) =>
eventMetric.lastUpdatedTimeStamp >= selectedStartTime &&
eventMetric.lastUpdatedTimeStamp <= selectedEndTime
);
latestRunMetric = availableRunMetrics[availableRunMetrics.length - 1];
}
chaosDataUpdates.queryIDs.push(queryResponse.queryid);
chaosDataUpdates.chaosData.push(
...queryResponse.legends.map((elem, index) => ({
metricName: elem[0] ?? DEFAULT_CHAOS_EVENT_NAME,
data: queryResponse.tsvs[index].map((dataPoint) => ({
date: parseInt(dataPoint.timestamp ?? '0', 10) * 1000,
value: parseInt(dataPoint.value ?? '0', 10),
})),
baseColor: chaosEventDetails ? chaosEventDetails.legend : '',
subData: [
{
subDataName: 'Workflow Status',
value: latestRunMetric
? latestRunMetric.workflowStatus
: STATUS_RUNNING,
},
{
subDataName: 'Experiment Status',
value: latestRunMetric
? latestRunMetric.experimentStatus
: STATUS_RUNNING,
},
{
subDataName: 'Resilience Score',
value:
latestRunMetric &&
latestRunMetric.workflowStatus !== STATUS_RUNNING &&
latestRunMetric.resilienceScore !==
INVALID_RESILIENCE_SCORE_STRING
? latestRunMetric.resilienceScore
: '--',
},
{
subDataName: 'Probe Success Percentage',
value: latestRunMetric
? latestRunMetric.probeSuccessPercentage
: '--',
},
{
subDataName: 'Experiment Verdict',
value: latestRunMetric
? latestRunMetric.experimentVerdict +
(latestRunMetric.experimentVerdict ===
CHAOS_EXPERIMENT_VERDICT_PASS ||
latestRunMetric.experimentVerdict ===
CHAOS_EXPERIMENT_VERDICT_FAIL
? 'ed'
: '')
: '--',
},
],
// Filter subData within the start and end time of interleaving on experiment's lastUpdatedTimeStamp.
// Add one extra subData field - lastUpdatedTimeStamp to filter subData in graph.
// This method sends the latest run details within selected dashboard time range as the subData.
// Needs to be updated to send every run detail with lastUpdatedTimeStamp in the ${NEW_SUBDATA_FIELD}.
/*
Schema:
(to be updated as)
- subData: [
{ subDataName: "subData-1-1", value: "1-1", lastUpdatedTimeStamp: 1616832979 },
{ subDataName: "subData-1-2", value: "1-2", lastUpdatedTimeStamp: 1616833006 },
],
subData:
(to be filtered by lastUpdatedTimeStamp in litmus-ui graph on hovering over chaos events)
- Workflow Status
- Experiment Status
- Resilience Score
- Probe Success Percentage
- Experiment Verdict
*/
}))
);
chaosDataUpdates.latestEventResult.push(
latestRunMetric ? latestRunMetric.experimentVerdict : '--'
);
}
});
if (workflowAnalyticsData.ListWorkflow) {
if (eventData?.GetPromQuery.length < chaosDataUpdates.chaosData.length) {
chaosDataUpdates.reGenerate = true;
}
if (
numOfWorkflows !== workflowAnalyticsData.ListWorkflow.length &&
workflowAnalyticsData.ListWorkflow.length !== 0
) {
chaosDataUpdates.reGenerate = true;
}
}
return chaosDataUpdates;
};
export const getPromQueryInput = (
prom_queries: PromQuery[],
timeRangeDiff: number
) => {
const promQueries: promQueryInput[] = [];
prom_queries.forEach((query: PromQuery) => {
promQueries.push({
queryid: query.queryid,
query: query.prom_query_name,
legend: query.legend,
resolution: query.resolution,
minstep:
Math.floor(timeRangeDiff / parseInt(query.minstep, 10)) *
prom_queries.length <
PROMETHEUS_QUERY_RESOLUTION_LIMIT - 1
? parseInt(query.minstep, 10)
: Math.floor(
(timeRangeDiff * prom_queries.length) /
(PROMETHEUS_QUERY_RESOLUTION_LIMIT + 1)
),
});
});
return promQueries;
};
export const seriesDataParserForPrometheus = (
prometheusData: PrometheusResponse,
lineGraph: string[]
) => {
const seriesData: Array<GraphMetric> = [];
prometheusData.GetPromQuery.forEach((queryResponse, mainIndex) => {
if (queryResponse.legends && queryResponse.legends[0]) {
seriesData.push(
...queryResponse.legends.map((elem, index) => ({
metricName: elem[0] ?? DEFAULT_METRIC_SERIES_NAME,
data: queryResponse.tsvs[index].map((dataPoint) => ({
date: parseInt(dataPoint.timestamp ?? '0', 10) * 1000,
value: parseFloat(dataPoint.value ?? '0.0'),
})),
baseColor:
lineGraph[
(mainIndex + (index % lineGraph.length)) % lineGraph.length
],
}))
);
}
});
return seriesData;
};

View File

@ -1,42 +1,15 @@
/* eslint-disable no-unused-expressions */
/* eslint-disable prefer-destructuring */
/* eslint-disable @typescript-eslint/no-unused-vars */
import { useMutation, useQuery } from '@apollo/client';
import { useMutation } from '@apollo/client';
import { IconButton, Menu, MenuItem, Typography } from '@material-ui/core';
import MoreVertIcon from '@material-ui/icons/MoreVert';
import { ButtonFilled, ButtonOutlined, Modal } from 'litmus-ui';
import moment from 'moment';
import React, { useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { v4 as uuidv4 } from 'uuid';
import YAML from 'yaml';
import DashboardList from '../../../../components/PreconfiguredDashboards/data';
import { SCHEDULE_DETAILS } from '../../../../graphql';
import { DELETE_DASHBOARD, UPDATE_PANEL } from '../../../../graphql/mutations';
import {
Artifact,
CronWorkflowYaml,
Parameter,
Template,
WorkflowYaml,
} from '../../../../models/chaosWorkflowYaml';
import { ChaosResultNamesAndNamespacesMap } from '../../../../models/dashboardsData';
import { DELETE_DASHBOARD } from '../../../../graphql/mutations';
import {
DeleteDashboardInput,
ListDashboardResponse,
Panel,
PanelGroup,
PanelGroupResponse,
PanelOption,
PanelResponse,
PromQuery,
UpdatePanelInput,
} from '../../../../models/graphql/dashboardsDetails';
import {
ScheduleDataVars,
Schedules,
ScheduleWorkflow,
} from '../../../../models/graphql/scheduleData';
import useActions from '../../../../redux/actions';
import * as DashboardActions from '../../../../redux/actions/dashboards';
import * as DataSourceActions from '../../../../redux/actions/dataSource';
@ -47,12 +20,6 @@ import {
getProjectID,
getProjectRole,
} from '../../../../utils/getSearchParams';
import getEngineNameAndNamespace from '../../../../utils/promUtils';
import { validateWorkflowParameter } from '../../../../utils/validate';
import {
generateChaosQuery,
getWorkflowParameter,
} from '../../../../utils/yamlUtils';
import useStyles, { StyledTableCell } from './styles';
interface TableDataProps {
@ -77,20 +44,6 @@ const TableData: React.FC<TableDataProps> = ({ data }) => {
] = React.useState<DeleteDashboardInput>({
dbID: '',
});
// Apollo query to get the scheduled data
const { data: workflowSchedules } = useQuery<Schedules, ScheduleDataVars>(
SCHEDULE_DETAILS,
{
variables: { projectID },
fetchPolicy: 'cache-and-network',
}
);
const [updatePanel] = useMutation<UpdatePanelInput>(UPDATE_PANEL, {
onError: () => {
console.error('error updating dashboard details');
},
});
const [deleteDashboard] = useMutation<boolean, DeleteDashboardInput>(
DELETE_DASHBOARD,
@ -126,239 +79,6 @@ const TableData: React.FC<TableDataProps> = ({ data }) => {
setAnchorEl(null);
};
const reSyncChaosQueries = () => {
const chaosResultNamesAndNamespacesMap: ChaosResultNamesAndNamespacesMap[] = [];
workflowSchedules?.getScheduledWorkflows.forEach(
(schedule: ScheduleWorkflow) => {
if (schedule.cluster_id === data.cluster_id && !schedule.isRemoved) {
let workflowYaml: WorkflowYaml | CronWorkflowYaml;
let parametersMap: Parameter[];
let workflowYamlCheck: boolean = true;
try {
workflowYaml = JSON.parse(schedule.workflow_manifest);
parametersMap = (workflowYaml as WorkflowYaml).spec.arguments
.parameters;
} catch (err) {
workflowYaml = JSON.parse(schedule.workflow_manifest);
parametersMap = (workflowYaml as CronWorkflowYaml).spec.workflowSpec
.arguments.parameters;
workflowYamlCheck = false;
}
(workflowYamlCheck
? (workflowYaml as WorkflowYaml).spec.templates
: (workflowYaml as CronWorkflowYaml).spec.workflowSpec.templates
).forEach((template: Template) => {
if (template.inputs && template.inputs.artifacts) {
template.inputs.artifacts.forEach((artifact: Artifact) => {
const parsedEmbeddedYaml = YAML.parse(artifact.raw.data);
if (parsedEmbeddedYaml.kind === 'ChaosEngine') {
let engineNamespace: string = '';
if (
typeof parsedEmbeddedYaml.metadata.namespace === 'string'
) {
engineNamespace = (parsedEmbeddedYaml.metadata
.namespace as string).substring(
1,
(parsedEmbeddedYaml.metadata.namespace as string).length -
1
);
} else {
engineNamespace = Object.keys(
parsedEmbeddedYaml.metadata.namespace
)[0];
}
if (validateWorkflowParameter(engineNamespace)) {
engineNamespace = getWorkflowParameter(engineNamespace);
parametersMap.forEach((parameterKeyValue: Parameter) => {
if (parameterKeyValue.name === engineNamespace) {
engineNamespace = parameterKeyValue.value;
}
});
} else {
engineNamespace = parsedEmbeddedYaml.metadata.namespace;
}
let matchIndex: number = -1;
const check: number = chaosResultNamesAndNamespacesMap.filter(
(data, index) => {
if (
data.resultName.includes(
parsedEmbeddedYaml.metadata.name
) &&
data.resultNamespace === engineNamespace
) {
matchIndex = index;
return true;
}
return false;
}
).length;
if (check === 0) {
chaosResultNamesAndNamespacesMap.push({
resultName: `${parsedEmbeddedYaml.metadata.name}-${parsedEmbeddedYaml.spec.experiments[0].name}`,
resultNamespace: engineNamespace,
workflowName: workflowYaml.metadata.name,
experimentName:
parsedEmbeddedYaml.spec.experiments[0].name,
});
} else {
chaosResultNamesAndNamespacesMap[
matchIndex
].workflowName = `${chaosResultNamesAndNamespacesMap[matchIndex].workflowName}, \n${workflowYaml.metadata.name}`;
}
}
});
}
});
}
}
);
const isChaosQueryPresent: number[] = Array(
chaosResultNamesAndNamespacesMap.length
).fill(0);
data.panel_groups[0].panels[0].prom_queries.forEach(
(existingPromQuery: PromQuery) => {
if (
existingPromQuery.prom_query_name.startsWith(
'litmuschaos_awaited_experiments'
)
) {
const chaosDetails: ChaosResultNamesAndNamespacesMap = getEngineNameAndNamespace(
existingPromQuery.prom_query_name
);
chaosResultNamesAndNamespacesMap.forEach(
(
chaosDetailsFomSchedule: ChaosResultNamesAndNamespacesMap,
index: number
) => {
if (
chaosDetailsFomSchedule.resultName.includes(
chaosDetails.resultName
) &&
chaosDetailsFomSchedule.resultNamespace ===
chaosDetails.resultNamespace
) {
isChaosQueryPresent[index] = 1;
}
}
);
}
}
);
const dashboardTemplateID: number =
data.db_type === 'Kubernetes Platform' ? 0 : 1;
const updatedPanelGroups: PanelGroup[] = [];
data.panel_groups.forEach((panelGroup: PanelGroupResponse) => {
const updatedPanels: Panel[] = [];
panelGroup.panels.forEach((panel: PanelResponse) => {
const updatedQueries: PromQuery[] = [];
panel.prom_queries.forEach((query: PromQuery) => {
let updatedLegend: string = query.legend;
if (
query.prom_query_name.startsWith('litmuschaos_awaited_experiments')
) {
const chaosDetails: ChaosResultNamesAndNamespacesMap = getEngineNameAndNamespace(
query.prom_query_name
);
chaosResultNamesAndNamespacesMap.forEach(
(chaosDetailsFomSchedule: ChaosResultNamesAndNamespacesMap) => {
if (
chaosDetailsFomSchedule.resultName.includes(
chaosDetails.resultName
) &&
chaosDetailsFomSchedule.resultNamespace ===
chaosDetails.resultNamespace &&
!query.legend.includes(chaosDetailsFomSchedule.workflowName)
) {
updatedLegend = `${chaosDetailsFomSchedule.workflowName}, \n${query.legend}`;
}
}
);
}
const updatedQuery: PromQuery = {
queryid: query.queryid,
prom_query_name: query.prom_query_name,
resolution: query.resolution,
minstep: query.minstep,
line: query.line,
close_area: query.close_area,
legend: updatedLegend,
};
updatedQueries.push(updatedQuery);
});
chaosResultNamesAndNamespacesMap.forEach(
(keyValue: ChaosResultNamesAndNamespacesMap, index: number) => {
if (isChaosQueryPresent[index] === 0) {
updatedQueries.push({
queryid: uuidv4(),
prom_query_name: generateChaosQuery(
DashboardList[dashboardTemplateID].chaosEventQueryTemplate,
keyValue.resultName,
keyValue.resultNamespace
),
legend: `${keyValue.workflowName} / \n${keyValue.experimentName}`,
resolution: '1/1',
minstep: '1',
line: false,
close_area: true,
});
}
}
);
const existingPanelOptions: PanelOption = {
points: panel.panel_options.points,
grids: panel.panel_options.grids,
left_axis: panel.panel_options.left_axis,
};
const updatedPanel: Panel = {
panel_id: panel.panel_id,
panel_name: panel.panel_name,
panel_options: existingPanelOptions,
prom_queries: updatedQueries,
y_axis_left: panel.y_axis_left,
y_axis_right: panel.y_axis_right,
x_axis_down: panel.x_axis_down,
unit: panel.unit,
};
updatedPanels.push(updatedPanel);
});
updatedPanelGroups.push({
panel_group_id: panelGroup.panel_group_id,
panel_group_name: panelGroup.panel_group_name,
panels: updatedPanels,
});
});
const panelInputData: Panel[] = [];
updatedPanelGroups.forEach((panelGroup: PanelGroup) => {
panelGroup.panels.forEach((panel: Panel) => {
panelInputData.push({
panel_id: panel.panel_id,
db_id: data.db_id,
panel_group_id: panelGroup.panel_group_id,
prom_queries: panel.prom_queries,
panel_options: panel.panel_options,
panel_name: panel.panel_name,
y_axis_left: panel.y_axis_left,
y_axis_right: panel.y_axis_right,
x_axis_down: panel.x_axis_down,
unit: panel.unit,
});
});
});
updatePanel({
variables: { panelInput: panelInputData },
});
return true;
};
const onDashboardLoadRoutine = async () => {
dashboard.selectDashboard({
selectedDashboardID: data.db_id,
@ -366,8 +86,14 @@ const TableData: React.FC<TableDataProps> = ({ data }) => {
selectedDashboardTemplateName: data.db_type,
selectedAgentID: data.cluster_id,
selectedAgentName: data.cluster_name,
refreshRate: 0,
});
return Promise.resolve(reSyncChaosQueries());
dataSource.selectDataSource({
selectedDataSourceURL: '',
selectedDataSourceID: '',
selectedDataSourceName: '',
});
return true;
};
useEffect(() => {
@ -439,11 +165,6 @@ const TableData: React.FC<TableDataProps> = ({ data }) => {
value="Analysis"
onClick={() => {
onDashboardLoadRoutine().then(() => {
dataSource.selectDataSource({
selectedDataSourceURL: '',
selectedDataSourceID: '',
selectedDataSourceName: '',
});
history.push({
pathname: '/analytics/dashboard',
search: `?projectID=${projectID}&projectRole=${projectRole}`,

View File

@ -0,0 +1,125 @@
import { IconButton, Typography, withStyles } from '@material-ui/core';
import MuiAccordion from '@material-ui/core/Accordion';
import AccordionDetails from '@material-ui/core/AccordionDetails';
import MuiAccordionSummary from '@material-ui/core/AccordionSummary';
import ArrowDropDownIcon from '@material-ui/icons/ArrowDropDown';
import ArrowDropUpIcon from '@material-ui/icons/ArrowDropUp';
import { ButtonFilled } from 'litmus-ui';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { ChaosEventDetails } from '../../../../models/dashboardsData';
import ChaosTable from '../ChaosTable';
import useStyles from './styles';
const Accordion = withStyles((theme) => ({
root: {
border: 0,
boxShadow: 'none',
'&:not(:last-child)': {
borderBottom: 0,
},
'&:before': {
display: 'none',
},
'&$expanded': {
margin: 'auto',
},
'& .MuiAccordionSummary-root.Mui-expanded': {
cursor: 'default',
minHeight: '1rem !important',
height: '2.75rem',
paddingTop: theme.spacing(0.5),
},
'& .MuiAccordionSummary-root': {
cursor: 'default',
minHeight: '1rem !important',
height: '2.75rem',
paddingTop: theme.spacing(0.5),
},
'& .MuiButtonBase-root:hover': {
cursor: 'default',
},
},
}))(MuiAccordion);
const AccordionSummary = withStyles({
content: {
flexGrow: 0,
},
})(MuiAccordionSummary);
const StyledAccordionDetails = withStyles((theme) => ({
root: {
padding: theme.spacing(0, 0, 1),
},
}))(AccordionDetails);
interface ChaosAccordionProps {
dashboardKey: string;
chaosEventsToBeShown: ChaosEventDetails[];
postEventSelectionRoutine: (selectedEvents: string[]) => void;
}
const ChaosAccordion: React.FC<ChaosAccordionProps> = ({
dashboardKey,
chaosEventsToBeShown,
postEventSelectionRoutine,
}) => {
const classes = useStyles();
const { t } = useTranslation();
const [chaosTableOpen, setChaosTableOpen] = React.useState<boolean>(false);
return (
<Accordion expanded={chaosTableOpen}>
<AccordionSummary
aria-controls="panel1a-content"
id="panel1a-header"
className={classes.accordionSummary}
key={`chaos-table-${dashboardKey}`}
>
<ButtonFilled
className={classes.button}
onClick={() => {
setChaosTableOpen(!chaosTableOpen);
}}
startIcon={
!chaosTableOpen ? (
<ArrowDropDownIcon className={classes.tableDropIcon} />
) : (
<ArrowDropUpIcon className={classes.tableDropIcon} />
)
}
>
<Typography className={classes.chaosHelperText}>
{!chaosTableOpen
? `${t(
'analyticsDashboard.monitoringDashboardPage.chaosTable.showTable'
)}`
: `${t(
'analyticsDashboard.monitoringDashboardPage.chaosTable.hideTable'
)}`}
</Typography>
</ButtonFilled>
<IconButton
aria-label="edit chaos query"
aria-haspopup="true"
disabled
data-cy="editChaosQueryButton"
className={classes.editIconButton}
>
<img src="/icons/editIcon.svg" alt="Edit" />
</IconButton>
</AccordionSummary>
<StyledAccordionDetails className={classes.accordionDetails}>
<ChaosTable
chaosList={chaosEventsToBeShown}
selectEvents={(selectedEvents: string[]) => {
postEventSelectionRoutine(selectedEvents);
}}
/>
</StyledAccordionDetails>
</Accordion>
);
};
export default ChaosAccordion;

View File

@ -0,0 +1,42 @@
import { makeStyles } from '@material-ui/core';
const useStyles = makeStyles((theme) => ({
accordionSummary: {
display: 'flex',
justifyItems: 'center',
background: theme.palette.disabledBackground,
},
accordionDetails: {
width: '100%',
},
button: {
background: 'none',
boxShadow: 'none',
padding: 0,
'&:hover': {
background: 'none',
boxShadow: 'none',
cursor: 'pointer !important',
},
},
chaosHelperText: {
fontWeight: 500,
fontSize: '1rem',
color: theme.palette.primary.main,
},
tableDropIcon: {
width: '1.75rem',
height: '1.75rem',
color: theme.palette.primary.main,
},
editIconButton: {
marginTop: theme.spacing(-0.75),
},
}));
export default useStyles;

View File

@ -0,0 +1,70 @@
import { Typography } from '@material-ui/core';
import React from 'react';
import CheckBox from '../../../../components/CheckBox';
import { StyledTableCell } from '../../../../components/StyledTableCell';
import { ChaosEventDetails } from '../../../../models/dashboardsData';
import {
CHAOS_EXPERIMENT_VERDICT_FAIL,
CHAOS_EXPERIMENT_VERDICT_PASS,
} from '../../../../pages/MonitoringDashboardPage/constants';
import useStyles from './styles';
interface TableDataProps {
data: ChaosEventDetails;
itemSelectionStatus: boolean;
labelIdentifier: string;
}
const TableData: React.FC<TableDataProps> = ({
data,
itemSelectionStatus,
labelIdentifier,
}) => {
const classes = useStyles();
return (
<>
<StyledTableCell padding="checkbox" className={classes.checkbox}>
<CheckBox
checked={itemSelectionStatus}
inputProps={{ 'aria-labelledby': labelIdentifier }}
/>
</StyledTableCell>
<StyledTableCell>
<div
className={classes.colorBar}
style={{
background: data.legend,
}}
/>
</StyledTableCell>
<StyledTableCell>
<Typography className={classes.tableObjects}>
{data.workflow}
</Typography>
</StyledTableCell>
<StyledTableCell>
<Typography className={classes.tableObjects}>
{data.experiment}
</Typography>
</StyledTableCell>
<StyledTableCell>
<Typography className={classes.tableObjects}>{data.target}</Typography>
</StyledTableCell>
<StyledTableCell>
<Typography
className={`${classes.tableObjects} ${
data.result === CHAOS_EXPERIMENT_VERDICT_PASS
? classes.passColor
: data.result === CHAOS_EXPERIMENT_VERDICT_FAIL
? classes.failColor
: classes.awaitedColor
}`}
>
{data.result}
</Typography>
</StyledTableCell>
</>
);
};
export default TableData;

View File

@ -0,0 +1,83 @@
import { TableHead, TableRow } from '@material-ui/core';
import React from 'react';
import { useTranslation } from 'react-i18next';
import CheckBox from '../../../../components/CheckBox';
import { StyledTableCell } from '../../../../components/StyledTableCell';
import useStyles from './styles';
interface TableHeaderProps {
onSelectAllClick: (event: React.ChangeEvent<HTMLInputElement>) => void;
numSelected: number;
rowCount: number;
}
const TableHeader: React.FC<TableHeaderProps> = ({
onSelectAllClick,
numSelected,
rowCount,
}) => {
const classes = useStyles();
const { t } = useTranslation();
return (
<TableHead>
<TableRow className={classes.tableHead}>
<StyledTableCell padding="checkbox" className={classes.checkbox}>
<CheckBox
indeterminate={numSelected > 0 && numSelected < rowCount}
checked={rowCount > 0 && numSelected === rowCount}
onChange={onSelectAllClick}
inputProps={{ 'aria-label': 'select all desserts' }}
/>
</StyledTableCell>
<StyledTableCell className={classes.headSpacing}>
<div className={classes.nameContent}>
<div className={classes.nameHead}>
{t(
'analyticsDashboard.monitoringDashboardPage.chaosTable.tableHead1'
)}
</div>
</div>
</StyledTableCell>
<StyledTableCell className={classes.headSpacing}>
<div className={classes.nameContent}>
<div className={classes.nameHead}>
{t(
'analyticsDashboard.monitoringDashboardPage.chaosTable.tableHead2'
)}
</div>
</div>
</StyledTableCell>
<StyledTableCell className={classes.headSpacing}>
<div className={classes.nameContent}>
<div className={classes.nameHead}>
{t(
'analyticsDashboard.monitoringDashboardPage.chaosTable.tableHead3'
)}
</div>
</div>
</StyledTableCell>
<StyledTableCell className={classes.headSpacing}>
<div className={classes.nameContent}>
<div className={classes.nameHead}>
{t(
'analyticsDashboard.monitoringDashboardPage.chaosTable.tableHead4'
)}
</div>
</div>
</StyledTableCell>
<StyledTableCell className={classes.headSpacing}>
<div className={classes.nameContent}>
<div className={classes.nameHead}>
{t(
'analyticsDashboard.monitoringDashboardPage.chaosTable.tableHead5'
)}
</div>
</div>
</StyledTableCell>
</TableRow>
</TableHead>
);
};
export default TableHeader;

View File

@ -0,0 +1,165 @@
import {
Paper,
Table,
TableBody,
TableCell,
TableContainer,
TablePagination,
TableRow,
Typography,
} from '@material-ui/core';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { ChaosEventDetails } from '../../../../models/dashboardsData';
import useStyles from './styles';
import TableData from './TableData';
import TableHeader from './TableHeader';
interface ChaosTableProps {
chaosList: ChaosEventDetails[];
selectEvents: (selectedEvents: string[]) => void;
}
const ChaosTable: React.FC<ChaosTableProps> = ({ chaosList, selectEvents }) => {
const classes = useStyles();
const { t } = useTranslation();
const [page, setPage] = React.useState(0);
const [rowsPerPage, setRowsPerPage] = React.useState(5);
const [selected, setSelected] = React.useState<string[]>([]);
const isSelected = (name: string) => selected.indexOf(name) !== -1;
const emptyRows =
rowsPerPage - Math.min(rowsPerPage, chaosList.length - page * rowsPerPage);
const handleChangePage = (event: unknown, newPage: number) => {
setPage(newPage);
};
const handleChangeRowsPerPage = (
event: React.ChangeEvent<HTMLInputElement>
) => {
setRowsPerPage(parseInt(event.target.value, 10));
setPage(0);
};
const handleSelectAllClick = (event: React.ChangeEvent<HTMLInputElement>) => {
if (event.target.checked) {
const newSelecteds = chaosList.map((n: ChaosEventDetails) => n.id);
setSelected(newSelecteds);
selectEvents(newSelecteds);
return;
}
setSelected([]);
selectEvents([]);
};
const handleClick = (name: string) => {
const selectedIndex = selected.indexOf(name);
let newSelected: string[] = [];
if (selectedIndex === -1) {
newSelected = newSelected.concat(selected, name);
} else if (selectedIndex === 0) {
newSelected = newSelected.concat(selected.slice(1));
} else if (selectedIndex === selected.length - 1) {
newSelected = newSelected.concat(selected.slice(0, -1));
} else if (selectedIndex > 0) {
newSelected = newSelected.concat(
selected.slice(0, selectedIndex),
selected.slice(selectedIndex + 1)
);
}
setSelected(newSelected);
selectEvents(newSelected);
};
return (
<div className={classes.root} id="chaos">
<div>
<section className="table section">
<Paper className={classes.tableBody}>
<TableContainer className={classes.tableMain}>
<Table aria-label="simple table">
<TableHeader
onSelectAllClick={handleSelectAllClick}
numSelected={selected.length}
rowCount={chaosList.length}
/>
<TableBody>
{chaosList.length ? (
chaosList
.slice(0)
.slice(
page * rowsPerPage,
page * rowsPerPage + rowsPerPage
)
.map((data: ChaosEventDetails, index: number) => {
const isItemSelected = isSelected(data.id);
const labelId = `enhanced-table-checkbox-${index}`;
return (
<TableRow
hover
onClick={() => {
handleClick(data.id);
}}
role="checkbox"
aria-checked={isItemSelected}
tabIndex={-1}
key={data.id}
selected={isItemSelected}
>
<TableData
data={data}
itemSelectionStatus={isItemSelected}
labelIdentifier={labelId}
/>
</TableRow>
);
})
) : (
<TableRow>
<TableCell colSpan={6}>
<div className={classes.noRecords}>
<img
src="/icons/cloudIcon.svg"
className={classes.cloudIcon}
alt="Chaos cloud"
/>
<Typography
align="center"
className={classes.noRecordsText}
>
{t(
'analyticsDashboard.monitoringDashboardPage.chaosTable.noRecords'
)}
</Typography>
</div>
</TableCell>
</TableRow>
)}
{chaosList.length > 0 && emptyRows > 0 && (
<TableRow style={{ height: 75 * emptyRows }}>
<TableCell colSpan={6} />
</TableRow>
)}
</TableBody>
</Table>
</TableContainer>
{chaosList.length > 0 && (
<TablePagination
rowsPerPageOptions={[5, 10, 25, 50]}
component="div"
count={chaosList.length}
rowsPerPage={rowsPerPage}
page={page}
onChangePage={handleChangePage}
onChangeRowsPerPage={handleChangeRowsPerPage}
/>
)}
</Paper>
</section>
</div>
</div>
);
};
export default ChaosTable;

View File

@ -0,0 +1,109 @@
import { makeStyles } from '@material-ui/core';
const useStyles = makeStyles((theme) => ({
root: {
height: '100%',
width: '100%',
flexDirection: 'column',
overflow: 'hidden',
},
tableMain: {
background: theme.palette.cards.header,
maxHeight: '30rem',
'&::-webkit-scrollbar': {
width: '0.2em',
},
'&::-webkit-scrollbar-track': {
webkitBoxShadow: `inset 0 0 6px ${theme.palette.common.black}`,
},
'&::-webkit-scrollbar-thumb': {
backgroundColor: theme.palette.primary.main,
},
'&:not(:last-child)': {
borderBottom: 0,
},
'& td': {
borderBottom: `1px solid ${theme.palette.border.main}`,
},
},
tableBody: {
background: theme.palette.cards.header,
},
tableHead: {
background: theme.palette.cards.header,
},
nameHead: {
color: theme.palette.text.hint,
margin: theme.spacing(2, 0, 1.5),
fontSize: '0.875rem',
lineHeight: '150%',
fontWeight: 500,
letterSpacing: '0.02em',
},
tableObjects: {
paddingLeft: theme.spacing(1),
color: theme.palette.text.primary,
height: '1.75rem',
marginTop: theme.spacing(2),
fontSize: '0.875rem',
lineHeight: '130%',
},
headSpacing: {
paddingLeft: theme.spacing(2.5),
},
nameContent: {
color: theme.palette.text.primary,
display: 'flex',
flexDirection: 'row',
fontSize: '0.8rem',
},
checkbox: {
padding: theme.spacing(0.5, 0, 0, 2.5),
},
noRecords: {
height: '10rem',
display: 'flex',
padding: theme.spacing(5, 3),
justifyContent: 'center',
},
cloudIcon: {
height: '5rem',
width: '5rem',
},
noRecordsText: {
color: theme.palette.text.hint,
padding: theme.spacing(2),
fontSize: '1.5rem',
},
passColor: {
color: theme.palette.success.main,
},
failColor: {
color: theme.palette.error.main,
},
awaitedColor: {
color: theme.palette.text.hint,
},
colorBar: {
height: '0.45rem',
width: '2.75rem',
margin: theme.spacing(0.75, 0, 0, 0.75),
},
}));
export default useStyles;

View File

@ -1,14 +0,0 @@
import React from 'react';
import { PanelGroupResponse } from '../../../models/graphql/dashboardsDetails';
import PanelGroupContainer from './PanelGroupContainer';
import PanelGroupContent from './PanelGroupContent';
const DashboardPanelGroup: React.FC<PanelGroupResponse> = ({ ...props }) => {
return (
<PanelGroupContainer>
<PanelGroupContent {...props} />
</PanelGroupContainer>
);
};
export default DashboardPanelGroup;

View File

@ -0,0 +1,103 @@
import { Typography } from '@material-ui/core';
import { ButtonFilled, Modal } from 'litmus-ui';
import React from 'react';
import { useTranslation } from 'react-i18next';
import {
DASHBOARD_TYPE_1,
DASHBOARD_TYPE_2,
} from '../../../../pages/MonitoringDashboardPage/constants';
import useActions from '../../../../redux/actions';
import * as DashboardActions from '../../../../redux/actions/dashboards';
import { history } from '../../../../redux/configureStore';
import { ReactComponent as CrossMarkIcon } from '../../../../svg/crossmark.svg';
import {
getProjectID,
getProjectRole,
} from '../../../../utils/getSearchParams';
import useStyles from './styles';
interface DataSourceInactiveModalProps {
dataSourceStatus: string;
dashboardType: string;
dashboardID: string;
dashboardName: string;
}
const DataSourceInactiveModal: React.FC<DataSourceInactiveModalProps> = ({
dataSourceStatus,
dashboardType,
dashboardID,
dashboardName,
}) => {
const classes = useStyles();
const { t } = useTranslation();
// get ProjectID
const projectID = getProjectID();
const projectRole = getProjectRole();
const dashboard = useActions(DashboardActions);
return (
<Modal open onClose={() => {}} width="60%">
<div className={classes.modal}>
<Typography align="center">
<CrossMarkIcon className={classes.icon} />
</Typography>
<Typography
className={classes.modalHeading}
align="center"
variant="h3"
>
{`${t(
'analyticsDashboard.monitoringDashboardPage.dataSourceIs'
)} ${dataSourceStatus}`}
</Typography>
<Typography
align="center"
variant="body1"
className={classes.modalBody}
>
{t('analyticsDashboard.monitoringDashboardPage.dataSourceError')}
</Typography>
<div className={classes.flexButtons}>
<ButtonFilled
variant="success"
onClick={() => {
let dashboardTemplateID: number = -1;
if (dashboardType === DASHBOARD_TYPE_1) {
dashboardTemplateID = 0;
} else if (dashboardType === DASHBOARD_TYPE_2) {
dashboardTemplateID = 1;
}
dashboard.selectDashboard({
selectedDashboardID: dashboardID,
selectedDashboardName: dashboardName,
selectedDashboardTemplateID: dashboardTemplateID,
});
history.push({
pathname: '/analytics/dashboard/configure',
search: `?projectID=${projectID}&projectRole=${projectRole}`,
});
}}
>
{t(
'analyticsDashboard.monitoringDashboardPage.reConfigureDashboard'
)}
</ButtonFilled>
<ButtonFilled
variant="success"
onClick={() => {
history.push({
pathname: '/analytics/datasource/configure',
search: `?projectID=${projectID}&projectRole=${projectRole}`,
});
}}
>
{t('analyticsDashboard.monitoringDashboardPage.updateDataSource')}
</ButtonFilled>
</div>
</div>
</Modal>
);
};
export default DataSourceInactiveModal;

View File

@ -0,0 +1,28 @@
import { makeStyles } from '@material-ui/core';
const useStyles = makeStyles((theme) => ({
icon: {
width: '6rem',
height: '6rem',
},
modalHeading: {
margin: theme.spacing(3.5, 0, 4.5),
fontSize: '2.25rem',
},
modalBody: {
marginBottom: theme.spacing(4.5),
},
flexButtons: {
display: 'flex',
justifyContent: 'space-evenly',
},
modal: {
padding: theme.spacing(15, 0),
},
}));
export default useStyles;

View File

@ -1,14 +0,0 @@
import React from 'react';
import { PanelResponse } from '../../../models/graphql/dashboardsDetails';
import PanelContainer from './PanelContainer';
import PanelContent from './PanelContent';
const GraphPanel: React.FC<PanelResponse> = ({ ...props }) => {
return (
<PanelContainer>
<PanelContent {...props} />
</PanelContainer>
);
};
export default GraphPanel;

View File

@ -0,0 +1,9 @@
import React from 'react';
import { GraphPanelGroupProps } from '../../../../models/dashboardsData';
import PanelGroupContent from './PanelGroupContent';
const DashboardPanelGroup: React.FC<GraphPanelGroupProps> = ({ ...props }) => {
return <PanelGroupContent {...props} />;
};
export default DashboardPanelGroup;

View File

@ -0,0 +1,9 @@
import React from 'react';
import { GraphPanelProps } from '../../../../models/dashboardsData';
import PanelContent from './PanelContent';
const GraphPanel: React.FC<GraphPanelProps> = ({ ...props }) => {
return <PanelContent {...props} />;
};
export default GraphPanel;

View File

@ -0,0 +1,304 @@
import { ApolloError, useQuery } from '@apollo/client';
import { IconButton, Tooltip, Typography } from '@material-ui/core';
import useTheme from '@material-ui/core/styles/useTheme';
import { ButtonOutlined, GraphMetric, LineAreaGraph, Modal } from 'litmus-ui';
import moment from 'moment';
import React, { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';
import { PROM_QUERY } from '../../../../graphql';
import { GraphPanelProps } from '../../../../models/dashboardsData';
import {
PrometheusQueryInput,
PrometheusQueryVars,
PrometheusResponse,
promQueryInput,
} from '../../../../models/graphql/prometheus';
import {
DEFAULT_REFRESH_RATE,
DEFAULT_TOLERANCE_LIMIT,
MAX_REFRESH_RATE,
MINIMUM_TOLERANCE_LIMIT,
PROMETHEUS_ERROR_QUERY_RESOLUTION_LIMIT_REACHED,
} from '../../../../pages/MonitoringDashboardPage/constants';
import useActions from '../../../../redux/actions';
import * as DashboardActions from '../../../../redux/actions/dashboards';
import { RootState } from '../../../../redux/reducers';
import { ReactComponent as ViewChaosMetric } from '../../../../svg/aligment.svg';
import { ReactComponent as DisableViewChaosMetric } from '../../../../svg/alignmentStriked.svg';
import { ReactComponent as Expand } from '../../../../svg/arrowsOut.svg';
import { ReactComponent as Edit } from '../../../../svg/edit.svg';
import {
getPromQueryInput,
seriesDataParserForPrometheus,
} from '../../../../utils/promUtils';
import useStyles from './styles';
interface PrometheusQueryDataInterface {
promInput: PrometheusQueryInput;
firstLoad: Boolean;
}
const PanelContent: React.FC<GraphPanelProps> = ({
panel_name,
prom_queries,
y_axis_left,
unit,
chaos_data,
className,
}) => {
const { palette } = useTheme();
const classes = useStyles();
const { t } = useTranslation();
const dashboard = useActions(DashboardActions);
const lineGraph: string[] = palette.graph.line;
const [popout, setPopout] = useState(false);
const [viewEventMetric, setViewEventMetric] = useState(false);
const [
prometheusQueryData,
setPrometheusQueryData,
] = React.useState<PrometheusQueryDataInterface>({
promInput: {
url: '',
start: '',
end: '',
queries: [],
},
firstLoad: true,
});
const [graphData, setGraphData] = React.useState<Array<GraphMetric>>([]);
const selectedDashboard = useSelector(
(state: RootState) => state.selectDashboard
);
const selectedDataSource = useSelector(
(state: RootState) => state.selectDataSource
);
// Apollo query to get the prometheus data
useQuery<PrometheusResponse, PrometheusQueryVars>(PROM_QUERY, {
variables: {
prometheusInput: prometheusQueryData?.promInput ?? {
url: '',
start: '',
end: '',
queries: [],
},
},
fetchPolicy: 'no-cache',
skip:
prometheusQueryData?.promInput.queries?.length === 0 ||
prometheusQueryData?.promInput.url === '',
onCompleted: (prometheusData) => {
let seriesData: Array<GraphMetric> = [];
if (prometheusData) {
seriesData = seriesDataParserForPrometheus(prometheusData, lineGraph);
setGraphData(seriesData);
seriesData = [];
}
},
onError: (error: ApolloError) => {
if (error.message === PROMETHEUS_ERROR_QUERY_RESOLUTION_LIMIT_REACHED) {
if (selectedDashboard.refreshRate !== MAX_REFRESH_RATE) {
dashboard.selectDashboard({
refreshRate: MAX_REFRESH_RATE,
});
}
setPrometheusQueryData({ ...prometheusQueryData, firstLoad: true });
}
},
});
const generatePromQueries = () => {
let promQueries: promQueryInput[] = prometheusQueryData.promInput.queries;
if (prometheusQueryData.firstLoad) {
const timeRangeDiff: number =
new Date(moment(selectedDashboard.range.endDate).format()).getTime() /
1000 -
new Date(moment(selectedDashboard.range.startDate).format()).getTime() /
1000;
promQueries = getPromQueryInput(prom_queries, timeRangeDiff);
}
setPrometheusQueryData({
promInput: {
url: selectedDataSource.selectedDataSourceURL,
start: `${
new Date(
moment(selectedDashboard.range.startDate).format()
).getTime() / 1000
}`,
end: `${
new Date(moment(selectedDashboard.range.endDate).format()).getTime() /
1000
}`,
queries: promQueries,
},
firstLoad: false,
});
promQueries = [];
};
useEffect(() => {
if (prometheusQueryData.firstLoad) {
generatePromQueries();
}
if (!prometheusQueryData.firstLoad) {
setTimeout(
() => {
generatePromQueries();
},
selectedDashboard.refreshRate !== 0
? selectedDashboard.refreshRate
: DEFAULT_REFRESH_RATE
);
}
}, [prometheusQueryData]);
useEffect(() => {
const endDate: number =
new Date(moment(selectedDashboard.range.endDate).format()).getTime() /
1000;
const now: number = Math.round(new Date().getTime() / 1000);
const diff: number = Math.abs(now - endDate);
const maxLim: number =
(selectedDashboard.refreshRate ?? DEFAULT_REFRESH_RATE) / 1000 !== 0
? (selectedDashboard.refreshRate ?? DEFAULT_REFRESH_RATE) / 1000 +
MINIMUM_TOLERANCE_LIMIT
: DEFAULT_TOLERANCE_LIMIT;
if (
!(diff >= 0 && diff <= maxLim) &&
selectedDashboard.refreshRate !== MAX_REFRESH_RATE
) {
setPrometheusQueryData({ ...prometheusQueryData, firstLoad: true });
dashboard.selectDashboard({
refreshRate: MAX_REFRESH_RATE,
});
}
if (
diff >= 0 &&
diff <= maxLim &&
selectedDashboard.refreshRate === MAX_REFRESH_RATE
) {
setPrometheusQueryData({ ...prometheusQueryData, firstLoad: true });
}
}, [selectedDashboard.range]);
useEffect(() => {
if (selectedDashboard.forceUpdate) {
setPrometheusQueryData({ ...prometheusQueryData, firstLoad: true });
}
}, [selectedDashboard.forceUpdate]);
return (
<div
className={` ${classes.rootPanel} ${className} ${
viewEventMetric ? classes.expand : ''
}`}
>
<div className={classes.wrapperParentIconsTitle}>
<Typography className={classes.title}>{panel_name}</Typography>
<div className={classes.wrapperIcons}>
{viewEventMetric ? (
<Tooltip
title={`${t('analyticsDashboard.toolTip.hideChaosMetric')}`}
>
<IconButton
className={classes.panelIconButton}
onClick={() => {
setViewEventMetric(false);
}}
>
<DisableViewChaosMetric className={classes.panelIcon} />
</IconButton>
</Tooltip>
) : (
<Tooltip
title={`${t('analyticsDashboard.toolTip.viewChaosMetric')}`}
>
<IconButton
className={classes.panelIconButton}
onClick={() => {
setViewEventMetric(true);
}}
>
<ViewChaosMetric className={classes.panelIcon} />
</IconButton>
</Tooltip>
)}
<Tooltip title={`${t('analyticsDashboard.toolTip.editPanel')}`}>
<IconButton
disabled
className={classes.panelIconButton}
onClick={() => {}}
>
<Edit className={classes.panelIcon} />
</IconButton>
</Tooltip>
<Tooltip title={`${t('analyticsDashboard.toolTip.popout')}`}>
<IconButton
className={classes.panelIconButton}
onClick={() => {
setPopout(true);
}}
>
<Expand className={classes.panelIcon} />
</IconButton>
</Tooltip>
</div>
</div>
<div>
<Modal
open={popout}
onClose={() => setPopout(false)}
disableBackdropClick
disableEscapeKeyDown
modalActions={
<ButtonOutlined onClick={() => setPopout(false)}>
&#x2715;
</ButtonOutlined>
}
height="95% !important"
width="95%"
>
<div className={classes.popOutModal}>
<Typography className={classes.title}>{panel_name}</Typography>
<LineAreaGraph
legendTableHeight={120}
openSeries={graphData}
eventSeries={chaos_data}
showPoints={false}
showLegendTable
showEventTable
showTips
showEventMarkers
marginLeftEventTable={10}
unit={unit}
yLabel={y_axis_left}
yLabelOffset={55}
margin={{ left: 75, right: 20, top: 20, bottom: 10 }}
/>
</div>
</Modal>
</div>
<div className={classes.singleGraph}>
<LineAreaGraph
legendTableHeight={120}
openSeries={graphData}
eventSeries={chaos_data}
showPoints={false}
showEventTable={viewEventMetric}
showLegendTable
showTips
showEventMarkers
unit={unit}
yLabel={y_axis_left}
yLabelOffset={55}
margin={{ left: 75, right: 20, top: 20, bottom: 10 }}
/>
</div>
</div>
);
};
export default PanelContent;

View File

@ -0,0 +1,92 @@
import { Typography, withStyles } from '@material-ui/core';
import MuiAccordion from '@material-ui/core/Accordion';
import AccordionDetails from '@material-ui/core/AccordionDetails';
import AccordionSummary from '@material-ui/core/AccordionSummary';
import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
import React from 'react';
import { EventMetric } from '../../../../models/dashboardsData';
import { PanelResponse } from '../../../../models/graphql/dashboardsDetails';
import GraphPanel from './GraphPanel';
import useStyles from './styles';
const Accordion = withStyles({
root: {
border: 0,
boxShadow: 'none',
'&:not(:last-child)': {
borderBottom: 0,
},
'&:before': {
display: 'none',
},
'&$expanded': {
margin: 'auto',
},
'& .MuiAccordionSummary-root.Mui-expanded': {
minHeight: '1rem !important',
height: '2.5rem',
},
'& .MuiAccordionSummary-root': {
minHeight: '1rem !important',
height: '2.5rem',
},
},
})(MuiAccordion);
interface DashboardPanelGroupContentProps {
panels: PanelResponse[];
panel_group_name: string;
panel_group_id: string;
chaos_data?: Array<EventMetric>;
}
const DashboardPanelGroupContent: React.FC<DashboardPanelGroupContentProps> = ({
panels,
panel_group_id,
panel_group_name,
chaos_data,
}) => {
const classes = useStyles();
const [open, setOpen] = React.useState<boolean>(true);
return (
<div className={classes.rootPanelGroup}>
<Accordion expanded={open}>
<AccordionSummary
expandIcon={<ExpandMoreIcon />}
aria-controls="panel1a-content"
id="panel1a-header"
className={classes.panelGroup}
key={`${panel_group_id}`}
onClick={() => {
setOpen(!open);
}}
>
<Typography className={classes.panelGroupTitle}>
{panel_group_name}
</Typography>
</AccordionSummary>
<AccordionDetails className={classes.panelGroupContainer}>
{panels &&
panels.map((panel: PanelResponse) => (
<GraphPanel
key={panel.panel_id}
data-cy="dashboardPanel"
panel_id={panel.panel_id}
panel_name={panel.panel_name}
panel_options={panel.panel_options}
prom_queries={panel.prom_queries}
y_axis_left={panel.y_axis_left}
y_axis_right={panel.y_axis_right}
x_axis_down={panel.x_axis_down}
unit={panel.unit}
chaos_data={chaos_data}
/>
))}
</AccordionDetails>
</Accordion>
</div>
);
};
export default DashboardPanelGroupContent;

View File

@ -0,0 +1,88 @@
import { makeStyles } from '@material-ui/core/styles';
const useStyles = makeStyles((theme) => ({
rootPanel: {
display: 'inline-block',
background: theme.palette.background.paper,
padding: theme.spacing(2),
margin: theme.spacing(1.5, 0, 1),
},
rootPanelGroup: {
width: '100%',
display: 'inline-block',
background: theme.palette.background.paper,
padding: theme.spacing(2),
paddingBottom: 0,
marginBottom: theme.spacing(1),
},
panelGroup: {
display: 'flex',
alignContent: 'left',
background: theme.palette.disabledBackground,
},
panelGroupContainer: {
width: '100%',
background: theme.palette.cards.header,
display: 'inline-grid',
gridTemplateColumns: '49% 49%',
gridGap: theme.spacing(1.75),
padding: theme.spacing(1, 1, 1, 1.75),
},
expand: {
gridColumnStart: 1,
gridColumnEnd: 3,
},
panelGroupTitle: {
fontWeight: 700,
fontSize: '1rem',
},
title: {
fontWeight: 700,
fontSize: '0.9rem',
color: theme.palette.text.primary,
textAlign: 'center',
backgroundColor: theme.palette.background.paper,
paddingTop: theme.spacing(2),
},
singleGraph: {
backgroundColor: theme.palette.background.paper,
position: 'relative',
height: '27.5rem',
},
popOutModal: {
width: '85%',
height: '95%',
padding: `${theme.spacing(4)} ${theme.spacing(4)} ${theme.spacing(4)} 10%`,
},
wrapperParentIconsTitle: {
display: 'flex',
justifyContent: 'space-between',
},
wrapperIcons: {
display: 'flex',
},
panelIcon: {
width: '0.9rem',
height: '0.9rem',
},
panelIconButton: {
backgroundColor: 'transparent !important',
cursor: 'pointer',
display: 'flex',
padding: theme.spacing(0.5, 0.5, 0, 0.5),
},
}));
export default useStyles;

View File

@ -1,6 +0,0 @@
import React from 'react';
const PanelContainer: React.FC = ({ children }) => {
return <div>{children}</div>;
};
export default PanelContainer;

View File

@ -1,258 +0,0 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { useQuery } from '@apollo/client';
import { Typography } from '@material-ui/core';
import useTheme from '@material-ui/core/styles/useTheme';
import { GraphMetric, LineAreaGraph } from 'litmus-ui';
import React, { useEffect } from 'react';
import { useSelector } from 'react-redux';
import { PROM_QUERY } from '../../../graphql';
import {
PanelResponse,
PromQuery,
} from '../../../models/graphql/dashboardsDetails';
import {
PrometheusQueryInput,
PrometheusQueryVars,
PrometheusResponse,
promQueryInput,
} from '../../../models/graphql/prometheus';
import { RootState } from '../../../redux/reducers';
import useStyles from './styles';
interface PrometheusQueryDataInterface {
promInput: PrometheusQueryInput;
chaosInput: string[];
}
interface GraphDataInterface {
seriesData: Array<GraphMetric>;
eventData: Array<GraphMetric>;
}
interface SynchronizerInterface {
updateQueries: Boolean;
firstLoad: Boolean;
fetch: Boolean;
}
const PanelContent: React.FC<PanelResponse> = ({
panel_id,
panel_name,
panel_options,
prom_queries,
y_axis_left,
y_axis_right,
x_axis_down,
unit,
}) => {
const { palette } = useTheme();
const classes = useStyles();
const lineGraph: string[] = palette.graph.line;
const areaGraph: string[] = palette.graph.area;
const [
prometheusQueryData,
setPrometheusQueryData,
] = React.useState<PrometheusQueryDataInterface>({
promInput: {
url: '',
start: '',
end: '',
queries: [],
},
chaosInput: [],
});
const [graphData, setGraphData] = React.useState<GraphDataInterface>({
seriesData: [],
eventData: [],
});
const [synchronizer, setSynchronizer] = React.useState<SynchronizerInterface>(
{
updateQueries: false,
firstLoad: true,
fetch: false,
}
);
const selectedDashboard = useSelector(
(state: RootState) => state.selectDashboard
);
const selectedDataSource = useSelector(
(state: RootState) => state.selectDataSource
);
// Apollo query to get the prometheus data
const { data: prometheusData } = useQuery<
PrometheusResponse,
PrometheusQueryVars
>(PROM_QUERY, {
variables: { prometheusInput: prometheusQueryData.promInput },
fetchPolicy: 'no-cache',
});
const generatePrometheusQueryData = () => {
let promQueries: promQueryInput[] = [];
let chaosQueries: string[] = [];
prom_queries.forEach((query: PromQuery) => {
if (query.prom_query_name.startsWith('litmuschaos_awaited_experiments')) {
chaosQueries.push(query.queryid);
}
promQueries.push({
queryid: query.queryid,
query: query.prom_query_name,
legend: query.legend,
resolution: query.resolution,
minstep: parseInt(query.minstep, 10),
});
});
const prometheusQueryInput: PrometheusQueryInput = {
url: selectedDataSource.selectedDataSourceURL,
start: `${Math.round(new Date().getTime() / 1000) - 1800}`,
end: `${Math.round(new Date().getTime() / 1000)}`,
queries: promQueries,
};
setPrometheusQueryData({
promInput: prometheusQueryInput,
chaosInput: chaosQueries,
});
promQueries = [];
chaosQueries = [];
};
const updateGraphData = () => {
let seriesData: Array<GraphMetric> = [];
let eventData: Array<GraphMetric> = [];
if (prometheusData) {
prometheusData.GetPromQuery.forEach((queryResponse) => {
if (prometheusQueryData.chaosInput.includes(queryResponse.queryid)) {
if (queryResponse.legends && queryResponse.legends[0]) {
eventData.push(
...queryResponse.legends.map((elem, index) => ({
metricName: elem[0] ?? 'chaos',
data: queryResponse.tsvs[index].map((dataPoint) => ({
date: parseInt(dataPoint.timestamp ?? '0', 10) * 1000,
value: parseInt(dataPoint.value ?? '0', 10),
})),
baseColor: palette.error.main,
}))
);
}
} else if (queryResponse.legends && queryResponse.legends[0]) {
seriesData.push(
...queryResponse.legends.map((elem, index) => ({
metricName: elem[0] ?? 'metric',
data: queryResponse.tsvs[index].map((dataPoint) => ({
date: parseInt(dataPoint.timestamp ?? '0', 10) * 1000,
value: parseFloat(dataPoint.value ?? '0.0'),
})),
baseColor: lineGraph[index % lineGraph.length],
}))
);
}
});
setGraphData({
seriesData,
eventData,
});
seriesData = [];
eventData = [];
}
};
useEffect(() => {
if (
synchronizer.firstLoad === true &&
synchronizer.updateQueries === false
) {
if (prom_queries.length) {
generatePrometheusQueryData();
setSynchronizer({
updateQueries: true,
firstLoad: false,
fetch: true,
});
}
}
if (
synchronizer.updateQueries === true &&
synchronizer.firstLoad === false
) {
setTimeout(() => {
if (prom_queries.length) {
generatePrometheusQueryData();
setSynchronizer({
...synchronizer,
fetch: true,
});
}
}, selectedDashboard.refreshRate);
}
}, [prometheusQueryData]);
useEffect(() => {
if (
prometheusData &&
prometheusData.GetPromQuery.length &&
prometheusData.GetPromQuery[0].legends?.length &&
prometheusData.GetPromQuery[0].legends !== null &&
prometheusData.GetPromQuery[0].legends[0] !== null
) {
if (synchronizer.fetch === true) {
updateGraphData();
setSynchronizer({
...synchronizer,
fetch: false,
});
}
}
}, [prometheusData]);
return (
<div className={classes.rootPanel}>
<div>
{/* <Typography>panel_id: {panel_id}</Typography>
<Typography>panel_name: {panel_name}</Typography>
<Typography>
panel_options: {JSON.stringify(panel_options)}
</Typography>
<Typography>
panel_axes {y_axis_left}, {y_axis_right}, {x_axis_down}
</Typography>
<Typography>
panel_unit: {unit}
</Typography>
<Typography>prom_queries: {JSON.stringify(prom_queries)}</Typography>
<Typography>
data_for_graph: {JSON.stringify(prometheusData)}
</Typography> */}
<Typography className={classes.title}>{panel_name}</Typography>
{/* <Typography
style={{ display: 'none' }}
>{`${panel_id}-${panel_options.grids}`}</Typography> */}
{/* panel_id and panel_options has been used temporaryly will be removed later */}
<div className={classes.singleGraph}>
<LineAreaGraph
legendTableHeight={120}
openSeries={graphData.seriesData}
eventSeries={graphData.eventData}
showPoints={false}
showLegendTable
showTips
showEventMarkers
unit={unit}
yLabel={y_axis_left}
yLabelOffset={55}
margin={{ left: 75, right: 20, top: 20, bottom: 10 }}
/>
</div>
{/* <Typography>
data_for_graph: {JSON.stringify(prometheusData)}
</Typography> */}
</div>
</div>
);
};
export default PanelContent;

View File

@ -1,6 +0,0 @@
import React from 'react';
const PanelGroupContainer: React.FC = ({ children }) => {
return <div>{children}</div>;
};
export default PanelGroupContainer;

View File

@ -1,68 +0,0 @@
import { Typography } from '@material-ui/core';
import AccordionDetails from '@material-ui/core/AccordionDetails';
import AccordionSummary from '@material-ui/core/AccordionSummary';
import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
import React from 'react';
import { PanelResponse } from '../../../models/graphql/dashboardsDetails';
import GraphPanel from './GraphPanel';
import useStyles, { Accordion } from './styles';
interface DashboardPanelGroupContentProps {
panels: PanelResponse[];
panel_group_name: string;
panel_group_id: string;
}
const DashboardPanelGroupContent: React.FC<DashboardPanelGroupContentProps> = ({
panels,
panel_group_id,
panel_group_name,
}) => {
const classes = useStyles();
const [open, setOpen] = React.useState<boolean>(true);
return (
<div className={classes.rootPanelGroup}>
{/* <Typography>panel_group_id: {panel_group_id}</Typography> */}
{/* <Typography>panel_group_name: {panel_group_name}</Typography> */}
<div>
<Accordion expanded={open}>
<AccordionSummary
expandIcon={<ExpandMoreIcon />}
aria-controls="panel1a-content"
id="panel1a-header"
className={classes.panelGroup}
key={`${panel_group_id}`}
onClick={() => {
setOpen(!open);
}}
>
<Typography className={classes.panelGroupTitle}>
{panel_group_name}
</Typography>
</AccordionSummary>
<AccordionDetails className={classes.panelGroupContainer}>
{panels &&
panels.map((panel: PanelResponse) => (
<div key={panel.panel_id} data-cy="dashboardPanel">
<GraphPanel
panel_id={panel.panel_id}
panel_name={panel.panel_name}
panel_options={panel.panel_options}
prom_queries={panel.prom_queries}
y_axis_left={panel.y_axis_left}
y_axis_right={panel.y_axis_right}
x_axis_down={panel.x_axis_down}
unit={panel.unit}
/>
</div>
))}
</AccordionDetails>
</Accordion>
</div>
</div>
);
};
export default DashboardPanelGroupContent;

View File

@ -1,81 +0,0 @@
import MuiAccordion from '@material-ui/core/Accordion';
import { makeStyles, withStyles } from '@material-ui/core/styles';
const useStyles = makeStyles((theme) => ({
rootPanel: {
display: 'inline-block',
background: theme.palette.background.paper,
padding: theme.spacing(2),
margin: theme.spacing(3, 1),
},
rootPanelGroup: {
width: '100%',
display: 'inline-block',
background: theme.palette.background.paper,
padding: theme.spacing(2),
paddingBottom: 0,
marginBottom: theme.spacing(1),
},
panelGroup: {
display: 'flex',
alignItems: 'left',
background: theme.palette.disabledBackground,
},
panelGroupContainer: {
background: theme.palette.cards.header,
display: 'flex',
justifyContent: 'space-around',
[theme.breakpoints.down('sm')]: {
flexDirection: 'column',
},
},
panelGroupTitle: {
fontWeight: 700,
fontSize: '1rem',
},
title: {
fontWeight: 700,
fontSize: '0.9rem',
color: theme.palette.text.primary,
textAlign: 'center',
backgroundColor: theme.palette.background.paper,
paddingTop: theme.spacing(2.5),
},
singleGraph: {
// TODO remove this later
'& hr': {
position: 'relative !important',
},
width: '32.5vw',
height: '27.5rem',
[theme.breakpoints.down(1200)]: {
width: '30vw',
},
[theme.breakpoints.down('sm')]: {
width: '42.5vw',
},
},
}));
export const Accordion = withStyles({
root: {
border: 0,
boxShadow: 'none',
'&:not(:last-child)': {
borderBottom: 0,
},
'&:before': {
display: 'none',
},
'&$expanded': {
margin: 'auto',
},
},
expanded: {},
})(MuiAccordion);
export default useStyles;

View File

@ -1,8 +1,3 @@
/* eslint-disable no-unused-expressions */
/* eslint-disable prefer-destructuring */
/* eslint-disable @typescript-eslint/no-unused-vars */
import { useMutation, useQuery } from '@apollo/client';
import {
IconButton,
Paper,
@ -13,33 +8,7 @@ import {
} from '@material-ui/core';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { v4 as uuidv4 } from 'uuid';
import YAML from 'yaml';
import DashboardTemplatesList from '../../../../components/PreconfiguredDashboards/data';
import { SCHEDULE_DETAILS, UPDATE_PANEL } from '../../../../graphql';
import {
Artifact,
CronWorkflowYaml,
Parameter,
Template,
WorkflowYaml,
} from '../../../../models/chaosWorkflowYaml';
import { ChaosResultNamesAndNamespacesMap } from '../../../../models/dashboardsData';
import {
ListDashboardResponse,
Panel,
PanelGroup,
PanelGroupResponse,
PanelOption,
PanelResponse,
PromQuery,
UpdatePanelInput,
} from '../../../../models/graphql/dashboardsDetails';
import {
ScheduleDataVars,
Schedules,
ScheduleWorkflow,
} from '../../../../models/graphql/scheduleData';
import { ListDashboardResponse } from '../../../../models/graphql/dashboardsDetails';
import useActions from '../../../../redux/actions';
import * as DashboardActions from '../../../../redux/actions/dashboards';
import * as DataSourceActions from '../../../../redux/actions/dataSource';
@ -50,13 +19,7 @@ import {
getProjectID,
getProjectRole,
} from '../../../../utils/getSearchParams';
import getEngineNameAndNamespace from '../../../../utils/promUtils';
import { GetTimeDiff } from '../../../../utils/timeDifferenceString';
import { validateWorkflowParameter } from '../../../../utils/validate';
import {
generateChaosQuery,
getWorkflowParameter,
} from '../../../../utils/yamlUtils';
import useStyles from '../styles';
interface TableDashboardData {
@ -71,262 +34,9 @@ const TableDashboardData: React.FC<TableDashboardData> = ({
const currentTime = new Date().valueOf();
const dataSource = useActions(DataSourceActions);
const dashboard = useActions(DashboardActions);
// selecedProjectID
const projectID = getProjectID();
const projectRole = getProjectRole();
// schedule data
const { data: schedulesData } = useQuery<Schedules, ScheduleDataVars>(
SCHEDULE_DETAILS,
{
variables: {
projectID,
},
}
);
// schedule data end
// update pannel
const [updatePanel] = useMutation<UpdatePanelInput>(UPDATE_PANEL, {
onError: () => {
console.error('error updating dashboard details');
},
});
// update pannel end
// reSyncChaos
const reSyncChaosQueries = (data: ListDashboardResponse) => {
const chaosResultNamesAndNamespacesMap: ChaosResultNamesAndNamespacesMap[] = [];
schedulesData?.getScheduledWorkflows.forEach(
(schedule: ScheduleWorkflow) => {
if (schedule.cluster_id === data.cluster_id && !schedule.isRemoved) {
let workflowYaml: WorkflowYaml | CronWorkflowYaml;
let parametersMap: Parameter[];
let workflowYamlCheck: boolean = true;
try {
workflowYaml = JSON.parse(schedule.workflow_manifest);
parametersMap = (workflowYaml as WorkflowYaml).spec.arguments
.parameters;
} catch (err) {
workflowYaml = JSON.parse(schedule.workflow_manifest);
parametersMap = (workflowYaml as CronWorkflowYaml).spec.workflowSpec
.arguments.parameters;
workflowYamlCheck = false;
}
(workflowYamlCheck
? (workflowYaml as WorkflowYaml).spec.templates
: (workflowYaml as CronWorkflowYaml).spec.workflowSpec.templates
).forEach((template: Template) => {
if (template.inputs && template.inputs.artifacts) {
template.inputs.artifacts.forEach((artifact: Artifact) => {
const parsedEmbeddedYaml = YAML.parse(artifact.raw.data);
if (parsedEmbeddedYaml.kind === 'ChaosEngine') {
let engineNamespace: string = '';
if (
typeof parsedEmbeddedYaml.metadata.namespace === 'string'
) {
engineNamespace = (parsedEmbeddedYaml.metadata
.namespace as string).substring(
1,
(parsedEmbeddedYaml.metadata.namespace as string).length -
1
);
} else {
engineNamespace = Object.keys(
parsedEmbeddedYaml.metadata.namespace
)[0];
}
if (validateWorkflowParameter(engineNamespace)) {
engineNamespace = getWorkflowParameter(engineNamespace);
parametersMap.forEach((parameterKeyValue: Parameter) => {
if (parameterKeyValue.name === engineNamespace) {
engineNamespace = parameterKeyValue.value;
}
});
} else {
engineNamespace = parsedEmbeddedYaml.metadata.namespace;
}
let matchIndex: number = -1;
const check: number = chaosResultNamesAndNamespacesMap.filter(
(data, index) => {
if (
data.resultName.includes(
parsedEmbeddedYaml.metadata.name
) &&
data.resultNamespace === engineNamespace
) {
matchIndex = index;
return true;
}
return false;
}
).length;
if (check === 0) {
chaosResultNamesAndNamespacesMap.push({
resultName: `${parsedEmbeddedYaml.metadata.name}-${parsedEmbeddedYaml.spec.experiments[0].name}`,
resultNamespace: engineNamespace,
workflowName: workflowYaml.metadata.name,
experimentName:
parsedEmbeddedYaml.spec.experiments[0].name,
});
} else {
chaosResultNamesAndNamespacesMap[
matchIndex
].workflowName = `${chaosResultNamesAndNamespacesMap[matchIndex].workflowName}, \n${workflowYaml.metadata.name}`;
}
}
});
}
});
}
}
);
const isChaosQueryPresent: number[] = Array(
chaosResultNamesAndNamespacesMap.length
).fill(0);
data.panel_groups[0].panels[0].prom_queries.forEach(
(existingPromQuery: PromQuery) => {
if (
existingPromQuery.prom_query_name.startsWith(
'litmuschaos_awaited_experiments'
)
) {
const chaosDetails: ChaosResultNamesAndNamespacesMap = getEngineNameAndNamespace(
existingPromQuery.prom_query_name
);
chaosResultNamesAndNamespacesMap.forEach(
(
chaosDetailsFomSchedule: ChaosResultNamesAndNamespacesMap,
index: number
) => {
if (
chaosDetailsFomSchedule.resultName.includes(
chaosDetails.resultName
) &&
chaosDetailsFomSchedule.resultNamespace ===
chaosDetails.resultNamespace
) {
isChaosQueryPresent[index] = 1;
}
}
);
}
}
);
const dashboardTemplateID: number =
data.db_type === 'Kubernetes Platform' ? 0 : 1;
const updatedPanelGroups: PanelGroup[] = [];
data.panel_groups.forEach((panelGroup: PanelGroupResponse) => {
const updatedPanels: Panel[] = [];
panelGroup.panels.forEach((panel: PanelResponse) => {
const updatedQueries: PromQuery[] = [];
panel.prom_queries.forEach((query: PromQuery) => {
let updatedLegend: string = query.legend;
if (
query.prom_query_name.startsWith('litmuschaos_awaited_experiments')
) {
const chaosDetails: ChaosResultNamesAndNamespacesMap = getEngineNameAndNamespace(
query.prom_query_name
);
chaosResultNamesAndNamespacesMap.forEach(
(chaosDetailsFomSchedule: ChaosResultNamesAndNamespacesMap) => {
if (
chaosDetailsFomSchedule.resultName.includes(
chaosDetails.resultName
) &&
chaosDetailsFomSchedule.resultNamespace ===
chaosDetails.resultNamespace &&
!query.legend.includes(chaosDetailsFomSchedule.workflowName)
) {
updatedLegend = `${chaosDetailsFomSchedule.workflowName}, \n${query.legend}`;
}
}
);
}
const updatedQuery: PromQuery = {
queryid: query.queryid,
prom_query_name: query.prom_query_name,
resolution: query.resolution,
minstep: query.minstep,
line: query.line,
close_area: query.close_area,
legend: updatedLegend,
};
updatedQueries.push(updatedQuery);
});
chaosResultNamesAndNamespacesMap.forEach(
(keyValue: ChaosResultNamesAndNamespacesMap, index: number) => {
if (isChaosQueryPresent[index] === 0) {
updatedQueries.push({
queryid: uuidv4(),
prom_query_name: generateChaosQuery(
DashboardTemplatesList[dashboardTemplateID]
.chaosEventQueryTemplate,
keyValue.resultName,
keyValue.resultNamespace
),
legend: `${keyValue.workflowName} / \n${keyValue.experimentName}`,
resolution: '1/1',
minstep: '1',
line: false,
close_area: true,
});
}
}
);
const existingPanelOptions: PanelOption = {
points: panel.panel_options.points,
grids: panel.panel_options.grids,
left_axis: panel.panel_options.left_axis,
};
const updatedPanel: Panel = {
panel_id: panel.panel_id,
panel_name: panel.panel_name,
panel_options: existingPanelOptions,
prom_queries: updatedQueries,
y_axis_left: panel.y_axis_left,
y_axis_right: panel.y_axis_right,
x_axis_down: panel.x_axis_down,
unit: panel.unit,
};
updatedPanels.push(updatedPanel);
});
updatedPanelGroups.push({
panel_group_id: panelGroup.panel_group_id,
panel_group_name: panelGroup.panel_group_name,
panels: updatedPanels,
});
});
const panelInputData: Panel[] = [];
updatedPanelGroups.forEach((panelGroup: PanelGroup) => {
panelGroup.panels.forEach((panel: Panel) => {
panelInputData.push({
panel_id: panel.panel_id,
db_id: data.db_id,
panel_group_id: panelGroup.panel_group_id,
prom_queries: panel.prom_queries,
panel_options: panel.panel_options,
panel_name: panel.panel_name,
y_axis_left: panel.y_axis_left,
y_axis_right: panel.y_axis_right,
x_axis_down: panel.x_axis_down,
unit: panel.unit,
});
});
});
updatePanel({
variables: { panelInput: panelInputData },
});
return true;
};
//
const onDashboardLoadRoutine = async (data: ListDashboardResponse) => {
dashboard.selectDashboard({
selectedDashboardID: data.db_id,
@ -334,9 +44,16 @@ const TableDashboardData: React.FC<TableDashboardData> = ({
selectedDashboardTemplateName: data.db_type,
selectedAgentID: data.cluster_id,
selectedAgentName: data.cluster_name,
refreshRate: 0,
});
return Promise.resolve(reSyncChaosQueries(data));
dataSource.selectDataSource({
selectedDataSourceURL: '',
selectedDataSourceID: '',
selectedDataSourceName: '',
});
return true;
};
return (
<div>
{dashboardDataList && dashboardDataList.length > 0 ? (
@ -391,11 +108,6 @@ const TableDashboardData: React.FC<TableDashboardData> = ({
disableFocusRipple
onClick={() => {
onDashboardLoadRoutine(dashboard).then(() => {
dataSource.selectDataSource({
selectedDataSourceURL: '',
selectedDataSourceID: '',
selectedDataSourceName: '',
});
history.push({
pathname: '/analytics/dashboard',
search: `?projectID=${projectID}&projectRole=${projectRole}`,

View File

@ -8,7 +8,7 @@ import {
} from '@material-ui/core';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { ScheduleWorkflow } from '../../../../models/graphql/scheduleData';
import { Workflow } from '../../../../models/graphql/workflowListData';
import useActions from '../../../../redux/actions';
import * as TabActions from '../../../../redux/actions/tabs';
import { history } from '../../../../redux/configureStore';
@ -21,7 +21,7 @@ import { GetTimeDiff } from '../../../../utils/timeDifferenceString';
import useStyles from '../styles';
interface TableScheduleWorkflow {
scheduleWorkflowList: ScheduleWorkflow[] | undefined;
scheduleWorkflowList: Workflow[] | undefined;
}
const TableScheduleWorkflow: React.FC<TableScheduleWorkflow> = ({

View File

@ -10,7 +10,7 @@ import { LocalQuickActionCard } from '../../../components/LocalQuickActionCard';
import {
LIST_DASHBOARD,
LIST_DATASOURCE,
SCHEDULE_DETAILS,
WORKFLOW_LIST_DETAILS,
} from '../../../graphql/queries';
import {
DashboardList,
@ -23,10 +23,10 @@ import {
ListDataSourceVars,
} from '../../../models/graphql/dataSourceDetails';
import {
ScheduleDataVars,
Schedules,
ScheduleWorkflow,
} from '../../../models/graphql/scheduleData';
Workflow,
WorkflowList,
WorkflowListDataVars,
} from '../../../models/graphql/workflowListData';
import { getProjectID } from '../../../utils/getSearchParams';
import { sortNumAsc } from '../../../utils/sort';
import { OverviewConfigureBanner } from './OverviewConfigureBanner';
@ -41,25 +41,26 @@ const Overview: React.FC = () => {
const classes = useStyles();
const projectID = getProjectID();
// Apollo query to get the scheduled data
const { data: schedulesData } = useQuery<Schedules, ScheduleDataVars>(
SCHEDULE_DETAILS,
// Apollo query to get the scheduled workflow data
const { data: schedulesData } = useQuery<WorkflowList, WorkflowListDataVars>(
WORKFLOW_LIST_DETAILS,
{
variables: {
projectID,
workflowIDs: [],
},
fetchPolicy: 'cache-and-network',
pollInterval: 10000,
}
);
const filteredScheduleData = schedulesData?.getScheduledWorkflows
.slice()
.sort((a: ScheduleWorkflow, b: ScheduleWorkflow) => {
const filteredScheduleData = schedulesData?.ListWorkflow.slice().sort(
(a: Workflow, b: Workflow) => {
const x = parseInt(a.updated_at, 10);
const y = parseInt(b.updated_at, 10);
return sortNumAsc(y, x);
});
}
);
// Apollo query to get the dashboard data
const { data: dashboardsList } = useQuery<DashboardList, ListDashboardVars>(

View File

@ -56,7 +56,8 @@ const useStyles = makeStyles((theme: Theme) => ({
background: 'transparent',
},
banner: {
height: '18rem',
height: 'fit-content',
minHeight: '18rem',
width: '100%',
overflow: 'hidden',
},