Skip to content

Commit

Permalink
Merge pull request #207 from jorgenbele/feat-incidents-use-filter-min…
Browse files Browse the repository at this point in the history
…imal

Add functionality for using predefined filters in incident filter toolbar
  • Loading branch information
hmpf committed Jan 20, 2021
2 parents cf3f460 + 32bb82c commit 49d9340
Show file tree
Hide file tree
Showing 10 changed files with 257 additions and 35 deletions.
4 changes: 3 additions & 1 deletion src/api/hooks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
Timeslot,
TimeslotPK,
} from "../api";
import { toMap, pkGetter } from "../utils";
import { toMap, pkGetter, identity } from "../utils";

type UsePromiseReturnType<R> = {
result: R | undefined;
Expand Down Expand Up @@ -84,6 +84,8 @@ export const useApiNotificationProfiles = (
onResult?: (result: Map<NotificationProfilePK, NotificationProfile>) => void,
) => createUsePromise<NotificationProfile[], Map<NotificationProfilePK, NotificationProfile>>(asMap, onResult);

export const useApiFiltersList = () => createUsePromise<Filter[], Filter[]>((filters: Filter[]) => filters);

export const useApiFilters = (onResult?: (result: Map<FilterPK, Filter>) => void) =>
createUsePromise<Filter[], Map<FilterPK, Filter>>(asMap, onResult);

Expand Down
2 changes: 2 additions & 0 deletions src/api/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,8 @@ export type IncidentsFilterOptions = {
sourceSystemIds?: number[] | string[];
sourceSystemNames?: string[];
tags?: string[];

filter?: Filter["pk"];
// NOT COMPLETE
};

Expand Down
92 changes: 84 additions & 8 deletions src/components/incident/IncidentFilterToolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@ import Typography from "@material-ui/core/Typography";
import Toolbar from "@material-ui/core/Toolbar";
import IconButton from "@material-ui/core/IconButton";
import SettingsIcon from "@material-ui/icons/Settings";
import Autocomplete from "@material-ui/lab/Autocomplete";
import TextField from "@material-ui/core/TextField";

import InputLabel from "@material-ui/core/InputLabel";
import MenuItem from "@material-ui/core/MenuItem";
import FormHelperText from "@material-ui/core/FormHelperText";
import FormControl from "@material-ui/core/FormControl";
import Select from "@material-ui/core/Select";

import { ENABLE_WEBSOCKETS_SUPPORT } from "../../config";

Expand All @@ -14,7 +22,7 @@ import { IncidentsFilter, AutoUpdate } from "../../components/incidenttable/Filt
import TagSelector, { Tag } from "../../components/tagselector";
import SourceSelector from "../../components/sourceselector";

import api, { IncidentMetadata, SourceSystem } from "../../api";
import api, { Filter, IncidentMetadata, SourceSystem } from "../../api";

import { createStyles, makeStyles, Theme } from "@material-ui/core/styles";

Expand Down Expand Up @@ -74,6 +82,7 @@ type ButtonGroupSwitchPropsType<T> = {
getLabel: (option: T) => string;
getColor: (selected: boolean) => "inherit" | "default" | "primary";
onSelect: (option: T) => void;
disabled?: boolean;
};

export function ButtonGroupSwitch<T>({
Expand All @@ -82,12 +91,18 @@ export function ButtonGroupSwitch<T>({
getLabel,
getColor,
onSelect,
disabled,
}: ButtonGroupSwitchPropsType<T>) {
return (
<ButtonGroup variant="contained" color="default" aria-label="text primary button group">
{options.map((option: T, index: number) => {
return (
<Button key={index} color={getColor(selected === option)} onClick={() => onSelect(option)}>
<Button
disabled={disabled}
key={index}
color={getColor(selected === option)}
onClick={() => onSelect(option)}
>
{getLabel(option)}
</Button>
);
Expand Down Expand Up @@ -136,6 +151,45 @@ export const DropdownToolbar: React.FC<DropdownToolbarPropsType> = ({
);
};

type UseExistingFilterToolbarItemPropsType = {
selectedFilter: number;
existingFilters: Filter[];
onSelect: (filterIndex: number) => void;
className?: string;
};

export const UseExistingFilterToolbarItem: React.FC<UseExistingFilterToolbarItemPropsType> = ({
selectedFilter,
existingFilters,
onSelect,
className,
}: UseExistingFilterToolbarItemPropsType) => {
return (
<FormControl size="small" className={className}>
<Select
displayEmpty
variant="outlined"
labelId="filter-select"
id="filter-select"
value={selectedFilter}
onChange={(event: React.ChangeEvent<{ value: unknown }>) => {
onSelect(event.target.value as number);
}}
>
<MenuItem key="none" value={-1}>
None
</MenuItem>
{existingFilters.map((filter: Filter, index: number) => (
<MenuItem key={filter.pk} value={index}>
{filter.name}
</MenuItem>
))}
</Select>
<FormHelperText>Select from your filters</FormHelperText>
</FormControl>
);
};

type MoreSettingsToolbarItemPropsType = {
open: boolean;
onChange: (open: boolean) => void;
Expand Down Expand Up @@ -163,13 +217,19 @@ export const MoreSettingsToolbarItem: React.FC<MoreSettingsToolbarItemPropsType>
};

type IncidentFilterToolbarPropsType = {
existingFilter: number;
existingFilters: Filter[];
filter: IncidentsFilter;
onExistingFilterChange: (filterIndex: number) => void;
onFilterChange: (filter: IncidentsFilter) => void;
disabled?: boolean;
};

export const IncidentFilterToolbar: React.FC<IncidentFilterToolbarPropsType> = ({
existingFilter,
existingFilters,
filter,
onExistingFilterChange,
onFilterChange,
disabled,
}: IncidentFilterToolbarPropsType) => {
Expand Down Expand Up @@ -204,9 +264,13 @@ export const IncidentFilterToolbar: React.FC<IncidentFilterToolbarPropsType> = (
? ["never", "realtime", "interval"]
: ["never", "interval"];

const useExistingFilter =
existingFilter != -1 && existingFilter >= 0 && existingFilter < existingFilters.length ? true : false;

const autoUpdateToolbarItem = (
<ToolbarItem name="Auto Update">
<ButtonGroupSwitch
disabled={useExistingFilter}
selected={filter.autoUpdate}
options={autoUpdateOptions}
getLabel={(autoUpdate: AutoUpdate) =>
Expand All @@ -232,7 +296,8 @@ export const IncidentFilterToolbar: React.FC<IncidentFilterToolbarPropsType> = (
<Toolbar className={style.toolbarContainer}>
<ToolbarItem name="Open State">
<ButtonGroupSwitch
selected={filter.show}
disabled={useExistingFilter}
selected={useExistingFilter ? "open" : filter.show}
options={["open", "closed", "both"]}
getLabel={(show: "open" | "closed" | "both") => ({ open: "Open", closed: "Closed", both: "Both" }[show])}
getColor={(selected: boolean) => (selected ? "primary" : "default")}
Expand All @@ -242,7 +307,8 @@ export const IncidentFilterToolbar: React.FC<IncidentFilterToolbarPropsType> = (

<ToolbarItem name="Acked">
<ButtonGroupSwitch
selected={filter.showAcked}
disabled={useExistingFilter}
selected={useExistingFilter ? true : filter.showAcked}
options={[false, true]}
getLabel={(showAcked: boolean) => (showAcked ? "Both" : "Unacked")}
getColor={(selected: boolean) => (selected ? "primary" : "default")}
Expand All @@ -252,7 +318,7 @@ export const IncidentFilterToolbar: React.FC<IncidentFilterToolbarPropsType> = (

<ToolbarItem name="Sources" className={classNames(style.medium)}>
<SourceSelector
disabled={disabled}
disabled={disabled || useExistingFilter}
sources={knownSources}
onSelectionChange={(selection: string[]) => {
onSourcesChange((selection.length !== 0 && selection) || "AllSources");
Expand All @@ -261,10 +327,20 @@ export const IncidentFilterToolbar: React.FC<IncidentFilterToolbarPropsType> = (
/>
</ToolbarItem>

<ToolbarItem name="Tags" className={classNames(style.large, style.rightAligned)}>
<TagSelector disabled={disabled} tags={filter.tags} onSelectionChange={handleTagSelectionChange} />
<ToolbarItem name="Tags" className={classNames(style.medium)}>
<TagSelector
disabled={disabled || useExistingFilter}
tags={filter.tags}
onSelectionChange={handleTagSelectionChange}
/>
</ToolbarItem>
<ToolbarItem name="Filter" className={classNames(style.medium)}>
<UseExistingFilterToolbarItem
selectedFilter={existingFilter}
existingFilters={existingFilters}
onSelect={(filterIndex: number) => onExistingFilterChange(filterIndex)}
/>
</ToolbarItem>

<MoreSettingsToolbarItem
open={dropdownToolbarOpen}
onChange={(open: boolean) => setDropdownToolbarOpen(open)}
Expand Down
3 changes: 2 additions & 1 deletion src/components/incidenttable/FilteredIncidentTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import "../../components/incidenttable/incidenttable.css";
import { Tag } from "../../components/tagselector";
import TablePagination from "@material-ui/core/TablePagination";

import api, { IncidentsFilterOptions } from "../../api";
import api, { Filter, IncidentsFilterOptions } from "../../api";
import { useApiPaginatedIncidents } from "../../api/hooks";

import { DEFAULT_AUTO_REFRESH_INTERVAL } from "../../config";
Expand Down Expand Up @@ -51,6 +51,7 @@ export type IncidentsFilter = {
};

type FilteredIncidentsTablePropsType = {
existingFilter?: Filter["pk"];
filter: IncidentsFilter;
onLoad?: () => void;
};
Expand Down
36 changes: 36 additions & 0 deletions src/contexts.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import React, { createContext, useReducer } from "react";

import { Filter } from "./api";
import { filterReducer, FilterActions } from "./reducers/filter";

export type InitialStateType = {
// List of all filters that the currently
// logged in user has access to.
filters: Filter[];
};

const initialState: InitialStateType = {
filters: [],
};

type ActionsType = FilterActions /* | OtherAction | AnotherActoin ... */;

const AppContext = createContext<{
state: InitialStateType;
dispatch: React.Dispatch<ActionsType>;
}>({
state: initialState,
dispatch: () => null,
});

const mainReducer = ({ filters }: InitialStateType, action: ActionsType) => ({
filters: filterReducer(filters, action),
});

const AppProvider: React.FC = ({ children }: { children?: React.ReactNode }) => {
const [state, dispatch] = useReducer(mainReducer, initialState);

return <AppContext.Provider value={{ state, dispatch }}>{children}</AppContext.Provider>;
};

export { AppContext, AppProvider };
6 changes: 5 additions & 1 deletion src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,17 @@ import ReactDOM from "react-dom";
import "./index.css";
import App from "./App";
import * as serviceWorker from "./serviceWorker";
import { AppProvider } from "./contexts";
// TODO: Remove use of store provider
import { StoreProvider } from "./store";
import { BrowserRouter } from "react-router-dom";

ReactDOM.render(
<BrowserRouter>
<StoreProvider>
<App />
<AppProvider>
<App />
</AppProvider>
</StoreProvider>
</BrowserRouter>,
document.getElementById("root"),
Expand Down
10 changes: 10 additions & 0 deletions src/reducers/common.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export type ActionMap<M extends { [index: string]: any }> = {
[Key in keyof M]: M[Key] extends undefined
? {
type: Key;
}
: {
type: Key;
payload: M[Key];
};
};
55 changes: 55 additions & 0 deletions src/reducers/filter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { Filter } from "../api";

import { ActionMap } from "./common";

export enum FilterType {
Create = "CREATE_FILTER",
Delete = "DELETE_FILTER",
Modify = "MODIFY_FILTERS",
LoadAll = "LOAD_FILTERS",
}

type FilterPayload = {
// Used to override all local modifications with
// data from backend
[FilterType.LoadAll]: Filter[];

// When a filter is modified.
[FilterType.Modify]: Filter;

// When a new filter is created.
[FilterType.Create]: Filter;

// When a filter is deleted
[FilterType.Delete]: Filter["pk"];
};

export type FilterActions = ActionMap<FilterPayload>[keyof ActionMap<FilterPayload>];
export const filterReducer = (state: Filter[], action: FilterActions) => {
switch (action.type) {
case FilterType.LoadAll:
return action.payload;
case FilterType.Modify: {
const index = state.findIndex((f: Filter) => f.pk === action.payload.pk);
const updated = [...state];
updated[index] = { ...updated[index], ...action.payload };
return updated;
}
case FilterType.Create:
return [...state, action.payload];
case FilterType.Delete:
return [...state.filter((filter: Filter) => filter.pk !== action.payload)];
}
};

type Action<T, P> = (payload: P) => { type: T; payload: P };
function makeAction<T, P>(type: T): Action<T, P> {
return (payload: P): { type: T; payload: P } => {
return { type, payload };
};
}

export const loadAllFilters = makeAction<FilterType.LoadAll, Filter[]>(FilterType.LoadAll);
export const modifyFilter = makeAction<FilterType.Modify, Filter>(FilterType.Modify);
export const createFilter = makeAction<FilterType.Create, Filter>(FilterType.Create);
export const deleteFilter = makeAction<FilterType.Delete, Filter["pk"]>(FilterType.Delete);
Loading

0 comments on commit 49d9340

Please sign in to comment.