Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add upload modal #38

Open
wants to merge 16 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
10 changes: 10 additions & 0 deletions api/api.js
@@ -0,0 +1,10 @@
// @flow
import apiDev from './apiDev';
import apiServer from './apiServer';
import type { Api } from './apiType';

const isProd = true;

const api: Api = isProd ? apiServer : apiDev

export default api;
34 changes: 34 additions & 0 deletions api/apiDev.js
@@ -0,0 +1,34 @@
// @flow
import type { Api, Files } from './apiType';
import { tracks, mockRoleToLevel } from '../constants'

const wait = () => {
return new Promise((resolve, reject) => {
setTimeout(() => resolve(), 500);
})
}

const apiDev: Api = {
async submitFiles(files: Files) {
// does some stuff
await wait();
return;
},
async getMasterConfig(department: string) {
return {
rating: tracks,
role: mockRoleToLevel
}
},
async fetchUsers() {
return []
},
async fetchUser() {
return
},
async saveUser() {
return
}
}

export default apiDev;
101 changes: 101 additions & 0 deletions api/apiServer.js
@@ -0,0 +1,101 @@
// @flow
import type { Api, Files, MasterConfig } from './apiType';
import type { MilestoneMap } from '../constants'
import { tracks } from '../constants'
import { UserData } from '../models/UserData'
import _ from 'lodash';


const apiServer: Api = {
submitFiles(files: Files) {
let formdata = new FormData();
_.forEach(files, (file, key) => {
formdata.append(key, file);
});
formdata.append("department", "ENGINEERING");
// update endpoint
return fetch("http://localhost:8080/api/performancematrix/config", {
body: formdata,
method: 'POST',
}).then(response => undefined)
},
getMasterConfig(dept: string) {
return fetch(`http://localhost:8080/api/performancematrix/config/${dept}`)
// stubbed
.then(res => res.json())
.then(data => {
return sanitizeData(data);
})
},
fetchUsers() {
return fetch(`http://localhost:8080/users`)
.then(res => res.json())
.then(users => users.map(user => user.email))
},
fetchUser(user: string) {
return fetch(`http://localhost:8080/api/performancematrix/fetchuser/${user}`)
.then(res => res.json())
.then(data => {
if (_.isEmpty(data)) {
return {};
}

return {
...data,
ratings: parseRating(data.ratings)
}
})
},
saveUser(ratings: MilestoneMap, currentRole: string, username: string) {
const formData = new FormData();
formData.append("userId", username);
formData.append("curRole", currentRole);
formData.append("ratings", JSON.stringify(ratings));

return fetch(`http://localhost:8080/api/performancematrix/saveuser`, {
method: 'POST',
header: {'Content-Type': 'application/json' },
body: formData
})
.then()
}
};

const parseRating = (ratings: string) => {
const jsonRating = JSON.parse(ratings);
return _.mapValues(jsonRating, strVal => +strVal)
}



// Not sure what this column is, but we don't want it
const BLACKLIST = ['Ratings']

const sortByCategory = (ratings) => {
return _(ratings)
.groupBy(rating => rating.category)
.reduce((memo, group) => {
group.forEach(entry => {
memo[entry.displayName] = entry;
})
return memo;
}, {})
}

const sanitizeData = ({ rating, role }: MasterConfig) => {
// sometimes the category milestones doesn't have any data - so we filter these out
const newRating = _.chain(rating)
.mapValues(category => {
return {
...category,
milestones: category.milestones.filter(milestone => milestone.summary),
}
})
.pickBy((category, categoryName) => category.milestones.length > 0 && !BLACKLIST.includes(categoryName))
.value();
return { rating: sortByCategory(newRating), role }
}

export default apiServer;


22 changes: 22 additions & 0 deletions api/apiType.js
@@ -0,0 +1,22 @@
// @flow
import type { Tracks, RoleToLevel } from '../constants'
import type { UserData } from '../models/UserData'
import type { MilestoneMap } from '../constants'

export type Files = {
[fileName: string]: File
}

export type MasterConfig = {
role: RoleToLevel,
rating: Tracks
}

export type Api = {
submitFiles: (files: File) => Promise<void>,
getMasterConfig: (dept: string) => Promise<MasterConfig>,
fetchUsers: () => Promise<Array<string>>,
fetchUser: (username: string) => Promise<UserData>,
saveUser: (ratings: MilestoneMap, currentRole: string, username: string) => Promise<void>
}

43 changes: 43 additions & 0 deletions components/DepartmentDropDown.js
@@ -0,0 +1,43 @@
// @flow
import * as React from 'react';
import glamorous, { Div } from 'glamorous';
import { departments } from './../constants';


type DropDownStates = {
value: string,
}

class DepartmentDropDown extends React.Component<{}, DropDownStates> {
constructor() {
super()

this.state = {
value: "ENGINEERING",
}

}

handleChange = (e: any) => {
e.preventDefault();
this.setState({value: e.target.value})
}

render() {
const departmentOptions = departments.map((department, index) => {
return (
<option value={department} key={index}>{department}</option>
)
});
return (
<Div marginBottom="25px">
<select value={this.state.value} onChange={this.handleChange}>
{departmentOptions}
</select>
</Div>
)
}

}

export default DepartmentDropDown
42 changes: 42 additions & 0 deletions components/FileUploadButton.js
@@ -0,0 +1,42 @@
// @flow
import * as React from 'react';
import glamorous from 'glamorous';
import { teal, teal2 } from '../palette'

