mirror of
https://github.com/Unleash/unleash.git
synced 2025-02-09 00:18:00 +01:00
refactor: port ReportCard to TS/SWR (#674)
* refactor: remove unused reporting code * refactor: port ReportCard to TS/SWR
This commit is contained in:
parent
72acf2309c
commit
234bab6cb4
@ -1,23 +1,19 @@
|
||||
import classnames from 'classnames';
|
||||
import { Paper } from '@material-ui/core';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import CheckIcon from '@material-ui/icons/Check';
|
||||
import ReportProblemOutlinedIcon from '@material-ui/icons/ReportProblemOutlined';
|
||||
import ConditionallyRender from '../../common/ConditionallyRender/ConditionallyRender';
|
||||
|
||||
import styles from './ReportCard.module.scss';
|
||||
import ReactTimeAgo from 'react-timeago';
|
||||
import { IProjectHealthReport } from "../../../interfaces/project";
|
||||
|
||||
const ReportCard = ({
|
||||
health,
|
||||
activeCount,
|
||||
staleCount,
|
||||
potentiallyStaleCount,
|
||||
lastUpdate,
|
||||
}) => {
|
||||
const healthLessThan50 = health < 50;
|
||||
const healthLessThan75 = health < 75;
|
||||
interface IReportCardProps {
|
||||
healthReport: IProjectHealthReport
|
||||
}
|
||||
|
||||
export const ReportCard = ({ healthReport }: IReportCardProps) => {
|
||||
const healthLessThan50 = healthReport.health < 50;
|
||||
const healthLessThan75 = healthReport.health < 75;
|
||||
|
||||
const healthClasses = classnames(styles.reportCardHealthRating, {
|
||||
[styles.healthWarning]: healthLessThan75,
|
||||
@ -27,21 +23,21 @@ const ReportCard = ({
|
||||
const renderActiveToggles = () => (
|
||||
<>
|
||||
<CheckIcon className={styles.check} />
|
||||
<span>{activeCount} active toggles</span>
|
||||
<span>{healthReport.activeCount} active toggles</span>
|
||||
</>
|
||||
);
|
||||
|
||||
const renderStaleToggles = () => (
|
||||
<>
|
||||
<ReportProblemOutlinedIcon className={styles.danger} />
|
||||
<span>{staleCount} stale toggles</span>
|
||||
<span>{healthReport.staleCount} stale toggles</span>
|
||||
</>
|
||||
);
|
||||
|
||||
const renderPotentiallyStaleToggles = () => (
|
||||
<>
|
||||
<ReportProblemOutlinedIcon className={styles.danger} />
|
||||
<span>{potentiallyStaleCount} potentially stale toggles</span>
|
||||
<span>{healthReport.potentiallyStaleCount} potentially stale toggles</span>
|
||||
</>
|
||||
);
|
||||
|
||||
@ -52,14 +48,14 @@ const ReportCard = ({
|
||||
<h2 className={styles.header}>Health rating</h2>
|
||||
<div className={styles.reportCardHealthInnerContainer}>
|
||||
<ConditionallyRender
|
||||
condition={health > -1}
|
||||
condition={healthReport.health > -1}
|
||||
show={
|
||||
<div>
|
||||
<p className={healthClasses}>{health}%</p>
|
||||
<p className={healthClasses}>{healthReport.health}%</p>
|
||||
<p className={styles.lastUpdate}>
|
||||
Last updated:{' '}
|
||||
<ReactTimeAgo
|
||||
date={lastUpdate}
|
||||
date={healthReport.updatedAt}
|
||||
live={false}
|
||||
/>
|
||||
</p>
|
||||
@ -73,12 +69,12 @@ const ReportCard = ({
|
||||
<ul className={styles.reportCardList}>
|
||||
<li>
|
||||
<ConditionallyRender
|
||||
condition={activeCount}
|
||||
condition={Boolean(healthReport.activeCount)}
|
||||
show={renderActiveToggles}
|
||||
/>
|
||||
</li>
|
||||
<ConditionallyRender
|
||||
condition={activeCount}
|
||||
condition={Boolean(healthReport.activeCount)}
|
||||
show={
|
||||
<p className={styles.reportCardActionText}>
|
||||
Also includes potentially stale toggles.
|
||||
@ -88,7 +84,7 @@ const ReportCard = ({
|
||||
|
||||
<li>
|
||||
<ConditionallyRender
|
||||
condition={staleCount}
|
||||
condition={Boolean(healthReport.staleCount)}
|
||||
show={renderStaleToggles}
|
||||
/>
|
||||
</li>
|
||||
@ -101,13 +97,13 @@ const ReportCard = ({
|
||||
<ul className={styles.reportCardList}>
|
||||
<li>
|
||||
<ConditionallyRender
|
||||
condition={potentiallyStaleCount}
|
||||
condition={Boolean(healthReport.potentiallyStaleCount)}
|
||||
show={renderPotentiallyStaleToggles}
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
<ConditionallyRender
|
||||
condition={potentiallyStaleCount}
|
||||
condition={Boolean(healthReport.potentiallyStaleCount)}
|
||||
show={
|
||||
<p className={styles.reportCardActionText}>
|
||||
Review your feature toggles and delete
|
||||
@ -126,9 +122,3 @@ const ReportCard = ({
|
||||
</Paper>
|
||||
);
|
||||
};
|
||||
|
||||
ReportCard.propTypes = {
|
||||
features: PropTypes.array.isRequired,
|
||||
};
|
||||
|
||||
export default ReportCard;
|
@ -1,19 +0,0 @@
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import ReportCard from './ReportCard';
|
||||
import { filterByProject } from '../utils';
|
||||
|
||||
const mapStateToProps = (state, ownProps) => {
|
||||
const features = state.features.toJS();
|
||||
|
||||
const sameProject = filterByProject(ownProps.selectedProject);
|
||||
const featuresByProject = features.filter(sameProject);
|
||||
|
||||
return {
|
||||
features: featuresByProject,
|
||||
};
|
||||
};
|
||||
|
||||
const ReportCardContainer = connect(mapStateToProps, null)(ReportCard);
|
||||
|
||||
export default ReportCardContainer;
|
@ -1,107 +0,0 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import Select from '../common/select';
|
||||
import ReportCardContainer from './ReportCard/ReportCardContainer';
|
||||
import ReportToggleList from './ReportToggleList/ReportToggleList';
|
||||
|
||||
import ConditionallyRender from '../common/ConditionallyRender/ConditionallyRender';
|
||||
|
||||
import { formatProjectOptions } from './utils';
|
||||
import { REPORTING_SELECT_ID } from '../../testIds';
|
||||
|
||||
import styles from './Reporting.module.scss';
|
||||
import useHealthReport from '../../hooks/api/getters/useHealthReport/useHealthReport';
|
||||
import ApiError from '../common/ApiError/ApiError';
|
||||
import useQueryParams from '../../hooks/useQueryParams';
|
||||
|
||||
const Reporting = ({ projects }) => {
|
||||
const [projectOptions, setProjectOptions] = useState([
|
||||
{ key: 'default', label: 'Default' },
|
||||
]);
|
||||
const [selectedProject, setSelectedProject] = useState('default');
|
||||
const { project, error, refetch } = useHealthReport(selectedProject);
|
||||
|
||||
const params = useQueryParams();
|
||||
const projectId = params.get('project');
|
||||
|
||||
useEffect(() => {
|
||||
if (projectId) {
|
||||
return setSelectedProject(projectId);
|
||||
}
|
||||
setSelectedProject(projects[0].id);
|
||||
|
||||
/* eslint-disable-next-line */
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setProjectOptions(formatProjectOptions(projects));
|
||||
}, [projects]);
|
||||
|
||||
const onChange = e => {
|
||||
const { value } = e.target;
|
||||
|
||||
const selectedProject = projectOptions.find(
|
||||
option => option.key === value
|
||||
);
|
||||
|
||||
setSelectedProject(selectedProject.key);
|
||||
};
|
||||
|
||||
const renderSelect = () => (
|
||||
<div className={styles.projectSelector}>
|
||||
<h1 className={styles.header}>Project</h1>
|
||||
<Select
|
||||
name="project"
|
||||
className={styles.select}
|
||||
options={projectOptions}
|
||||
value={selectedProject}
|
||||
onChange={onChange}
|
||||
inputProps={{ ['data-testid']: REPORTING_SELECT_ID }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
const multipleProjects = projects.length > 1;
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<ConditionallyRender
|
||||
condition={multipleProjects}
|
||||
show={renderSelect}
|
||||
/>
|
||||
|
||||
<ConditionallyRender
|
||||
condition={error}
|
||||
show={
|
||||
<ApiError
|
||||
data-loading
|
||||
style={{ maxWidth: '500px', marginTop: '1rem' }}
|
||||
onClick={refetch}
|
||||
text={`Could not fetch health rating for ${selectedProject}`}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<ReportCardContainer
|
||||
health={project?.health}
|
||||
staleCount={project?.staleCount}
|
||||
activeCount={project?.activeCount}
|
||||
potentiallyStaleCount={project?.potentiallyStaleCount}
|
||||
selectedProject={selectedProject}
|
||||
/>
|
||||
<ReportToggleList
|
||||
features={project.features}
|
||||
selectedProject={selectedProject}
|
||||
/>
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
Reporting.propTypes = {
|
||||
fetchFeatureToggles: PropTypes.func.isRequired,
|
||||
projects: PropTypes.array.isRequired,
|
||||
features: PropTypes.array,
|
||||
};
|
||||
|
||||
export default Reporting;
|
@ -1,176 +0,0 @@
|
||||
.header {
|
||||
font-size: var(--h1-size);
|
||||
font-weight: 700;
|
||||
margin: 0 0 0.5rem 0;
|
||||
}
|
||||
|
||||
.card {
|
||||
width: 100%;
|
||||
padding: var(--card-padding);
|
||||
margin: var(--card-margin-y) 0;
|
||||
}
|
||||
|
||||
.projectSelector {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.select {
|
||||
background-color: #fff;
|
||||
border: none;
|
||||
padding: 0.5rem 1rem;
|
||||
position: static;
|
||||
}
|
||||
|
||||
.select select {
|
||||
border: none;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.select select:active {
|
||||
border: none;
|
||||
}
|
||||
|
||||
/** ReportCard **/
|
||||
|
||||
.reportCardContainer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.reportCardHealthInnerContainer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 80%;
|
||||
}
|
||||
|
||||
.reportCardHealthRating {
|
||||
font-size: 2rem;
|
||||
font-weight: bold;
|
||||
color: var(--success);
|
||||
}
|
||||
|
||||
.reportCardList {
|
||||
list-style-type: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.reportCardList li {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
.reportCardList li span {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
margin-left: 0.5rem;
|
||||
font-size: var(--p-size);
|
||||
}
|
||||
|
||||
.check,
|
||||
.danger {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.check {
|
||||
color: var(--success);
|
||||
}
|
||||
|
||||
.danger {
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
.reportCardActionContainer {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.reportCardActionText {
|
||||
max-width: 300px;
|
||||
font-size: var(--p-size);
|
||||
}
|
||||
|
||||
.reportCardBtn {
|
||||
background-color: #f2f2f2;
|
||||
}
|
||||
|
||||
.healthDanger {
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
.healthWarning {
|
||||
color: var(--warning);
|
||||
}
|
||||
|
||||
/** ReportToggleList **/
|
||||
.reportToggleList {
|
||||
width: 100%;
|
||||
margin: var(--card-margin-y) 0;
|
||||
}
|
||||
|
||||
.bulkAction {
|
||||
background-color: #f2f2f2;
|
||||
font-size: var(--p-size);
|
||||
}
|
||||
|
||||
.sortIcon {
|
||||
margin-left: 8px;
|
||||
transform: translateY(6px);
|
||||
}
|
||||
|
||||
.reportToggleListHeader {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-bottom: var(--default-border);
|
||||
padding: 1rem var(--card-padding-x);
|
||||
}
|
||||
|
||||
.reportToggleListInnerContainer {
|
||||
padding: var(--card-padding);
|
||||
}
|
||||
|
||||
.reportToggleListHeading {
|
||||
font-size: var(--h1-size);
|
||||
margin: 0;
|
||||
font-weight: 'bold';
|
||||
}
|
||||
|
||||
.reportingToggleTable {
|
||||
width: 100%;
|
||||
border-spacing: 0 0.8rem;
|
||||
}
|
||||
|
||||
.reportingToggleTable th {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.expired {
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
.active {
|
||||
color: var(--success);
|
||||
}
|
||||
|
||||
.stale {
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
.reportStatus {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.reportIcon {
|
||||
font-size: 1.5rem;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.tableRow {
|
||||
cursor: pointer;
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { fetchFeatureToggles } from '../../store/feature-toggle/actions';
|
||||
|
||||
import Reporting from './Reporting.jsx';
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
projects: state.projects.toJS(),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = {
|
||||
fetchFeatureToggles,
|
||||
};
|
||||
|
||||
const ReportingContainer = connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(Reporting);
|
||||
|
||||
export default ReportingContainer;
|
@ -177,10 +177,3 @@ export const getDates = dateString => {
|
||||
|
||||
export const filterByProject = selectedProject => feature =>
|
||||
feature.project === selectedProject;
|
||||
|
||||
export const isFeatureExpired = feature => {
|
||||
const [date, now] = getDates(feature.createdAt);
|
||||
const diff = getDiffInDays(date, now);
|
||||
|
||||
return expired(diff, feature.type);
|
||||
};
|
||||
|
@ -1,40 +1,38 @@
|
||||
import useHealthReport from '../../../../hooks/api/getters/useHealthReport/useHealthReport';
|
||||
import { useHealthReport } from '../../../../hooks/api/getters/useHealthReport/useHealthReport';
|
||||
import ApiError from '../../../common/ApiError/ApiError';
|
||||
import ConditionallyRender from '../../../common/ConditionallyRender';
|
||||
import ReportCardContainer from '../../../Reporting/ReportCard/ReportCardContainer';
|
||||
import ReportToggleList from '../../../Reporting/ReportToggleList/ReportToggleList';
|
||||
import { ReportCard } from '../../../Reporting/ReportCard/ReportCard';
|
||||
|
||||
interface ProjectHealthProps {
|
||||
projectId: string;
|
||||
}
|
||||
|
||||
const ProjectHealth = ({ projectId }: ProjectHealthProps) => {
|
||||
const { project, error, refetch } = useHealthReport(projectId);
|
||||
const { healthReport, refetchHealthReport, error } =
|
||||
useHealthReport(projectId);
|
||||
|
||||
if (!healthReport) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ConditionallyRender
|
||||
condition={error}
|
||||
condition={Boolean(error)}
|
||||
show={
|
||||
<ApiError
|
||||
data-loading
|
||||
style={{ maxWidth: '500px', marginTop: '1rem' }}
|
||||
onClick={refetch}
|
||||
onClick={refetchHealthReport}
|
||||
text={`Could not fetch health rating for ${projectId}`}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<ReportCardContainer
|
||||
health={project?.health}
|
||||
staleCount={project?.staleCount}
|
||||
activeCount={project?.activeCount}
|
||||
potentiallyStaleCount={project?.potentiallyStaleCount}
|
||||
selectedProject={project.name}
|
||||
lastUpdate={project.updatedAt}
|
||||
/>
|
||||
<ReportCard healthReport={healthReport} />
|
||||
<ReportToggleList
|
||||
features={project.features}
|
||||
selectedProject={projectId}
|
||||
features={healthReport.features}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
@ -1,51 +1,42 @@
|
||||
import useSWR, { mutate, SWRConfiguration } from 'swr';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
import { IProjectHealthReport } from '../../../../interfaces/project';
|
||||
import { fallbackProject } from '../useProject/fallbackProject';
|
||||
import useSort from '../../../useSort';
|
||||
import { formatApiPath } from '../../../../utils/format-path';
|
||||
import handleErrorResponses from '../httpErrorResponseHandler';
|
||||
|
||||
const useHealthReport = (id: string, options: SWRConfiguration = {}) => {
|
||||
const KEY = `api/admin/projects/${id}/health-report`;
|
||||
interface IUseHealthReportOutput {
|
||||
healthReport: IProjectHealthReport | undefined;
|
||||
refetchHealthReport: () => void;
|
||||
loading: boolean;
|
||||
error?: Error;
|
||||
}
|
||||
|
||||
const fetcher = () => {
|
||||
const path = formatApiPath(`api/admin/projects/${id}/health-report`);
|
||||
return fetch(path, {
|
||||
method: 'GET',
|
||||
})
|
||||
.then(handleErrorResponses('Health report'))
|
||||
.then(res => res.json());
|
||||
};
|
||||
export const useHealthReport = (
|
||||
projectId: string,
|
||||
options?: SWRConfiguration
|
||||
): IUseHealthReportOutput => {
|
||||
const path = formatApiPath(`api/admin/projects/${projectId}/health-report`);
|
||||
|
||||
const [sort] = useSort();
|
||||
const { data, error } = useSWR<IProjectHealthReport>(
|
||||
path,
|
||||
fetchHealthReport,
|
||||
options
|
||||
);
|
||||
|
||||
const { data, error } = useSWR<IProjectHealthReport>(KEY, fetcher, options);
|
||||
const [loading, setLoading] = useState(!error && !data);
|
||||
|
||||
const refetch = () => {
|
||||
mutate(KEY);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(!error && !data);
|
||||
}, [data, error]);
|
||||
|
||||
const sortedData = (
|
||||
data: IProjectHealthReport | undefined
|
||||
): IProjectHealthReport => {
|
||||
if (data) {
|
||||
return { ...data, features: sort(data.features || []) };
|
||||
}
|
||||
return fallbackProject;
|
||||
};
|
||||
const refetchHealthReport = useCallback(() => {
|
||||
mutate(path).catch(console.warn);
|
||||
}, [path]);
|
||||
|
||||
return {
|
||||
project: sortedData(data),
|
||||
healthReport: data,
|
||||
refetchHealthReport,
|
||||
loading: !error && !data,
|
||||
error,
|
||||
loading,
|
||||
refetch,
|
||||
};
|
||||
};
|
||||
|
||||
export default useHealthReport;
|
||||
const fetchHealthReport = (path: string): Promise<IProjectHealthReport> => {
|
||||
return fetch(path)
|
||||
.then(handleErrorResponses('Health report'))
|
||||
.then(res => res.json());
|
||||
};
|
||||
|
@ -24,7 +24,7 @@ export interface IProjectHealthReport extends IProject {
|
||||
staleCount: number;
|
||||
potentiallyStaleCount: number;
|
||||
activeCount: number;
|
||||
updatedAt: Date;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface IPermission {
|
||||
|
Loading…
Reference in New Issue
Block a user