Skip to content

Commit

Permalink
feat(alerts): add ui (#26)
Browse files Browse the repository at this point in the history
  • Loading branch information
lionelB committed Jun 17, 2020
1 parent 4c3874e commit 0fd4f21
Show file tree
Hide file tree
Showing 18 changed files with 638 additions and 16 deletions.
20 changes: 17 additions & 3 deletions hasura/migrations/1591618615701_alerts/up.sql
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,16 @@ INSERT INTO public.status (name) VALUES ('rejected');

CREATE TABLE "public"."sources"(
"repository" text NOT NULL,
"label" text NOT NULL,
"tag" text NOT NULL,
"created_at" timestamptz NOT NULL DEFAULT now(),
PRIMARY KEY ("repository")
);

COMMENT ON TABLE "public"."sources" IS E'sources are git repository that acts as data sources to track changes';

INSERT INTO public.sources (repository, tag) VALUES ('socialgouv/legi-data', 'v1.9.0');
INSERT INTO public.sources (repository, tag) VALUES ('socialgouv/kali-data', 'v1.60.0');
INSERT INTO public.sources (repository, label, tag) VALUES ('socialgouv/legi-data', 'code du travail', 'v1.9.0');
INSERT INTO public.sources (repository, label, tag) VALUES ('socialgouv/kali-data', 'conventions collectives', 'v1.60.0');

CREATE EXTENSION IF NOT EXISTS pgcrypto;
CREATE TABLE "public"."alerts"(
Expand All @@ -27,5 +28,18 @@ CREATE TABLE "public"."alerts"(
"repository" text NOT NULL,
"ref" text NOT NULL,
"changes" jsonb NOT NULL,
"created_at" timestamptz NULL DEFAULT now(),
"updated_at" timestamptz NULL DEFAULT now();
PRIMARY KEY ("id") ,
FOREIGN KEY ("status") REFERENCES "public"."status"("name") ON UPDATE restrict ON DELETE restrict, FOREIGN KEY ("repository") REFERENCES "public"."sources"("repository") ON UPDATE restrict ON DELETE cascade); COMMENT ON TABLE "public"."alerts" IS E'alerts reprensent a change in a text from a source';
FOREIGN KEY ("status") REFERENCES "public"."status"("name") ON UPDATE restrict ON DELETE restrict, FOREIGN KEY ("repository") REFERENCES "public"."sources"("repository") ON UPDATE restrict ON DELETE cascade);

COMMENT ON TABLE "public"."alerts" IS
E'alerts reprensent a change in a text from a source';

CREATE TRIGGER "set_public_alerts_updated_at"
BEFORE UPDATE ON public.alerts
FOR EACH ROW
EXECUTE PROCEDURE trigger_set_timestamp();

COMMENT ON TRIGGER "set_public_alerts_updated_at" ON public.alerts
IS 'trigger to set value of column "updated_at" to current timestamp on row update';
8 changes: 6 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"dependencies": {
"@hapi/boom": "^9.1.0",
"@hapi/joi": "^17.1.1",
"@reach/accordion": "^0.10.3",
"@reach/dialog": "^0.10.3",
"@reach/menu-button": "^0.10.3",
"@reach/visually-hidden": "^0.10.2",
Expand All @@ -17,6 +18,7 @@
"@zeit/next-source-maps": "0.0.4-canary.1",
"argon2": "^0.26.2",
"cookie": "^0.4.1",
"diff": "^4.0.2",
"graphql": "^15.0.0",
"http-proxy-middleware": "^1.0.4",
"isomorphic-unfetch": "^3.0.0",
Expand Down Expand Up @@ -54,10 +56,12 @@
"scripts": {
"dev": "next dev",
"build": "next build",
"prestart": "node scripts/update-alert.js",
"start": "next start",
"alert": " node scripts/update-alert.js",
"alert:dev": "GRAPHQL_ENDPOINT=http://localhost:8080/v1/graphql HASURA_GRAPHQL_ADMIN_SECRET=admin1 node scripts/update-alert.js",
"alert:dump": "GRAPHQL_ENDPOINT=http://localhost:8080/v1/graphql HASURA_GRAPHQL_ADMIN_SECRET=admin1 DUMP=true node scripts/update-alert.js > data/dump.json",
"alert:dev": "GRAPHQL_ENDPOINT=http://localhost:8080/v1/graphql HASURA_GRAPHQL_ADMIN_SECRET=admin1 node scripts/update-alerts.js",
"alert:dump": "GRAPHQL_ENDPOINT=http://localhost:8080/v1/graphql HASURA_GRAPHQL_ADMIN_SECRET=admin1 DUMP=true node scripts/update-alerts.js > data/dump.json",
"alert:populate": "GRAPHQL_ENDPOINT=http://localhost:8080/v1/graphql HASURA_GRAPHQL_ADMIN_SECRET=admin1 node scripts/add-alerts.js",
"lint": "eslint src/*",
"test": "jest"
},
Expand Down
28 changes: 28 additions & 0 deletions scripts/add-alerts.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
const { promises: fs } = require("fs");
const path = require("path");
const filename =
process.env.DUMP || path.join(__dirname, "..", "data", "dump.json");
const { updateSource, insertAlert } = require("./update-alerts");

async function main() {
const fileContent = (await fs.readFile(filename)).toString();
const data = JSON.parse(fileContent);

for (const result of data) {
if (result.changes.length === 0) {
console.log(`no update for ${result.repository}`);
continue;
}
const inserts = await Promise.all(
result.changes.map((diff) => insertAlert(result.repository, diff))
);
inserts.forEach((insert) => {
console.log("insert alert", insert.returning[0]);
});
const update = await updateSource(result.repository, result.newRef);
console.log(`update source ${update.repository} to ${update.tag}`);
}
return Promise.resolve();
}

main().catch(console.error);
14 changes: 10 additions & 4 deletions scripts/update-alert.js → scripts/update-alerts.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,16 +84,16 @@ async function getSources() {
return result.data.sources;
}

async function insertAlert(changes) {
async function insertAlert(repository, changes) {
const data = {
repository,
info: {
num: changes.num,
title: changes.title,
id: changes.id,
file: changes.file,
},
ref: changes.ref,
repository: changes.repository,
changes: {
added: changes.added,
removed: changes.removed,
Expand Down Expand Up @@ -250,9 +250,9 @@ async function main() {
continue;
}
const inserts = await Promise.all(
result.changes.map((diff) => insertAlert(diff))
result.changes.map((diff) => insertAlert(result.repository, diff))
);
inserts.forEeach((insert) => {
inserts.forEach((insert) => {
console.log("insert alert", insert.returning[0]);
});
const update = await updateSource(result.repository, result.newRef);
Expand All @@ -262,3 +262,9 @@ async function main() {
}

main().catch(console.error);

module.exports = {
getSources,
insertAlert,
updateSource,
};
18 changes: 18 additions & 0 deletions src/components/alerts/AlertTitle.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/** @jsx jsx */

import { jsx, Flex, Box } from "theme-ui";
import PropTypes from "prop-types";
import { AlertStatus } from "./Status";

export function AlertTitle({ alertId, ...props }) {
return (
<Flex sx={{ my: "large", alignItems: "center" }}>
<AlertStatus alertId={alertId} />
<Box as="h2" sx={{ flex: "1 1 auto" }} {...props} />
</Flex>
);
}

AlertTitle.propTypes = {
alertId: PropTypes.string.isRequired,
};
45 changes: 45 additions & 0 deletions src/components/alerts/Status.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/** @jsx jsx */

import { IoIosCheckmark, IoIosClose } from "react-icons/io";
import { MenuButton, MenuItem } from "../button";
import { jsx } from "theme-ui";
import PropTypes from "prop-types";
import { useMutation } from "urql";

export const alertMutation = `
mutation updateAlertStatus($id:uuid!, $status:String!) {
update_alerts_by_pk(
pk_columns: {
id: $id
}
_set: { status: $status }
){
__typename
}
}
`;

export function AlertStatus({ alertId }) {
const [, executeUpdate] = useMutation(alertMutation);
function updateStatus(status) {
console.log("update statys", alertId, status);
executeUpdate({ id: alertId, status });
}
return (
<MenuButton variant="secondary">
<MenuItem onSelect={() => updateStatus("doing")}>
<span sx={{ display: "inline-block", width: "1.5em" }} />
En cours
</MenuItem>
<MenuItem onSelect={() => updateStatus("done")}>
<IoIosCheckmark style={{ width: "1.5em", height: "1.5em" }} /> Traité
</MenuItem>
<MenuItem onSelect={() => updateStatus("rejected")}>
<IoIosClose style={{ width: "1.5em", height: "1.5em" }} /> Rejeté
</MenuItem>
</MenuButton>
);
}
AlertStatus.propTypes = {
alertId: PropTypes.string.isRequired,
};
33 changes: 32 additions & 1 deletion src/components/button/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,13 @@ import {
MenuList,
MenuItem as ReachMenuItem,
} from "@reach/menu-button";
import { IoMdMore } from "react-icons/io";

import {
AccordionButton as ReachAccordionButton,
useAccordionItemContext,
} from "@reach/accordion";

import { IoMdMore, IoIosArrowForward, IoIosArrowDown } from "react-icons/io";

const buttonPropTypes = {
variant: PropTypes.oneOf(["secondary", "primary", "link"]),
Expand Down Expand Up @@ -188,3 +194,28 @@ export function MenuItem(props) {
/>
);
}

export function AccordionButton({ children, ...props }) {
return (
<ReachAccordionButton
{...props}
sx={{
...defaultButtonStyles,
color: "text",
bg: "white",
border: "none",
"&[aria-expanded=true]": {
color: "accent",
},
}}
>
<ExpandedIcon />
{children}
</ReachAccordionButton>
);
}

export function ExpandedIcon() {
const { isExpanded } = useAccordionItemContext();
return isExpanded ? <IoIosArrowDown /> : <IoIosArrowForward />;
}
27 changes: 27 additions & 0 deletions src/components/changes/ChangeGroup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import React from "react";
import PropTypes from "prop-types";
import { AccordionButton } from "src/components/button";
import { AccordionItem, AccordionPanel } from "@reach/accordion";

export const ChangesGroup = ({ changes, label, renderChange }) => {
return changes.length > 0 ? (
<AccordionItem>
<AccordionButton>{label}</AccordionButton>
<AccordionPanel>
<ul>{changes.map(renderChange)}</ul>
</AccordionPanel>
</AccordionItem>
) : null;
};

ChangesGroup.propTypes = {
label: PropTypes.string.isRequired,
renderChange: PropTypes.func.isRequired,
changes: PropTypes.arrayOf(
PropTypes.shape({
added: PropTypes.object,
removed: PropTypes.object,
modified: PropTypes.object,
})
),
};
69 changes: 69 additions & 0 deletions src/components/changes/ViewDiff.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// adapted from https://github.com/davidmason/react-stylable-diff/blob/master/lib/react-diff.js
/** @jsx jsx */

import { useState, useMemo } from "react";
import { jsx } from "theme-ui";
var jsdiff = require("diff");

const fnMap = {
chars: jsdiff.diffChars,
words: jsdiff.diffWords,
sentences: jsdiff.diffSentences,
json: jsdiff.diffJson,
};

export const ViewDiff = ({ sx, type, inputA, inputB }) => {
const [mode, setMode] = useState(type);
const diff = fnMap[mode](inputA, inputB);

const groupName = useMemo(() => Math.random());

const result = diff.map((part, index) => {
if (part.added) {
return (
<ins sx={{ bg: "positive" }} key={index}>
{part.value}
</ins>
);
}
if (part.removed) {
return (
<del sx={{ bg: "critical" }} key={index}>
{part.value}
</del>
);
}
return <span key={index}>{part.value}</span>;
});
return (
<div sx={sx}>
<div sx={{ marginBottom: 20 }}>
Diff mode :
<input
type="radio"
name={groupName}
onClick={() => setMode("words")}
style={{ marginLeft: 10 }}
checked={mode === "words"}
/>{" "}
Mots
<input
type="radio"
name={groupName}
onClick={() => setMode("sentences")}
style={{ marginLeft: 10 }}
checked={mode === "sentences"}
/>{" "}
Phrases
</div>
{result}
</div>
);
};

ViewDiff.defaultProps = {
inputA: "",
inputB: "",
type: "chars",
className: "Difference",
};
Loading

0 comments on commit 0fd4f21

Please sign in to comment.