) =>
- setFilter((f) => ({
- ...f,
- [filterType]: filter,
- })),
- [filterType],
- );
-
- const onRepositoriesFilterChanged = useFilterChanged("repositories");
- const onUsersFilterChanged = useFilterChanged("users");
- const onTypesFilterChanged = useFilterChanged("types");
- const onObjectTypesFilterChanged = useFilterChanged("objectTypes");
-
const markAllAsRead = React.useCallback(() => {
setChanges((oldChanges) => oldChanges.map((change) => ({ ...cloneDeep(change), seen: true })));
}, [setChanges]);
@@ -73,45 +49,9 @@ export function ChangesList({
>
{notifyHiddenChanges ? "✅" : "❌"} Notify hidden changes
-
-
-
- Filters
-
-
-
- c.repository, [])}
- onChanged={onRepositoriesFilterChanged}
- />
- (c.change.user ? c.change.user.name : ""), [])}
- textSelector={useCallback((o: string) => (o === "" ? No user : o), [])}
- onChanged={onUsersFilterChanged}
- />
- c.change.type, [])}
- onChanged={onTypesFilterChanged}
- />
- c.change.objectType, [])}
- onChanged={onObjectTypesFilterChanged}
- />
-
-
-
-
- Total: {changes.length}, Visible: {changes.filter((c) => !isHidden(c)).length}
+
+ Total: {changes.length}, Visible: {changes.filter((c) => !isHidden(c)).length}
+
{changes
.map((change) => )
diff --git a/Frontend/src/components/changes/filters/ChangeFilterRegexInput.tsx b/Frontend/src/components/changes/filters/ChangeFilterRegexInput.tsx
new file mode 100644
index 0000000..20e81e4
--- /dev/null
+++ b/Frontend/src/components/changes/filters/ChangeFilterRegexInput.tsx
@@ -0,0 +1,43 @@
+import { isNil } from "lodash";
+import React, { useEffect, useState } from "react";
+import { Col, FormFeedback, Input, Row } from "reactstrap";
+
+type Props = {
+ name: string;
+ defaultValue?: string;
+ onChanged: (value: RegExp) => void;
+};
+
+export function ChangeFilterRegexInput({ name, defaultValue, onChanged }: Props): React.ReactElement {
+ const [text, setText] = useState(defaultValue ?? "^.*$");
+ const [error, setError] = useState();
+
+ useEffect(() => {
+ try {
+ // Validate regex
+ const regex = new RegExp(text);
+
+ onChanged(regex);
+ setError(undefined);
+ } catch (e) {
+ if (e instanceof SyntaxError) {
+ setError("Invalid Regex: " + e.message[0].toUpperCase() + e.message.slice(1));
+ } else {
+ setError("Invalid Regex");
+ console.error("Unknown regex error", e);
+ }
+ }
+ }, [text, onChanged]);
+
+ return (
+
+
+ {name}:
+
+
+ setText(e.target.value)} />
+ {!isNil(error) ? {error} : undefined}
+
+
+ );
+}
diff --git a/Frontend/src/components/changes/ChangeFilterSelector.tsx b/Frontend/src/components/changes/filters/ChangeFilterSelector.tsx
similarity index 97%
rename from Frontend/src/components/changes/ChangeFilterSelector.tsx
rename to Frontend/src/components/changes/filters/ChangeFilterSelector.tsx
index 0e1dfe3..4d4b859 100644
--- a/Frontend/src/components/changes/ChangeFilterSelector.tsx
+++ b/Frontend/src/components/changes/filters/ChangeFilterSelector.tsx
@@ -1,7 +1,7 @@
import { isNil } from "lodash";
import React, { useCallback, useEffect, useState } from "react";
import { Button, Col, Row } from "reactstrap";
-import { ChangeWrapper } from "../../types/changes";
+import { ChangeWrapper } from "../../../types/changes";
type Props = {
name: string;
@@ -71,7 +71,7 @@ export function ChangeFilterSelector({
return (
- {name}:{" "}
+ {name}:
{Array.from(options.keys())
diff --git a/Frontend/src/components/changes/filters/ChangesFilters.tsx b/Frontend/src/components/changes/filters/ChangesFilters.tsx
new file mode 100644
index 0000000..944e79d
--- /dev/null
+++ b/Frontend/src/components/changes/filters/ChangesFilters.tsx
@@ -0,0 +1,93 @@
+import React, { Dispatch, SetStateAction, useCallback } from "react";
+import { useId } from "react-id-generator";
+import { Button, Col, Container, Row, UncontrolledCollapse } from "reactstrap";
+import { Filter } from "../../../hooks/useChanges";
+import { ChangeObjectType, ChangeType, ChangeWrapper } from "../../../types/changes";
+import { ChangeFilterRegexInput } from "./ChangeFilterRegexInput";
+import { ChangeFilterSelector } from "./ChangeFilterSelector";
+
+type Props = {
+ changes: ChangeWrapper[];
+ filter: Filter;
+ setFilter: Dispatch>;
+};
+
+const changeTypes = Object.keys(ChangeType).filter((t) => typeof t === "string");
+const changeObjectTypes = Object.keys(ChangeObjectType).filter((t) => typeof t === "string");
+
+export function ChangesFilters({ changes, setFilter }: Props): React.ReactElement {
+ const [filtersTogglerId] = useId();
+
+ const useFilterChanged = (
+ filterType: keyof Filter,
+ ): ((elements: Map | RegExp) => void) =>
+ useCallback(
+ (filter: Map | RegExp) =>
+ setFilter((f) => ({
+ ...f,
+ [filterType]: filter,
+ })),
+ [filterType],
+ );
+
+ const onRepositoriesFilterChanged = useFilterChanged("repositories");
+ const onUsersFilterChanged = useFilterChanged("users");
+ const onTypesFilterChanged = useFilterChanged("types");
+ const onObjectTypesFilterChanged = useFilterChanged("objectTypes");
+
+ const onBranchFilterChanged = useFilterChanged("branchRegex");
+ const onTagFilterChanged = useFilterChanged("tagRegex");
+ const onCommitFilterChanged = useFilterChanged("commitRegex");
+
+ return (
+
+
+
+
+
+
+
+ c.repository, [])}
+ onChanged={onRepositoriesFilterChanged}
+ />
+ (c.change.user ? c.change.user.name : ""), [])}
+ textSelector={useCallback((o: string) => (o === "" ? No user : o), [])}
+ onChanged={onUsersFilterChanged}
+ />
+ c.change.type, [])}
+ onChanged={onTypesFilterChanged}
+ />
+ c.change.objectType, [])}
+ onChanged={onObjectTypesFilterChanged}
+ />
+
+
+
+
+
+
+
+ );
+}
diff --git a/Frontend/src/components/changes/filters/CustomFilterCreationModal.tsx b/Frontend/src/components/changes/filters/CustomFilterCreationModal.tsx
new file mode 100644
index 0000000..91d0aad
--- /dev/null
+++ b/Frontend/src/components/changes/filters/CustomFilterCreationModal.tsx
@@ -0,0 +1,17 @@
+import React from "react";
+import { Modal, ModalBody, ModalHeader } from "reactstrap";
+import { ChangeWrapper } from "../../../types/changes";
+
+type Props = {
+ onAddFilter: (accessor: (change: ChangeWrapper) => string, regex: RegExp) => void;
+ onClose: () => void;
+};
+
+export default function CustomFilterCreationModal({ onAddFilter, onClose }: Props): React.ReactElement {
+ return (
+
+
+
+
+ );
+}
diff --git a/Frontend/src/hooks/useChanges.ts b/Frontend/src/hooks/useChanges.ts
index e10d8ba..4b13982 100644
--- a/Frontend/src/hooks/useChanges.ts
+++ b/Frontend/src/hooks/useChanges.ts
@@ -1,7 +1,7 @@
import { Dispatch, SetStateAction, useCallback, useEffect, useMemo, useState } from "react";
import * as uuid from "uuid";
import { config } from "../Config";
-import { ChangesNotification, ChangeWrapper } from "../types/changes";
+import { ChangeObjectType, ChangesNotification, ChangeWrapper } from "../types/changes";
import { useLocalStorage } from "./useLocalStorage";
import { useSignalR } from "./useSignalR";
@@ -10,6 +10,14 @@ export type Filter = {
users: Map;
types: Map;
objectTypes: Map;
+ branchRegex: RegExp;
+ tagRegex: RegExp;
+ commitRegex: RegExp;
+};
+
+export type CustomFilter = {
+ accessor: (change: ChangeWrapper) => string;
+ regex: RegExp;
};
export type ChangesData = {
@@ -35,6 +43,9 @@ export function useChanges(): ChangesData {
users: new Map(),
types: new Map(),
objectTypes: new Map(),
+ branchRegex: /^.*$/,
+ tagRegex: /^.*$/,
+ commitRegex: /^.*$/,
});
const [notifyHiddenChanges, setNotifyHiddenChanges] = useState(true);
@@ -48,6 +59,21 @@ export function useChanges(): ChangesData {
return true;
} else if (!filter.objectTypes.get(change.change.objectType)) {
return true;
+ } else if (
+ change.change.objectType === ChangeObjectType.Branch &&
+ !filter.branchRegex.test(change.change.objectName)
+ ) {
+ return true;
+ } else if (
+ change.change.objectType === ChangeObjectType.Tag &&
+ !filter.tagRegex.test(change.change.objectName)
+ ) {
+ return true;
+ } else if (
+ change.change.objectType === ChangeObjectType.Commit &&
+ !filter.commitRegex.test(change.change.objectName)
+ ) {
+ return true;
}
return false;
diff --git a/docker-compose.yml b/docker-compose.yml
index 317ed7f..134cee2 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -8,6 +8,6 @@ services:
- "80:80"
- "443:443"
volumes:
- - "./data:/app/data"
+ - "./Backend/data:/app/data"
environment:
Application__ConfigPath: ./data/config.yml
\ No newline at end of file
diff --git a/git-monitor.sln b/git-monitor.sln
new file mode 100644
index 0000000..a5e46dd
--- /dev/null
+++ b/git-monitor.sln
@@ -0,0 +1,48 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio 15
+VisualStudioVersion = 15.0.26124.0
+MinimumVisualStudioVersion = 15.0.26124.0
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "git-monitor", "Backend\Backend.csproj", "{3DAC0622-D874-4188-9EBD-3B919B7E7322}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Backend.Tests", "Backend.Tests\Backend.Tests.csproj", "{6BDE5219-30FD-4E77-BDEA-AE3573E37A3A}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Debug|x64 = Debug|x64
+ Debug|x86 = Debug|x86
+ Release|Any CPU = Release|Any CPU
+ Release|x64 = Release|x64
+ Release|x86 = Release|x86
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {3DAC0622-D874-4188-9EBD-3B919B7E7322}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {3DAC0622-D874-4188-9EBD-3B919B7E7322}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {3DAC0622-D874-4188-9EBD-3B919B7E7322}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {3DAC0622-D874-4188-9EBD-3B919B7E7322}.Debug|x64.Build.0 = Debug|Any CPU
+ {3DAC0622-D874-4188-9EBD-3B919B7E7322}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {3DAC0622-D874-4188-9EBD-3B919B7E7322}.Debug|x86.Build.0 = Debug|Any CPU
+ {3DAC0622-D874-4188-9EBD-3B919B7E7322}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {3DAC0622-D874-4188-9EBD-3B919B7E7322}.Release|Any CPU.Build.0 = Release|Any CPU
+ {3DAC0622-D874-4188-9EBD-3B919B7E7322}.Release|x64.ActiveCfg = Release|Any CPU
+ {3DAC0622-D874-4188-9EBD-3B919B7E7322}.Release|x64.Build.0 = Release|Any CPU
+ {3DAC0622-D874-4188-9EBD-3B919B7E7322}.Release|x86.ActiveCfg = Release|Any CPU
+ {3DAC0622-D874-4188-9EBD-3B919B7E7322}.Release|x86.Build.0 = Release|Any CPU
+ {6BDE5219-30FD-4E77-BDEA-AE3573E37A3A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {6BDE5219-30FD-4E77-BDEA-AE3573E37A3A}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {6BDE5219-30FD-4E77-BDEA-AE3573E37A3A}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {6BDE5219-30FD-4E77-BDEA-AE3573E37A3A}.Debug|x64.Build.0 = Debug|Any CPU
+ {6BDE5219-30FD-4E77-BDEA-AE3573E37A3A}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {6BDE5219-30FD-4E77-BDEA-AE3573E37A3A}.Debug|x86.Build.0 = Debug|Any CPU
+ {6BDE5219-30FD-4E77-BDEA-AE3573E37A3A}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {6BDE5219-30FD-4E77-BDEA-AE3573E37A3A}.Release|Any CPU.Build.0 = Release|Any CPU
+ {6BDE5219-30FD-4E77-BDEA-AE3573E37A3A}.Release|x64.ActiveCfg = Release|Any CPU
+ {6BDE5219-30FD-4E77-BDEA-AE3573E37A3A}.Release|x64.Build.0 = Release|Any CPU
+ {6BDE5219-30FD-4E77-BDEA-AE3573E37A3A}.Release|x86.ActiveCfg = Release|Any CPU
+ {6BDE5219-30FD-4E77-BDEA-AE3573E37A3A}.Release|x86.Build.0 = Release|Any CPU
+ EndGlobalSection
+EndGlobal