From 4826f684a5c34be48f03525108a8a10a157a7e2e Mon Sep 17 00:00:00 2001 From: Lei Nelissen Date: Mon, 2 Nov 2020 13:46:09 +0100 Subject: [PATCH 1/3] Fix #145: Add provider and update type to commit message --- src/main/providers/index.ts | 23 ++++++++++++++++++++--- src/main/providers/types.ts | 5 +++++ 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/src/main/providers/index.ts b/src/main/providers/index.ts index d09124ca..d50c071a 100644 --- a/src/main/providers/index.ts +++ b/src/main/providers/index.ts @@ -1,7 +1,7 @@ import { EventEmitter } from 'events'; import { differenceInDays } from 'date-fns'; import Instagram from './instagram'; -import { Provider, ProviderFile, DataRequestProvider, DataRequestStatus, ProviderEvents } from './types'; +import { Provider, ProviderFile, DataRequestProvider, DataRequestStatus, ProviderEvents, ProviderUpdateType } from './types'; import Repository, { REPOSITORY_PATH } from '../lib/repository'; import Notifications from 'main/lib/notifications'; import ProviderBridge from './bridge'; @@ -139,7 +139,12 @@ class ProviderManager extends EventEmitter { /** * Save a bunch of files and auto-commit the result */ - saveFilesAndCommit = async (files: ProviderFile[], key: string, message: string): Promise => { + saveFilesAndCommit = async ( + files: ProviderFile[], + key: string, + message: string, + updateType: ProviderUpdateType + ): Promise => { console.log(`Saving and committing files for ${key}...`); // Then store all files using the repositor save and add handler @@ -169,8 +174,20 @@ class ProviderManager extends EventEmitter { return; } + // Gather the set of data that is to be appended to the commit + const messageData: Record = { + 'Aeon-Provider': key, + 'Aeon-Update-Type': updateType, + } + + // Parse the object as a series of "key: value \n" statements + const augmentedMessage = Object.keys(messageData).reduce((sum, key) => { + return `${sum}\n${key}: ${messageData[key]}` + }, message); + + // Acutally create the commit console.log('Creating commit: ', message); - await this.repository.commit(message); + await this.repository.commit(augmentedMessage); return changedFiles.length; } diff --git a/src/main/providers/types.ts b/src/main/providers/types.ts index 84e61163..a575ef4b 100644 --- a/src/main/providers/types.ts +++ b/src/main/providers/types.ts @@ -236,4 +236,9 @@ export interface ProviderParser { transformer?(object: unknown): Partial>[]; // transformer?: (obj: any) => any | any[]; }[] +} + +export enum ProviderUpdateType { + UPDATE = 'update', + DATA_REQUEST = 'data_request' } \ No newline at end of file From 8a131fd6ded7c97a3d88790f6028248dcaaff82e Mon Sep 17 00:00:00 2001 From: Lei Nelissen Date: Mon, 2 Nov 2020 16:21:12 +0100 Subject: [PATCH 2/3] Setup base Request screen --- src/app/components/Button.tsx | 2 +- src/app/components/Menu.tsx | 4 +- src/app/components/PanelGrid.tsx | 116 ++++++++++++++++-- src/app/components/RightSideOverlay.tsx | 26 ++-- src/app/screens/Data/index.tsx | 14 ++- .../Requests/components/ProviderOverlay.tsx | 83 +++++++++++++ src/app/screens/Requests/getDescription.ts | 17 +++ src/app/screens/Requests/index.tsx | 63 ++++++++++ src/app/screens/Timeline/index.tsx | 59 +++++---- src/app/screens/index.tsx | 4 + src/app/screens/types.ts | 3 + src/app/utilities/Providers.ts | 2 +- src/app/utilities/useRequests.tsx | 25 ++++ 13 files changed, 354 insertions(+), 64 deletions(-) create mode 100644 src/app/screens/Requests/components/ProviderOverlay.tsx create mode 100644 src/app/screens/Requests/getDescription.ts create mode 100644 src/app/screens/Requests/index.tsx create mode 100644 src/app/utilities/useRequests.tsx diff --git a/src/app/components/Button.tsx b/src/app/components/Button.tsx index 2dbc2615..a9922987 100644 --- a/src/app/components/Button.tsx +++ b/src/app/components/Button.tsx @@ -60,7 +60,7 @@ export const LinkButton = styled.button` padding: 0; `; -export const GhostButton = styled(LinkButton)` +export const GhostButton = styled(LinkButton)` color: black; opacity: 0.3; font-size: 14px; diff --git a/src/app/components/Menu.tsx b/src/app/components/Menu.tsx index 5c45745c..1b5db744 100644 --- a/src/app/components/Menu.tsx +++ b/src/app/components/Menu.tsx @@ -125,10 +125,10 @@ export default function Menu(): JSX.Element { Timeline - {/* + Requests - */} + Data diff --git a/src/app/components/PanelGrid.tsx b/src/app/components/PanelGrid.tsx index 1de694eb..48906024 100644 --- a/src/app/components/PanelGrid.tsx +++ b/src/app/components/PanelGrid.tsx @@ -1,5 +1,11 @@ +import React, { PropsWithChildren } from 'react'; +import { IconProp } from '@fortawesome/fontawesome-svg-core'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import theme from 'app/styles/theme'; -import styled from 'styled-components'; +import { Link } from 'react-router-dom'; +import styled, { css } from 'styled-components'; +import { faChevronRight } from 'app/assets/fa-light'; +import { PullRight } from './Utility'; export const ListItem = styled.div` padding: 8px 32px; @@ -9,39 +15,127 @@ export const ListItem = styled.div` export const RowHeading = styled(ListItem)` border-bottom: 1px solid ${theme.colors.border}; - text-transform: uppercase; font-weight: 400; - font-size: 12px; - letter-spacing: 0.5px; position: sticky; top: 0; align-self: flex-start; z-index: 2; + font-size: 14px; width: 100%; - background-color: ${theme.colors.grey.light}; `; export const SubHeading = styled(RowHeading)` font-size: 10px; + font-family: 'IBM Plex Mono'; + text-transform: uppercase; color: ${theme.colors.grey.dark}; + letter-spacing: 0.3px; `; - -export const PanelGrid = styled.div` +export const PanelGrid = styled.div<{ columns?: number; noTopPadding?: boolean; }>` display: grid; grid-auto-columns: auto; - grid-template-columns: 1fr 1fr 1fr; - padding-top: 40px; + grid-template-columns: repeat(${props => props.columns || 3}, 1fr); + padding-top: ${props => props.noTopPadding ? 0 : 40}px; height: 100%; position: relative; overflow: hidden; `; -export const List = styled.div` +export const List = styled.div<{ topMargin?: boolean }>` display: flex; flex-direction: column; border-right: 1px solid ${theme.colors.border}; flex-grow: 1; overflow-y: auto; position: relative; -`; \ No newline at end of file + + ${props => props.topMargin && css` + margin-top: 40px; + `} +`; + +const IconWrapper = styled.div` + margin: 0 8px; +`; + +type ListButtonProps = { + active?: boolean; + disabled?: boolean; + deleted?: boolean; + modified?: boolean; + added?: boolean; + large?: boolean; +}; + +export const NavigatableListEntryContainer = styled>(Link)` + border: 0; + background: transparent; + display: flex; + align-items: center; + font-size: 14px; + margin: 0; + padding: 14px 24px; + font-weight: 400; + color: ${theme.colors.black}; + + img { + max-height: 100px; + width: auto; + border-radius: 5px; + } + + ${props => props.active ? css` + background: ${theme.colors.grey.medium}; + ` : css` + &:hover { + background: ${theme.colors.grey.medium}BB; + opacity: 0.8; + } + `} + + ${props => props.added && css` + background-color: ${theme.colors.green}${props.active ? 33 : 22};; + `} + + ${props => props.deleted && css` + background-color: ${theme.colors.red}${props.active ? 33 : 22};; + `} + + ${props => props.modified && css` + background-color: ${theme.colors.yellow}${props.active ? 33 : 22}; + `} + + ${props => props.large && css` + font-size: 16px; + `} + + &:disabled { + opacity: 0.25; + } +`; + +type NavigatableListEntryProps = PropsWithChildren<{ + to: string, + icon?: IconProp, +} & ListButtonProps>; + +export function NavigatableListEntry({ + icon, + children, + ...props +}: NavigatableListEntryProps): JSX.Element { + return ( + + {icon ? + + + + : null} + {children} + + + + + ) +} \ No newline at end of file diff --git a/src/app/components/RightSideOverlay.tsx b/src/app/components/RightSideOverlay.tsx index afa8d89c..1f83ecb4 100644 --- a/src/app/components/RightSideOverlay.tsx +++ b/src/app/components/RightSideOverlay.tsx @@ -9,17 +9,16 @@ import { faChevronRight } from 'app/assets/fa-light'; export interface RightSideOverlayProps { children: JSX.Element; onClose?: () => void; - columnPosition: number; } -const Container = styled.div>` +const Container = styled.div` position: absolute; z-index: 2; height: 100%; - width: ${props => 100 / props.columnPosition}%; + width: 100%; + max-width: 500px; right: 0; top: 0; - padding-top: 40px; `; const InnerContainer = styled.div` @@ -30,12 +29,12 @@ const InnerContainer = styled.div` overflow-y: auto; max-height: calc(100% - 20px); background-color: white; - box-shadow: 0 -1px 1px rgba(0,0,0,0.01), - 0 -2px 2px rgba(0,0,0,0.01), - 0 -4px 4px rgba(0,0,0,0.01), - 0 -8px 8px rgba(0,0,0,0.01), - 0 -16px 16px rgba(0,0,0,0.01), - 0 -32px 32px rgba(0,0,0,0.01); + box-shadow: 0 1px 1px rgba(0,0,0,0.01), + 0 2px 2px rgba(0,0,0,0.01), + 0 4px 4px rgba(0,0,0,0.01), + 0 8px 8px rgba(0,0,0,0.01), + 0 16px 16px rgba(0,0,0,0.01), + 0 32px 32px rgba(0,0,0,0.01); `; export const CloseButton = styled(GhostButton)` @@ -48,6 +47,10 @@ export const Section = styled.div<{ smallPadding?: boolean}>` border-bottom: 1px solid #eee; padding: ${props => props.smallPadding ? 15: 25}px; + p:first-child { + margin-top: 0; + } + img { max-width: 100%; height: auto; @@ -67,7 +70,6 @@ const RightSideOverlay = (props: RightSideOverlayProps): JSX.Element => { const { onClose: handleClose, children, - columnPosition } = props; return ( @@ -77,7 +79,7 @@ const RightSideOverlay = (props: RightSideOverlayProps): JSX.Element => { > {children => children && (props => - + {handleClose ? diff --git a/src/app/screens/Data/index.tsx b/src/app/screens/Data/index.tsx index e93ba4a9..ffe3a49e 100644 --- a/src/app/screens/Data/index.tsx +++ b/src/app/screens/Data/index.tsx @@ -125,7 +125,7 @@ class Data extends Component { return ( - CATEGORIES + Categories {Object.values(ProvidedDataTypes).map((key) => ( { ))} - DATA POINTS + Data Points {category && groupedData[category].map((datum, index) => ( { /> ))} - + + + diff --git a/src/app/screens/Requests/components/ProviderOverlay.tsx b/src/app/screens/Requests/components/ProviderOverlay.tsx new file mode 100644 index 00000000..67bacdc5 --- /dev/null +++ b/src/app/screens/Requests/components/ProviderOverlay.tsx @@ -0,0 +1,83 @@ +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faCheck, faClock, faPlus, faQuestion, faSync } from 'app/assets/fa-light'; +import Button from 'app/components/Button'; +import RightSideOverlay, { Section } from 'app/components/RightSideOverlay'; +import { H2 } from 'app/components/Typography'; +import Providers from 'app/utilities/Providers'; +import { formatDistanceToNow } from 'date-fns'; +import { DataRequestStatus } from 'main/providers/types'; +import React, { useCallback } from 'react'; + +interface Props { + selectedProvider: string; + status?: DataRequestStatus; +} + +function ProviderOverlay({ selectedProvider, status }: Props): JSX.Element { + const handleNewRequest = useCallback(() => { + Providers.dispatchDataRequest(selectedProvider); + }, [selectedProvider]); + + return ( + + {selectedProvider && ( + <> +
+

