Skip to content

Commit

Permalink
Package detail page (#187)
Browse files Browse the repository at this point in the history
* Show package details (name and description)
* Link from package overview to package detail page
* Render settings form if available (via react-jsonschema-form)
* ADB wrapper functionalities to push and pull files
  • Loading branch information
benlumley authored Oct 8, 2022
1 parent 9e7c47f commit 41947ca
Show file tree
Hide file tree
Showing 27 changed files with 526 additions and 9 deletions.
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@
"@mui/icons-material": "^5.6.2",
"@mui/material": "^5.6.3",
"@reduxjs/toolkit": "^1.8.1",
"@rjsf/core": "^5.0.0-beta.10",
"@rjsf/mui": "^5.0.0-beta.10",
"@rjsf/utils": "^5.0.0-beta.10",
"@rjsf/validator-ajv6": "^5.0.0-beta.10",
"@sentry/react": "^7.5.0",
"@sentry/tracing": "^7.5.0",
"@testing-library/jest-dom": "^5.16.4",
Expand Down
8 changes: 8 additions & 0 deletions src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import Header from "./features/navigation/Header";
import Healthcheck from "./features/healthcheck/Healthcheck";
import Home from "./features/home/Main";
import Packages from "./features/packages/Packages";
import Package from "./features/package/Package";
import Startup from "./features/startup/Startup";

import Setup from "./features/setup/Setup";
Expand All @@ -43,6 +44,7 @@ import { selectPassed } from "./features/healthcheck/healthcheckSlice";

import { selectCanClaim } from "./features/tabGovernor/tabGovernorSlice";


function App({
adb,
handleAdbConnectClick,
Expand Down Expand Up @@ -122,6 +124,12 @@ function App({
render={isConnected && adb}
/>

<Route
element={(isConnected && adb) ? <Package adb={adb} /> : null}
path="package/:repo/:packageSlug"
render={isConnected && adb}
/>

<Route
element={(isConnected && adb) ? <Startup adb={adb} /> : null}
path="startup"
Expand Down
2 changes: 2 additions & 0 deletions src/app/store.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import deviceReducer from "../features/device/deviceSlice";
import donateReducer from "../features/donate/donateSlice";
import healthcheckReducer from "../features/healthcheck/healthcheckSlice";
import packagesReducer from "../features/packages/packagesSlice";
import packageReducer from "../features/package/packageSlice";
import rootReducer from "../features/root/rootSlice";
import settingsReducer from "../features/settings/settingsSlice";
import startupReducer from "../features/startup/startupSlice";
Expand All @@ -17,6 +18,7 @@ export const store = configureStore({
donate: donateReducer,
healthcheck: healthcheckReducer,
packages: packagesReducer,
package: packageReducer,
root: rootReducer,
settings: settingsReducer,
startup: startupReducer,
Expand Down
136 changes: 136 additions & 0 deletions src/features/package/Package.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import PropTypes from "prop-types";
import React, {
useCallback,
useEffect,
} from "react";
import {
useDispatch,
useSelector,
} from "react-redux";
import { useTranslation } from "react-i18next";
import { useParams } from "react-router-dom";

import validator from "@rjsf/validator-ajv6";
import Form from "@rjsf/mui";

import Alert from "@mui/material/Alert";
import Box from "@mui/material/Box";
import Button from "@mui/material/Button";
import Paper from "@mui/material/Paper";
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";

import {
fetchPackage,
fetchConfig,
reset,
selectConfig,
selectDescription,
selectError,
selectFetched,
selectInstalled,
selectLoading,
selectName,
selectSchema,
selectWriting,
writeConfig,
} from "./packageSlice";

import { selectPassed } from "../healthcheck/healthcheckSlice";
import Spinner from "../loading/Spinner";

export default function Package({ adb }) {
const { t } = useTranslation("package");
const dispatch = useDispatch();

let { packageSlug } = useParams();

const healthchecksPassed = useSelector(selectPassed);

const packageName = useSelector(selectName);
const description = useSelector(selectDescription);
const installed = useSelector(selectInstalled);

const fetched = useSelector(selectFetched);
const config = useSelector(selectConfig);
const schema = useSelector(selectSchema);

const loading = useSelector(selectLoading);
const writing = useSelector(selectWriting);
const error = useSelector(selectError);

/**
* Fetch package details if healthchecks passed and dtails are not yet
* set for the selected package.
*/
useEffect(() => {
if (!fetched && healthchecksPassed) {
dispatch(fetchPackage({
adb,
name: packageSlug,
}));
}
}, [adb, dispatch, fetched, healthchecksPassed, packageSlug]);

useEffect(() => {
if(packageName !== packageSlug) {
dispatch(reset());
}
}, [dispatch, packageName, packageSlug]);

// Fetch config and schema if package is installed
useEffect(() => {
if(installed) {
dispatch(fetchConfig(adb));
}
}, [adb, dispatch, installed]);

const saveConfig = useCallback(({ formData }) => {
dispatch(writeConfig({
adb,
config: formData,
}));
}, [adb, dispatch]);

return (
<Paper>
<Box p={2}>
<Stack spacing={2}>
<Typography variant="h4">
{t("detailsFor", { name: packageSlug })}
</Typography>

{loading &&
<Spinner text={t("loading")} />}

<Typography variant="body1">
{description}
</Typography>

{schema &&
<Form
formData={config}
onSubmit={saveConfig}
schema={JSON.parse(JSON.stringify(schema))}
validator={validator}
>
<Button
disabled={writing}
type="submit"
variant="contained"
>
{t("submit")}
</Button>
</Form>}

{error &&
<Alert severity="error">
{error}
</Alert>}
</Stack>
</Box>
</Paper>
);
}

Package.propTypes = { adb: PropTypes.shape().isRequired };
102 changes: 102 additions & 0 deletions src/features/package/packageSlice.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import {
createAsyncThunk,
createSlice,
} from "@reduxjs/toolkit";

const initialState = {
loading: true,
fetched: false,

name: null,
description: null,
installed: false,

schema: null,
config: null,

writing: false,
error: null,
};

export const fetchPackage = createAsyncThunk(
"package/fetchPackage",
async ({
adb,
name,
}) => {
return adb.getPackageDetails(name);
}
);

export const fetchConfig = createAsyncThunk(
"package/fetchConfig",
async (adb, thunk) => {
const name = thunk.getState().package.name;
return adb.getPackageConfig(name);
}
);

export const writeConfig = createAsyncThunk(
"package/writeConfig",
async ({
adb,
config,
}, thunk) => {
const name = thunk.getState().package.name;
const stringified = JSON.stringify(config, null, " ");
await adb.writePackageConfig(name, stringified);

return config;
}
);

export const packageSlice = createSlice({
name: "package",
initialState,
reducers: { reset: () => initialState },
extraReducers: (builder) => {
builder
.addCase(fetchPackage.pending, (state) => {
state.loading = true;
})
.addCase(fetchPackage.fulfilled, (state, action) => {
state.loading = false;
state.fetched = true;

state.name = action.payload.name;
state.description = action.payload.description;
state.installed = action.payload.installed;
}).addCase(fetchConfig.pending, (state, action) => {
state.config = null;
state.schema = null;
}).addCase(fetchConfig.fulfilled, (state, action) => {
state.config = action.payload.config;
state.schema = action.payload.schema;
}).addCase(writeConfig.pending, (state, action) => {
state.writing = true;
}).addCase(writeConfig.fulfilled, (state, action) => {
state.config = action.payload;
state.writing = false;
}).addCase(writeConfig.rejected, (state, action) => {
state.error = action.error.message;
});
},

});

export const { reset } = packageSlice.actions;

export const selectFetched = (state) => state.package.fetched;

export const selectName = (state) => state.package.name;
export const selectDescription = (state) => state.package.description;
export const selectInstalled = (state) => state.package.installed;

export const selectWriting = (state) => state.package.writing;
export const selectError = (state) => state.package.error;
export const selectLoading = (state) => state.package.loading;

export const selectConfig = (state) => state.package.config;
export const selectSchema = (state) => state.package.schema;

export default packageSlice.reducer;
21 changes: 19 additions & 2 deletions src/features/packages/Packages.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import {
} from "react-redux";
import { useTranslation } from "react-i18next";

import { Link as RouterLink } from "react-router-dom";

import Box from "@mui/material/Box";
import DeleteIcon from "@mui/icons-material/Delete";
import DownloadIcon from "@mui/icons-material/Download";
Expand All @@ -31,6 +33,7 @@ import TableRow from "@mui/material/TableRow";
import TextField from "@mui/material/TextField";
import Typography from "@mui/material/Typography";
import Paper from "@mui/material/Paper";
import { styled } from "@mui/material/styles";

import ReactGA from "react-ga4";

Expand Down Expand Up @@ -66,6 +69,16 @@ export default function Packages({ adb }) {
const tableEl = useRef();
const scrollListenerId = useRef();

const StyledRouterLink = styled(RouterLink)(() => ({
"&": {
whiteSpace: "nowrap",
color: "#1676c7",
textDecoration: "underline",
textDecorationColor: "rgba(22, 118, 199, 0.4)",
},
"&:hover": { textDecorationColor: "inherit" },
}));

const dispatch = useDispatch();

const fetched = useSelector(selectFetched);
Expand Down Expand Up @@ -172,11 +185,14 @@ export default function Packages({ adb }) {
}, [dispatch]);

const rows = renderRows.map((item) => {
console.log(item.details.homepage);
return (
<TableRow key={item.name}>
<TableCell sx={{ width: 250 }}>
{item.name}
<Typography variant="body2">
<StyledRouterLink to={`/package/${item.repo}/${item.name}`}>
{item.name}
</StyledRouterLink>
</Typography>
</TableCell>

<TableCell sx={{
Expand Down Expand Up @@ -290,6 +306,7 @@ export default function Packages({ adb }) {
});

const packageString = t("matchCount", { count: filtered.length } );

return (
<>
{!hasOpkgBinary &&
Expand Down
1 change: 1 addition & 0 deletions src/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ for(const lang of languageKeys) {
healthcheck: require(`./translations/${lang}/healthcheck.json`),
home: require(`./translations/${lang}/home.json`),
navigation: require(`./translations/${lang}/navigation.json`),
package: require(`./translations/${lang}/package.json`),
packages: require(`./translations/${lang}/packages.json`),
root: require(`./translations/${lang}/root.json`),
settings: require(`./translations/${lang}/settings.json`),
Expand Down
3 changes: 3 additions & 0 deletions src/translations/de/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"detailsFor": "Details for:"
}
3 changes: 3 additions & 0 deletions src/translations/el/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"detailsFor": "Details for: {{name}}"
}
5 changes: 5 additions & 0 deletions src/translations/en/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"detailsFor": "Details for {{name}}",
"submit": "Save settings",
"loading": "Loading package details..."
}
3 changes: 3 additions & 0 deletions src/translations/es/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"detailsFor": "Details for: {{name}}"
}
3 changes: 3 additions & 0 deletions src/translations/fr/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"detailsFor": "Details for: {{name}}"
}
3 changes: 3 additions & 0 deletions src/translations/it/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"detailsFor": "Details for: {{name}}"
}
3 changes: 3 additions & 0 deletions src/translations/nl/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"detailsFor": "Details for: {{name}}"
}
Loading

0 comments on commit 41947ca

Please sign in to comment.