-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Browse files
Browse the repository at this point in the history
…ns of filtered changes and fixed SignalR creating multiple connections
- Loading branch information
Showing
4 changed files
with
255 additions
and
125 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,107 +1,39 @@ | ||
import React, { useEffect, useState } from "react"; | ||
import { config } from "../Config"; | ||
import { Alert, Button, ButtonGroup } from "reactstrap"; | ||
import { cloneDeep } from "lodash"; | ||
import { ChangesNotification, ChangeWrapper } from "../types/changes"; | ||
import React from "react"; | ||
import { Alert } from "reactstrap"; | ||
import { ChangesList } from "./changes/ChangesList"; | ||
import { HubConnectionBuilder } from "@microsoft/signalr"; | ||
import "./styles/global.scss"; | ||
import { RepositoryErrors } from "./RepositoryErrors"; | ||
import { useLocalStorage } from "../hooks/useLocalStorage"; | ||
import * as uuid from "uuid"; | ||
import { useChanges } from "../hooks/useChanges"; | ||
|
||
export function Home(): React.ReactElement { | ||
const [error, setError] = useState<string>(); | ||
const [changes, setChanges] = useLocalStorage<ChangeWrapper[]>("changes", []); | ||
const [errors, setErrors] = useState<ChangesNotification["errors"]>({}); | ||
|
||
useEffect(() => { | ||
Notification.requestPermission(); | ||
|
||
setError("Connecting..."); | ||
|
||
const newHub = new HubConnectionBuilder().withUrl(config.url.API + "hubs/changes").build(); | ||
|
||
newHub.on("changes", (newChangesJson: string) => { | ||
const newChanges: ChangesNotification = JSON.parse(newChangesJson); | ||
const wrappedChanges = Object.entries(newChanges.changes).flatMap((e) => | ||
e[1].map<ChangeWrapper>((c) => ({ | ||
id: uuid.v4(), | ||
repository: e[0], | ||
date: new Date().toLocaleTimeString(), | ||
change: c, | ||
seen: false, | ||
})), | ||
); | ||
|
||
if (wrappedChanges.length > 0) { | ||
new Notification( | ||
wrappedChanges.length + | ||
" new change" + | ||
(wrappedChanges.length > 1 ? "s" : "") + | ||
" in " + | ||
Object.keys(newChanges.changes).sort().join(", "), | ||
); | ||
|
||
setChanges((c) => [...c, ...wrappedChanges]); | ||
} | ||
|
||
setErrors(newChanges.errors); | ||
}); | ||
|
||
newHub.onreconnecting((e) => setError("Reconnecting to the server..." + (e ? ` (${e.message})` : undefined))); | ||
|
||
newHub.onclose(() => { | ||
setError("Disconnected from the server, reconnecting..."); | ||
connect(); | ||
}); | ||
|
||
connect(); | ||
|
||
function connect(): void { | ||
newHub | ||
.start() | ||
.then(() => setError(undefined)) | ||
.catch((e) => { | ||
setError("Error connecting to the server: " + JSON.stringify(e)); | ||
|
||
setTimeout(connect, 5000); | ||
}); | ||
} | ||
}, [setChanges]); | ||
|
||
const toggleError = React.useCallback(() => setError(undefined), [setError]); | ||
|
||
const markAllAsRead = React.useCallback(() => { | ||
setChanges((oldChanges) => oldChanges.map((change) => ({ ...cloneDeep(change), seen: true }))); | ||
}, [setChanges]); | ||
|
||
const removeAllReadChanges = React.useCallback(() => { | ||
setChanges((oldChanges) => oldChanges.filter((change) => !change.seen)); | ||
}, [setChanges]); | ||
const { | ||
connectionError, | ||
setConnectionError, | ||
changes, | ||
setChanges, | ||
errors, | ||
setFilter, | ||
isHidden, | ||
notifyHiddenChanges, | ||
setNotifyHiddenChanges, | ||
} = useChanges(); | ||
|
||
const toggleError = React.useCallback(() => setConnectionError(undefined), [setConnectionError]); | ||
|
||
return ( | ||
<div> | ||
<Alert color="danger" isOpen={!!error} toggle={toggleError}> | ||
{error} | ||
<Alert color="danger" isOpen={!!connectionError} toggle={toggleError}> | ||
{connectionError} | ||
</Alert> | ||
<RepositoryErrors errors={errors} /> | ||
|
||
{changes.length == 0 ? ( | ||
"No changes yet" | ||
) : ( | ||
<> | ||
<ButtonGroup> | ||
<Button color="success" onClick={markAllAsRead}> | ||
Mark all as read | ||
</Button> | ||
<Button color="danger" onClick={removeAllReadChanges}> | ||
Remove all read changes | ||
</Button> | ||
</ButtonGroup> | ||
<ChangesList changes={changes} /> | ||
</> | ||
)} | ||
<ChangesList | ||
changes={changes} | ||
setChanges={setChanges} | ||
setFilter={setFilter} | ||
isHidden={isHidden} | ||
notifyHiddenChanges={notifyHiddenChanges} | ||
setNotifyHiddenChanges={setNotifyHiddenChanges} | ||
/> | ||
</div> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,121 @@ | ||
import { Dispatch, SetStateAction, useCallback, useEffect, useMemo, useState } from "react"; | ||
import { ChangesNotification, ChangeWrapper } from "../types/changes"; | ||
import { useLocalStorage } from "./useLocalStorage"; | ||
import { config } from "../Config"; | ||
import * as uuid from "uuid"; | ||
import { useSignalR } from "./useSignalR"; | ||
|
||
export type Filter = { | ||
repositories: Map<string, boolean>; | ||
users: Map<string, boolean>; | ||
types: Map<string, boolean>; | ||
objectTypes: Map<string, boolean>; | ||
}; | ||
|
||
export type ChangesData = { | ||
connectionError?: string; | ||
setConnectionError: Dispatch<SetStateAction<string | undefined>>; | ||
changes: ChangeWrapper[]; | ||
setChanges: Dispatch<SetStateAction<ChangeWrapper[]>>; | ||
errors: ChangesNotification["errors"]; | ||
setErrors: Dispatch<SetStateAction<ChangesNotification["errors"]>>; | ||
filter: Filter; | ||
setFilter: Dispatch<SetStateAction<Filter>>; | ||
isHidden: (change: ChangeWrapper) => boolean; | ||
notifyHiddenChanges: boolean; | ||
setNotifyHiddenChanges: Dispatch<SetStateAction<boolean>>; | ||
}; | ||
|
||
export function useChanges(): ChangesData { | ||
const [connectionError, setConnectionError] = useState<string>(); | ||
const [changes, setChanges] = useLocalStorage<ChangeWrapper[]>("changes", []); | ||
const [errors, setErrors] = useState<ChangesNotification["errors"]>({}); | ||
const [filter, setFilter] = useState<Filter>({ | ||
repositories: new Map(), | ||
users: new Map(), | ||
types: new Map(), | ||
objectTypes: new Map(), | ||
}); | ||
const [notifyHiddenChanges, setNotifyHiddenChanges] = useState(true); | ||
|
||
const isHidden = useCallback( | ||
(change: ChangeWrapper): boolean => { | ||
if (!filter.repositories.get(change.repository)) { | ||
return true; | ||
} else if (!filter.users.get(change.change.user?.name ?? "")) { | ||
return true; | ||
} else if (!filter.types.get(change.change.type)) { | ||
return true; | ||
} else if (!filter.objectTypes.get(change.change.objectType)) { | ||
return true; | ||
} | ||
|
||
return false; | ||
}, | ||
[filter], | ||
); | ||
|
||
useEffect(() => { | ||
Notification.requestPermission(); | ||
}, []); | ||
|
||
useSignalR( | ||
config.url.API + "hubs/changes", | ||
setConnectionError, | ||
useMemo( | ||
() => ({ | ||
changes: [ | ||
(newChangesJson: unknown) => { | ||
const newChanges: ChangesNotification = JSON.parse(newChangesJson as string); | ||
const wrappedChanges = Object.entries(newChanges.changes).flatMap((e) => | ||
e[1].map<ChangeWrapper>((c) => ({ | ||
id: uuid.v4(), | ||
repository: e[0], | ||
date: new Date().toLocaleTimeString(), | ||
change: c, | ||
seen: false, | ||
})), | ||
); | ||
|
||
if (wrappedChanges.length > 0) { | ||
const changesToNotify = notifyHiddenChanges | ||
? wrappedChanges | ||
: wrappedChanges.filter((c) => !isHidden(c)); | ||
|
||
if (changesToNotify.length > 0) { | ||
new Notification( | ||
changesToNotify.length + | ||
" new change" + | ||
(wrappedChanges.length > 1 ? "s" : "") + | ||
" in " + | ||
Array.from(new Set(changesToNotify.map((c) => c.repository))) | ||
.sort() | ||
.join(", "), | ||
); | ||
} | ||
|
||
setChanges((c) => [...c, ...wrappedChanges]); | ||
} | ||
|
||
setErrors(newChanges.errors); | ||
}, | ||
], | ||
}), | ||
[setChanges, isHidden, notifyHiddenChanges], | ||
), | ||
); | ||
|
||
return { | ||
connectionError, | ||
setConnectionError, | ||
changes, | ||
setChanges, | ||
errors, | ||
setErrors, | ||
filter, | ||
setFilter, | ||
isHidden, | ||
notifyHiddenChanges, | ||
setNotifyHiddenChanges, | ||
}; | ||
} |
Oops, something went wrong.