+ + {selectedProvider} +

+
+
+ + + Data requested: {status?.dispatched ? formatDistanceToNow(status.dispatched) + ' ago' : 'never'} +
+ + Last check: {status?.lastCheck ? formatDistanceToNow(status.lastCheck) + ' ago' : 'never'} +
+
+ {status?.dispatched ? +
+

The data request you issued has not been completed yet. We'll let you know as soon as it's completed.

+ +
+ : +
+

If you would like to retrieve your data, use the button below to start a new data request.

+

Note: you may be asked to confirm your password

+ +
+ } + + )} +
+ ); +} + +export default ProviderOverlay; \ No newline at end of file diff --git a/src/app/screens/Requests/getDescription.ts b/src/app/screens/Requests/getDescription.ts new file mode 100644 index 00000000..8946520e --- /dev/null +++ b/src/app/screens/Requests/getDescription.ts @@ -0,0 +1,17 @@ +import { formatDistanceToNow } from 'date-fns'; +import { DataRequestStatus } from 'main/providers/types'; + +/** + * A helper to convert a particular DataRequestStatus to a human-readable string + */ +export default function getDescription(status?: DataRequestStatus): string { + if (status?.completed) { + return `Received data ${formatDistanceToNow(status.completed)} ago`; + } + + if (status?.dispatched) { + return `Requested data ${formatDistanceToNow(status.dispatched)} ago`; + } + + return 'No data requested yet'; +} \ No newline at end of file diff --git a/src/app/screens/Requests/index.tsx b/src/app/screens/Requests/index.tsx new file mode 100644 index 00000000..42f9517b --- /dev/null +++ b/src/app/screens/Requests/index.tsx @@ -0,0 +1,63 @@ +import React from 'react'; +import { List, NavigatableListEntry, PanelGrid, RowHeading, SubHeading } from 'app/components/PanelGrid'; +import useRequests from 'app/utilities/useRequests'; +import Loading from 'app/components/Loading'; +import Providers from 'app/utilities/Providers'; +import { useParams } from 'react-router-dom'; +import { RouteProps } from '../types'; +import ProviderOverlay from './components/ProviderOverlay'; +import getDescription from './getDescription'; +import styled from 'styled-components'; + +const StatusDescription = styled.span` + font-size: 12px; + opacity: 0.5; +`; + +const Rows = styled.div` + display: flex; + flex-direction: column; + line-height: 1.5; +`; + +function Requests(): JSX.Element { + const requests = useRequests(); + const { provider: selectedProvider } = useParams(); + + if (!requests) { + return ; + } + + // Destructure the data + const { providers, dispatched } = requests; + console.log(requests); + + return ( + + + Your Accounts + Automated Requests + {providers.map(provider => + + + {provider} + {getDescription(dispatched.get(provider))} + + + )} + Email-based Requests + + + + + + ) +} + +export default Requests; \ No newline at end of file diff --git a/src/app/screens/Timeline/index.tsx b/src/app/screens/Timeline/index.tsx index 8adab559..7514d58a 100644 --- a/src/app/screens/Timeline/index.tsx +++ b/src/app/screens/Timeline/index.tsx @@ -12,6 +12,7 @@ import Store, { StoreProps } from 'app/store'; import { useHistory, useParams } from 'react-router-dom'; import { RouteProps } from '../types'; import { History } from 'history'; +import { List, PanelGrid } from 'app/components/PanelGrid'; interface State { log: CommitType[]; @@ -23,14 +24,6 @@ interface Props { history: History; } -const Container = styled.div` - display: grid; - height: 100%; - grid-template-columns: 50% 50%; - grid-template-areas: - "commits diff"; -`; - const CommitContainer = styled.div` display: flex; grid-area: "commits"; @@ -109,30 +102,34 @@ class Timeline extends Component { } return ( - - - - {newCommit ? - - : null} - {log.map((entry, i) => ( - - ))} - - + + + + + {newCommit ? + + : null} + {log.map((entry, i) => ( + + ))} + + + + + - + ); } } diff --git a/src/app/screens/index.tsx b/src/app/screens/index.tsx index 84f6701a..c25655a2 100644 --- a/src/app/screens/index.tsx +++ b/src/app/screens/index.tsx @@ -5,6 +5,7 @@ import Timeline from './Timeline'; import Data from './Data'; import Store from 'app/store'; import Menu, { ContentContainer, MenuContainer, TitleBar } from 'app/components/Menu'; +import Requests from './Requests'; /** * A helper to determine what the starting screen should be for the application. @@ -37,6 +38,9 @@ function Router(): JSX.Element { + + + diff --git a/src/app/screens/types.ts b/src/app/screens/types.ts index 5eebc5cf..4873ba0a 100644 --- a/src/app/screens/types.ts +++ b/src/app/screens/types.ts @@ -5,5 +5,8 @@ export interface RouteProps { data: { category?: string; datumId?: string; + }, + requests: { + provider?: string; } } \ No newline at end of file diff --git a/src/app/utilities/Providers.ts b/src/app/utilities/Providers.ts index 7e2f8aa0..c19c86e2 100644 --- a/src/app/utilities/Providers.ts +++ b/src/app/utilities/Providers.ts @@ -5,7 +5,7 @@ import { IpcRendererEvent } from 'electron'; const channel = 'providers'; -interface DataRequestReturnType { +export interface DataRequestReturnType { dispatched: Map; lastChecked: Date; providers: string[]; diff --git a/src/app/utilities/useRequests.tsx b/src/app/utilities/useRequests.tsx new file mode 100644 index 00000000..be3617ba --- /dev/null +++ b/src/app/utilities/useRequests.tsx @@ -0,0 +1,25 @@ +import { useCallback , useEffect, useState } from 'react'; + +import Providers, { DataRequestReturnType } from './Providers'; + +/** + * A helper to retrieve all providers from the Provider utility + */ +export default function useRequests(): DataRequestReturnType | null { + const [requests, setRequests] = useState(null); + + const retrieveDataRequests = useCallback(async () => { + setRequests(await Providers.getDataRequests()); + }, [setRequests]); + + useEffect(() => { + Providers.subscribe(retrieveDataRequests); + retrieveDataRequests(); + + return () => { + Providers.unsubscribe(retrieveDataRequests); + }; + }, []); + + return requests; +} \ No newline at end of file From 2e576d8360b2f06a8ade87736f2d217dd233a71b Mon Sep 17 00:00:00 2001 From: Lei Nelissen Date: Mon, 2 Nov 2020 16:58:08 +0100 Subject: [PATCH 3/3] (1) Only show first line in a commit (2) Correctly set update type in commits (3) Show completed times in UI --- .../screens/Requests/components/ProviderOverlay.tsx | 11 +++++++++++ src/main/lib/repository/index.ts | 3 ++- src/main/providers/index.ts | 4 ++-- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/app/screens/Requests/components/ProviderOverlay.tsx b/src/app/screens/Requests/components/ProviderOverlay.tsx index 67bacdc5..af74c12b 100644 --- a/src/app/screens/Requests/components/ProviderOverlay.tsx +++ b/src/app/screens/Requests/components/ProviderOverlay.tsx @@ -46,6 +46,17 @@ function ProviderOverlay({ selectedProvider, status }: Props): JSX.Element { fixedWidth /> Last check: {status?.lastCheck ? formatDistanceToNow(status.lastCheck) + ' ago' : 'never'} + {status?.completed && + <> +
+ + Completed: {formatDistanceToNow(status?.completed)} ago + + } {status?.dispatched ? diff --git a/src/main/lib/repository/index.ts b/src/main/lib/repository/index.ts index c11c0194..324c2c1d 100644 --- a/src/main/lib/repository/index.ts +++ b/src/main/lib/repository/index.ts @@ -258,7 +258,8 @@ class Repository extends EventEmitter { return { oid: commit.sha(), - message: commit.message(), + // Only show the first line of a commit + message: commit.message().split('\n')[0], author: { email: author.email(), name: author.name(), diff --git a/src/main/providers/index.ts b/src/main/providers/index.ts index d50c071a..ca244968 100644 --- a/src/main/providers/index.ts +++ b/src/main/providers/index.ts @@ -127,7 +127,7 @@ class ProviderManager extends EventEmitter { } // Alternatively, we save the files and attempt to commit - const changedFiles = await this.saveFilesAndCommit(files, key, `Auto-update ${new Date().toLocaleString()}`); + const changedFiles = await this.saveFilesAndCommit(files, key, `Auto-update ${new Date().toLocaleString()}`, ProviderUpdateType.UPDATE); // GUARD: Only log stuff if new data is found if (changedFiles) { @@ -276,7 +276,7 @@ class ProviderManager extends EventEmitter { // If it is complete now, we'll fetch the data and parse it const dirPath = path.join(REPOSITORY_PATH, key); const files = await instance.parseDataRequest(dirPath); - const changedFiles = await this.saveFilesAndCommit(files, key, `Data Request [${key}] ${new Date().toLocaleString()}`); + const changedFiles = await this.saveFilesAndCommit(files, key, `Data Request [${key}] ${new Date().toLocaleString()}`, ProviderUpdateType.DATA_REQUEST); Notifications.success(`The data request for ${key} was successfully completed. ${changedFiles} files were changed.`); // Set the flag for completion