Skip to content
Snippets Groups Projects
Commit 4175bec3 authored by Espen Boman Fosseide's avatar Espen Boman Fosseide :dart:
Browse files

Merge remote-tracking branch 'origin/master' into 8-favicon-title-etc

# Conflicts:
#	src/App.tsx
parents d8240c2f 68125653
No related branches found
No related tags found
No related merge requests found
Showing
with 235 additions and 66 deletions
import { ThemeProvider } from '@material-ui/styles';
import { useState } from 'react';
import { useEffect, useState } from 'react';
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
import './App.css';
import CommonLogic from './components/CommonLogic/index';
import NavBar from './components/NavBar';
import { OpenSettingsContext } from './helpers/context';
import { backgroundTheme } from './helpers/themes';
import { themes } from './helpers/themes';
import Home from './pages/Home/index';
import SettingsPage from './pages/SettingsPage/index';
import FeatsVsFixesPage from './pages/FeatsVsFixesPage';
import TimePerIssueLabelPage from './pages/TimePerIssueLabelPage/index';
import { Theme } from '@material-ui/core';
import { useLocalStorage } from './helpers/hooks';
function App() {
const [openSettings, setOpenSettings] = useState(false);
const [theme, setTheme] = useState<Theme>(themes.light);
const [themeName, setThemeName] = useLocalStorage<keyof typeof themes>(`theme`, 'light');
// Get the theme from localstorage
useEffect(() => {
if (themeName !== undefined) {
setTheme(themes[themeName]);
}
}, []);
return (
<div className="App">
<OpenSettingsContext.Provider value={[openSettings, setOpenSettings]}>
<ThemeProvider theme={backgroundTheme}>
<ThemeProvider theme={theme}>
<SettingsPage
open={openSettings}
onClose={() => setOpenSettings(false)}
setTheme={setTheme}
themeName={themeName}
setThemeName={setThemeName}
/>
<NavBar title="Gitlab data visualization" />
<SettingsPage open={openSettings} onClose={() => setOpenSettings(false)} />
<Router>
<CommonLogic />
<Switch>
......
......@@ -38,7 +38,7 @@ export default function NavBar(props: MenuProps) {
return (
<div>
<AppBar position="sticky" color="secondary">
<AppBar position="sticky" className={classes.appBar}>
<Toolbar className={classes.toolBar}>
<Link href={'/'} underline={'none'}>
<Typography variant="h6" component="div" className={classes.title}>
......
......@@ -9,12 +9,15 @@ const useStyles = makeStyles((theme) =>
color: theme.palette.secondary.contrastText,
},
menuItem: {
color: theme.palette.secondary.main,
color: 'black',
},
toolBar: {
display: 'flex',
justifyContent: 'space-between',
},
appBar: {
backgroundColor: theme.palette.secondary.main,
},
linkContainer: {
display: 'flex',
},
......
......@@ -9,7 +9,7 @@ type PageContainerProps = {
export default function PageContainer(props: PageContainerProps) {
const style = useStyles();
return (
<Grid container>
<Grid container className={style.background}>
<Container className={style.main} maxWidth="md">
{props.title ? <h3>{props.title}</h3> : false}
{props.children}
......
......@@ -11,6 +11,11 @@ const useStyles = makeStyles((theme) =>
borderRadius: '0 0 10px 10px',
},
},
background: {
backgroundColor: theme.palette.background.default,
width: '100%',
minHeight: '100vh',
},
}),
);
......
......@@ -2,9 +2,10 @@ import { XIcon } from '@heroicons/react/outline';
import { Dialog, DialogProps } from '@material-ui/core';
import IconButton from '@material-ui/core/IconButton';
import { useStyles } from './styles';
import { ReactNode } from 'react';
type PopupProps = {
children?: JSX.Element;
children?: ReactNode;
open: boolean;
onClose: () => void;
title: string;
......
......@@ -16,6 +16,7 @@ export const useStyles = makeStyles((theme: Theme) =>
},
dialog: {
backgroundColor: theme.palette.primary.main,
color: theme.palette.primary.contrastText,
},
}),
);
import { FormControl, FormControlLabel, FormLabel, Radio, RadioGroup } from '@material-ui/core';
import { useStyles } from './styles';
import { ChangeEvent } from 'react';
import { themes } from '../../helpers/themes';
type ThemeRadioGroupProps = {
onChange: (event: ChangeEvent<HTMLInputElement>, value: string) => void;
themeName: string;
};
/**
* A component giving the user the choice between all the current themes
*/
export default function ThemeRadioGroup(props: ThemeRadioGroupProps) {
const classes = useStyles();
return (
<FormControl component="fieldset">
<FormLabel component="legend" className={classes.legend}>
Theme
</FormLabel>
<RadioGroup onChange={props.onChange}>
{Object.keys(themes).map((themeName) => {
return (
<FormControlLabel
className={classes.radio}
key={themeName}
value={themeName}
checked={themeName === props.themeName}
label={themeName}
control={<Radio className={classes.radio} />}
/>
);
})}
</RadioGroup>
</FormControl>
);
}
import { createStyles, makeStyles, Theme } from '@material-ui/core';
export const useStyles = makeStyles((theme: Theme) =>
createStyles({
radio: {
color: theme.palette.primary.contrastText + '!important',
},
legend: {
color: theme.palette.primary.contrastText + '!important',
marginBottom: '1rem',
},
}),
);
import { Dispatch, SetStateAction, useCallback, useEffect, useState } from 'react';
import { Dispatch, SetStateAction, useEffect, useState } from 'react';
type StorageObject = typeof window.localStorage | typeof window.sessionStorage;
type PossibleValues = string | number | boolean;
// useStorage, useLocalStorage and useSessionStorage is taken from:
// https://github.com/WebDevSimplified/useful-custom-react-hooks/blob/main/src/8-useStorage/useStorage.js
/**
......@@ -11,14 +10,16 @@ type PossibleValues = string | number | boolean;
* @param storageObject Either localStorage or sessionStorage
* @returns An array of [value, setValue, remove]
*/
function useStorage<ValueType extends PossibleValues>(
function useStorage<ValueType>(
key: string,
defaultValue: ValueType,
storageObject: StorageObject,
): [ValueType | undefined, Dispatch<SetStateAction<ValueType | undefined>>, () => void] {
const [value, setValue] = useState<ValueType | undefined>(() => {
const jsonValue = storageObject.getItem(key);
if (jsonValue != null) return JSON.parse(jsonValue);
): [ValueType, Dispatch<SetStateAction<ValueType>>] {
const [value, setValue] = useState<ValueType>(() => {
const value = storageObject.getItem(key);
if (value != null) {
return JSON.parse(value);
}
return defaultValue;
});
......@@ -27,11 +28,7 @@ function useStorage<ValueType extends PossibleValues>(
storageObject.setItem(key, JSON.stringify(value));
}, [key, value, storageObject]);
const remove = useCallback(() => {
setValue(undefined);
}, []);
return [value, setValue, remove];
return [value, setValue];
}
/**
......@@ -40,10 +37,7 @@ function useStorage<ValueType extends PossibleValues>(
* @param defaultValue The default value of the item in localStorage.
* @returns An array of [value, setValue, remove]
*/
export function useLocalStorage<ValueType extends PossibleValues>(
key: string,
defaultValue: ValueType,
) {
export function useLocalStorage<ValueType>(key: string, defaultValue: ValueType) {
return useStorage(key, defaultValue, window.localStorage);
}
......@@ -53,9 +47,6 @@ export function useLocalStorage<ValueType extends PossibleValues>(
* @param defaultValue The default value of the item in sessionStorage.
* @returns An array of [value, setValue, remove]
*/
export function useSessionStorage<ValueType extends PossibleValues>(
key: string,
defaultValue: ValueType,
) {
export function useSessionStorage<ValueType>(key: string, defaultValue: ValueType) {
return useStorage(key, defaultValue, window.sessionStorage);
}
import { createTheme } from '@material-ui/core';
export const backgroundTheme = createTheme({
const lightTheme = createTheme(
{
palette: {
primary: {
main: '#f1f1e9', // Temporary secondary theme
main: '#f8f6f3',
contrastText: '#1b1f28',
},
secondary: {
main: '#342f32', // Temporary primary theme
main: '#1f1f1e',
contrastText: '#dedede',
},
info: {
main: '#f1f6f6', // Temporary info theme
main: '#f1f6f6',
contrastText: '#3d393e',
},
background: {
default: '#f4fffe',
},
});
},
},
{ name: 'light' },
);
const darkTheme = createTheme(
{
palette: {
primary: {
main: '#0c0c0c',
contrastText: '#d4daec',
},
secondary: {
main: '#222020',
contrastText: '#d7cdcd',
},
info: {
main: '#323737',
contrastText: '#afa7b1',
},
background: {
default: '#070606',
},
},
},
{ name: 'dark' },
);
const funkTheme = createTheme(
{
palette: {
primary: {
main: '#a70f0f',
contrastText: '#022c53',
},
secondary: {
main: '#316d72',
contrastText: '#31ff0d',
},
info: {
main: '#941681',
contrastText: '#e9cb14',
},
background: {
default: '#03801f',
},
},
},
{ name: 'dark' },
);
export const themes = { light: lightTheme, dark: darkTheme, wayTooManyColors: funkTheme };
import PageContainer from '../../components/PageContainer/index';
import { Checkbox } from '@material-ui/core';
import { useEffect, useState } from 'react';
import ChartPie from '../../components/ChartPie';
import PageContainer from '../../components/PageContainer/index';
import { getAllCommitsFromAPI } from '../../helpers/api-calls';
import { useEffect, useState } from 'react';
import { useSessionStorage } from '../../helpers/hooks';
import { CommitAuthor } from '../../helpers/types';
import { Checkbox } from '@material-ui/core';
import { parseCommitData } from './utils';
import { useStyles } from './styles';
export default function FeatsVsFixesPage() {
// The retrieved athor data form the api.
const [authorData, setAuthorData] = useState<CommitAuthor[]>([]);
// The selected showing authors (default all selected), settings are saved in sessions.
const [selectedAuthors, setSelectedAuthors] = useSessionStorage<boolean[]>(
'selectedAuthorsFeatsVsFixes',
new Array(authorData.length).fill(true),
);
const classes = useStyles();
const featsFixesGraphData: Array<{ commitType: string; val: number }> = [
{ commitType: 'feat', val: 0 },
{ commitType: 'fix', val: 0 },
......@@ -20,7 +29,7 @@ export default function FeatsVsFixesPage() {
];
for (let i = 0; i < authorData.length; i++) {
if (authorData[i].active) {
if (selectedAuthors[i]) {
featsFixesGraphData[0].val += authorData[i].feats;
featsFixesGraphData[1].val += authorData[i].fixes;
additionsDeletionsGraphData[0].val += authorData[i].additions;
......@@ -44,14 +53,16 @@ export default function FeatsVsFixesPage() {
{authorData.map((m, i) => {
if (m.feats || m.fixes) {
return (
<div key={i}>
<div key={JSON.stringify(m)}>
Person {i + 1}
<Checkbox
checked={m.active}
className={classes.checkbox}
checked={selectedAuthors[i]}
onChange={() => {
const temp_list = [...authorData];
temp_list[i].active = !temp_list[i].active;
setAuthorData(temp_list);
if (!selectedAuthors) return; // selectedAuthors will never be undefined
const tempList = [...selectedAuthors];
tempList[i] = !tempList[i];
setSelectedAuthors(tempList);
}}
/>
</div>
......
import { createStyles, makeStyles, Theme } from '@material-ui/core';
export const useStyles = makeStyles((theme: Theme) =>
createStyles({
checkbox: {
color: theme.palette.primary.contrastText + '!important',
},
}),
);
import Popup from '../../components/Popup';
import { Theme } from '@material-ui/core';
import { themes } from '../../helpers/themes';
import { ChangeEvent } from 'react';
import ThemeRadioGroup from '../../components/ThemeRadioGroup';
type SettingsPageProps = {
open: boolean;
onClose: () => void;
setTheme: (t: Theme) => void;
themeName: string;
setThemeName: (theme: keyof typeof themes) => void;
};
export default function SettingsPage(props: SettingsPageProps) {
function changeTheme(event: ChangeEvent<HTMLInputElement>, value: string) {
const theme = value as keyof typeof themes;
props.setTheme(themes[theme]);
props.setThemeName(theme);
}
return (
<Popup title="Settings" open={props.open} onClose={props.onClose} maxWidth="sm">
<div>Here comes the settings page</div>
<ThemeRadioGroup onChange={changeTheme} themeName={props.themeName} />
</Popup>
);
}
......@@ -3,25 +3,30 @@ import { useEffect, useState } from 'react';
import ChartBar from '../../components/ChartBar/index';
import PageContainer from '../../components/PageContainer';
import { getIssuesFromAPI } from '../../helpers/api-calls';
import {
difficultyLabels,
Label,
otherLabels,
techDescriptionLabels,
} from '../../helpers/constants';
import { difficultyLabels, otherLabels, techDescriptionLabels } from '../../helpers/constants';
import { useSessionStorage } from '../../helpers/hooks';
import { BarDataItem, Issue } from '../../helpers/types';
import useStyles from './styles';
import { avgTimePerIssueLabel } from './utils';
const difficultyLabelsString = JSON.stringify(difficultyLabels);
const techLabelsString = JSON.stringify(techDescriptionLabels);
const otherLabelsString = JSON.stringify(otherLabels);
export default function TimePerIssueLabelPage() {
const [allIssueData, setAllIssueData] = useState<Issue[] | null>(null);
const [data, setData] = useState<BarDataItem[] | null>(null);
const [selected, setSelected] = useState<Label[]>(difficultyLabels);
// The selected issueLabel groups are saved in each session.
const [selected, setSelected] = useSessionStorage<string>(
'issueLabelsSelected',
difficultyLabelsString,
);
const classes = useStyles();
// On page load, get the issues from the API and load it into a state.
useEffect(() => {
getIssuesFromAPI().then((res) => {
// Since console.error makes sense here.
// eslint-disable-next-line
if (!res.ok) return console.error(res.status, res.data);
setAllIssueData(res.data);
......@@ -32,10 +37,10 @@ export default function TimePerIssueLabelPage() {
useEffect(() => {
// If allIssueData or selected is null, something is not selected and or the data is not
if (allIssueData == null) return;
if (selected == null) return;
// Find the average time used to close an issue with one of the selected labels
const dataForRelevantIssues = avgTimePerIssueLabel(allIssueData, selected);
const selectedArray = JSON.parse(selected);
const dataForRelevantIssues = avgTimePerIssueLabel(allIssueData, selectedArray);
setData(dataForRelevantIssues);
}, [selected, allIssueData]);
......@@ -47,23 +52,24 @@ export default function TimePerIssueLabelPage() {
</p>
<div>
<FormControl className={classes.dropdown}>
<InputLabel id="demo-simple-select-label">Showing labels</InputLabel>
<InputLabel>Showing labels</InputLabel>
<Select
labelId="demo-simple-select-label"
id="demo-simple-select"
className={classes.select}
value={selected}
onChange={(e) => {
const newValue = e.target.value as Label[];
const newValue = e.target.value as string;
setSelected(newValue);
}}
>
<MenuItem key={JSON.stringify(difficultyLabels)} value={difficultyLabels}>
<MenuItem key={difficultyLabelsString} value={difficultyLabelsString}>
<div>Difficulty</div>
</MenuItem>
<MenuItem key={JSON.stringify(techDescriptionLabels)} value={techDescriptionLabels}>
<MenuItem key={techLabelsString} value={techLabelsString}>
<div>Tech Description</div>
</MenuItem>
<MenuItem key={JSON.stringify(otherLabels)} value={otherLabels}>
<MenuItem key={otherLabelsString} value={otherLabelsString}>
<div>Other</div>
</MenuItem>
</Select>
......
import { createStyles, makeStyles } from '@material-ui/core';
import { createStyles, makeStyles, Theme } from '@material-ui/core';
const useStyles = makeStyles(() =>
const useStyles = makeStyles((theme: Theme) =>
createStyles({
dropdown: {
width: '10em',
'& label': {
color: theme.palette.primary.contrastText + '!important',
},
},
select: {
color: theme.palette.info.contrastText + '!important',
backgroundColor: theme.palette.info.main,
paddingLeft: '1rem',
},
}),
);
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment