mirror of
https://github.com/nickzuber/meteorite.git
synced 2024-09-11 11:36:03 +03:00
Restructure a bit and sidebar
This commit is contained in:
parent
dd9462afbc
commit
733d0f96a6
@ -41,7 +41,7 @@ if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) {
|
||||
}
|
||||
|
||||
// Tools like Cloud9 rely on this.
|
||||
const DEFAULT_PORT = parseInt(process.env.PORT, 10) || 9009;
|
||||
const DEFAULT_PORT = parseInt(process.env.PORT, 10) || 9008;
|
||||
const HOST = process.env.HOST || '0.0.0.0';
|
||||
|
||||
if (process.env.HOST) {
|
||||
|
@ -11,6 +11,7 @@ import bookmarkAltWhite from './svg/bookmark-alt-white.svg';
|
||||
import bookmark from './svg/bookmark.svg';
|
||||
import bookmarks from './svg/bookmarks.svg';
|
||||
import check from './svg/check.svg';
|
||||
import clock from './svg/clock.svg';
|
||||
import convo from './svg/convo.svg';
|
||||
import doneAll from './svg/done-all.svg';
|
||||
import done from './svg/done.svg';
|
||||
@ -70,6 +71,7 @@ Icon.BookmarkAltWhite = createIcon(bookmarkAltWhite);
|
||||
Icon.Bookmark = createIcon(bookmark);
|
||||
Icon.Bookmarks = createIcon(bookmarks);
|
||||
Icon.Check = createIcon(check);
|
||||
Icon.Clock = createIcon(clock);
|
||||
Icon.Convo = createIcon(convo);
|
||||
Icon.DoneAll = createIcon(doneAll);
|
||||
Icon.Done = createIcon(done);
|
||||
|
1
src/components/Icon/svg/clock.svg
Normal file
1
src/components/Icon/svg/clock.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M11.99 2C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zM12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z"/><path d="M0 0h24v24H0z" fill="none"/><path d="M12.5 7H11v6l5.25 3.15.75-1.23-4.5-2.67z"/></svg>
|
After Width: | Height: | Size: 333 B |
@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
|
||||
const CLIENT_ID = '9478c90e57ef3d546ef0';
|
||||
const REDIRECT_URI = 'http://localhost:9009/login';
|
||||
const REDIRECT_URI = 'http://localhost:9008/login';
|
||||
const SCOPES = 'notifications';
|
||||
|
||||
const AuthenticationButton = props => (
|
||||
|
@ -79,10 +79,8 @@ const GeneralOptionsContainer = styled(NavigationContainer)({
|
||||
zIndex: '1',
|
||||
height: 'initial',
|
||||
minHeight: 60,
|
||||
width: '100%',
|
||||
width: '95%',
|
||||
margin: 0,
|
||||
marginLeft: 230,
|
||||
maxWidth: 1000,
|
||||
background: '#fff',
|
||||
padding: '8px 16px',
|
||||
paddingTop: 18,
|
||||
@ -94,8 +92,9 @@ const GeneralOptionsContainer = styled(NavigationContainer)({
|
||||
});
|
||||
|
||||
const Sidebar = styled('div')({
|
||||
flex: '0 0 200px',
|
||||
padding: '0 20px 20px',
|
||||
flex: '0 0 300px',
|
||||
padding: '32px 20px',
|
||||
paddingRight: 0,
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
});
|
||||
@ -110,6 +109,7 @@ const SidebarLink = styled('a')({}, ({active, color}) => ({
|
||||
alignItems: 'center',
|
||||
padding: '0 14px',
|
||||
height: 40,
|
||||
width: 200,
|
||||
fontSize: '12px',
|
||||
fontWeight: 600,
|
||||
letterSpacing: 0.5,
|
||||
@ -340,7 +340,14 @@ const Repository = styled('span')({
|
||||
|
||||
const PRIssue = styled(Repository)({
|
||||
fontWeight: 400,
|
||||
});
|
||||
}, ({after}) => ({
|
||||
':after': {
|
||||
content: `"#${after}"`,
|
||||
fontSize: 13,
|
||||
opacity: .3,
|
||||
marginLeft: 5
|
||||
}
|
||||
}));
|
||||
|
||||
const Table = styled('table')({
|
||||
width: '100%',
|
||||
@ -386,6 +393,7 @@ export default function Scene ({
|
||||
query,
|
||||
activeStatus,
|
||||
allNotificationsCount,
|
||||
stagedTodayCount,
|
||||
onChangePage,
|
||||
onSetActiveStatus,
|
||||
onClearQuery,
|
||||
@ -418,10 +426,9 @@ export default function Scene ({
|
||||
<div style={{marginTop: 60}}>
|
||||
<NavigationContainer>
|
||||
<div style={{
|
||||
maxWidth: 1200,
|
||||
textAlign: 'right',
|
||||
margin: '0 auto',
|
||||
padding: '0 20px 0 40px',
|
||||
width: '92%'
|
||||
}}>
|
||||
<Logo
|
||||
size={36}
|
||||
@ -465,6 +472,85 @@ export default function Scene ({
|
||||
</div>
|
||||
</div>
|
||||
</NavigationContainer>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'row'
|
||||
}}>
|
||||
<div style={{
|
||||
flex: '0 0 300px'
|
||||
}}>
|
||||
<Sidebar>
|
||||
<FixedContainer>
|
||||
<div style={{
|
||||
width: 220,
|
||||
padding: '0 14px',
|
||||
margin: '0 11px 12px',
|
||||
}}>
|
||||
<h3 style={{
|
||||
margin: 0
|
||||
}}>
|
||||
<Icon.Clock style={{
|
||||
display: 'inline-block',
|
||||
verticalAlign: 'middle',
|
||||
marginRight: '5px',
|
||||
top: '-3px',
|
||||
}} />
|
||||
{moment().format('h:mma')}
|
||||
</h3>
|
||||
<span style={{
|
||||
display: 'block',
|
||||
padding: '6px 0px',
|
||||
fontSize: 15,
|
||||
opacity: 0.7,
|
||||
}}>{moment().format('dddd, MMMM Do')}</span>
|
||||
<span style={{
|
||||
display: 'block',
|
||||
padding: '6px 0 12px',
|
||||
fontSize: 12,
|
||||
opacity: 0.5,
|
||||
}}>You've triaged {stagedTodayCount} notifications today</span>
|
||||
</div>
|
||||
<SidebarLink
|
||||
active={activeFilter === Filters.ALL}
|
||||
color="#6772e5"
|
||||
onClick={() => onSetActiveFilter(Filters.ALL)}>
|
||||
{activeFilter === Filters.ALL ? (
|
||||
<Icon.InboxWhite shrink={.6} />
|
||||
) : (
|
||||
<Icon.Inbox shrink={.6} />
|
||||
)}
|
||||
all notifications
|
||||
</SidebarLink>
|
||||
<SidebarLink
|
||||
active={activeFilter === Filters.PARTICIPATING}
|
||||
color="#00d19a"
|
||||
onClick={() => onSetActiveFilter(Filters.PARTICIPATING)}>
|
||||
{activeFilter === Filters.PARTICIPATING ? (
|
||||
<Icon.PeopleWhite shrink={.6} />
|
||||
) : (
|
||||
<Icon.People shrink={.6} />
|
||||
)}
|
||||
{/* participating */}
|
||||
your triage
|
||||
</SidebarLink>
|
||||
<SidebarLink
|
||||
active={activeFilter === Filters.COMMENT}
|
||||
color="#00A0F5"
|
||||
onClick={() => onSetActiveFilter(Filters.COMMENT)}>
|
||||
{activeFilter === Filters.COMMENT ? (
|
||||
<Icon.BookmarkAltWhite shrink={.6} />
|
||||
) : (
|
||||
<Icon.BookmarkAlt shrink={.6} />
|
||||
)}
|
||||
commented
|
||||
</SidebarLink>
|
||||
{/* <p>3 triaged in robin-dashboard</p> */}
|
||||
</FixedContainer>
|
||||
</Sidebar>
|
||||
</div>
|
||||
<div style={{
|
||||
flex: 1
|
||||
}}>
|
||||
<GeneralOptionsContainer>
|
||||
<Tab disabled={isLoading}>
|
||||
<Icon.Refresh
|
||||
@ -556,43 +642,6 @@ export default function Scene ({
|
||||
</NavTab>
|
||||
</GeneralOptionsContainer>
|
||||
<NotificationsContainer>
|
||||
<Sidebar>
|
||||
<FixedContainer>
|
||||
<SidebarLink
|
||||
active={activeFilter === Filters.ALL}
|
||||
color="#6772e5"
|
||||
onClick={() => onSetActiveFilter(Filters.ALL)}>
|
||||
{activeFilter === Filters.ALL ? (
|
||||
<Icon.InboxWhite shrink={.6} />
|
||||
) : (
|
||||
<Icon.Inbox shrink={.6} />
|
||||
)}
|
||||
all notifications
|
||||
</SidebarLink>
|
||||
<SidebarLink
|
||||
active={activeFilter === Filters.PARTICIPATING}
|
||||
color="#00d19a"
|
||||
onClick={() => onSetActiveFilter(Filters.PARTICIPATING)}>
|
||||
{activeFilter === Filters.PARTICIPATING ? (
|
||||
<Icon.PeopleWhite shrink={.6} />
|
||||
) : (
|
||||
<Icon.People shrink={.6} />
|
||||
)}
|
||||
participating
|
||||
</SidebarLink>
|
||||
<SidebarLink
|
||||
active={activeFilter === Filters.COMMENT}
|
||||
color="#00A0F5"
|
||||
onClick={() => onSetActiveFilter(Filters.COMMENT)}>
|
||||
{activeFilter === Filters.COMMENT ? (
|
||||
<Icon.BookmarkAltWhite shrink={.6} />
|
||||
) : (
|
||||
<Icon.BookmarkAlt shrink={.6} />
|
||||
)}
|
||||
commented
|
||||
</SidebarLink>
|
||||
</FixedContainer>
|
||||
</Sidebar>
|
||||
<Notifications>
|
||||
{isFetchingNotifications ? (
|
||||
<LoaderContainer>
|
||||
@ -627,7 +676,7 @@ export default function Scene ({
|
||||
onStageThread(n.id)
|
||||
}}>
|
||||
<NotificationTitle>
|
||||
<PRIssue>{n.name}</PRIssue>
|
||||
<PRIssue after={n.number}>{n.name}</PRIssue>
|
||||
</NotificationTitle>
|
||||
<Timestamp>{getRelativeTime(n.updated_at)}</Timestamp>
|
||||
</TableItem>
|
||||
@ -663,7 +712,7 @@ export default function Scene ({
|
||||
<NotificationTab>
|
||||
<Icon.Check
|
||||
opacity={0.9}
|
||||
onClick={!isLoading ? (() => onStageThread(n.id)) : undefined}
|
||||
onClick={!isLoading ? (() => onStageThread(n.id, n.repository)) : undefined}
|
||||
/>
|
||||
</NotificationTab>
|
||||
<NotificationTab>
|
||||
@ -681,5 +730,7 @@ export default function Scene ({
|
||||
</Notifications>
|
||||
</NotificationsContainer>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -88,7 +88,7 @@ function badgesOf (notification) {
|
||||
// If you've been tagged in for review and the most recent update happened over
|
||||
// 4 hours ago, that specific time is subject to change.
|
||||
if (notification.reasons.some(r => r.reason === Reasons.REVIEW_REQUESTED) &&
|
||||
moment().diff(moment(notification.reasons.pop().time).hours, 'hours') > 4) {
|
||||
moment().diff(moment(notification.reasons[notification.reasons.length - 1].time).hours, 'hours') > 4) {
|
||||
badges.push(Badges.OLD);
|
||||
}
|
||||
return badges;
|
||||
@ -169,6 +169,13 @@ class NotificationsPage extends React.Component {
|
||||
}, 500);
|
||||
}
|
||||
|
||||
enhancedOnStageThread = (thread_id, repository) => {
|
||||
console.warn('staging thread', thread_id, 'in repo', repository);
|
||||
this.props.storageApi.incrStat('stagedCount');
|
||||
this.props.storageApi.incrStat(repository + '-stagedCount');
|
||||
this.props.notificationsApi.stageThread(thread_id);
|
||||
}
|
||||
|
||||
render () {
|
||||
if (!this.props.authApi.token) {
|
||||
return <Redirect noThrow to={routes.LOGIN} />
|
||||
@ -176,7 +183,6 @@ class NotificationsPage extends React.Component {
|
||||
|
||||
const {
|
||||
fetchNotifications,
|
||||
stageThread,
|
||||
markAsRead,
|
||||
clearCache,
|
||||
notifications,
|
||||
@ -207,7 +213,6 @@ class NotificationsPage extends React.Component {
|
||||
}
|
||||
|
||||
const filteredNotifications = notifications.filter(filterMethod);
|
||||
const allNotificationsCount = filteredNotifications.length;
|
||||
|
||||
const notificationsQueued = filteredNotifications.filter(n => n.status === Status.QUEUED);
|
||||
const notificationsStaged = filteredNotifications.filter(n => n.status === Status.STAGED);
|
||||
@ -246,12 +251,15 @@ class NotificationsPage extends React.Component {
|
||||
lastNumbered = 0;
|
||||
}
|
||||
|
||||
const stagedTodayCount = this.props.storageApi.getStat('stagedCount')[0];
|
||||
|
||||
return (
|
||||
<Scene
|
||||
stagedTodayCount={stagedTodayCount || 0}
|
||||
first={firstNumbered}
|
||||
last={lastNumbered}
|
||||
lastPage={lastPage}
|
||||
allNotificationsCount={allNotificationsCount}
|
||||
allNotificationsCount={scoredAndSortedNotifications.length}
|
||||
notifications={notificationsOnPage}
|
||||
query={this.state.query}
|
||||
page={this.state.currentPage}
|
||||
@ -265,7 +273,7 @@ class NotificationsPage extends React.Component {
|
||||
onFetchNotifications={fetchNotifications}
|
||||
onMarkAsRead={markAsRead}
|
||||
onClearCache={clearCache}
|
||||
onStageThread={stageThread}
|
||||
onStageThread={this.enhancedOnStageThread}
|
||||
onRefreshNotifications={this.props.storageApi.refreshNotifications}
|
||||
isSearching={this.state.isSearching}
|
||||
isFetchingNotifications={isFetchingNotifications}
|
||||
|
@ -1,7 +1,6 @@
|
||||
import React from 'react';
|
||||
import {AuthConsumer} from './Auth';
|
||||
import {StorageProvider} from './Storage';
|
||||
import {MockNotifications} from '../utils/mocks';
|
||||
import {Status} from '../constants/status';
|
||||
|
||||
const BASE_GITHUB_API_URL = 'https://api.github.com';
|
||||
@ -107,16 +106,6 @@ class NotificationsProvider extends React.Component {
|
||||
});
|
||||
}
|
||||
|
||||
// @TODO remove this mock when ready
|
||||
mockRequestPage = page => {
|
||||
return new Promise((resolve, reject) => {
|
||||
setTimeout(() => resolve({
|
||||
headers: {},
|
||||
json: MockNotifications
|
||||
}), 1000)
|
||||
});
|
||||
}
|
||||
|
||||
requestFetchNotifications = (page = 1, optimizePolling = true) => {
|
||||
return this.requestPage(page, optimizePolling)
|
||||
.then(({headers, json}) => {
|
||||
|
@ -4,6 +4,7 @@ import {Status} from '../constants/status';
|
||||
import {Reasons} from '../constants/reasons';
|
||||
|
||||
const LOCAL_STORAGE_PREFIX = '__meteorite_noti_cache__';
|
||||
const LOCAL_STORAGE_STATISTIC_PREFIX = '__meteorite_statistic_cache__';
|
||||
|
||||
const getMockReasons = n => {
|
||||
const reasons = Object.values(Reasons);
|
||||
@ -58,11 +59,63 @@ class StorageProvider extends React.Component {
|
||||
// this.setState({ notifications: mockNotifications });
|
||||
}
|
||||
|
||||
/**
|
||||
* Stats are broken up since they are fetched and set often, we want to avoid
|
||||
* the JSON parsing overhead.
|
||||
*
|
||||
* Our statistics are indexed by the date (current unique day) we had recorded it.
|
||||
* This _does_ limit us by making this choice - the most granular we can get with
|
||||
* statistics is by day. This is an intentional design decision and we could always
|
||||
* change it later at some point if we really wanted to.
|
||||
*
|
||||
* Statistics take the form __DATE-NAME. For example:
|
||||
* ```
|
||||
* __meteorite_noti_cache__2018-11-05-robin-extension-staged -> 52
|
||||
* __meteorite_noti_cache__2018-11-06-robin-dashboard-staged -> 4
|
||||
* ```
|
||||
*/
|
||||
getStat = (stat, startTime = moment(), endTime = moment().add(1, 'day')) => {
|
||||
const response = [];
|
||||
|
||||
// Range reflects `[start, end)`
|
||||
for (let m = startTime.clone(); m.isBefore(endTime); m.add(1, 'day')) {
|
||||
const key = m.format('YYYY-MM-DD');
|
||||
const value = window.localStorage.getItem(`${LOCAL_STORAGE_STATISTIC_PREFIX}${key}-${stat}`);
|
||||
if (value !== null) {
|
||||
response.push(value);
|
||||
}
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Since our stats right now are just numbers, we can assume "setting" will always
|
||||
* increment. This is a pretty bold assumption that makes things simpler for now,
|
||||
* so we're going to go with it for the time being.
|
||||
*/
|
||||
incrStat = (stat, time = moment()) => {
|
||||
const key = time.format('YYYY-MM-DD');
|
||||
const oldValue = window.localStorage.getItem(`${LOCAL_STORAGE_STATISTIC_PREFIX}${key}-${stat}`);
|
||||
if (oldValue !== null) {
|
||||
window.localStorage.setItem(`${LOCAL_STORAGE_STATISTIC_PREFIX}${key}-${stat}`, parseInt(oldValue, 10) + 1);
|
||||
} else {
|
||||
window.localStorage.setItem(`${LOCAL_STORAGE_STATISTIC_PREFIX}${key}-${stat}`, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// val value : Object
|
||||
setItem = (id, value) => {
|
||||
window.localStorage.setItem(`${LOCAL_STORAGE_PREFIX}${id}`, JSON.stringify(value));
|
||||
}
|
||||
|
||||
getItem = id => {
|
||||
try {
|
||||
return JSON.parse(window.localStorage.getItem(`${LOCAL_STORAGE_PREFIX}${id}`));
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
removeItem = id => {
|
||||
// We never really want to purge anything from the cache if we can help it,
|
||||
// since there's always a chance that a read notification can be resurrected.
|
||||
@ -77,14 +130,6 @@ class StorageProvider extends React.Component {
|
||||
this.setItem(id, closed_cached_n);
|
||||
}
|
||||
|
||||
getItem = id => {
|
||||
try {
|
||||
return JSON.parse(window.localStorage.getItem(`${LOCAL_STORAGE_PREFIX}${id}`));
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
clearCache = () => {
|
||||
window.localStorage.clear();
|
||||
}
|
||||
@ -96,7 +141,9 @@ class StorageProvider extends React.Component {
|
||||
getItem: this.getItem,
|
||||
removeItem: this.removeItem,
|
||||
clearCache: this.clearCache,
|
||||
refreshNotifications: this.refreshNotifications
|
||||
refreshNotifications: this.refreshNotifications,
|
||||
getStat: this.getStat,
|
||||
incrStat: this.incrStat,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user