chore: (litmus-portal) Logs for workflows (#2131)

* fixed redux to fetch the information required for logs and removed redux logger
* completed subcription and display of logs
* fixed workflowDetails page styling
* Fixed Tab Switching

Signed-off-by: arkajyotiMukherjee <arkajyoti.mukherjee@mayadata.io>
Co-authored-by: Sayan Mondal <sayan.mondal@mayadata.io>
This commit is contained in:
Arkajyoti Mukherjee 2020-09-30 18:28:13 +05:30 committed by GitHub
parent 43b82de5ce
commit 8c3bd228a5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 374 additions and 211 deletions

View File

@ -7682,11 +7682,6 @@
"integrity": "sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw=", "integrity": "sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw=",
"dev": true "dev": true
}, },
"deep-diff": {
"version": "0.3.8",
"resolved": "https://registry.npmjs.org/deep-diff/-/deep-diff-0.3.8.tgz",
"integrity": "sha1-wB3mPvsO7JeYgB1Ax+Da4ltYLIQ="
},
"deep-equal": { "deep-equal": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.1.tgz", "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.1.tgz",
@ -18613,14 +18608,6 @@
"resolved": "https://registry.npmjs.org/redux-devtools-extension/-/redux-devtools-extension-2.13.8.tgz", "resolved": "https://registry.npmjs.org/redux-devtools-extension/-/redux-devtools-extension-2.13.8.tgz",
"integrity": "sha512-8qlpooP2QqPtZHQZRhx3x3OP5skEV1py/zUdMY28WNAocbafxdG2tRD1MWE7sp8obGMNYuLWanhhQ7EQvT1FBg==" "integrity": "sha512-8qlpooP2QqPtZHQZRhx3x3OP5skEV1py/zUdMY28WNAocbafxdG2tRD1MWE7sp8obGMNYuLWanhhQ7EQvT1FBg=="
}, },
"redux-logger": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/redux-logger/-/redux-logger-3.0.6.tgz",
"integrity": "sha1-91VZZvMJjzyIYExEnPC69XeCdL8=",
"requires": {
"deep-diff": "^0.3.5"
}
},
"redux-persist": { "redux-persist": {
"version": "6.0.0", "version": "6.0.0",
"resolved": "https://registry.npmjs.org/redux-persist/-/redux-persist-6.0.0.tgz", "resolved": "https://registry.npmjs.org/redux-persist/-/redux-persist-6.0.0.tgz",

View File

@ -47,7 +47,6 @@
"react-simple-maps": "^2.1.2", "react-simple-maps": "^2.1.2",
"redux": "^4.0.1", "redux": "^4.0.1",
"redux-devtools-extension": "^2.13.8", "redux-devtools-extension": "^2.13.8",
"redux-logger": "^3.0.6",
"redux-persist": "^6.0.0", "redux-persist": "^6.0.0",
"redux-thunk": "^2.3.0", "redux-thunk": "^2.3.0",
"subscriptions-transport-ws": "^0.9.17", "subscriptions-transport-ws": "^0.9.17",
@ -89,7 +88,6 @@
"@types/react-redux": "^7.1.7", "@types/react-redux": "^7.1.7",
"@types/react-router-dom": "^5.1.3", "@types/react-router-dom": "^5.1.3",
"@types/react-simple-maps": "^1.0.3", "@types/react-simple-maps": "^1.0.3",
"@types/redux-logger": "^3.0.7",
"@typescript-eslint/eslint-plugin": "^3.5.0", "@typescript-eslint/eslint-plugin": "^3.5.0",
"@typescript-eslint/parser": "^3.5.0", "@typescript-eslint/parser": "^3.5.0",
"cross-env": "^5.2.0", "cross-env": "^5.2.0",

View File

@ -1,10 +1,10 @@
import React from 'react';
import Typography from '@material-ui/core/Typography';
import { useSelector } from 'react-redux';
import { useTheme } from '@material-ui/core/styles'; import { useTheme } from '@material-ui/core/styles';
import useStyles from './styles'; import Typography from '@material-ui/core/Typography';
import React from 'react';
import { useSelector } from 'react-redux';
import { RootState } from '../../redux/reducers'; import { RootState } from '../../redux/reducers';
import formatCount from '../../utils/formatCount'; import formatCount from '../../utils/formatCount';
import useStyles from './styles';
interface CardValueData { interface CardValueData {
color: string; color: string;
@ -51,6 +51,7 @@ const InfoFilledWrap: React.FC = () => {
const cardArray = cardData.map((individualCard) => { const cardArray = cardData.map((individualCard) => {
return ( return (
<div <div
key={individualCard.value}
style={{ backgroundColor: `${individualCard.color}` }} style={{ backgroundColor: `${individualCard.color}` }}
className={classes.infoFilledDiv} className={classes.infoFilledDiv}
> >

View File

@ -1,11 +1,10 @@
import React from 'react';
import Modal from '@material-ui/core/Modal';
import Button from '@material-ui/core/Button'; import Button from '@material-ui/core/Button';
import Modal from '@material-ui/core/Modal';
import React from 'react';
import useStyles from './styles'; import useStyles from './styles';
/* DelUser, NewUserModal, ResetModal need to be shifted */ /* DelUser, NewUserModal, ResetModal need to be shifted */
interface UnimodalProps { interface UnimodalProps {
children?: React.ReactNode;
isOpen: boolean; isOpen: boolean;
handleClose: () => void; handleClose: () => void;
hasCloseBtn: boolean; hasCloseBtn: boolean;
@ -21,7 +20,8 @@ const Unimodal: React.FC<UnimodalProps> = ({
isDark, isDark,
textAlign, textAlign,
}) => { }) => {
const styleProps = { textAlign, isDark }; const isDarkBg = isDark ?? false;
const styleProps = { textAlign, isDarkBg };
const classes = useStyles(styleProps); const classes = useStyles(styleProps);
return ( return (

View File

@ -6,7 +6,7 @@ const useStyles = makeStyles((theme: Theme) => ({
width: '70%', width: '70%',
padding: '1rem', padding: '1rem',
margin: '2rem auto', margin: '2rem auto',
background: props.isDark background: props.isDarkBg
? theme.palette.editorBackground ? theme.palette.editorBackground
: theme.palette.common.white, : theme.palette.common.white,
borderRadius: 3, borderRadius: 3,
@ -30,14 +30,14 @@ const useStyles = makeStyles((theme: Theme) => ({
minHeight: 0, minHeight: 0,
minWidth: 0, minWidth: 0,
borderRadius: 3, borderRadius: 3,
color: props.isDark color: props.isDarkBg
? theme.palette.secondary.contrastText ? theme.palette.secondary.contrastText
: theme.palette.customColors.black(0.4), : theme.palette.customColors.black(0.4),
border: '1px solid', border: '1px solid',
borderColor: props.isDark borderColor: props.isDarkBg
? theme.palette.customColors.white(0.2) ? theme.palette.customColors.white(0.2)
: theme.palette.customColors.black(0.4), : theme.palette.customColors.black(0.4),
marginLeft: props.isDark ? '82.5%' : '60%', marginLeft: props.isDarkBg ? '82.5%' : '60%',
marginTop: theme.spacing(5), marginTop: theme.spacing(5),
}), }),

View File

@ -11,6 +11,7 @@ export const WORKFLOW_DETAILS = gql`
cluster_name cluster_name
last_updated last_updated
cluster_type cluster_type
cluster_id
} }
} }
`; `;

View File

@ -1,4 +1,3 @@
/* eslint-disable import/prefer-default-export */
import { gql } from '@apollo/client'; import { gql } from '@apollo/client';
export const WORKFLOW_EVENTS = gql` export const WORKFLOW_EVENTS = gql`
@ -11,6 +10,15 @@ export const WORKFLOW_EVENTS = gql`
project_id project_id
cluster_name cluster_name
last_updated last_updated
cluster_id
}
}
`;
export const WORKFLOW_LOGS = gql`
subscription podLog($podDetails: PodLogRequest!) {
getPodLog(podDetails: $podDetails) {
log
} }
} }
`; `;

View File

@ -0,0 +1,25 @@
export interface PodLogRequest {
cluster_id: string;
workflow_run_id: string;
pod_name: string;
pod_namespace: string;
pod_type: string;
exp_pod?: string;
runner_pod?: string;
chaos_namespace?: string;
}
export interface PodLogResponse {
workflow_run_id: string;
pod_name: string;
pod_type: string;
log: string;
}
export interface PodLogVars {
podDetails: PodLogRequest;
}
export interface PodLog {
getPodLog: PodLogResponse;
}

View File

@ -15,6 +15,7 @@ export interface ChaosData {
export interface Node { export interface Node {
children: string[] | null; children: string[] | null;
finishedAt: string; finishedAt: string;
message: string;
name: string; name: string;
phase: string; phase: string;
startedAt: string; startedAt: string;
@ -47,6 +48,7 @@ export interface WorkflowRun {
workflow_name: string; workflow_name: string;
workflow_run_id: string; workflow_run_id: string;
cluster_type: string; cluster_type: string;
cluster_id: string;
} }
export interface Workflow { export interface Workflow {

View File

@ -1,5 +1,9 @@
import { Node } from '../graphql/workflowData'; import { Node } from '../graphql/workflowData';
export interface SelectedNode extends Node {
pod_name: string;
}
export enum NodeSelectionActions { export enum NodeSelectionActions {
SELECT_NODE = 'SELECT_NODE', SELECT_NODE = 'SELECT_NODE',
} }
@ -11,5 +15,5 @@ interface NodeSelectionActionType<T, P> {
export type NodeSelectionAction = NodeSelectionActionType< export type NodeSelectionAction = NodeSelectionActionType<
typeof NodeSelectionActions.SELECT_NODE, typeof NodeSelectionActions.SELECT_NODE,
Node SelectedNode
>; >;

View File

@ -1,11 +1,13 @@
export interface TabState { export interface TabState {
workflows: number; workflows: number;
settings: number; settings: number;
node: number;
} }
export enum TabActions { export enum TabActions {
CHANGE_WORKFLOWS_TAB = 'CHANGE_WORKFLOWS_TAB', CHANGE_WORKFLOWS_TAB = 'CHANGE_WORKFLOWS_TAB',
CHANGE_SETTINGS_TAB = 'CHANGE_SETTINGS_TAB', CHANGE_SETTINGS_TAB = 'CHANGE_SETTINGS_TAB',
CHANGE_WORKFLOW_DETAILS_TAB = 'CHANGE_WORKFLOW_DETAILS_TAB',
} }
interface TabActionType<T, P> { interface TabActionType<T, P> {
@ -15,4 +17,5 @@ interface TabActionType<T, P> {
export type TabAction = export type TabAction =
| TabActionType<typeof TabActions.CHANGE_WORKFLOWS_TAB, number> | TabActionType<typeof TabActions.CHANGE_WORKFLOWS_TAB, number>
| TabActionType<typeof TabActions.CHANGE_SETTINGS_TAB, number>; | TabActionType<typeof TabActions.CHANGE_SETTINGS_TAB, number>
| TabActionType<typeof TabActions.CHANGE_WORKFLOW_DETAILS_TAB, number>;

View File

@ -91,10 +91,10 @@ const TopNavButtons: React.FC<Props> = ({ isToggled, setIsToggled }) => {
return ( return (
<div className={classes.button}> <div className={classes.button}>
<div className={classes.buttonLeft}> <div>
<BackButton isDisabled={false} /> <BackButton isDisabled={false} />
</div> </div>
<div className={classes.buttonRight}> <div>
{AnalyticsButton()} {AnalyticsButton()}
{ExportButton()} {ExportButton()}
{InfoButton()} {InfoButton()}

View File

@ -11,19 +11,18 @@ import Scaffold from '../../containers/layouts/Scaffold';
import { WORKFLOW_DETAILS, WORKFLOW_EVENTS } from '../../graphql'; import { WORKFLOW_DETAILS, WORKFLOW_EVENTS } from '../../graphql';
import { import {
ExecutionData, ExecutionData,
Node,
Workflow, Workflow,
WorkflowDataVars, WorkflowDataVars,
WorkflowSubscription, WorkflowSubscription,
} from '../../models/graphql/workflowData'; } from '../../models/graphql/workflowData';
import useActions from '../../redux/actions';
import * as TabActions from '../../redux/actions/tabs';
import { RootState } from '../../redux/reducers'; import { RootState } from '../../redux/reducers';
import ArgoWorkflow from '../../views/WorkflowDetails/ArgoWorkflow'; import ArgoWorkflow from '../../views/WorkflowDetails/ArgoWorkflow';
import WorkflowInfo from '../../views/WorkflowDetails/WorkflowInfo'; import WorkflowInfo from '../../views/WorkflowDetails/WorkflowInfo';
import WorkflowNodeInfo from '../../views/WorkflowDetails/WorkflowNodeInfo';
import useStyles from './styles'; import useStyles from './styles';
import TopNavButtons from './TopNavButtons'; import TopNavButtons from './TopNavButtons';
import WorkflowNodeInfo from '../../views/WorkflowDetails/NodeInfo';
import ButtonFilled from '../../components/Button/ButtonFilled';
import ButtonOutline from '../../components/Button/ButtonOutline';
interface TopNavButtonsProps { interface TopNavButtonsProps {
isAnalyticsToggled: boolean; isAnalyticsToggled: boolean;
@ -40,6 +39,7 @@ const WorkflowDetails: React.FC = () => {
isInfoToggled: false, isInfoToggled: false,
}); });
const tabs = useActions(TabActions);
const { pathname } = useLocation(); const { pathname } = useLocation();
// Getting the workflow nome from the pathname // Getting the workflow nome from the pathname
const workflowRunId = pathname.split('/')[3]; const workflowRunId = pathname.split('/')[3];
@ -49,8 +49,9 @@ const WorkflowDetails: React.FC = () => {
const selectedProjectID = useSelector( const selectedProjectID = useSelector(
(state: RootState) => state.userData.selectedProjectID (state: RootState) => state.userData.selectedProjectID
); );
// get Selected Node const workflowDetailsTabValue = useSelector(
const selectedNode = useSelector((state: RootState) => state.selectedNode); (state: RootState) => state.tabNumber.node
);
// Query to get workflows // Query to get workflows
const { subscribeToMore, data, error } = useQuery<Workflow, WorkflowDataVars>( const { subscribeToMore, data, error } = useQuery<Workflow, WorkflowDataVars>(
@ -97,12 +98,17 @@ const WorkflowDetails: React.FC = () => {
} }
}, [data]); }, [data]);
const [value, setValue] = React.useState(0);
const theme = useTheme(); const theme = useTheme();
const handleChange = (event: React.ChangeEvent<{}>, newValue: number) => { const handleChange = (event: React.ChangeEvent<{}>, newValue: number) => {
setValue(newValue); tabs.changeWorkflowDetailsTabs(newValue);
}; };
// On fresh screen refresh 'Workflow' Tab would be selected
useEffect(() => {
tabs.changeWorkflowDetailsTabs(0);
}, []);
return ( return (
<Scaffold> <Scaffold>
<TopNavButtons isToggled={isToggled} setIsToggled={setIsToggled} /> <TopNavButtons isToggled={isToggled} setIsToggled={setIsToggled} />
@ -116,75 +122,60 @@ const WorkflowDetails: React.FC = () => {
<Typography>{t('workflowDetails.detailedLog')}</Typography> <Typography>{t('workflowDetails.detailedLog')}</Typography>
{/* Argo Workflow DAG Graph */} {/* Argo Workflow DAG Graph */}
{isToggled.isInfoToggled ? ( <ArgoWorkflow
<div className={classes.w100}> nodes={
<ArgoWorkflow (JSON.parse(workflow.execution_data) as ExecutionData).nodes
nodes={ }
(JSON.parse(workflow.execution_data) as ExecutionData).nodes />
}
/>
</div>
) : (
<div className={classes.w140}>
<ArgoWorkflow
nodes={
(JSON.parse(workflow.execution_data) as ExecutionData).nodes
}
/>
</div>
)}
</div> </div>
{isToggled.isInfoToggled ? ( {isToggled.isInfoToggled ? (
<div> <div className={classes.workflowSideBar}>
<> <AppBar
<AppBar position="static"
position="static" color="default"
color="default" className={classes.appBar}
className={classes.appBar} >
<Tabs
value={workflowDetailsTabValue || 0}
onChange={handleChange}
TabIndicatorProps={{
style: {
backgroundColor: theme.palette.secondary.dark,
},
}}
variant="fullWidth"
> >
<Tabs <StyledTab label="Workflow" />
value={value} <StyledTab label="Nodes" />
onChange={handleChange} </Tabs>
TabIndicatorProps={{ </AppBar>
style: { <TabPanel value={workflowDetailsTabValue} index={0}>
backgroundColor: theme.palette.secondary.dark, <div data-cy="browseWorkflow">
}, <WorkflowInfo
}} workflow_name={workflow.workflow_name}
variant="fullWidth" execution_data={
> JSON.parse(workflow?.execution_data) as ExecutionData
<StyledTab label="Workflow" /> }
<StyledTab label="Nodes" /> cluster_name={workflow.cluster_name}
</Tabs> />
</AppBar> </div>
<TabPanel value={value} index={0}> </TabPanel>
<div data-cy="browseWorkflow"> <TabPanel
<WorkflowInfo data-cy="scheduleWorkflow"
workflow_name={workflow.workflow_name} value={workflowDetailsTabValue}
execution_data={ index={1}
JSON.parse(workflow?.execution_data) as ExecutionData >
} <div data-cy="browseWorkflow">
cluster_name={workflow.cluster_name} <WorkflowNodeInfo
/> cluster_id={workflow.cluster_id}
</div> workflow_run_id={workflow.workflow_run_id}
</TabPanel> pod_namespace={
<TabPanel data-cy="scheduleWorkflow" value={value} index={1}> (JSON.parse(workflow.execution_data) as ExecutionData)
<div data-cy="browseWorkflow"> .namespace
<WorkflowNodeInfo nodeDetails={selectedNode as Node} /> }
</div> />
</TabPanel> </div>
</> </TabPanel>
<div className={classes.footerButton}>
<ButtonFilled
isPrimary
isDisabled={false}
handleClick={() => {}}
>
Events
</ButtonFilled>
<ButtonOutline isDisabled={false} handleClick={() => {}}>
Logs
</ButtonOutline>
</div>
</div> </div>
) : ( ) : (
<></> <></>

View File

@ -3,6 +3,7 @@ import { makeStyles, Theme } from '@material-ui/core/styles';
const useStyles = makeStyles((theme: Theme) => ({ const useStyles = makeStyles((theme: Theme) => ({
root: { root: {
display: 'flex', display: 'flex',
justifyContent: 'flex-end',
marginTop: theme.spacing(3), marginTop: theme.spacing(3),
height: '75vh', height: '75vh',
}, },
@ -10,25 +11,11 @@ const useStyles = makeStyles((theme: Theme) => ({
margin: theme.spacing(-0.4, 1), margin: theme.spacing(-0.4, 1),
width: '1rem', width: '1rem',
}, },
w100: {
width: '100%',
height: '100%',
},
w140: {
width: '141%',
height: '100%',
},
button: { button: {
position: 'relative', position: 'relative',
display: 'flex', display: 'flex',
margin: theme.spacing(4, 4, 0, 4), justifyContent: 'space-between',
}, margin: theme.spacing(4, 0, 0, 4),
buttonLeft: {
float: 'left',
},
buttonRight: {
position: 'absolute',
right: '4%',
}, },
heading: { heading: {
fontSize: '2rem', fontSize: '2rem',
@ -36,7 +23,10 @@ const useStyles = makeStyles((theme: Theme) => ({
}, },
workflowGraph: { workflowGraph: {
padding: '0 3rem', padding: '0 3rem',
width: '70%', width: '100%',
},
workflowSideBar: {
width: '20rem',
}, },
loaderDiv: { loaderDiv: {
height: '100%', height: '100%',
@ -48,12 +38,6 @@ const useStyles = makeStyles((theme: Theme) => ({
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
}, },
footerButton: {
marginLeft: 'auto',
display: 'flex',
flexDirection: 'row',
padding: theme.spacing(3, 4, 4, 0),
},
})); }));
export default useStyles; export default useStyles;

View File

@ -1,11 +1,11 @@
/* eslint-disable import/prefer-default-export */ /* eslint-disable import/prefer-default-export */
import { Node } from '../../models/graphql/workflowData';
import { import {
NodeSelectionAction, NodeSelectionAction,
NodeSelectionActions, NodeSelectionActions,
SelectedNode,
} from '../../models/redux/nodeSelection'; } from '../../models/redux/nodeSelection';
export function selectNode(node: Node): NodeSelectionAction { export function selectNode(node: SelectedNode): NodeSelectionAction {
return { return {
type: NodeSelectionActions.SELECT_NODE, type: NodeSelectionActions.SELECT_NODE,
payload: node, payload: node,

View File

@ -13,3 +13,10 @@ export function changeSettingsTabs(tabNumber: number): TabAction {
payload: tabNumber, payload: tabNumber,
}; };
} }
export function changeWorkflowDetailsTabs(tabNumber: number): TabAction {
return {
type: TabActions.CHANGE_WORKFLOW_DETAILS_TAB,
payload: tabNumber,
};
}

View File

@ -2,7 +2,6 @@ import { createBrowserHistory } from 'history'; // eslint-disable-line import/no
import * as localforage from 'localforage'; import * as localforage from 'localforage';
import { applyMiddleware, createStore } from 'redux'; import { applyMiddleware, createStore } from 'redux';
import { composeWithDevTools } from 'redux-devtools-extension'; import { composeWithDevTools } from 'redux-devtools-extension';
import { createLogger } from 'redux-logger';
import { PersistConfig, persistReducer, persistStore } from 'redux-persist'; import { PersistConfig, persistReducer, persistStore } from 'redux-persist';
import thunk from 'redux-thunk'; import thunk from 'redux-thunk';
import rootReducer from './reducers'; import rootReducer from './reducers';
@ -14,12 +13,11 @@ const persistConfig: PersistConfig<any> = {
blacklist: [], blacklist: [],
}; };
const logger = (createLogger as any)();
const history = createBrowserHistory(); const history = createBrowserHistory();
const dev = process.env.NODE_ENV === 'development'; const dev = process.env.NODE_ENV === 'development';
let middleware = dev ? applyMiddleware(thunk, logger) : applyMiddleware(thunk); let middleware = applyMiddleware(thunk);
if (dev) { if (dev) {
middleware = composeWithDevTools(middleware); middleware = composeWithDevTools(middleware);

View File

@ -1,6 +1,6 @@
import { combineReducers } from 'redux'; import { combineReducers } from 'redux';
import { Node } from '../../models/graphql/workflowData';
import { CommunityData } from '../../models/redux/analytics'; import { CommunityData } from '../../models/redux/analytics';
import { SelectedNode } from '../../models/redux/nodeSelection';
import { TabState } from '../../models/redux/tabs'; import { TabState } from '../../models/redux/tabs';
import { TemplateData } from '../../models/redux/template'; import { TemplateData } from '../../models/redux/template';
import { UserData } from '../../models/redux/user'; import { UserData } from '../../models/redux/user';
@ -16,7 +16,7 @@ export interface RootState {
communityData: CommunityData; communityData: CommunityData;
userData: UserData; userData: UserData;
workflowData: WorkflowData; workflowData: WorkflowData;
selectedNode: Node; selectedNode: SelectedNode;
tabNumber: TabState; tabNumber: TabState;
selectTemplate: TemplateData; selectTemplate: TemplateData;
} }

View File

@ -1,21 +1,26 @@
import { Node } from '../../models/graphql/workflowData';
import { import {
NodeSelectionAction, NodeSelectionAction,
NodeSelectionActions, NodeSelectionActions,
SelectedNode,
} from '../../models/redux/nodeSelection'; } from '../../models/redux/nodeSelection';
import createReducer from './createReducer'; import createReducer from './createReducer';
const initialState: Node = { const initialState: SelectedNode = {
children: null, children: null,
finishedAt: '', finishedAt: '',
message: '',
name: '', name: '',
pod_name: '',
phase: '', phase: '',
startedAt: '', startedAt: '',
type: '', type: '',
}; };
export const selectedNode = createReducer<Node>(initialState, { export const selectedNode = createReducer<SelectedNode>(initialState, {
[NodeSelectionActions.SELECT_NODE](state: Node, action: NodeSelectionAction) { [NodeSelectionActions.SELECT_NODE](
state: SelectedNode,
action: NodeSelectionAction
) {
return { return {
...state, ...state,
...action.payload, ...action.payload,

View File

@ -5,6 +5,7 @@ import createReducer from './createReducer';
const initialState: TabState = { const initialState: TabState = {
workflows: 0, workflows: 0,
settings: 0, settings: 0,
node: 0,
}; };
export const tabNumber = createReducer<TabState>(initialState, { export const tabNumber = createReducer<TabState>(initialState, {
@ -20,6 +21,12 @@ export const tabNumber = createReducer<TabState>(initialState, {
settings: action.payload, settings: action.payload,
}; };
}, },
[TabActions.CHANGE_WORKFLOW_DETAILS_TAB](state: TabState, action: TabAction) {
return {
...state,
node: action.payload,
};
},
}); });
export default tabNumber; export default tabNumber;

View File

@ -7,13 +7,13 @@ import {
} from '@material-ui/core'; } from '@material-ui/core';
import MoreVertIcon from '@material-ui/icons/MoreVert'; import MoreVertIcon from '@material-ui/icons/MoreVert';
import React from 'react'; import React from 'react';
import LinearProgressBar from '../../../components/ProgressBar/LinearProgressBar';
import { import {
ExecutionData, ExecutionData,
WorkflowRun, WorkflowRun,
} from '../../../models/graphql/workflowData'; } from '../../../models/graphql/workflowData';
import { history } from '../../../redux/configureStore'; import { history } from '../../../redux/configureStore';
import timeDifferenceForDate from '../../../utils/datesModifier'; import timeDifferenceForDate from '../../../utils/datesModifier';
import LinearProgressBar from '../../../components/ProgressBar/LinearProgressBar';
import CustomStatus from '../CustomStatus/Status'; import CustomStatus from '../CustomStatus/Status';
import useStyles from './styles'; import useStyles from './styles';
@ -109,9 +109,9 @@ const TableData: React.FC<TableDataProps> = ({ data, exeData }) => {
> >
<MenuItem <MenuItem
value="Workflow" value="Workflow"
onClick={() => onClick={() => {
history.push(`/workflows/details/${data.workflow_run_id}`) history.push(`/workflows/details/${data.workflow_run_id}`);
} }}
> >
<div className={classes.expDiv} data-cy="workflowDetails"> <div className={classes.expDiv} data-cy="workflowDetails">
<img <img

View File

@ -3,6 +3,7 @@ import DagreGraph, { d3Link, d3Node } from '../../../components/DagreGraph';
import { Nodes } from '../../../models/graphql/workflowData'; import { Nodes } from '../../../models/graphql/workflowData';
import useActions from '../../../redux/actions'; import useActions from '../../../redux/actions';
import * as NodeSelectionActions from '../../../redux/actions/nodeSelection'; import * as NodeSelectionActions from '../../../redux/actions/nodeSelection';
import * as TabActions from '../../../redux/actions/tabs';
import useStyles from './styles'; import useStyles from './styles';
interface GraphData { interface GraphData {
@ -17,12 +18,18 @@ const ArgoWorkflow: React.FC<ArgoWorkflowProps> = ({ nodes }) => {
const classes = useStyles(); const classes = useStyles();
// Redux action call for updating selected node // Redux action call for updating selected node
const nodeSelection = useActions(NodeSelectionActions); const nodeSelection = useActions(NodeSelectionActions);
const tabs = useActions(TabActions);
const [graphData, setGraphData] = useState<GraphData>({ const [graphData, setGraphData] = useState<GraphData>({
nodes: [], nodes: [],
links: [], links: [],
}); });
// Get the selected Node
const [selectedNodeID, setSelectedNodeID] = useState<string>(
Object.keys(nodes)[0]
);
useEffect(() => { useEffect(() => {
const data: GraphData = { const data: GraphData = {
nodes: [], nodes: [],
@ -58,6 +65,13 @@ const ArgoWorkflow: React.FC<ArgoWorkflowProps> = ({ nodes }) => {
}); });
}, [nodes]); }, [nodes]);
useEffect(() => {
nodeSelection.selectNode({
...nodes[selectedNodeID],
pod_name: selectedNodeID,
});
}, [selectedNodeID]);
return graphData.nodes.length ? ( return graphData.nodes.length ? (
<DagreGraph <DagreGraph
className={classes.dagreGraph} className={classes.dagreGraph}
@ -74,7 +88,8 @@ const ArgoWorkflow: React.FC<ArgoWorkflowProps> = ({ nodes }) => {
const nodeID = Object.keys(nodes).filter( const nodeID = Object.keys(nodes).filter(
(key) => key === original?.id (key) => key === original?.id
)[0]; )[0];
nodeSelection.selectNode(nodes[nodeID]); setSelectedNodeID(nodeID);
tabs.changeWorkflowDetailsTabs(1);
}} }}
/> />
) : ( ) : (

View File

@ -1,61 +0,0 @@
import { Typography } from '@material-ui/core';
import React from 'react';
import { Node } from '../../../models/graphql/workflowData';
import timeDifference from '../../../utils/datesModifier';
import useStyles from './styles';
interface NodeInfoProps {
nodeDetails: Node;
}
const WorkflowNodeInfo: React.FC<NodeInfoProps> = ({ nodeDetails }) => {
const classes = useStyles();
return (
<div className={classes.root}>
{/* Node Name */}
<div className={classes.heightMaintainer}>
<Typography className={classes.nodeSpacing}>
<span className={classes.bold}>Node name:</span>
<br />
{nodeDetails.name}
</Typography>
</div>
<hr />
{/* Node Phase */}
<div className={classes.nodeSpacing}>
<div className={classes.heightMaintainer}>
<Typography>
<span className={classes.bold}>Phase:</span> {nodeDetails.phase}
</Typography>
</div>
</div>
<hr />
{/* Node Durations */}
<div className={classes.nodeSpacing}>
<div className={classes.heightMaintainer}>
<Typography>
<span className={classes.bold}>Start time:</span>{' '}
{timeDifference(nodeDetails.startedAt)}
</Typography>
<Typography>
<span className={classes.bold}>End time:</span>{' '}
{timeDifference(nodeDetails.finishedAt)}
</Typography>
<Typography>
<span className={classes.bold}>Duration: </span>{' '}
{(
(parseInt(nodeDetails.finishedAt, 10) -
parseInt(nodeDetails.startedAt, 10)) /
60
).toFixed(1)}{' '}
minutes
</Typography>
</div>
</div>
</div>
);
};
export default WorkflowNodeInfo;

View File

@ -0,0 +1,59 @@
import { useSubscription } from '@apollo/client';
import { Typography } from '@material-ui/core';
import React from 'react';
import Unimodal from '../../../containers/layouts/Unimodal';
import { WORKFLOW_LOGS } from '../../../graphql';
import {
PodLog,
PodLogRequest,
PodLogVars,
} from '../../../models/graphql/podLog';
import useStyles from './styles';
interface NodeLogsProps extends PodLogRequest {
logsOpen: boolean;
handleClose: () => void;
}
const NodeLogs: React.FC<NodeLogsProps> = ({
logsOpen,
handleClose,
cluster_id,
workflow_run_id,
pod_namespace,
pod_name,
pod_type,
}) => {
const classes = useStyles();
const { data } = useSubscription<PodLog, PodLogVars>(WORKFLOW_LOGS, {
variables: {
podDetails: {
cluster_id,
workflow_run_id,
pod_name,
pod_namespace,
pod_type,
},
},
});
return (
<Unimodal
isOpen={logsOpen}
handleClose={handleClose}
hasCloseBtn
textAlign="left"
>
<div className={classes.root}>
{data !== undefined ? (
<Typography variant="h5">{data.getPodLog.log}</Typography>
) : (
<Typography variant="h5">Fetching Logs...</Typography>
)}
</div>
</Unimodal>
);
};
export default NodeLogs;

View File

@ -0,0 +1,12 @@
import { makeStyles, Theme } from '@material-ui/core/styles';
const useStyles = makeStyles((theme: Theme) => ({
root: {
width: '100%',
height: '90%',
background: theme.palette.common.black,
color: theme.palette.common.white,
},
}));
export default useStyles;

View File

@ -1,10 +1,6 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
// TODO: remove this after creating UI for node details sidebar
import { Typography } from '@material-ui/core'; import { Typography } from '@material-ui/core';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { useSelector } from 'react-redux';
import { ExecutionData } from '../../../models/graphql/workflowData'; import { ExecutionData } from '../../../models/graphql/workflowData';
import { RootState } from '../../../redux/reducers';
import timeDifference from '../../../utils/datesModifier'; import timeDifference from '../../../utils/datesModifier';
import useStyles from './styles'; import useStyles from './styles';
@ -26,7 +22,6 @@ const WorkflowInfo: React.FC<WorkflowInfoProps> = ({
}) => { }) => {
const classes = useStyles(); const classes = useStyles();
// Get selected node data from redux // Get selected node data from redux
const selectedNode = useSelector((state: RootState) => state.selectedNode);
const [duration, setDuration] = useState<number>(0); const [duration, setDuration] = useState<number>(0);
const [data, setData] = useState<SidebarState>({ const [data, setData] = useState<SidebarState>({

View File

@ -0,0 +1,116 @@
/* eslint-disable */
import { Typography } from '@material-ui/core';
import React, { useState } from 'react';
import { useSelector } from 'react-redux';
import ButtonOutline from '../../../components/Button/ButtonOutline';
import { RootState } from '../../../redux/reducers';
import timeDifference from '../../../utils/datesModifier';
import NodeLogs from '../NodeLogs';
import useStyles from './styles';
interface WorkflowNodeInfoProps {
cluster_id: string;
workflow_run_id: string;
pod_namespace: string;
}
const WorkflowNodeInfo: React.FC<WorkflowNodeInfoProps> = ({
cluster_id,
workflow_run_id,
pod_namespace,
}) => {
const classes = useStyles();
const [logsOpen, setLogsOpen] = useState<boolean>(false);
// Get the nelected node from redux
const { name, phase, pod_name, type, startedAt, finishedAt } = useSelector(
(state: RootState) => state.selectedNode
);
const handleClose = () => {
setLogsOpen(false);
};
return (
<div className={classes.root}>
{/* Logs Modal */}
{logsOpen ? (
<NodeLogs
logsOpen={logsOpen}
handleClose={handleClose}
cluster_id={cluster_id}
workflow_run_id={workflow_run_id}
pod_namespace={pod_namespace}
pod_name={pod_name}
pod_type={type}
/>
) : (
<></>
)}
{/* Node Name */}
<div className={classes.heightMaintainer}>
<Typography className={classes.nodeSpacing}>
<span className={classes.bold}>Name:</span>
<br />
{pod_name}
</Typography>
</div>
{/* Node Type */}
<div className={classes.heightMaintainer}>
<Typography className={classes.nodeSpacing}>
<span className={classes.bold}>Type:</span> {type}
</Typography>
</div>
<hr />
{/* Node Phase */}
<div className={classes.nodeSpacing}>
<div className={classes.heightMaintainer}>
<Typography>
<span className={classes.bold}>Phase:</span> {phase}
</Typography>
</div>
</div>
<hr />
{/* Node Durations */}
<div className={classes.nodeSpacing}>
<div className={classes.heightMaintainer}>
<Typography>
<span className={classes.bold}>Start time:</span>{' '}
{timeDifference(startedAt)}
</Typography>
<Typography>
<span className={classes.bold}>End time:</span>{' '}
{timeDifference(finishedAt)}
</Typography>
<Typography>
<span className={classes.bold}>Duration: </span>{' '}
{(
(parseInt(finishedAt, 10) - parseInt(startedAt, 10)) /
60
).toFixed(1)}{' '}
minutes
</Typography>
</div>
</div>
<hr />
{/* Node Name */}
<div className={classes.nodeSpacing}>
<div className={classes.heightMaintainer}>
<Typography>
<span className={classes.bold}>Node Name:</span> {name}
</Typography>
</div>
</div>
<div className={classes.footerButton}>
<ButtonOutline isDisabled={false} handleClick={() => setLogsOpen(true)}>
Logs
</ButtonOutline>
</div>
</div>
);
};
export default WorkflowNodeInfo;

View File

@ -17,6 +17,12 @@ const useStyles = makeStyles((theme: Theme) => ({
heightMaintainer: { heightMaintainer: {
lineHeight: '2rem', lineHeight: '2rem',
}, },
footerButton: {
marginLeft: 'auto',
display: 'flex',
flexDirection: 'row',
padding: theme.spacing(3, 4, 4, 0),
},
})); }));
export default useStyles; export default useStyles;