Skip to content

Commit

Permalink
refactor: use RTK query for kit configurations rather than redux
Browse files Browse the repository at this point in the history
  • Loading branch information
tomcur committed Feb 6, 2024
1 parent 863033d commit c561df2
Show file tree
Hide file tree
Showing 20 changed files with 160 additions and 358 deletions.
163 changes: 0 additions & 163 deletions src/modules/kit/reducer.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import {
combineReducers,
createEntityAdapter,
createSelector,
createSlice,
} from "@reduxjs/toolkit";
import * as actions from "./actions";
Expand Down Expand Up @@ -147,172 +146,10 @@ const kitsSlice = createSlice({
}),
});

const configurationsAdapter = createEntityAdapter<KitConfigurationState>({
selectId: (configuration) => configuration.id,
});

const configurationsSlice = createSlice({
name: "configurations",
initialState: configurationsAdapter.getInitialState(),
reducers: {},
extraReducers: (builder) =>
builder
.addCase(actions.kitConfigurationsSuccess, (state, action) => {
configurationsAdapter.setMany(
state,
action.payload.configurations.map((conf) => ({
...conf,
peripherals: conf.peripherals.map((peripheral) => peripheral.id),
})),
);
})
.addCase(actions.kitConfigurationCreated, (state, action) => {
configurationsAdapter.setOne(state, {
...action.payload.configuration,
peripherals: [],
});
})
.addCase(actions.kitConfigurationUpdated, (state, action) => {
configurationsAdapter.updateOne(state, {
id: action.payload.configuration.id,
changes: action.payload.configuration,
});
})
.addCase(actions.kitConfigurationDeleted, (state, action) => {
configurationsAdapter.removeOne(
state,
action.payload.kitConfigurationId,
);
})
.addCase(actions.kitSetAllConfigurationsInactive, (state, action) => {
// O-linear time in the number of loaded configurations regardless of
// the number of kits. Not very efficient if many kits are loaded.
Object.values(state.entities)
.filter((conf) => conf?.kitId === action.payload.kitId)
.forEach((conf) => (conf!.active = false));
})
.addCase(actions.peripheralCreated, (state, action) => {
const configuration =
state.entities[action.payload.peripheral.kitConfigurationId];
if (configuration !== undefined) {
configuration.peripherals.push(action.payload.peripheral.id);
}
})
.addCase(actions.peripheralDeleted, (state, action) => {
const configuration = state.entities[action.payload.kitConfigurationId];
if (configuration !== undefined) {
configuration.peripherals = configuration.peripherals.filter(
(id) => id !== action.payload.peripheralId,
);
}
})
.addCase(actions.deleteKit, (state, action) => {
const ids = Object.values(state.entities)
.filter(
(configuration) => configuration?.kitId === action.payload.kitId,
)
.map((configuration) => configuration!.id);

configurationsAdapter.removeMany(state, ids);
}),
});

const peripheralsAdapter = createEntityAdapter<schemas["Peripheral"]>({
selectId: (peripheral) => peripheral.id,
});

const peripheralsSlice = createSlice({
name: "peripherals",
initialState: peripheralsAdapter.getInitialState(),
reducers: {},
extraReducers: (builder) =>
builder
.addCase(actions.kitConfigurationsSuccess, (state, action) => {
peripheralsAdapter.setMany(
state,
action.payload.configurations.flatMap((conf) => conf.peripherals),
);
})
.addCase(actions.peripheralCreated, (state, action) => {
peripheralsAdapter.addOne(state, action.payload.peripheral);
})
.addCase(actions.peripheralUpdated, (state, action) => {
peripheralsAdapter.setOne(state, action.payload.peripheral);
})
.addCase(actions.peripheralDeleted, (state, action) => {
peripheralsAdapter.removeOne(state, action.payload.peripheralId);
})
.addCase(actions.kitConfigurationDeleted, (state, action) => {
// Find peripherals belonging to the given configuration
const ids = Object.values(state.entities)
.filter(
(peripheral) =>
peripheral?.kitConfigurationId ===
action.payload.kitConfigurationId,
)
.map((peripheral) => peripheral!.id);

peripheralsAdapter.removeMany(state, ids);
})
.addCase(actions.deleteKit, (state, action) => {
const ids = Object.values(state.entities)
.filter((peripheral) => peripheral?.kitId === action.payload.kitId)
.map((peripheral) => peripheral!.id);

peripheralsAdapter.removeMany(state, ids);
}),
});

export default combineReducers({
kits: kitsSlice.reducer,
configurations: configurationsSlice.reducer,
peripherals: peripheralsSlice.reducer,
});

