diff --git a/build/build.ts b/build/build.ts
index bd9ad94..725e2d7 100644
--- a/build/build.ts
+++ b/build/build.ts
@@ -30,6 +30,8 @@ import postcss from "postcss";
import postcssImport from "postcss-import";
import postcssPresetEnv from "postcss-preset-env";
import cssnano from "cssnano";
+import SVGO from "svgo";
+import * as xmlParser from "../lib/xml-parser";
const MODE = process.env["NODE_ENV"] || "production";
@@ -99,6 +101,34 @@ function stripDevDeps(deps): void {
if (!Object.keys(deps["dependencies"]).length) delete deps["dependencies"];
}
+function xmlTostring(xml): string {
+ const children = [];
+ for (const c of xml.children || []) children.push(xmlTostring(c));
+
+ return xml.name === "root" && xml.bodyIndex === 0
+ ? children.join("")
+ : `<${xml.name} ${xml.attrs}>${children.join("")}${xml.name}>`;
+}
+
+function generateSymbol(id: string, svgStr: string): string {
+ const xml = xmlParser.parseXml(svgStr);
+ const svg = xml.children[0];
+ const svgAttrs = xmlParser.parseAttrs(svg.attrs);
+ let viewBox = "";
+ for (const a of svgAttrs) {
+ if (a.name === "viewBox") {
+ viewBox = `viewBox="${a.value}"`;
+ break;
+ }
+ }
+ const symbolBody = xml.children[0].children
+ .map(c => {
+ return xmlTostring(c);
+ })
+ .join("");
+ return `${symbolBody}`;
+}
+
async function init(): Promise {
// Delete any old output directory
rmDirSync(OUTPUT_DIR);
@@ -311,11 +341,28 @@ async function generateFrontendJs(): Promise {
process.stdout.write(stats.toString({ colors: true }) + "\n");
}
+async function generateIconsSprite(): Promise {
+ const svgo = new SVGO({ plugins: [{ removeViewBox: false }] });
+ const symbols = [];
+ const iconsDir = path.resolve(INPUT_DIR, "ui/icons");
+ for (const file of fs.readdirSync(iconsDir)) {
+ const id = path.parse(file).name;
+ const filePath = path.join(iconsDir, file);
+ const { data } = await svgo.optimize(fs.readFileSync(filePath).toString());
+ symbols.push(generateSymbol(id, data));
+ }
+ fs.writeFileSync(
+ path.resolve(OUTPUT_DIR, "public/icons.svg"),
+ ``
+ );
+}
+
init()
.then(() => {
Promise.all([
copyStatic(),
generateCss(),
+ generateIconsSprite(),
generateToolsJs(),
generateBackendJs(),
generateFrontendJs()
diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json
index 718d83e..545a678 100644
--- a/npm-shrinkwrap.json
+++ b/npm-shrinkwrap.json
@@ -659,6 +659,12 @@
"@types/mime": "*"
}
},
+ "@types/svgo": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@types/svgo/-/svgo-1.2.1.tgz",
+ "integrity": "sha512-UYSpRjkiiThPS3L2L7VhmmKGheGmyCBE382LCHaJIjLe6PeBb1HJvUSNqsZyuRyNMGcDcHcBQEV/CovlWjzFsw==",
+ "dev": true
+ },
"@typescript-eslint/eslint-plugin": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-1.9.0.tgz",
@@ -2591,12 +2597,12 @@
"dev": true
},
"css-tree": {
- "version": "1.0.0-alpha.28",
- "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0-alpha.28.tgz",
- "integrity": "sha512-joNNW1gCp3qFFzj4St6zk+Wh/NBv0vM5YbEreZk0SD4S23S+1xBKb6cLDg2uj4P4k/GUMlIm6cKIDqIG+vdt0w==",
+ "version": "1.0.0-alpha.33",
+ "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0-alpha.33.tgz",
+ "integrity": "sha512-SPt57bh5nQnpsTBsx/IXbO14sRc9xXu5MtMAVuo0BaQQmyf0NupNPPSoMaqiAF5tDFafYsTkfeH4Q/HCKXkg4w==",
"dev": true,
"requires": {
- "mdn-data": "~1.1.0",
+ "mdn-data": "2.0.4",
"source-map": "^0.5.3"
}
},
@@ -2606,12 +2612,6 @@
"integrity": "sha1-2bkoGtz9jO2TW9urqDeGiX9k6ZY=",
"dev": true
},
- "css-url-regex": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/css-url-regex/-/css-url-regex-1.1.0.tgz",
- "integrity": "sha1-g4NCMMyfdMRX3lnuvRVD/uuDt+w=",
- "dev": true
- },
"css-what": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/css-what/-/css-what-2.1.3.tgz",
@@ -2725,6 +2725,12 @@
"mdn-data": "~1.1.0",
"source-map": "^0.5.3"
}
+ },
+ "mdn-data": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-1.1.4.tgz",
+ "integrity": "sha512-FSYbp3lyKjyj3E7fMl6rYvUdX0FBXaluGqlFoYESWQlyUTq8R+wp0rkFxoYFqZlHCvsUXGjyJmLQSnXToYhOSA==",
+ "dev": true
}
}
},
@@ -5563,9 +5569,9 @@
}
},
"mdn-data": {
- "version": "1.1.4",
- "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-1.1.4.tgz",
- "integrity": "sha512-FSYbp3lyKjyj3E7fMl6rYvUdX0FBXaluGqlFoYESWQlyUTq8R+wp0rkFxoYFqZlHCvsUXGjyJmLQSnXToYhOSA==",
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.4.tgz",
+ "integrity": "sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA==",
"dev": true
},
"media-typer": {
@@ -8790,17 +8796,16 @@
}
},
"svgo": {
- "version": "1.2.2",
- "resolved": "https://registry.npmjs.org/svgo/-/svgo-1.2.2.tgz",
- "integrity": "sha512-rAfulcwp2D9jjdGu+0CuqlrAUin6bBWrpoqXWwKDZZZJfXcUXQSxLJOFJCQCSA0x0pP2U0TxSlJu2ROq5Bq6qA==",
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/svgo/-/svgo-1.3.0.tgz",
+ "integrity": "sha512-MLfUA6O+qauLDbym+mMZgtXCGRfIxyQoeH6IKVcFslyODEe/ElJNwr0FohQ3xG4C6HK6bk3KYPPXwHVJk3V5NQ==",
"dev": true,
"requires": {
"chalk": "^2.4.1",
"coa": "^2.0.2",
"css-select": "^2.0.0",
"css-select-base-adapter": "^0.1.1",
- "css-tree": "1.0.0-alpha.28",
- "css-url-regex": "^1.1.0",
+ "css-tree": "1.0.0-alpha.33",
"csso": "^3.5.1",
"js-yaml": "^3.13.1",
"mkdirp": "~0.5.1",
diff --git a/package.json b/package.json
index 1e54190..3e21a8c 100644
--- a/package.json
+++ b/package.json
@@ -47,6 +47,7 @@
"@types/mongodb": "^3.1.28",
"@types/parsimmon": "^1.10.0",
"@types/seedrandom": "^2.4.28",
+ "@types/svgo": "^1.2.1",
"@typescript-eslint/eslint-plugin": "^1.9.0",
"ava": "^2.0.0",
"codemirror": "^5.47.0",
@@ -64,6 +65,7 @@
"rollup-plugin-replace": "^2.2.0",
"rollup-plugin-terser": "^5.0.0",
"rollup-plugin-typescript": "^1.0.1",
+ "svgo": "^1.3.0",
"ts-node": "^8.2.0",
"typescript": "^3.5.1",
"webpack": "^4.32.2"
diff --git a/ui/components/all-parameters.ts b/ui/components/all-parameters.ts
index f121754..0c344d8 100644
--- a/ui/components/all-parameters.ts
+++ b/ui/components/all-parameters.ts
@@ -22,6 +22,7 @@ import { m } from "../components";
import * as taskQueue from "../task-queue";
import { parse } from "../../lib/common/expression-parser";
import memoize from "../../lib/common/memoize";
+import { getIcon } from "../icons";
const memoizedParse = memoize(parse);
@@ -86,7 +87,7 @@ const component: ClosureComponent = (): Component => {
});
}
},
- "✕"
+ getIcon("delete-instance")
)
);
} else {
@@ -103,7 +104,7 @@ const component: ClosureComponent = (): Component => {
});
}
},
- "🞢"
+ getIcon("add-instance")
)
);
}
@@ -122,7 +123,7 @@ const component: ClosureComponent = (): Component => {
});
}
},
- "↺"
+ getIcon("refresh")
)
);
diff --git a/ui/components/device-faults.ts b/ui/components/device-faults.ts
index c49ba3f..6392a45 100644
--- a/ui/components/device-faults.ts
+++ b/ui/components/device-faults.ts
@@ -21,6 +21,7 @@ import { ClosureComponent, Component } from "mithril";
import { m } from "../components";
import * as store from "../store";
import * as notifications from "../notifications";
+import { getIcon } from "../icons";
const component: ClosureComponent = (): Component => {
return {
@@ -71,7 +72,7 @@ const component: ClosureComponent = (): Component => {
});
}
},
- "✕"
+ getIcon("remove")
)
)
]);
diff --git a/ui/components/parameter-table.ts b/ui/components/parameter-table.ts
index a222ab7..c2a03a0 100644
--- a/ui/components/parameter-table.ts
+++ b/ui/components/parameter-table.ts
@@ -22,6 +22,7 @@ import { m } from "../components";
import * as taskQueue from "../task-queue";
import * as store from "../store";
import * as expressionParser from "../../lib/common/expression-parser";
+import { getIcon } from "../icons";
const component: ClosureComponent = (): Component => {
return {
@@ -104,7 +105,7 @@ const component: ClosureComponent = (): Component => {
});
}
},
- "✕"
+ getIcon("delete-instance")
)
)
);
@@ -137,7 +138,7 @@ const component: ClosureComponent = (): Component => {
});
}
},
- "🞢"
+ getIcon("add-instance")
)
)
)
diff --git a/ui/components/parameter.ts b/ui/components/parameter.ts
index 3407132..612c4dd 100644
--- a/ui/components/parameter.ts
+++ b/ui/components/parameter.ts
@@ -24,6 +24,7 @@ import * as store from "../store";
import * as expression from "../../lib/common/expression";
import memoize from "../../lib/common/memoize";
import timeAgo from "../timeago";
+import { getIcon } from "../icons";
const evaluateParam = memoize((exp, obj, now: number) => {
let timestamp = now;
@@ -82,7 +83,7 @@ const component: ClosureComponent = (): Component => {
});
}
},
- "✎"
+ getIcon("edit")
);
}
diff --git a/ui/components/tags.ts b/ui/components/tags.ts
index d1fccf7..3eceb69 100644
--- a/ui/components/tags.ts
+++ b/ui/components/tags.ts
@@ -21,6 +21,7 @@ import { ClosureComponent, Component } from "mithril";
import { m } from "../components";
import * as notifications from "../notifications";
import * as store from "../store";
+import { getIcon } from "../icons";
const component: ClosureComponent = (): Component => {
return {
@@ -68,12 +69,13 @@ const component: ClosureComponent = (): Component => {
});
}
},
- "✕"
+ getIcon("remove")
)
)
),
m(
"span.tag.writable",
+ m.trust(" "),
m(
"button",
{
@@ -99,7 +101,7 @@ const component: ClosureComponent = (): Component => {
});
}
},
- "🞢"
+ getIcon("add")
)
)
);
diff --git a/ui/config-page.ts b/ui/config-page.ts
index fe0db95..7e6a5b4 100644
--- a/ui/config-page.ts
+++ b/ui/config-page.ts
@@ -26,6 +26,7 @@ import * as overlay from "./overlay";
import { parse, stringify } from "../lib/common/expression-parser";
import { loadCodeMirror, loadYaml } from "./dynamic-loader";
import { ClosureComponent, Component, Children } from "mithril";
+import { getIcon } from "./icons";
const attributes = [
{ id: "_id", label: "Key" },
@@ -179,7 +180,7 @@ function renderTable(confsResponse, searchString): Children {
overlay.open(cb);
}
},
- "✎"
+ getIcon("edit")
);
const del = m(
@@ -194,7 +195,7 @@ function renderTable(confsResponse, searchString): Children {
});
}
},
- "✕"
+ getIcon("remove")
);
rows.push(
diff --git a/ui/css/app.css b/ui/css/app.css
index d10dc57..4f36e67 100644
--- a/ui/css/app.css
+++ b/ui/css/app.css
@@ -421,6 +421,9 @@ button {
color: var(--color4);
border-radius: 4px;
transition: background-color var(--fade), color var(--fade);
+ display: inline-block;
+ vertical-align: middle;
+
&.primary {
border: 1px solid var(--color1);
background-color: var(--color3);
@@ -725,3 +728,18 @@ span.parameter-value {
border: 1px solid var(--color1);
box-shadow: 0 0 10px #ccc;
}
+
+.icon {
+ display: inline-block;
+ vertical-align: middle;
+ width: 1em;
+ height: 1em;
+ fill: none;
+ stroke: currentColor;
+ stroke-width: 2;
+ stroke-linecap: round;
+ stroke-linejoin: round;
+ &.icon-unsorted {
+ opacity: 0.3;
+ }
+}
diff --git a/ui/drawer-component.ts b/ui/drawer-component.ts
index 6aaf874..3baf99e 100644
--- a/ui/drawer-component.ts
+++ b/ui/drawer-component.ts
@@ -27,6 +27,7 @@ import m, {
import * as taskQueue from "./task-queue";
import * as store from "./store";
import * as notifications from "./notifications";
+import { getIcon } from "./icons";
function mparam(param): Children {
return m("span.parameter", { title: param }, `${param}`);
@@ -217,7 +218,7 @@ function renderQueue(queue): Child[] {
taskQueue.queueTask(t);
}
},
- "↺"
+ getIcon("retry")
)
);
}
@@ -231,7 +232,7 @@ function renderQueue(queue): Child[] {
taskQueue.deleteTask(t);
}
},
- "✕"
+ getIcon("remove")
)
);
diff --git a/ui/icons.ts b/ui/icons.ts
new file mode 100644
index 0000000..be6299f
--- /dev/null
+++ b/ui/icons.ts
@@ -0,0 +1,29 @@
+/**
+ * Copyright 2013-2019 GenieACS Inc.
+ *
+ * This file is part of GenieACS.
+ *
+ * GenieACS is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * GenieACS is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with GenieACS. If not, see .
+ */
+
+import { Children } from "mithril";
+import m from "mithril";
+
+export function getIcon(name: string): Children {
+ return m(
+ `svg.icon.icon-${name}`,
+ { key: `icon-${name}` },
+ m("use", { href: `icons.svg#icon-${name}` })
+ );
+}
diff --git a/ui/icons/add-instance.svg b/ui/icons/add-instance.svg
new file mode 100644
index 0000000..c380e24
--- /dev/null
+++ b/ui/icons/add-instance.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/icons/add.svg b/ui/icons/add.svg
new file mode 100644
index 0000000..703c5b7
--- /dev/null
+++ b/ui/icons/add.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/icons/delete-instance.svg b/ui/icons/delete-instance.svg
new file mode 100644
index 0000000..4862832
--- /dev/null
+++ b/ui/icons/delete-instance.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/icons/edit.svg b/ui/icons/edit.svg
new file mode 100644
index 0000000..d728efc
--- /dev/null
+++ b/ui/icons/edit.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/icons/refresh.svg b/ui/icons/refresh.svg
new file mode 100644
index 0000000..83dca35
--- /dev/null
+++ b/ui/icons/refresh.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/icons/remove.svg b/ui/icons/remove.svg
new file mode 100644
index 0000000..7d5875c
--- /dev/null
+++ b/ui/icons/remove.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/icons/retry.svg b/ui/icons/retry.svg
new file mode 100644
index 0000000..c7657b0
--- /dev/null
+++ b/ui/icons/retry.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/icons/sorted-asc.svg b/ui/icons/sorted-asc.svg
new file mode 100644
index 0000000..80313af
--- /dev/null
+++ b/ui/icons/sorted-asc.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/icons/sorted-dsc.svg b/ui/icons/sorted-dsc.svg
new file mode 100644
index 0000000..ab7a941
--- /dev/null
+++ b/ui/icons/sorted-dsc.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/icons/unsorted.svg b/ui/icons/unsorted.svg
new file mode 100644
index 0000000..dca6515
--- /dev/null
+++ b/ui/icons/unsorted.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/index-table-component.ts b/ui/index-table-component.ts
index 613f7be..d16a2e1 100644
--- a/ui/index-table-component.ts
+++ b/ui/index-table-component.ts
@@ -19,6 +19,7 @@
import { ClosureComponent, Component, Children } from "mithril";
import { m } from "./components";
+import { getIcon } from "./icons";
interface Attribute {
id?: string;
@@ -78,9 +79,9 @@ function renderTable(
let direction = 1;
- let symbol = "\u2981";
- if (sortAttributes[i] > 0) symbol = "\u2bc6";
- else if (sortAttributes[i] < 0) symbol = "\u2bc5";
+ let symbol = getIcon("unsorted");
+ if (sortAttributes[i] > 0) symbol = getIcon("sorted-dsc");
+ else if (sortAttributes[i] < 0) symbol = getIcon("sorted-asc");
const sortable = m(
"button",
diff --git a/ui/permissions-page.ts b/ui/permissions-page.ts
index f25fd14..9153128 100644
--- a/ui/permissions-page.ts
+++ b/ui/permissions-page.ts
@@ -29,6 +29,7 @@ import * as smartQuery from "./smart-query";
import { map, parse, stringify } from "../lib/common/expression-parser";
import filterComponent from "./filter-component";
import { Children, ClosureComponent, Component } from "mithril";
+import { getIcon } from "./icons";
const PAGE_SIZE = config.ui.pageSize || 10;
@@ -270,7 +271,7 @@ export const component: ClosureComponent = (): Component => {
});
}
},
- "✕"
+ getIcon("remove")
)
];
};