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:
Amit Kumar Das 2021-06-22 09:11:46 +05:30 committed by GitHub
parent fca61d2e55
commit ff4e56277f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 933 additions and 2 deletions

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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"

View File

@ -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" />

View File

@ -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
}
}
}
}
`;

View File

@ -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;

View File

@ -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;

View File

@ -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

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;