Skip to content

Commit

Permalink
[#24, #25] Always show filters, added check to don't show notificatio…
Browse files Browse the repository at this point in the history
…ns of filtered changes and fixed SignalR creating multiple connections
  • Loading branch information
ivancea committed Nov 6, 2020
1 parent c9433f4 commit 1731aae
Show file tree
Hide file tree
Showing 4 changed files with 255 additions and 125 deletions.
120 changes: 26 additions & 94 deletions Frontend/src/components/Home.tsx
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>
);
}
76 changes: 45 additions & 31 deletions Frontend/src/components/changes/ChangesList.tsx
Original file line number Diff line number Diff line change
@@ -1,45 +1,31 @@
import React, { useCallback, useState } from "react";
import { ListGroup } from "reactstrap";
import { cloneDeep } from "lodash";
import React, { Dispatch, SetStateAction, useCallback } from "react";
import { Button, ButtonGroup, ListGroup } from "reactstrap";
import { Filter } from "../../hooks/useChanges";
import { ChangeObjectType, ChangeType, ChangeWrapper } from "../../types/changes";
import { Change } from "./Change";
import { ChangeFilterSelector } from "./ChangeFilterSelector";

type Props = {
changes: ChangeWrapper[];
};

type Filter = {
repositories: Map<string, boolean>;
users: Map<string, boolean>;
types: Map<string, boolean>;
objectTypes: Map<string, boolean>;
setChanges: Dispatch<SetStateAction<ChangeWrapper[]>>;
setFilter: Dispatch<SetStateAction<Filter>>;
isHidden: (change: ChangeWrapper) => boolean;
notifyHiddenChanges: boolean;
setNotifyHiddenChanges: Dispatch<SetStateAction<boolean>>;
};

const changeTypes = Object.keys(ChangeType).filter((t) => typeof t === "string");
const changeObjectTypes = Object.keys(ChangeObjectType).filter((t) => typeof t === "string");

export function ChangesList({ changes }: Props): React.ReactElement {
const [filter, setFilter] = useState<Filter>({
repositories: new Map(),
users: new Map(),
types: new Map(),
objectTypes: new Map(),
});

const isHidden = (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;
};

export function ChangesList({
changes,
setChanges,
setFilter,
isHidden,
notifyHiddenChanges,
setNotifyHiddenChanges,
}: Props): React.ReactElement {
const useFilterChanged = (filterType: keyof Filter): ((elements: Map<string | undefined, boolean>) => void) =>
useCallback(
(filter: Map<string | undefined, boolean>) =>
Expand All @@ -55,8 +41,36 @@ export function ChangesList({ changes }: Props): React.ReactElement {
const onTypesFilterChanged = useFilterChanged("types");
const onObjectTypesFilterChanged = useFilterChanged("objectTypes");

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 toggleNotifyHiddenChanges = React.useCallback(() => {
setNotifyHiddenChanges((oldNotifyHiddenChanges) => !oldNotifyHiddenChanges);
}, [setNotifyHiddenChanges]);

return (
<>
<ButtonGroup>
<Button color="success" onClick={markAllAsRead}>
Mark all as read
</Button>
<Button color="danger" onClick={removeAllReadChanges}>
Remove all read changes
</Button>
</ButtonGroup>
<Button
className="ml-1"
color={notifyHiddenChanges ? "success" : "secondary"}
outline={!notifyHiddenChanges}
onClick={toggleNotifyHiddenChanges}
>
{notifyHiddenChanges ? "✅" : "❌"} Notify hidden changes
</Button>
<ChangeFilterSelector
name="Repositories"
changes={changes}
Expand Down
121 changes: 121 additions & 0 deletions Frontend/src/hooks/useChanges.ts
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,
};
}
Loading

0 comments on commit 1731aae

Please sign in to comment.