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("")}`; +} + +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"), + `${symbols.join("")}` + ); +} + 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") ) ]; };