Skip to content

Commit

Permalink
Split the webapp into components files
Browse files Browse the repository at this point in the history
  • Loading branch information
elwinar committed Nov 25, 2020
1 parent 300cad8 commit 97568c8
Show file tree
Hide file tree
Showing 11 changed files with 86 additions and 66 deletions.
16 changes: 10 additions & 6 deletions web/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,18 +21,18 @@ export default function App() {

// Initialize the state, essentially updating it to use the query encoded in
// the URL, if any.
React.useEffect(function () {
React.useEffect(function initializeQuery() {
const raw = new URLSearchParams(window.location.search).get("q");
if (raw === null) {
return;
}
setQuery(api.decodeQuery(raw));
});
}, []);

// When the query change, we want to run the search query and update
// the cores.
React.useEffect(
function () {
function runQuery() {
api
.search(query)
.then((res) => {
Expand All @@ -54,7 +54,7 @@ export default function App() {
// (or other similar event). Here we add an event listener when the app is
// mounted, and remove it when it's unmounted. The handler is defined in the
// effect function so its reference is the same for the full app lifecycle.
React.useEffect(function () {
React.useEffect(function popstateHandler() {
function handler() {
setQuery(api.decodeQuery(new URLSearchPArams(window.location.search).get("q")));
}
Expand All @@ -69,7 +69,7 @@ export default function App() {
// because the popstate history event already does this, and doing it
// again break the forward-history.
React.useEffect(
function () {
function updateHash() {
const q = api.encodeQuery(query);
if (new URLSearchParams(window.location.search).get("q") === q) {
return;
Expand All @@ -79,6 +79,10 @@ export default function App() {
[query]
);

function queryHandler(q) {
setQuery(q);
}

function deleteCoreHandler() {
if (!window.confirm(`are you sure you want to delete this core?`)) {
return;
Expand All @@ -105,7 +109,7 @@ export default function App() {
return (
<React.Fragment>
<Header />
<Searchbar query={query} />
<Searchbar query={query} onSubmit={queryHandler} />
{error !== null && (
<React.Fragment>
<h2>Unexpected error</h2>
Expand Down
2 changes: 2 additions & 0 deletions web/Core.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import React from "react";
import styles from "./Core.scss";
import { formatSize, formatDate } from "./utils.js";
import api from "./api.js";
import QueryLink from "./QueryLink.js";

// Core is a view of a core's details.
export default function Core({ core, onDelete }) {
Expand Down
7 changes: 6 additions & 1 deletion web/Core.scss
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
@import "./color.scss";
@import "./colors.scss";
@import "./mixins.scss";

.Core {
ul {
Expand Down Expand Up @@ -38,3 +39,7 @@
white-space: pre-wrap;
}
}

.Button {
@include button;
}
7 changes: 4 additions & 3 deletions web/QueryLink.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
import React from "react";
import api from "./api.js";

// QueryLink can be used to make a direct link to a query search. The link is a
// standard HTML link with a valid href, but the navigation is intercepted to
// be handled by the app. This allow the user to copy-paste the link via his
// navigator contextual menu, while making internal navigation easy.
export default function QueryLink({ query, onClick }) {
export default function QueryLink({ children, query, onClick }) {
return (
<a
href={`/?q=${encodeQuery({ q: query })}`}
href={`/?q=${api.encodeQuery({ q: query })}`}
onClick={(e) => {
e.preventDefault();
onClick();
}}
>
{props.children}
{children}
</a>
);
}
47 changes: 24 additions & 23 deletions web/Searchbar.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,28 +5,29 @@ import { boolattr } from "./utils.js";
// Searchbar is one of the top-level components, tasked with handling the
// interface to edit the search query.
export default function Searchbar({ query, onSubmit }) {
// The local state is initialized to the current value, and will hold dirty
// values until the user submit the form. We want to update the current state
// when the query change. As the searchbar is never unmounted, this isn't
// done automatically.
const [value, setValue] = React.useState(query);
// The local payload is initialized from the current query, and will hold
// dirty values until the user submit the form.
const [payload, setPayload] = React.useState(query);
const [dirty, setDirty] = React.useState(false);

// We want to update the current payload when the query change. As the
// searchbar is never unmounted, this isn't done automatically.
React.useEffect(resetHandler, [query]);

// dirty is used to activate or not the apply and reset buttons when
// the state isn't equivalent to the initial query.
const [dirty, setDirty] = React.useState(false);
React.useEffect(
function () {
setDirty(Object.keys(query).some((prop) => value[prop] !== query[prop]));
function updateDirty() {
setDirty(Object.keys(query).some((prop) => payload[prop] !== query[prop]));
},
[value]
[payload]
);

// changeHandler is used by form component when their value change to update
// changeHandler is used by form component when their payload change to update
// the local state.
function changeHandler(e) {
setValue({
...value,
setPayload({
...payload,
[e.target.name]: e.target.value,
});
}
Expand All @@ -35,13 +36,13 @@ export default function Searchbar({ query, onSubmit }) {
// propagate the state to the parent component.
function submitHandler(e) {
e.preventDefault();
onSubmit(value);
onSubmit(payload);
}

// resetHandler is used by the reset button when it is clicked so we can
// reset the state to the query value.
// reset the state to the query payload.
function resetHandler() {
setValue(query);
setPayload(query);
}

return (
Expand All @@ -50,9 +51,9 @@ export default function Searchbar({ query, onSubmit }) {
<div>
<fieldset>
{["dumped_at", "hostname"].map((field) => {
const isActive = boolattr(value.sort === field);
const isDirty = boolattr(value.sort === field && value.sort !== query.sort);
const isChecked = value.sort === field;
const isActive = boolattr(payload.sort === field);
const isDirty = boolattr(payload.sort === field && payload.sort !== query.sort);
const isChecked = payload.sort === field;
return (
<label
className={styles.Radio}
Expand All @@ -75,9 +76,9 @@ export default function Searchbar({ query, onSubmit }) {
</fieldset>
<fieldset>
{["asc", "desc"].map((field) => {
const isActive = boolattr(value.order === field);
const isDirty = boolattr(value.order === field && value.order !== query.order);
const isChecked = value.order === field;
const isActive = boolattr(payload.order === field);
const isDirty = boolattr(payload.order === field && payload.order !== query.order);
const isChecked = payload.order === field;
return (
<label
className={styles.Radio}
Expand All @@ -104,9 +105,9 @@ export default function Searchbar({ query, onSubmit }) {
type="text"
placeholder="coredump search query"
name="q"
value={value.q}
value={payload.q}
onChange={changeHandler}
dirty={boolattr(value.q !== query.q)}
dirty={boolattr(payload.q !== query.q)}
/>
<button type="submit" disabled={!dirty}>
apply
Expand Down
4 changes: 4 additions & 0 deletions web/Searchbar.scss
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
@import "./colors.scss";
@import "./mixins.scss";

.Searchbar {
div {
Expand Down Expand Up @@ -32,6 +33,9 @@
}

.Radio {
@include button;
@include button-focusable;

cursor: pointer;
border-width: 1px 0;

Expand Down
5 changes: 3 additions & 2 deletions web/Table.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from "react";
import styles from "./Table.scss";
import { boolattr } from "./utils.js";
import Core from "./Core.js";
import { boolattr, formatDate } from "./utils.js";

// Table is the top-level component tasked with displaying the cores.
export default function Table({ cores, total }) {
Expand Down Expand Up @@ -85,7 +86,7 @@ export default function Table({ cores, total }) {
<td>{x.lang}</td>
</tr>
{selected == x.uid && (
<tr>
<tr className={styles.Detail}>
<td colSpan="5">
<Core core={x} />
</td>
Expand Down
2 changes: 1 addition & 1 deletion web/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ function search(query) {
for (const name in query) {
params.push(encodeURIComponent(name) + "=" + encodeURIComponent(query[name]));
}
return call(`/cores?${params.join("&")}`);
return call(`/cores?${params.join("&")}`).then((res) => res.json());
}

function deleteCore(uid) {
Expand Down
26 changes: 5 additions & 21 deletions web/index.scss
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
@import url("normalize.css/normalize.css");
@import "./colors.scss";
@import "./mixins.scss";

// Border box is better for everyone.
html {
Expand Down Expand Up @@ -61,29 +62,12 @@ button {
}

input[type="text"],
.Radio,
button,
.Button {
background: $background-light;
border-color: $gray;
border-radius: 0;
border-style: solid;
border-width: 1px;
display: inline-block;
padding: 0.5em;
text-decoration: none;
button {
@include button;
}

input[type="text"],
.Radio {
&[active],
&:focus {
border-color: $primary;
}

&[dirty] {
border-color: $secondary;
}
input[type="text"] {
@include button-focusable;
}

button {
Expand Down
23 changes: 23 additions & 0 deletions web/mixins.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
@import "./colors.scss";

@mixin button {
background: $background-light;
border-color: $gray;
border-radius: 0;
border-style: solid;
border-width: 1px;
display: inline-block;
padding: 0.5em;
text-decoration: none;
}

@mixin button-focusable {
&[active],
&:focus {
border-color: $primary;
}

&[dirty] {
border-color: $secondary;
}
}
13 changes: 4 additions & 9 deletions web/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@ var utc = require("dayjs/plugin/utc");
dayjs.extend(utc);

// Format the date in a more friendly manner for display.
function formatDate(date) {
export function formatDate(date) {
return dayjs(date).local().format("YYYY-MM-DD HH:mm:ss");
}

// Format a size in bytes into a human-readable string.
function formatSize(bytes) {
export function formatSize(bytes) {
const threshold = 1000;
const units = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
let u = 0;
Expand All @@ -22,12 +23,6 @@ function formatSize(bytes) {
}
//
// boolattr return the value for a non-HTML boolean attribute.
function boolattr(b) {
export function boolattr(b) {
return b ? "true" : undefined;
}

export default {
formatDate,
formatSize,
boolattr,
};

0 comments on commit 97568c8

Please sign in to comment.