export const kitSelectors = kitsAdapter.getSelectors(
(state: RootState) => state.kit.kits,
);
export const configurationSelectors = configurationsAdapter.getSelectors(
(state: RootState) => state.kit.configurations,
);
export const peripheralSelectors = peripheralsAdapter.getSelectors(
(state: RootState) => state.kit.peripherals,
);

export const configurationsById = createSelector(
[configurationSelectors.selectEntities, (_state, ids: number[]) => ids],
(configurations, ids) => ids.map((id) => configurations[id]),
);

/**
* Fetch all configurations of a kit by kit serial.
*/
export const allConfigurationsOfKit: (
state: RootState,
serial: string,
) => { [id: string]: KitConfigurationState } | null = createSelector(
[kitSelectors.selectById, configurationSelectors.selectEntities],
(kit, configurations) => {
if (kit === undefined) {
return null;
}

const kitConfigurations = kit.configurations.map(
(configurationId) => configurations[configurationId],
);

// Invariant
if (
kitConfigurations.some((configuration) => configuration === undefined)
) {
throw new Error("expected all kit configurations to be loaded");
}

return (kitConfigurations as KitConfigurationState[]).reduce<{
[id: string]: KitConfigurationState;
}>((acc, val) => {
acc[val.id.toString()] = val;
return acc;
}, {});
},
);
28 changes: 7 additions & 21 deletions src/scenes/kit/Configurations.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,8 @@ import { useContext, useMemo } from "react";
import { Link, Route, Routes } from "react-router-dom";
import { useTranslation } from "react-i18next";