const HiddenFileInput = glamorous.input({
width: '100%',
height: '100%',
opacity: 0,
overflow: 'hidden',
position: 'absolute',
left: 0,
top: 0
})

const UploadLabel = glamorous.button({
position: 'relative',
backgroundColor: teal,
padding: '10px 20px',
borderRadius: '5px',
fontWeight: 600,
':hover': {
backgroundColor: teal2
},
transition: 'background-color 0.25s',
margin: '0 auto',
textAlign: 'center',
width: '80px'
})

type FileUploadButtonProps = {
onChange: () => void
}

const FileUploadButton = ({ onChange }: FileUploadButtonProps) => (
<UploadLabel>
Upload
<HiddenFileInput type="file" onChange={onChange}/>
</UploadLabel>
)

export default FileUploadButton;
46 changes: 34 additions & 12 deletions components/NightingaleChart.js
@@ -1,27 +1,38 @@
// @flow

import { reduce } from 'lodash'
import React from 'react'
import * as d3 from 'd3'
import { trackIds, milestones, tracks, categoryColorScale } from '../constants'
import { milestones, categoryColorScale } from '../constants'
import type { Tracks } from '../constants'
import type { TrackId, Milestone, MilestoneMap } from '../constants'
import { gray2 } from '../palette'

const width = 400
const arcMilestones = milestones.slice(1) // we'll draw the '0' milestone with a circle, not an arc.

type Props = {
label: string,
tracks: Tracks,
milestoneByTrack: MilestoneMap,
focusedTrackId: TrackId,
handleTrackMilestoneChangeFn: (TrackId, Milestone) => void
focusedTrackId?: TrackId,
handleTrackMilestoneChangeFn: (TrackId, Milestone) => void,
trackIds: Array<string>
}

let arcMilestones = [];
class NightingaleChart extends React.Component<Props> {
colorScale: any
radiusScale: any
arcFn: any

constructor(props: *) {
constructor(props: Props) {
super(props)

const maxNumMilestones = reduce(props.tracks, (max, category) => {
return Math.max(max, category.milestones.length)
}, 0)
arcMilestones = [...new Array(maxNumMilestones)].map((_, i) => i)

this.colorScale = d3.scaleSequential(d3.interpolateWarm)
.domain([0, 5])

Expand All @@ -33,20 +44,22 @@ class NightingaleChart extends React.Component<Props> {
this.arcFn = d3.arc()
.innerRadius(milestone => this.radiusScale(milestone))
.outerRadius(milestone => this.radiusScale(milestone) + this.radiusScale.bandwidth())
.startAngle(- Math.PI / trackIds.length)
.endAngle(Math.PI / trackIds.length)
.startAngle(- Math.PI / this.props.trackIds.length)
.endAngle(Math.PI / this.props.trackIds.length)
.padAngle(Math.PI / 200)
.padRadius(.45 * width)
.cornerRadius(2)
}

render() {
const currentMilestoneId = this.props.milestoneByTrack[this.props.focusedTrackId]
const { label, trackIds, tracks } = this.props;
const currentMilestoneId = this.props.focusedTrackId && this.props.milestoneByTrack[this.props.focusedTrackId]
return (
<figure>
<style jsx>{`
figure {
margin: 0;
padding: 7.5px;
}
svg {
width: ${width}px;
Expand All @@ -56,25 +69,34 @@ class NightingaleChart extends React.Component<Props> {
fill: #eee;
cursor: pointer;
}
.track-milestone-disabled {
fill: ${gray2};
}
.track-milestone-current, .track-milestone:hover {
stroke: #000;
stroke-width: 4px;
stroke-linejoin: round;
}
`}</style>
<h2>{label}</h2>
<svg>
<g transform={`translate(${width/2},${width/2}) rotate(-33.75)`}>
{trackIds.map((trackId, i) => {
const isCurrentTrack = trackId == this.props.focusedTrackId
const numMilestones = tracks[trackId].milestones.length;

return (
<g key={trackId} transform={`rotate(${i * 360 / trackIds.length})`}>
{arcMilestones.map((milestone) => {
{arcMilestones.map((milestone, i) => {
const outOfRange = i > numMilestones - 1;

const isCurrentMilestone = isCurrentTrack && milestone == currentMilestoneId
const isMet = this.props.milestoneByTrack[trackId] >= milestone || milestone == 0
const isMet = this.props.milestoneByTrack[trackId] >= milestone + 1 //|| milestone == 0

return (
<path
key={milestone}
className={'track-milestone ' + (isMet ? 'is-met ' : ' ') + (isCurrentMilestone ? 'track-milestone-current' : '')}
className={outOfRange ? 'track-milestone-disabled' : 'track-milestone ' + (isMet ? 'is-met ' : ' ')}
onClick={() => this.props.handleTrackMilestoneChangeFn(trackId, milestone)}
d={this.arcFn(milestone)}
style={{fill: isMet ? categoryColorScale(tracks[trackId].category) : undefined}} />
Expand All @@ -85,7 +107,7 @@ class NightingaleChart extends React.Component<Props> {
cx="0"
cy="-50"
style={{fill: categoryColorScale(tracks[trackId].category)}}
className={"track-milestone " + (isCurrentTrack && !currentMilestoneId ? "track-milestone-current" : "")}
className={"track-milestone " + (isCurrentTrack ? "track-milestone-current" : "")}
onClick={() => this.props.handleTrackMilestoneChangeFn(trackId, 0)} />
</g>
)})}
Expand Down