1
0
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:
olav 2022-02-07 15:30:33 +01:00 committed by GitHub
parent 72acf2309c
commit 234bab6cb4
9 changed files with 60 additions and 409 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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());
};

View File

@ -24,7 +24,7 @@ export interface IProjectHealthReport extends IProject {
staleCount: number;
potentiallyStaleCount: number;
activeCount: number;
updatedAt: Date;
updatedAt: string;
}
export interface IPermission {