import { useAppDispatch, useAppSelector } from "~/hooks";
import {
KitConfigurationState,
KitState,
configurationsById,
} from "~/modules/kit/reducer";
import { useAppDispatch } from "~/hooks";
import { KitState } from "~/modules/kit/reducer";
import { default as apiButton } from "~/Components/ApiButton";
import { Response, api, schemas } from "~/api";
import {
Expand All @@ -25,7 +21,7 @@ import {
IconPlayerPlay,
IconTrash,
} from "@tabler/icons-react";
import { PermissionsContext } from "./contexts";
import { ConfigurationsContext, PermissionsContext } from "./contexts";

const ApiButton = apiButton<any>();

Expand All @@ -39,7 +35,7 @@ function ConfigurationRow({
showActivate,
}: {
kit: KitState;
configuration: KitConfigurationState;
configuration: schemas["KitConfigurationWithPeripherals"];
showActivate: boolean;
}) {
const { t } = useTranslation();
Expand Down Expand Up @@ -156,24 +152,14 @@ function ConfigurationRow({

export function Configurations({ kit }: ConfigurationsProps) {
const permissions = useContext(PermissionsContext);

const configurations_ = useAppSelector((state) =>
configurationsById(state, kit.configurations),
);
const configurations: KitConfigurationState[] = useMemo(() => {
return configurations_.filter(
// This is clearly safe, but the TS checker doesn't actually prove type safety here :/
// Change the inequality into equality and it still compiles.
(c): c is KitConfigurationState => c !== undefined,
);
}, [configurations_]);
const configurations = useContext(ConfigurationsContext);

const activeConfiguration = useMemo(
() => configurations.find((c) => c.active) ?? null,
() => Object.values(configurations).find((c) => c.active) ?? null,
[configurations],
);
const inactiveConfigurations = useMemo(
() => configurations.filter((c) => !c.active) ?? null,
() => Object.values(configurations).filter((c) => !c.active) ?? null,
[configurations],
);

Expand Down
2 changes: 1 addition & 1 deletion src/scenes/kit/contexts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { KitPermissions, NO_PERMISSIONS } from "~/permissions";

export const KitContext = React.createContext<schemas["Kit"]>(null as any);
export const ConfigurationsContext = React.createContext<{
[id: string]: KitConfigurationState;
[id: string]: schemas["KitConfigurationWithPeripherals"];
}>({});
export const MembershipContext = React.createContext<KitMembership | null>(
null,
Expand Down
80 changes: 46 additions & 34 deletions src/scenes/kit/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,7 @@ import { Menu } from "~/Components/Menu";
import { KitAvatar } from "~/Components/KitAvatar";
import { Badge } from "~/Components/Badge";

import {
KitState,
allConfigurationsOfKit,
kitSelectors,
} from "~/modules/kit/reducer";
import { KitState, kitSelectors } from "~/modules/kit/reducer";
import { startWatching, stopWatching, fetchKit } from "~/modules/kit/actions";
import { KitMembership } from "~/modules/me/reducer";
import { KitPermissions, kitPermissionsFromMembership } from "~/permissions";
Expand All @@ -44,6 +40,8 @@ import Access from "./access";
import Rpc from "./rpc";

import style from "./index.module.css";
import { rtkApi } from "~/services/astroplant";
import { skipToken } from "@reduxjs/toolkit/query";

type Params = { kitSerial: string };

Expand Down Expand Up @@ -105,41 +103,32 @@ const KitDashboard = (props: KitDashboardProps) => {
const { kitState, membership } = props;

const kit = kitState.details!;
const configurations = useAppSelector((state) =>
allConfigurationsOfKit(state, kit.serial),
);

const permissions = useMemo(
() => kitPermissionsFromMembership(membership),
[membership],
);

if (configurations === null) {
return <Loading />;
}

return (
<KitContext.Provider value={kit}>
<ConfigurationsContext.Provider value={configurations}>
<MembershipContext.Provider value={membership}>
<PermissionsContext.Provider value={permissions}>
<KitHeader kit={kit} permissions={permissions} />
<Routes>
{/* redirect 404, or should an error message be given? */}
<Route path="*" element={<Navigate to="data" replace />} />
<Route path="/data/*" element={<KitData kitState={kitState} />} />
<Route
path="/configurations/*"
element={<Configurations kit={kitState} />}
/>
<Route path="/details/*" element={<Details />} />
<Route path="/download" element={<Download />} />
<Route path="/access" element={<Access />} />
<Route path="/rpc" element={<Rpc />} />
</Routes>
</PermissionsContext.Provider>
</MembershipContext.Provider>
</ConfigurationsContext.Provider>
<MembershipContext.Provider value={membership}>
<PermissionsContext.Provider value={permissions}>
<KitHeader kit={kit} permissions={permissions} />
<Routes>
{/* redirect 404, or should an error message be given? */}
<Route path="*" element={<Navigate to="data" replace />} />
<Route path="/data/*" element={<KitData kitState={kitState} />} />
<Route
path="/configurations/*"
element={<Configurations kit={kitState} />}
/>
<Route path="/details/*" element={<Details />} />
<Route path="/download" element={<Download />} />
<Route path="/access" element={<Access />} />
<Route path="/rpc" element={<Rpc />} />
</Routes>
</PermissionsContext.Provider>
</MembershipContext.Provider>
</KitContext.Provider>
);
};
Expand Down Expand Up @@ -170,7 +159,26 @@ const Kit = ({}) => {
}
}, [kitAccessible, kitSerial, startWatching, stopWatching]);

if (kit === undefined) {
const { data: configurations } = rtkApi.useGetKitConfigurationsQuery(
kit === undefined
? skipToken
: {
kitSerial: kit.serial,
},
);
const configurations_ = useMemo(() => {
if (configurations === undefined) {
return;
}

const v: { [id: string]: schemas["KitConfigurationWithPeripherals"] } = {};
for (const conf of configurations) {
v[conf.id] = conf;
}
return v;
}, [configurations]);

if (kit === undefined || configurations === undefined) {
return <Loading />;
}

Expand All @@ -180,7 +188,11 @@ const Kit = ({}) => {
kit.details !== null &&
kit.configurations !== null)
) {
return <KitDashboard kitState={kit} membership={membership} />;
return (
<ConfigurationsContext.Provider value={configurations_!}>
<KitDashboard kitState={kit} membership={membership} />
</ConfigurationsContext.Provider>
);
} else if (
kit.status === "None" ||
kit.status === "Fetching" ||
Expand Down
9 changes: 3 additions & 6 deletions src/scenes/kit/kit-data/Configuration.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,7 @@ import Description from "./configure/Description";
import Rules from "./configure/Rules";
import ActivateDeactivate from "./configure/ActivateDeactivate";
import Peripherals from "./configure/Peripherals";
import {
KitConfigurationState,
KitState,
kitSelectors,
} from "~/modules/kit/reducer";
import { KitState, kitSelectors } from "~/modules/kit/reducer";
import { Button, ButtonLink } from "~/Components/Button";
import { Navigate, Route, Routes } from "react-router-dom";
import { useContext, useState } from "react";
Expand All @@ -22,10 +18,11 @@ import { kitConfigurationCreated } from "~/modules/kit/actions";

import style from "./Configuration.module.css";
import { PermissionsContext } from "../contexts";
import { schemas } from "~/api";

export type ConfigurationProps = {
kit: KitState;
configuration: KitConfigurationState;
configuration: schemas["KitConfigurationWithPeripherals"];
};

function InnerConfiguration({ kit, configuration }: ConfigurationProps) {
Expand Down
Loading

0 comments on commit c561df2

Please sign in to comment.