chore(litmus-portal): Added overall usage and project stats page (#2921)
* Added global stats cards Signed-off-by: Amit Kumar Das <amit@chaosnative.com> * Moved inline styles to styles file Signed-off-by: Amit Kumar Das <amit@chaosnative.com> * Making Litmus Cards Reusable and Modular Signed-off-by: Sayan Mondal <sayan@chaosnative.com> * Added project details query and basic table Signed-off-by: Amit Kumar Das <amit@chaosnative.com> * Adding Header for table Signed-off-by: Sayan Mondal <sayan@chaosnative.com> * Adding Search for table Signed-off-by: Sayan Mondal <sayan@chaosnative.com> * Fixed search functionality Signed-off-by: Amit Kumar Das <amit@chaosnative.com> * Added tarnslation for main page + Usage Cards Signed-off-by: Sayan Mondal <sayan@chaosnative.com> * Added projects table and refactoring Signed-off-by: Amit Kumar Das <amit@chaosnative.com> * 🔨 Changed Active SVG stroke and Fixed DeepScan 🔧 Signed-off-by: Sayan Mondal <sayan@chaosnative.com> * Addsort functionality Signed-off-by: Amit Kumar Das <amit@chaosnative.com> * Minor fix Signed-off-by: Amit Kumar Das <amit@chaosnative.com> Co-authored-by: Sayan Mondal <sayan@chaosnative.com>
This commit is contained in:
parent
fca61d2e55
commit
ff4e56277f
|
@ -0,0 +1,7 @@
|
|||
<svg width="15" height="16" viewBox="0 0 15 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M9.69793 7.48119V1.84277H10.5768V0.963867H4.42449V1.84277H5.3034V7.48119L1.01475 13.9142C0.744514 14.3196 0.719466 14.8381 0.949329 15.2676C1.17916 15.6971 1.62453 15.9639 2.11165 15.9639H12.8897C13.3768 15.9639 13.8222 15.6971 14.052 15.2676C14.2819 14.8381 14.2568 14.3195 13.9866 13.9142L9.69793 7.48119ZM6.18231 7.74732V1.84277H8.81903V7.74732L9.94854 9.44159C9.72849 9.39967 9.49438 9.37207 9.25848 9.37207C8.28699 9.37207 7.34381 9.83807 7.30414 9.8579C7.08547 9.96724 6.37877 10.251 5.74285 10.251C5.34966 10.251 4.94782 10.1468 4.65005 10.0457L6.18231 7.74732ZM13.2771 14.8529C13.2398 14.9227 13.1272 15.085 12.8897 15.085H2.11165C1.87408 15.085 1.76156 14.9227 1.7242 14.8529C1.68688 14.7831 1.61422 14.5995 1.746 14.4018L4.14802 10.7987C4.51775 10.9411 5.12434 11.1299 5.74285 11.1299C6.71434 11.1299 7.65752 10.6639 7.69719 10.6441C7.91583 10.5347 8.62253 10.251 9.25848 10.251C9.86029 10.251 10.4826 10.4948 10.7197 10.5983L13.2553 14.4017C13.3871 14.5995 13.3145 14.7831 13.2771 14.8529Z" fill="#1C0732"/>
|
||||
<path d="M8.81906 11.5693H9.69796V12.4482H8.81906V11.5693Z" fill="#1C0732"/>
|
||||
<path d="M7.06124 13.3271H7.94015V14.2061H7.06124V13.3271Z" fill="#1C0732"/>
|
||||
<path d="M5.30343 12.4482H6.18234V13.3271H5.30343V12.4482Z" fill="#1C0732"/>
|
||||
<path d="M7.06124 8.05371H7.94015V8.93262H7.06124V8.05371Z" fill="#1C0732"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.4 KiB |
|
@ -0,0 +1,6 @@
|
|||
<svg width="16" height="17" viewBox="0 0 16 17" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M5.5 10.5C7.29493 10.5 8.75 9.04493 8.75 7.25C8.75 5.45507 7.29493 4 5.5 4C3.70507 4 2.25 5.45507 2.25 7.25C2.25 9.04493 3.70507 10.5 5.5 10.5Z" stroke="#1C0732" stroke-miterlimit="10"/>
|
||||
<path d="M9.71289 4.12132C10.1599 3.99537 10.6287 3.96668 11.0878 4.03718C11.5468 4.10767 11.9854 4.27572 12.374 4.52999C12.7626 4.78427 13.0922 5.11887 13.3407 5.51127C13.5891 5.90366 13.7505 6.34473 13.8141 6.80477C13.8777 7.26481 13.842 7.73314 13.7093 8.17821C13.5767 8.62329 13.3502 9.03477 13.0452 9.38494C12.7401 9.73511 12.3636 10.0158 11.9409 10.2082C11.5182 10.4006 11.0592 10.5002 10.5947 10.5003" stroke="#1C0732" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M1 12.8373C1.50758 12.1153 2.18143 11.526 2.96466 11.1192C3.74788 10.7124 4.61748 10.5 5.50005 10.5C6.38262 10.5 7.25224 10.7123 8.0355 11.119C8.81875 11.5258 9.49264 12.115 10.0003 12.837" stroke="#1C0732" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M10.5947 10.5C11.4774 10.4994 12.3472 10.7114 13.1305 11.1182C13.9138 11.525 14.5876 12.1146 15.0947 12.837" stroke="#1C0732" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.2 KiB |
|
@ -0,0 +1,6 @@
|
|||
<svg width="16" height="17" viewBox="0 0 16 17" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M13 3H3C2.72386 3 2.5 3.22386 2.5 3.5V13.5C2.5 13.7761 2.72386 14 3 14H13C13.2761 14 13.5 13.7761 13.5 13.5V3.5C13.5 3.22386 13.2761 3 13 3Z" stroke="black" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M11 2V4" stroke="black" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M5 2V4" stroke="black" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M2.5 6H13.5" stroke="black" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
After Width: | Height: | Size: 567 B |
|
@ -0,0 +1,3 @@
|
|||
<svg width="19" height="19" viewBox="0 0 19 19" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M1 11.6667H0.5V12.1667V17.5V18H1H6.10958H6.60958V17.5V12.1667V11.6667H6.10958H1ZM10.8581 4.3541L10.5267 4.7L10.8581 5.0459L13.7485 8.06289L14.1096 8.43974L14.4706 8.06289L17.361 5.0459L17.6924 4.7L17.361 4.3541L14.4706 1.33711L14.1096 0.960257L13.7485 1.33711L10.8581 4.3541ZM3.55479 7.33333C5.26189 7.33333 6.60958 5.89516 6.60958 4.16667C6.60958 2.43818 5.26189 1 3.55479 1C1.8477 1 0.5 2.43818 0.5 4.16667C0.5 5.89516 1.8477 7.33333 3.55479 7.33333ZM13.774 18C15.4811 18 16.8287 16.5618 16.8287 14.8333C16.8287 13.1048 15.4811 11.6667 13.774 11.6667C12.0669 11.6667 10.7192 13.1048 10.7192 14.8333C10.7192 16.5618 12.0669 18 13.774 18Z" stroke="#1C0732"/>
|
||||
</svg>
|
After Width: | Height: | Size: 771 B |
|
@ -1338,3 +1338,36 @@ customWorkflow:
|
|||
dragexp: Drag the selected experiment between the rows or to the end of rows to make it sequential experiment or drag into the rows to make it parallel experiment.
|
||||
saveChanges: Save changes
|
||||
discard: Discard changes
|
||||
|
||||
######################################
|
||||
############ Usage ##############
|
||||
######################################
|
||||
usage:
|
||||
usageHeader: Usage
|
||||
usageSubtitle: Global and project level usage details. Available only to the admin user.
|
||||
projectStatistics: Project Statistics
|
||||
projectSubtitle: Includes the number of schedules, workflow runs and experiment run in each project.
|
||||
card:
|
||||
userHeader: Users
|
||||
userSubtitle: Total number of Litmus users.
|
||||
projectHeader: Projects
|
||||
projectSubtitle: Total number of Litmus projects.
|
||||
agentHeader: Agents
|
||||
agentSubtitle: Total number of Litmus agents connected to Litmus center.
|
||||
agentClusterScope: cluster scope
|
||||
agentNamespaceScope: namespace scope
|
||||
workflowScheduleHeader: Workflow Schedules
|
||||
workflowScheduleSubtitle: Total number of chaos workflows scheduled in this month.
|
||||
workflowRunHeader: Workflow Runs
|
||||
workflowRunSubtitle: Number of workflows ran this month.
|
||||
experimentRunHeader: Experiments Runs
|
||||
experimentRunSubtitle: Total number of chaos experiments ran this month.
|
||||
table:
|
||||
project: Projects
|
||||
owner: Project Owner
|
||||
agents: Agents
|
||||
schedules: Workflow Schedules
|
||||
wfRuns: Workflow runs
|
||||
expRuns: Experiment runs
|
||||
team: Team Members
|
||||
noProject: No Projects Found
|
||||
|
|
|
@ -7,6 +7,7 @@ import ListItemText from '@material-ui/core/ListItemText';
|
|||
import moment from 'moment';
|
||||
import React from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { UserRole } from '../../models/graphql/user';
|
||||
import { history } from '../../redux/configureStore';
|
||||
import { ReactComponent as AnalyticsIcon } from '../../svg/analytics.svg';
|
||||
import { ReactComponent as CommunityIcon } from '../../svg/community.svg';
|
||||
|
@ -15,7 +16,9 @@ import { ReactComponent as HomeIcon } from '../../svg/home.svg';
|
|||
import { ReactComponent as MyHubIcon } from '../../svg/myhub.svg';
|
||||
import { ReactComponent as SettingsIcon } from '../../svg/settings.svg';
|
||||
import { ReactComponent as TargetsIcon } from '../../svg/targets.svg';
|
||||
import { ReactComponent as UsageIcon } from '../../svg/usage.svg';
|
||||
import { ReactComponent as WorkflowsIcon } from '../../svg/workflows.svg';
|
||||
import { getUserRole } from '../../utils/auth';
|
||||
import { getProjectID, getProjectRole } from '../../utils/getSearchParams';
|
||||
import useStyles from './styles';
|
||||
|
||||
|
@ -49,6 +52,7 @@ const SideBar: React.FC = () => {
|
|||
const classes = useStyles();
|
||||
const projectID = getProjectID();
|
||||
const projectRole = getProjectRole();
|
||||
const role = getUserRole();
|
||||
const pathName = useLocation().pathname.split('/')[1];
|
||||
const version = process.env.REACT_APP_KB_CHAOS_VERSION;
|
||||
const buildTime = moment
|
||||
|
@ -135,6 +139,7 @@ const SideBar: React.FC = () => {
|
|||
>
|
||||
<AnalyticsIcon />
|
||||
</CustomisedListItem>
|
||||
|
||||
{projectRole === 'Owner' && (
|
||||
<CustomisedListItem
|
||||
key="settings"
|
||||
|
@ -150,6 +155,22 @@ const SideBar: React.FC = () => {
|
|||
<SettingsIcon />
|
||||
</CustomisedListItem>
|
||||
)}
|
||||
|
||||
{role === UserRole.admin && (
|
||||
<CustomisedListItem
|
||||
key="usage"
|
||||
handleClick={() => {
|
||||
history.push({
|
||||
pathname: `/usage`,
|
||||
search: `?projectID=${projectID}&projectRole=${projectRole}`,
|
||||
});
|
||||
}}
|
||||
label="Usage"
|
||||
selected={pathName === 'usage'}
|
||||
>
|
||||
<UsageIcon />
|
||||
</CustomisedListItem>
|
||||
)}
|
||||
<hr id="quickActions" />
|
||||
<CustomisedListItem
|
||||
key="litmusDocs"
|
||||
|
|
|
@ -4,11 +4,16 @@ import React, { lazy, Suspense, useEffect, useState } from 'react';
|
|||
import { Redirect, Route, Router, Switch } from 'react-router-dom';
|
||||
import Loader from '../../components/Loader';
|
||||
import { GET_PROJECT, LIST_PROJECTS } from '../../graphql';
|
||||
import { Member, ProjectDetail, Projects } from '../../models/graphql/user';
|
||||
import {
|
||||
Member,
|
||||
ProjectDetail,
|
||||
Projects,
|
||||
UserRole,
|
||||
} from '../../models/graphql/user';
|
||||
import useActions from '../../redux/actions';
|
||||
import * as AnalyticsActions from '../../redux/actions/analytics';
|
||||
import { history } from '../../redux/configureStore';
|
||||
import { getToken, getUserId } from '../../utils/auth';
|
||||
import { getToken, getUserId, getUserRole } from '../../utils/auth';
|
||||
import { getProjectID, getProjectRole } from '../../utils/getSearchParams';
|
||||
|
||||
const ErrorPage = lazy(() => import('../../pages/ErrorPage'));
|
||||
|
@ -20,6 +25,7 @@ const WorkflowDetails = lazy(() => import('../../pages/WorkflowDetails'));
|
|||
const HomePage = lazy(() => import('../../pages/HomePage'));
|
||||
const Community = lazy(() => import('../../pages/Community'));
|
||||
const Settings = lazy(() => import('../../pages/Settings'));
|
||||
const Usage = lazy(() => import('../../pages/Usage'));
|
||||
const Targets = lazy(() => import('../../pages/Targets'));
|
||||
const EditSchedule = lazy(() => import('../../pages/EditSchedule'));
|
||||
const SetNewSchedule = lazy(() => import('../../pages/EditSchedule/Schedule'));
|
||||
|
@ -41,6 +47,7 @@ const Routes: React.FC = () => {
|
|||
const baseRoute = window.location.pathname.split('/')[1];
|
||||
const projectIDFromURL = getProjectID();
|
||||
const projectRoleFromURL = getProjectRole();
|
||||
const role = getUserRole();
|
||||
const [projectID, setprojectID] = useState<string>(projectIDFromURL);
|
||||
const [projectRole, setprojectRole] = useState<string>(projectRoleFromURL);
|
||||
const [isProjectMember, setIsProjectMember] = useState<boolean>(false);
|
||||
|
@ -206,6 +213,16 @@ const Routes: React.FC = () => {
|
|||
}}
|
||||
/>
|
||||
)}
|
||||
{role === UserRole.admin ? (
|
||||
<Route path="/usage" component={Usage} />
|
||||
) : (
|
||||
<Redirect
|
||||
to={{
|
||||
pathname: '/home',
|
||||
search: `?projectID=${projectID}&projectRole=${projectRole}`,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Route exact path="/404" component={ErrorPage} />
|
||||
{/* Redirects */}
|
||||
<Redirect exact path="/getStarted" to="/home" />
|
||||
|
|
|
@ -596,3 +596,51 @@ export const GET_IMAGE_REGISTRY = gql`
|
|||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const GET_GLOBAL_STATS = gql`
|
||||
query getGlobalStats($query: UsageQuery!) {
|
||||
UsageQuery(query: $query) {
|
||||
TotalCount {
|
||||
Workflows {
|
||||
Runs
|
||||
ExpRuns
|
||||
Schedules
|
||||
}
|
||||
Agents {
|
||||
Ns
|
||||
Cluster
|
||||
Total
|
||||
}
|
||||
Projects
|
||||
Users
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const GLOBAL_PROJECT_DATA = gql`
|
||||
query getStats($query: UsageQuery!) {
|
||||
UsageQuery(query: $query) {
|
||||
Projects {
|
||||
Name
|
||||
Workflows {
|
||||
Schedules
|
||||
ExpRuns
|
||||
Runs
|
||||
}
|
||||
Agents {
|
||||
Total
|
||||
Ns
|
||||
Cluster
|
||||
}
|
||||
Members {
|
||||
Owner {
|
||||
Name
|
||||
Username
|
||||
}
|
||||
Total
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
import { Typography } from '@material-ui/core';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Scaffold from '../../containers/layouts/Scaffold';
|
||||
import UsageStats from '../../views/Usage/UsageStats';
|
||||
import UsageTable from '../../views/Usage/UsageTable';
|
||||
import useStyles from './styles';
|
||||
|
||||
const Usage = () => {
|
||||
const classes = useStyles();
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Scaffold>
|
||||
<Typography variant="h3">{t('usage.usageHeader')}</Typography>
|
||||
<Typography className={classes.description}>
|
||||
{t('usage.usageSubtitle')}
|
||||
</Typography>
|
||||
<UsageStats />
|
||||
<br />
|
||||
<br />
|
||||
<Typography variant="h4">{t('usage.projectStatistics')}</Typography>
|
||||
<Typography className={classes.description}>
|
||||
{t('usage.projectSubtitle')}
|
||||
</Typography>
|
||||
<UsageTable />
|
||||
</Scaffold>
|
||||
);
|
||||
};
|
||||
|
||||
export default Usage;
|
|
@ -0,0 +1,14 @@
|
|||
import { makeStyles, Theme } from '@material-ui/core/styles';
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) => ({
|
||||
Head: {
|
||||
margin: theme.spacing(1, 0, 2.5),
|
||||
},
|
||||
description: {
|
||||
fontWeight: 400,
|
||||
fontSize: '1rem',
|
||||
margin: theme.spacing(3, 0),
|
||||
color: theme.palette.text.hint,
|
||||
},
|
||||
}));
|
||||
export default useStyles;
|
|
@ -0,0 +1,6 @@
|
|||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8 8V2" fill="#1C0732" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M13.197 5L2.80469 11" stroke="black" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M2.10005 9.09726C1.84027 7.6914 2.09022 6.23897 2.80499 5.00081C3.51977 3.76265 4.65255 2.81988 5.99992 2.3418V6.84568L2.10005 9.09726Z" stroke="black" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M8.00013 2C9.05134 2.00019 10.0841 2.27657 10.9949 2.80144C11.9056 3.32632 12.6626 4.08127 13.1898 4.9907C13.717 5.90013 13.9961 6.93213 13.999 7.98333C14.0019 9.03454 13.7286 10.0681 13.2064 10.9804C12.6843 11.8928 11.9316 12.6519 11.0237 13.1818C10.1158 13.7118 9.08466 13.9939 8.03347 13.9999C6.98227 14.0059 5.94794 13.7357 5.03405 13.2162C4.12016 12.6968 3.35879 11.9463 2.82617 11.04" stroke="black" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
After Width: | Height: | Size: 953 B |
|
@ -0,0 +1,75 @@
|
|||
import { Typography, useTheme } from '@material-ui/core';
|
||||
import { LitmusCard } from 'litmus-ui';
|
||||
import React from 'react';
|
||||
import useStyles from './styles';
|
||||
|
||||
interface CardProps extends Partial<ICards> {
|
||||
subData?: Array<SubData>;
|
||||
}
|
||||
|
||||
interface SubData {
|
||||
option1: any;
|
||||
option2: string;
|
||||
}
|
||||
|
||||
interface ICards {
|
||||
image: string;
|
||||
header: string;
|
||||
subtitle: string;
|
||||
color: string;
|
||||
data: any;
|
||||
split?: boolean;
|
||||
}
|
||||
|
||||
const Card: React.FC<CardProps> = ({
|
||||
image,
|
||||
header,
|
||||
subtitle,
|
||||
color,
|
||||
data,
|
||||
split,
|
||||
subData,
|
||||
}) => {
|
||||
const classes = useStyles();
|
||||
const { palette } = useTheme();
|
||||
return (
|
||||
<LitmusCard
|
||||
borderColor={palette.border.main}
|
||||
width="22.5rem"
|
||||
height="10.3rem"
|
||||
className={classes.litmusCard}
|
||||
>
|
||||
<div>
|
||||
<div className={classes.flex}>
|
||||
<img src={image} alt="Alternate" />
|
||||
<Typography variant="h6" className={classes.cardTitle}>
|
||||
{header}
|
||||
</Typography>
|
||||
</div>
|
||||
<Typography className={classes.cardDescription}>{subtitle}</Typography>
|
||||
{split ? (
|
||||
<div className={classes.flex}>
|
||||
<Typography className={`${color} ${classes.dataField}`}>
|
||||
{data}
|
||||
</Typography>
|
||||
<div className={classes.subData}>
|
||||
{subData?.map((option) => {
|
||||
return (
|
||||
<Typography key={option.option1} style={{ opacity: 0.5 }}>
|
||||
<strong>{option.option1}</strong> {option.option2}
|
||||
</Typography>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<Typography className={`${color} ${classes.dataField}`}>
|
||||
{data}
|
||||
</Typography>
|
||||
)}
|
||||
</div>
|
||||
</LitmusCard>
|
||||
);
|
||||
};
|
||||
|
||||
export default Card;
|
|
@ -0,0 +1,95 @@
|
|||
import { useQuery } from '@apollo/client';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Loader from '../../../components/Loader';
|
||||
import { GET_GLOBAL_STATS } from '../../../graphql';
|
||||
import Card from './Cards';
|
||||
import useStyles from './styles';
|
||||
|
||||
const UsageStats = () => {
|
||||
const classes = useStyles();
|
||||
const { t } = useTranslation();
|
||||
const { data, loading } = useQuery(GET_GLOBAL_STATS, {
|
||||
variables: {
|
||||
query: {
|
||||
DateRange: {
|
||||
start_date: Math.trunc(
|
||||
new Date(
|
||||
new Date().getFullYear(),
|
||||
new Date().getMonth(),
|
||||
1
|
||||
).getTime() / 1000
|
||||
).toString(),
|
||||
end_date: Math.trunc(new Date().getTime() / 1000).toString(),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={classes.cardDiv}>
|
||||
{loading ? (
|
||||
<Loader />
|
||||
) : (
|
||||
<>
|
||||
<Card
|
||||
image="./icons/users.svg"
|
||||
header={t('usage.card.userHeader')}
|
||||
subtitle={t('usage.card.userSubtitle')}
|
||||
color={classes.usersData}
|
||||
data={data?.UsageQuery.TotalCount.Users}
|
||||
/>
|
||||
<Card
|
||||
image="./icons/viewProjects.svg"
|
||||
header={t('usage.card.projectHeader')}
|
||||
subtitle={t('usage.card.projectSubtitle')}
|
||||
color={classes.projectData}
|
||||
data={data?.UsageQuery.TotalCount.Projects}
|
||||
/>
|
||||
<Card
|
||||
image="./icons/targets.svg"
|
||||
header={t('usage.card.agentHeader')}
|
||||
subtitle={t('usage.card.agentSubtitle')}
|
||||
color={classes.agentsData}
|
||||
data={data?.UsageQuery.TotalCount.Agents.Total}
|
||||
split
|
||||
subData={[
|
||||
{
|
||||
option1: data?.UsageQuery.TotalCount.Agents.Cluster,
|
||||
option2: `${t('usage.card.agentClusterScope')}`,
|
||||
},
|
||||
{
|
||||
option1: data?.UsageQuery.TotalCount.Agents.Ns,
|
||||
option2: `${t('usage.card.agentNamespaceScope')}`,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
<Card
|
||||
image="./icons/workflow-calender.svg"
|
||||
header={t('usage.card.workflowScheduleHeader')}
|
||||
subtitle={t('usage.card.workflowScheduleSubtitle')}
|
||||
color={classes.schedules}
|
||||
data={data?.UsageQuery.TotalCount.Workflows.Schedules}
|
||||
/>
|
||||
<Card
|
||||
image="./icons/workflows-outline.svg"
|
||||
header={t('usage.card.workflowRunHeader')}
|
||||
subtitle={t('usage.card.workflowRunSubtitle')}
|
||||
color={classes.wfRuns}
|
||||
data={data?.UsageQuery.TotalCount.Workflows.Runs}
|
||||
/>
|
||||
<Card
|
||||
image="./icons/myhub.svg"
|
||||
header={t('usage.card.experimentRunHeader')}
|
||||
subtitle={t('usage.card.experimentRunSubtitle')}
|
||||
color={classes.expRuns}
|
||||
data={data?.UsageQuery.TotalCount.Workflows.ExpRuns}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default UsageStats;
|
|
@ -0,0 +1,64 @@
|
|||
import { makeStyles, Theme } from '@material-ui/core/styles';
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) => ({
|
||||
Head: {
|
||||
margin: theme.spacing(1, 0, 2.5),
|
||||
},
|
||||
litmusCard: {
|
||||
margin: theme.spacing(2.5, 5, 2.5, 0),
|
||||
borderRadius: 5,
|
||||
padding: theme.spacing(2.5, 1.875),
|
||||
flexGrow: 1,
|
||||
},
|
||||
cardDiv: {
|
||||
marginTop: theme.spacing(2.5),
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
},
|
||||
flex: {
|
||||
display: 'flex',
|
||||
},
|
||||
cardTitle: {
|
||||
marginLeft: theme.spacing(1.25),
|
||||
},
|
||||
cardDescription: {
|
||||
color: theme.palette.text.hint,
|
||||
marginTop: theme.spacing(1.5),
|
||||
height: '1.875rem',
|
||||
},
|
||||
usersData: {
|
||||
color: theme.palette.status.experiment.skipped,
|
||||
},
|
||||
projectData: {
|
||||
color: theme.palette.status.experiment.completed,
|
||||
},
|
||||
agentsData: {
|
||||
color: theme.palette.status.experiment.running,
|
||||
},
|
||||
schedules: {
|
||||
color: theme.palette.status.experiment.failed,
|
||||
},
|
||||
wfRuns: {
|
||||
color: theme.palette.status.experiment.omitted,
|
||||
},
|
||||
expRuns: {
|
||||
color: theme.palette.status.experiment.error,
|
||||
},
|
||||
agentType: {
|
||||
marginLeft: 'auto',
|
||||
marginTop: theme.spacing(2.5),
|
||||
},
|
||||
agentTypeText: {
|
||||
opacity: 0.5,
|
||||
},
|
||||
dataField: {
|
||||
marginTop: theme.spacing(1.875),
|
||||
fontSize: '1.875rem',
|
||||
},
|
||||
subData: {
|
||||
marginLeft: 'auto',
|
||||
marginTop: theme.spacing(2.5),
|
||||
},
|
||||
}));
|
||||
export default useStyles;
|
|
@ -0,0 +1,415 @@
|
|||
import { useQuery } from '@apollo/client';
|
||||
import {
|
||||
IconButton,
|
||||
Paper,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TablePagination,
|
||||
TableRow,
|
||||
Typography,
|
||||
} from '@material-ui/core';
|
||||
import { Search } from 'litmus-ui';
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import ExpandLessIcon from '@material-ui/icons/ExpandLess';
|
||||
import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
|
||||
import Loader from '../../../components/Loader';
|
||||
import { GLOBAL_PROJECT_DATA } from '../../../graphql';
|
||||
import { Pagination } from '../../../models/graphql/workflowListData';
|
||||
import useStyles from './styles';
|
||||
|
||||
interface SortInput {
|
||||
Field:
|
||||
| 'Project'
|
||||
| 'Owner'
|
||||
| 'Agents'
|
||||
| 'Schedules'
|
||||
| 'WorkflowRuns'
|
||||
| 'ExperimentRuns'
|
||||
| 'TeamMembers';
|
||||
Descending?: boolean;
|
||||
}
|
||||
|
||||
const UsageTable = () => {
|
||||
const classes = useStyles();
|
||||
const { t } = useTranslation();
|
||||
const [paginationData, setPaginationData] = useState<Pagination>({
|
||||
page: 0,
|
||||
limit: 10,
|
||||
});
|
||||
const [sortData, setSortData] = useState<SortInput>({
|
||||
Field: 'Project',
|
||||
Descending: false,
|
||||
});
|
||||
const [search, setSearch] = useState<string>('');
|
||||
const { data, loading } = useQuery(GLOBAL_PROJECT_DATA, {
|
||||
variables: {
|
||||
query: {
|
||||
DateRange: {
|
||||
start_date: Math.trunc(
|
||||
new Date(
|
||||
new Date().getFullYear(),
|
||||
new Date().getMonth(),
|
||||
1
|
||||
).getTime() / 1000
|
||||
).toString(),
|
||||
end_date: Math.trunc(new Date().getTime() / 1000).toString(),
|
||||
},
|
||||
Pagination: {
|
||||
page: paginationData.page,
|
||||
limit: paginationData.limit,
|
||||
},
|
||||
SearchProject: search,
|
||||
Sort: sortData,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={classes.table}>
|
||||
<div className={classes.headerSection}>
|
||||
{/* Search Bar */}
|
||||
<Search
|
||||
className={classes.search}
|
||||
data-cy="projectSearch"
|
||||
id="input-with-icon-textfield"
|
||||
placeholder="Search Projects"
|
||||
value={search}
|
||||
onChange={(event: any) => setSearch(event.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Paper>
|
||||
<TableContainer className={classes.tableMain}>
|
||||
<Table stickyHeader aria-label="simple table">
|
||||
<TableHead>
|
||||
<TableRow className={classes.tableHead}>
|
||||
{/* Projects */}
|
||||
<TableCell className={classes.projectName}>
|
||||
<div style={{ display: 'flex' }}>
|
||||
<Typography className={classes.tableHeadName}>
|
||||
{t('usage.table.project')}
|
||||
</Typography>
|
||||
<div className={classes.sortIconDiv}>
|
||||
<IconButton
|
||||
aria-label="sort project ascending"
|
||||
size="small"
|
||||
onClick={() =>
|
||||
setSortData({
|
||||
Field: 'Project',
|
||||
Descending: false,
|
||||
})
|
||||
}
|
||||
className={classes.imgSize}
|
||||
>
|
||||
<ExpandLessIcon fontSize="inherit" />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
aria-label="sort project descending"
|
||||
size="small"
|
||||
onClick={() =>
|
||||
setSortData({
|
||||
Field: 'Project',
|
||||
Descending: true,
|
||||
})
|
||||
}
|
||||
className={classes.imgSize}
|
||||
>
|
||||
<ExpandMoreIcon fontSize="inherit" />
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
|
||||
{/* Owners */}
|
||||
<TableCell align="left">
|
||||
<div style={{ display: 'flex' }}>
|
||||
<Typography className={classes.tableHeadName}>
|
||||
{t('usage.table.owner')}
|
||||
</Typography>
|
||||
<div className={classes.sortIconDiv}>
|
||||
<IconButton
|
||||
aria-label="sort owner ascending"
|
||||
size="small"
|
||||
onClick={() =>
|
||||
setSortData({
|
||||
Field: 'Owner',
|
||||
Descending: false,
|
||||
})
|
||||
}
|
||||
className={classes.imgSize}
|
||||
>
|
||||
<ExpandLessIcon fontSize="inherit" />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
aria-label="sort owner descending"
|
||||
size="small"
|
||||
onClick={() =>
|
||||
setSortData({
|
||||
Field: 'Owner',
|
||||
Descending: true,
|
||||
})
|
||||
}
|
||||
className={classes.imgSize}
|
||||
>
|
||||
<ExpandMoreIcon fontSize="inherit" />
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
|
||||
{/* Agents */}
|
||||
<TableCell align="center">
|
||||
<div style={{ display: 'flex' }}>
|
||||
<Typography className={classes.tableHeadName}>
|
||||
{t('usage.table.agents')}
|
||||
</Typography>
|
||||
<div className={classes.sortIconDiv}>
|
||||
<IconButton
|
||||
aria-label="sort agent ascending"
|
||||
size="small"
|
||||
onClick={() =>
|
||||
setSortData({
|
||||
Field: 'Agents',
|
||||
Descending: false,
|
||||
})
|
||||
}
|
||||
className={classes.imgSize}
|
||||
>
|
||||
<ExpandLessIcon fontSize="inherit" />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
aria-label="sort agents descending"
|
||||
size="small"
|
||||
onClick={() =>
|
||||
setSortData({
|
||||
Field: 'Agents',
|
||||
Descending: true,
|
||||
})
|
||||
}
|
||||
className={classes.imgSize}
|
||||
>
|
||||
<ExpandMoreIcon fontSize="inherit" />
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
|
||||
{/* Schedules */}
|
||||
<TableCell align="center">
|
||||
<div style={{ display: 'flex' }}>
|
||||
<Typography className={classes.tableHeadName}>
|
||||
{t('usage.table.schedules')}
|
||||
</Typography>
|
||||
<div className={classes.sortIconDiv}>
|
||||
<IconButton
|
||||
aria-label="sort schedules ascending"
|
||||
size="small"
|
||||
onClick={() =>
|
||||
setSortData({
|
||||
Field: 'Schedules',
|
||||
Descending: false,
|
||||
})
|
||||
}
|
||||
className={classes.imgSize}
|
||||
>
|
||||
<ExpandLessIcon fontSize="inherit" />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
aria-label="sort schedules descending"
|
||||
size="small"
|
||||
onClick={() =>
|
||||
setSortData({
|
||||
Field: 'Schedules',
|
||||
Descending: true,
|
||||
})
|
||||
}
|
||||
className={classes.imgSize}
|
||||
>
|
||||
<ExpandMoreIcon fontSize="inherit" />
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
|
||||
{/* WorkflowRuns */}
|
||||
<TableCell align="center">
|
||||
<div style={{ display: 'flex' }}>
|
||||
<Typography className={classes.tableHeadName}>
|
||||
{t('usage.table.wfRuns')}
|
||||
</Typography>
|
||||
<div className={classes.sortIconDiv}>
|
||||
<IconButton
|
||||
aria-label="sort WorkflowRuns ascending"
|
||||
size="small"
|
||||
onClick={() =>
|
||||
setSortData({
|
||||
Field: 'WorkflowRuns',
|
||||
Descending: false,
|
||||
})
|
||||
}
|
||||
className={classes.imgSize}
|
||||
>
|
||||
<ExpandLessIcon fontSize="inherit" />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
aria-label="sort WorkflowRuns descending"
|
||||
size="small"
|
||||
onClick={() =>
|
||||
setSortData({
|
||||
Field: 'WorkflowRuns',
|
||||
Descending: true,
|
||||
})
|
||||
}
|
||||
className={classes.imgSize}
|
||||
>
|
||||
<ExpandMoreIcon fontSize="inherit" />
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
|
||||
{/* ExperimentRuns */}
|
||||
<TableCell align="center">
|
||||
<div style={{ display: 'flex' }}>
|
||||
<Typography className={classes.tableHeadName}>
|
||||
{t('usage.table.expRuns')}
|
||||
</Typography>
|
||||
<div className={classes.sortIconDiv}>
|
||||
<IconButton
|
||||
aria-label="sort ExperimentRuns ascending"
|
||||
size="small"
|
||||
onClick={() =>
|
||||
setSortData({
|
||||
Field: 'ExperimentRuns',
|
||||
Descending: false,
|
||||
})
|
||||
}
|
||||
className={classes.imgSize}
|
||||
>
|
||||
<ExpandLessIcon fontSize="inherit" />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
aria-label="sort ExperimentRuns descending"
|
||||
size="small"
|
||||
onClick={() =>
|
||||
setSortData({
|
||||
Field: 'ExperimentRuns',
|
||||
Descending: true,
|
||||
})
|
||||
}
|
||||
className={classes.imgSize}
|
||||
>
|
||||
<ExpandMoreIcon fontSize="inherit" />
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
|
||||
{/* Team Mambers */}
|
||||
<TableCell align="center">
|
||||
<div style={{ display: 'flex' }}>
|
||||
<Typography className={classes.tableHeadName}>
|
||||
{t('usage.table.team')}
|
||||
</Typography>
|
||||
<div className={classes.sortIconDiv}>
|
||||
<IconButton
|
||||
aria-label="sort TeamMembers ascending"
|
||||
size="small"
|
||||
onClick={() =>
|
||||
setSortData({
|
||||
Field: 'TeamMembers',
|
||||
Descending: false,
|
||||
})
|
||||
}
|
||||
className={classes.imgSize}
|
||||
>
|
||||
<ExpandLessIcon fontSize="inherit" />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
aria-label="sort TeamMembers descending"
|
||||
size="small"
|
||||
onClick={() =>
|
||||
setSortData({
|
||||
Field: 'TeamMembers',
|
||||
Descending: true,
|
||||
})
|
||||
}
|
||||
className={classes.imgSize}
|
||||
>
|
||||
<ExpandMoreIcon fontSize="inherit" />
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{loading ? (
|
||||
<Loader />
|
||||
) : data?.UsageQuery.Projects.length > 0 ? (
|
||||
data?.UsageQuery.Projects.map((project: any) => (
|
||||
<TableRow key={project.Name} className={classes.projectData}>
|
||||
<TableCell
|
||||
component="th"
|
||||
scope="row"
|
||||
className={classes.tableDataProjectName}
|
||||
>
|
||||
{project.Name}
|
||||
</TableCell>
|
||||
<TableCell align="left">
|
||||
{project.Members.Owner.Username}
|
||||
</TableCell>
|
||||
<TableCell align="center">{project.Agents.Total}</TableCell>
|
||||
<TableCell align="center">
|
||||
{project.Workflows.Schedules}
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
{project.Workflows.Runs}
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
{project.Workflows.Runs}
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
{project.Members.Total}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={7}>
|
||||
<Typography className={classes.center}>
|
||||
<strong>{t('usage.table.noProject')}</strong>
|
||||
</Typography>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
<TablePagination
|
||||
rowsPerPageOptions={[10, 25, 50]}
|
||||
component="div"
|
||||
count={data?.UsageQuery.Projects.length ?? 0}
|
||||
rowsPerPage={paginationData.limit}
|
||||
page={paginationData.page}
|
||||
onChangePage={(_, page) =>
|
||||
setPaginationData({ ...paginationData, page })
|
||||
}
|
||||
onChangeRowsPerPage={(event) => {
|
||||
setPaginationData({
|
||||
...paginationData,
|
||||
page: 0,
|
||||
limit: parseInt(event.target.value, 10),
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Paper>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default UsageTable;
|
|
@ -0,0 +1,91 @@
|
|||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
import { makeStyles, Theme } from '@material-ui/core/styles';
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) => ({
|
||||
// Header Section Properties
|
||||
headerSection: {
|
||||
marginBottom: theme.spacing(2),
|
||||
width: '100%',
|
||||
height: '5rem',
|
||||
display: 'flex',
|
||||
backgroundColor: theme.palette.cards.background,
|
||||
},
|
||||
|
||||
search: {
|
||||
margin: theme.spacing(3, 2),
|
||||
},
|
||||
|
||||
noProjects: {
|
||||
padding: theme.spacing(3),
|
||||
},
|
||||
|
||||
center: {
|
||||
margin: 'auto',
|
||||
textAlign: 'center',
|
||||
},
|
||||
|
||||
table: {
|
||||
marginBottom: theme.spacing(5),
|
||||
},
|
||||
|
||||
root: {
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
},
|
||||
|
||||
tableMain: {
|
||||
marginTop: theme.spacing(4.25),
|
||||
backgroundColor: theme.palette.cards.background,
|
||||
height: '28rem',
|
||||
'&::-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,
|
||||
},
|
||||
},
|
||||
|
||||
tableHead: {
|
||||
height: '4.6875rem',
|
||||
'& p': {
|
||||
fontSize: '0.75rem',
|
||||
fontWeight: 500,
|
||||
},
|
||||
'& th': {
|
||||
backgroundColor: theme.palette.cards.background,
|
||||
color: theme.palette.text.hint,
|
||||
},
|
||||
},
|
||||
|
||||
projectData: {
|
||||
height: '4.6875rem',
|
||||
},
|
||||
|
||||
projectName: {
|
||||
paddingLeft: theme.spacing(5),
|
||||
},
|
||||
|
||||
tableDataProjectName: {
|
||||
fontSize: '0.875rem',
|
||||
fontWeight: 500,
|
||||
paddingLeft: theme.spacing(5),
|
||||
},
|
||||
|
||||
tableHeadName: {
|
||||
marginTop: theme.spacing(1.25),
|
||||
marginRight: theme.spacing(1.875),
|
||||
},
|
||||
|
||||
sortIconDiv: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
},
|
||||
|
||||
imgSize: {
|
||||
width: '1.25rem',
|
||||
height: '1.25rem',
|
||||
},
|
||||
}));
|
||||
export default useStyles;
|
Loading…
Reference in New Issue