diff --git a/package-lock.json b/package-lock.json index 53c2cba..02f5091 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1220,14 +1220,58 @@ } }, "@svgr/plugin-svgo": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/@svgr/plugin-svgo/-/plugin-svgo-4.0.3.tgz", - "integrity": "sha512-MgL1CrlxvNe+1tQjPUc2bIJtsdJOIE5arbHlPgW+XVWGjMZTUcyNNP8R7/IjM2Iyrc98UJY+WYiiWHrinnY9ZQ==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@svgr/plugin-svgo/-/plugin-svgo-4.2.0.tgz", + "integrity": "sha512-zUEKgkT172YzHh3mb2B2q92xCnOAMVjRx+o0waZ1U50XqKLrVQ/8dDqTAtnmapdLsGurv8PSwenjLCUpj6hcvw==", "dev": true, "requires": { - "cosmiconfig": "^5.0.7", + "cosmiconfig": "^5.2.0", "merge-deep": "^3.0.2", - "svgo": "^1.1.1" + "svgo": "^1.2.1" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "svgo": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-1.2.1.tgz", + "integrity": "sha512-Y1+LyT4/y1ms4/0yxPMSlvx6dIbgklE9w8CIOnfeoFGB74MEkq8inSfEr6NhocTaFbyYp0a1dvNgRKGRmEBlzA==", + "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", + "csso": "^3.5.1", + "js-yaml": "^3.13.0", + "mkdirp": "~0.5.1", + "object.values": "^1.1.0", + "sax": "~1.2.4", + "stable": "^0.1.8", + "unquote": "~1.1.1", + "util.promisify": "~1.0.0" + } + } } }, "@svgr/webpack": { @@ -7588,9 +7632,9 @@ "dev": true }, "fsevents": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.4.tgz", - "integrity": "sha512-z8H8/diyk76B7q5wg+Ud0+CqzcAF3mBBI/bA5ne5zrRUUIvNkJY//D3BqyH571KuAC4Nr7Rw7CjWX4r0y9DvNg==", + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.7.tgz", + "integrity": "sha512-Pxm6sI2MeBD7RdD12RYsqaP0nMiwx8eZBXCa6z2L+mRHm2DYrOYwihmhjpkdjUHwQhslWQjRpEgNq4XvBmaAuw==", "dev": true, "optional": true, "requires": { @@ -7617,7 +7661,7 @@ "optional": true }, "are-we-there-yet": { - "version": "1.1.4", + "version": "1.1.5", "bundled": true, "dev": true, "optional": true, @@ -7643,7 +7687,7 @@ } }, "chownr": { - "version": "1.0.1", + "version": "1.1.1", "bundled": true, "dev": true, "optional": true @@ -7682,7 +7726,7 @@ } }, "deep-extend": { - "version": "0.5.1", + "version": "0.6.0", "bundled": true, "dev": true, "optional": true @@ -7731,7 +7775,7 @@ } }, "glob": { - "version": "7.1.2", + "version": "7.1.3", "bundled": true, "dev": true, "optional": true, @@ -7751,12 +7795,12 @@ "optional": true }, "iconv-lite": { - "version": "0.4.21", + "version": "0.4.24", "bundled": true, "dev": true, "optional": true, "requires": { - "safer-buffer": "^2.1.0" + "safer-buffer": ">= 2.1.2 < 3" } }, "ignore-walk": { @@ -7821,17 +7865,17 @@ "optional": true }, "minipass": { - "version": "2.2.4", + "version": "2.3.5", "bundled": true, "dev": true, "optional": true, "requires": { - "safe-buffer": "^5.1.1", + "safe-buffer": "^5.1.2", "yallist": "^3.0.0" } }, "minizlib": { - "version": "1.1.0", + "version": "1.2.1", "bundled": true, "dev": true, "optional": true, @@ -7855,7 +7899,7 @@ "optional": true }, "needle": { - "version": "2.2.0", + "version": "2.2.4", "bundled": true, "dev": true, "optional": true, @@ -7866,18 +7910,18 @@ } }, "node-pre-gyp": { - "version": "0.10.0", + "version": "0.10.3", "bundled": true, "dev": true, "optional": true, "requires": { "detect-libc": "^1.0.2", "mkdirp": "^0.5.1", - "needle": "^2.2.0", + "needle": "^2.2.1", "nopt": "^4.0.1", "npm-packlist": "^1.1.6", "npmlog": "^4.0.2", - "rc": "^1.1.7", + "rc": "^1.2.7", "rimraf": "^2.6.1", "semver": "^5.3.0", "tar": "^4" @@ -7894,13 +7938,13 @@ } }, "npm-bundled": { - "version": "1.0.3", + "version": "1.0.5", "bundled": true, "dev": true, "optional": true }, "npm-packlist": { - "version": "1.1.10", + "version": "1.2.0", "bundled": true, "dev": true, "optional": true, @@ -7977,12 +8021,12 @@ "optional": true }, "rc": { - "version": "1.2.7", + "version": "1.2.8", "bundled": true, "dev": true, "optional": true, "requires": { - "deep-extend": "^0.5.1", + "deep-extend": "^0.6.0", "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" @@ -8012,16 +8056,16 @@ } }, "rimraf": { - "version": "2.6.2", + "version": "2.6.3", "bundled": true, "dev": true, "optional": true, "requires": { - "glob": "^7.0.5" + "glob": "^7.1.3" } }, "safe-buffer": { - "version": "5.1.1", + "version": "5.1.2", "bundled": true, "dev": true, "optional": true @@ -8039,7 +8083,7 @@ "optional": true }, "semver": { - "version": "5.5.0", + "version": "5.6.0", "bundled": true, "dev": true, "optional": true @@ -8092,17 +8136,17 @@ "optional": true }, "tar": { - "version": "4.4.1", + "version": "4.4.8", "bundled": true, "dev": true, "optional": true, "requires": { - "chownr": "^1.0.1", + "chownr": "^1.1.1", "fs-minipass": "^1.2.5", - "minipass": "^2.2.4", - "minizlib": "^1.1.0", + "minipass": "^2.3.4", + "minizlib": "^1.1.1", "mkdirp": "^0.5.0", - "safe-buffer": "^5.1.1", + "safe-buffer": "^5.1.2", "yallist": "^3.0.2" } }, @@ -8113,12 +8157,12 @@ "optional": true }, "wide-align": { - "version": "1.1.2", + "version": "1.1.3", "bundled": true, "dev": true, "optional": true, "requires": { - "string-width": "^1.0.2" + "string-width": "^1.0.2 || 2" } }, "wrappy": { @@ -8128,7 +8172,7 @@ "optional": true }, "yallist": { - "version": "3.0.2", + "version": "3.0.3", "bundled": true, "dev": true, "optional": true @@ -8319,23 +8363,15 @@ "dev": true }, "handlebars": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.1.1.tgz", - "integrity": "sha512-3Zhi6C0euYZL5sM0Zcy7lInLXKQ+YLcF/olbN010mzGQ4XVm50JeyBnMqofHh696GrciGruC7kCcApPDJvVgwA==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.1.2.tgz", + "integrity": "sha512-nvfrjqvt9xQ8Z/w0ijewdD/vvWDTOweBUm96NTr66Wfvo1mJenBLwcYmPs3TIBP5ruzYGD7Hx/DaM9RmhroGPw==", "dev": true, "requires": { "neo-async": "^2.6.0", "optimist": "^0.6.1", "source-map": "^0.6.1", "uglify-js": "^3.1.4" - }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - } } }, "har-schema": { @@ -10857,9 +10893,9 @@ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" }, "js-yaml": { - "version": "3.13.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.0.tgz", - "integrity": "sha512-pZZoSxcCYco+DIKBTimr67J6Hy+EYGZDY/HCWC+iAEA9h1ByhMXAIVUXMcMFpOCxQ/xjXmPI2MkDL5HRm5eFrQ==", + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", + "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", "requires": { "argparse": "^1.0.7", "esprima": "^4.0.0" @@ -17492,183 +17528,733 @@ "webpack-dev-server": "3.1.14", "webpack-manifest-plugin": "2.0.4", "workbox-webpack-plugin": "3.6.3" - } - }, - "react-timeago": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/react-timeago/-/react-timeago-4.4.0.tgz", - "integrity": "sha512-Zj8RchTqZEH27LAANemzMR2RpotbP2aMd+UIajfYMZ9KW4dMcViUVKzC7YmqfiqlFfz8B0bjDw2xUBjmcxDngA==" - }, - "read-pkg": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", - "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", - "dev": true, - "requires": { - "load-json-file": "^1.0.0", - "normalize-package-data": "^2.3.2", - "path-type": "^1.0.0" - } - }, - "read-pkg-up": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", - "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=", - "dev": true, - "requires": { - "find-up": "^1.0.0", - "read-pkg": "^1.0.0" - } - }, - "readable-stream": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - }, - "dependencies": { - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true - } - } - }, - "readdirp": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz", - "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.11", - "micromatch": "^3.1.10", - "readable-stream": "^2.0.2" }, "dependencies": { - "arr-diff": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", - "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", - "dev": true - }, - "array-unique": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", - "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", - "dev": true - }, - "braces": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", - "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "fsevents": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.4.tgz", + "integrity": "sha512-z8H8/diyk76B7q5wg+Ud0+CqzcAF3mBBI/bA5ne5zrRUUIvNkJY//D3BqyH571KuAC4Nr7Rw7CjWX4r0y9DvNg==", "dev": true, + "optional": true, "requires": { - "arr-flatten": "^1.1.0", - "array-unique": "^0.3.2", - "extend-shallow": "^2.0.1", - "fill-range": "^4.0.0", - "isobject": "^3.0.1", - "repeat-element": "^1.1.2", - "snapdragon": "^0.8.1", - "snapdragon-node": "^2.0.1", - "split-string": "^3.0.2", - "to-regex": "^3.0.1" + "nan": "^2.9.2", + "node-pre-gyp": "^0.10.0" }, "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "abbrev": { + "version": "1.1.1", + "bundled": true, "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "expand-brackets": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", - "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", - "dev": true, - "requires": { - "debug": "^2.3.3", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "posix-character-classes": "^0.1.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "optional": true + }, + "ansi-regex": { + "version": "2.1.1", + "bundled": true, "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } + "optional": true }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "aproba": { + "version": "1.2.0", + "bundled": true, + "dev": true, + "optional": true + }, + "are-we-there-yet": { + "version": "1.1.4", + "bundled": true, "dev": true, + "optional": true, "requires": { - "is-extendable": "^0.1.0" + "delegates": "^1.0.0", + "readable-stream": "^2.0.6" } }, - "is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "balanced-match": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "brace-expansion": { + "version": "1.1.11", + "bundled": true, "dev": true, + "optional": true, "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" } }, - "is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "chownr": { + "version": "1.0.1", + "bundled": true, "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "optional": true + }, + "code-point-at": { + "version": "1.1.0", + "bundled": true, + "dev": true, + "optional": true + }, + "concat-map": { + "version": "0.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "console-control-strings": { + "version": "1.1.0", + "bundled": true, + "dev": true, + "optional": true + }, + "core-util-is": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "debug": { + "version": "2.6.9", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "ms": "2.0.0" + } + }, + "deep-extend": { + "version": "0.5.1", + "bundled": true, + "dev": true, + "optional": true + }, + "delegates": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "detect-libc": { + "version": "1.0.3", + "bundled": true, + "dev": true, + "optional": true + }, + "fs-minipass": { + "version": "1.2.5", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "minipass": "^2.2.1" + } + }, + "fs.realpath": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "gauge": { + "version": "2.7.4", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "aproba": "^1.0.3", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.0", + "object-assign": "^4.1.0", + "signal-exit": "^3.0.0", + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wide-align": "^1.1.0" + } + }, + "glob": { + "version": "7.1.2", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "has-unicode": { + "version": "2.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "iconv-lite": { + "version": "0.4.21", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "safer-buffer": "^2.1.0" + } + }, + "ignore-walk": { + "version": "3.0.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "minimatch": "^3.0.4" + } + }, + "inflight": { + "version": "1.0.6", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.3", + "bundled": true, + "dev": true, + "optional": true + }, + "ini": { + "version": "1.3.5", + "bundled": true, + "dev": true, + "optional": true + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "isarray": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "minimatch": { + "version": "3.0.4", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "0.0.8", + "bundled": true, + "dev": true, + "optional": true + }, + "minipass": { + "version": "2.2.4", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "safe-buffer": "^5.1.1", + "yallist": "^3.0.0" + } + }, + "minizlib": { + "version": "1.1.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "minipass": "^2.2.1" + } + }, + "mkdirp": { + "version": "0.5.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "minimist": "0.0.8" + } + }, + "ms": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "needle": { + "version": "2.2.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "debug": "^2.1.2", + "iconv-lite": "^0.4.4", + "sax": "^1.2.4" + } + }, + "node-pre-gyp": { + "version": "0.10.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "detect-libc": "^1.0.2", + "mkdirp": "^0.5.1", + "needle": "^2.2.0", + "nopt": "^4.0.1", + "npm-packlist": "^1.1.6", + "npmlog": "^4.0.2", + "rc": "^1.1.7", + "rimraf": "^2.6.1", + "semver": "^5.3.0", + "tar": "^4" + } + }, + "nopt": { + "version": "4.0.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "abbrev": "1", + "osenv": "^0.1.4" + } + }, + "npm-bundled": { + "version": "1.0.3", + "bundled": true, + "dev": true, + "optional": true + }, + "npm-packlist": { + "version": "1.1.10", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "ignore-walk": "^3.0.1", + "npm-bundled": "^1.0.1" + } + }, + "npmlog": { + "version": "4.1.2", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "are-we-there-yet": "~1.1.2", + "console-control-strings": "~1.1.0", + "gauge": "~2.7.3", + "set-blocking": "~2.0.0" + } + }, + "number-is-nan": { + "version": "1.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "object-assign": { + "version": "4.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "once": { + "version": "1.4.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "wrappy": "1" + } + }, + "os-homedir": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "os-tmpdir": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "osenv": { + "version": "0.1.5", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "os-homedir": "^1.0.0", + "os-tmpdir": "^1.0.0" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "process-nextick-args": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "rc": { + "version": "1.2.7", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "deep-extend": "^0.5.1", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "bundled": true, + "dev": true, + "optional": true + } + } + }, + "readable-stream": { + "version": "2.3.6", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "rimraf": { + "version": "2.6.2", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "glob": "^7.0.5" + } + }, + "safe-buffer": { + "version": "5.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "safer-buffer": { + "version": "2.1.2", + "bundled": true, + "dev": true, + "optional": true + }, + "sax": { + "version": "1.2.4", + "bundled": true, + "dev": true, + "optional": true + }, + "semver": { + "version": "5.5.0", + "bundled": true, + "dev": true, + "optional": true + }, + "set-blocking": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "signal-exit": { + "version": "3.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "string-width": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + }, + "string_decoder": { + "version": "1.1.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "strip-json-comments": { + "version": "2.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "tar": { + "version": "4.4.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "chownr": "^1.0.1", + "fs-minipass": "^1.2.5", + "minipass": "^2.2.4", + "minizlib": "^1.1.0", + "mkdirp": "^0.5.0", + "safe-buffer": "^5.1.1", + "yallist": "^3.0.2" + } + }, + "util-deprecate": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "wide-align": { + "version": "1.1.2", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "string-width": "^1.0.2" + } + }, + "wrappy": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "yallist": { + "version": "3.0.2", + "bundled": true, + "dev": true, + "optional": true + } + } + } + } + }, + "react-timeago": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/react-timeago/-/react-timeago-4.4.0.tgz", + "integrity": "sha512-Zj8RchTqZEH27LAANemzMR2RpotbP2aMd+UIajfYMZ9KW4dMcViUVKzC7YmqfiqlFfz8B0bjDw2xUBjmcxDngA==" + }, + "read-pkg": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", + "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", + "dev": true, + "requires": { + "load-json-file": "^1.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^1.0.0" + } + }, + "read-pkg-up": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", + "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=", + "dev": true, + "requires": { + "find-up": "^1.0.0", + "read-pkg": "^1.0.0" + } + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + } + } + }, + "readdirp": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz", + "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.11", + "micromatch": "^3.1.10", + "readable-stream": "^2.0.2" + }, + "dependencies": { + "arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", + "dev": true + }, + "array-unique": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", + "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", + "dev": true + }, + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": true, + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "expand-brackets": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", + "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", + "dev": true, + "requires": { + "debug": "^2.3.3", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "posix-character-classes": "^0.1.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", "dev": true, "requires": { @@ -17857,6 +18443,20 @@ "minimatch": "3.0.4" } }, + "redux": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-4.0.1.tgz", + "integrity": "sha512-R7bAtSkk7nY6O/OYMVR9RiBI+XghjF9rlbl5806HJbQph0LJVHZrU5oaO4q70eUKiqMRqm4y07KLTlMZ2BlVmg==", + "requires": { + "loose-envify": "^1.4.0", + "symbol-observable": "^1.2.0" + } + }, + "redux-react-hook": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/redux-react-hook/-/redux-react-hook-3.3.1.tgz", + "integrity": "sha512-a5RHGYT2ZV8Zo1ATP2ubxQGm0fPwn62loSieTeCL7PRgGfdTi8jR0S1K+1/1b4P6RSnEXvs5qC9IVlh2clPbxg==" + }, "regenerate": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.0.tgz", @@ -19097,6 +19697,12 @@ "integrity": "sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==", "dev": true }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, "source-map-resolve": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.2.tgz", @@ -19600,6 +20206,11 @@ } } }, + "symbol-observable": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz", + "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==" + }, "symbol-tree": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.2.tgz", diff --git a/package.json b/package.json index a5fcaad..f1cca8d 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,9 @@ "react": "^16.8.1", "react-dom": "^16.8.1", "react-router-dom": "^4.3.1", - "react-timeago": "^4.3.0" + "react-timeago": "^4.3.0", + "redux": "^4.0.1", + "redux-react-hook": "^3.3.1" }, "eslintConfig": { "extends": "react-app" diff --git a/src/client.ts b/src/client.ts deleted file mode 100644 index 83f3c55..0000000 --- a/src/client.ts +++ /dev/null @@ -1,104 +0,0 @@ -import axios from "axios" - -export interface IAccount { - id: string; - name: string; - email: string; - avatarUrl: string; - apiKey: string; - admin: boolean; - createdAt: Date; -} - -export interface IContainer { - key: string - name: string - status: string - image: string - endpoint: string - memory: number - tags: string[] - createdAt: Date -} - -export interface CreateContainerRequest { - name: string - image: string - size: string - tags: string[] -} - -const remapFields = (obj: any, fields: any): any => - Object.entries(obj) - .map(([key, value]) => [fields[key] || key, value]) - .reduce((prev, [key, value]) => ({ ...prev, [key]: value }), {}) - -const toAccount = (data: object): IAccount => { - const account = remapFields(data, { - avatar_url: "avatarUrl", - api_key: "apiKey", - created_at: "createdAt", - }) - account.createdAt = new Date(account.createdAt) - return account -} - -const toContainer = (data: object): IContainer => { - const container = remapFields(data, { - created_at: "createdAt", - }) - container.createdAt = new Date(container.createdAt) - return container -} - -export const client = axios.create({ - baseURL: process.env.API_URL || "http://localhost:3000", - transformRequest(data, headers) { - headers.Authorization = document.cookie.split("=")[1] - return data - }, - validateStatus() { - return true - }, -}) - -export const getAccount = (): Promise => - client.get("/v1/account").then((res) => { - if (res.status !== 200) { - throw new Error(res.data.message) - } - return toAccount(res.data.account) - }) - -export const syncAccount = (): Promise => - client.post("/v1/account/sync").then((res) => { - if (res.status !== 200) { - throw new Error(res.data.message) - } - return toAccount(res.data.account) - }) - -export const regenerateApiKey = (): Promise => - client.post("/v1/account/regenerate_apikey").then((res) => { - if (res.status !== 200) { - throw new Error(res.data.message) - } - return toAccount(res.data.account) - }) - -export const getContainers = (): Promise => - client.get("/v1/containers").then((res) => { - if (res.status !== 200) { - throw new Error(res.data.message) - } - return res.data.containers.map((data: object) => toContainer(data)) - }) - -export const createContainer = (req: CreateContainerRequest): Promise => - client.post("/v1/containers", req) - .then((res) => { - if (res.status !== 200) { - throw new Error(res.data.message) - } - return toContainer(res.data.container) - }) diff --git a/src/client/account.ts b/src/client/account.ts new file mode 100644 index 0000000..41fe889 --- /dev/null +++ b/src/client/account.ts @@ -0,0 +1,48 @@ +import { ApiResponse, client, remapFields } from "./index" + +export interface Account { + id: string + name: string + email: string + avatarUrl: string + apiKey: string + admin: boolean + createdAt: Date +} + +const toAccount = (data: object): Account => { + const account = remapFields(data, { + avatar_url: "avatarUrl", + api_key: "apiKey", + created_at: "createdAt", + }) + account.createdAt = new Date(account.createdAt) + return account +} + +export const getAccount = (): Promise> => + client.get("/v1/account") + .then((res) => { + if (res.status !== 200) { + return { status: res.status, error: res.data } + } + return { status: res.status, data: toAccount(res.data.account) } + }) + +export const syncAccount = (): Promise> => + client.post("/v1/account/sync") + .then((res) => { + if (res.status !== 200) { + return { status: res.status, error: res.data } + } + return { status: res.status, data: toAccount(res.data.account) } + }) + +export const regenerateApiKey = (): Promise> => + client.post("/v1/account/regenerate_apikey") + .then((res) => { + if (res.status !== 200) { + return { status: res.status, error: res.data } + } + return { status: res.status, data: toAccount(res.data.account) } + }) diff --git a/src/client/container.ts b/src/client/container.ts new file mode 100644 index 0000000..d25fb5d --- /dev/null +++ b/src/client/container.ts @@ -0,0 +1,45 @@ +import { ApiResponse, client, remapFields } from "./index" + +export interface Container { + id: string + name: string + status: string + image: string + endpoint: string + memory: number + tags: string[] + createdAt: Date +} + +const toContainer = (data: object): Container => { + const container = remapFields(data, { + created_at: "createdAt", + }) + container.createdAt = new Date(container.createdAt) + return container +} + +export const getContainers = (): Promise> => + client.get("/v1/containers") + .then((res) => { + if (res.status !== 200) { + return { status: res.status, error: res.data } + } + return { status: res.status, data: res.data.containers.map((data: object) => toContainer(data)) } + }) + +export interface CreateContainerRequest { + name: string + image: string + size: string + tags: string[] +} + +export const createContainer = (req: CreateContainerRequest): Promise> => + client.post("/v1/containers", req) + .then((res) => { + if (res.status !== 200) { + return { status: res.status, error: res.data } + } + return { status: res.status, data: toContainer(res.data.container) } + }) diff --git a/src/client/image.ts b/src/client/image.ts new file mode 100644 index 0000000..e6a437e --- /dev/null +++ b/src/client/image.ts @@ -0,0 +1,22 @@ +import { ApiResponse, client, remapFields } from "./index" + +export interface ImageSummary { + name: string + tag: string + namespaceId: string + lastPush: Date +} + +const toImageSummary = (data: object): ImageSummary => + remapFields(data, { + namespace_id: "namespaceId", + last_push: "lastPush", + }) + +export const getImages = (): Promise> => + client.get("/v1/images").then((res) => { + if (res.status !== 200) { + return { status: res.status, error: res.data } + } + return { status: res.status, data: res.data.images.map((data: object) => toImageSummary(data)) } + }) diff --git a/src/client/index.ts b/src/client/index.ts new file mode 100644 index 0000000..3be2e00 --- /dev/null +++ b/src/client/index.ts @@ -0,0 +1,32 @@ +import axios from "axios" +import * as container from "./container" +import * as account from "./account" +import * as image from "./image" + +export const client = axios.create({ + baseURL: process.env.API_URL || "http://localhost:3000", + transformRequest(data, headers) { + headers.Authorization = document.cookie.split("=")[1] + return data + }, + validateStatus() { + return true + }, +}) + +export const remapFields = (obj: any, fields: any): any => + Object.entries(obj) + .map(([key, value]) => [fields[key] || key, value]) + .reduce((prev, [key, value]) => ({ ...prev, [key]: value }), {}) + +export interface ApiResponse { + status: number + data?: T + error?: { + message: string + fields?: object + } +} +const a = { ...container, ...account, ...image } +console.log(a) +export default a diff --git a/src/client/plan.ts b/src/client/plan.ts new file mode 100644 index 0000000..ead516c --- /dev/null +++ b/src/client/plan.ts @@ -0,0 +1 @@ +export default () => {} diff --git a/src/components/Account/Account.tsx b/src/components/Account/Account.tsx index 6c5f220..ad346de 100644 --- a/src/components/Account/Account.tsx +++ b/src/components/Account/Account.tsx @@ -1,38 +1,81 @@ -import React, { useState } from "react" -import { getAccount, regenerateApiKey, syncAccount } from "../../client" +import React, { useReducer } from "react" import { Header } from "../Layout" -import { FormGroup, FormSection, Input } from "../Form" -import { usePromise } from "../../hooks" -import { Container } from "../Responsive" -import { Loader } from "../Loader" +import { Button, FormGroup, FormSection, Input } from "../Form" +import { Col, Container, Row } from "../Responsive" +import { useDispatch, useMappedState } from "redux-react-hook" +import client from "../../client" + +type Action = + { type: "SET_LOADING", loading: boolean } | + { type: "SET_REVEAL", reveal: boolean } | + { type: "SET_ERROR", error: string } + +interface IState { + loading: boolean + reveal: boolean + error?: string +} + +const reducer = (state: IState, action: Action) => { + switch (action.type) { + case "SET_LOADING": + return { + ...state, + loading: action.loading, + } + case "SET_REVEAL": + return { + ...state, + reveal: action.reveal, + } + case "SET_ERROR": + return { + ...state, + error: action.error, + } + default: + return state + } +} export default () => { - const { loading, data, error, dispatch } = usePromise(() => getAccount(), []) - const [reveal, setReveal] = useState(false) - const [apiError, setApiError] = useState() + const [state, dispatch] = useReducer(reducer, { + loading: false, + reveal: false, + }) + const account = useMappedState(state => state.account.account) + const reduxDispatch = useDispatch() const regenerateApiKeyHandler = () => { - regenerateApiKey() - .then((data) => { - dispatch({ action: "SET_DATA", data }) - document.cookie = `token=${data.apiKey}` - setReveal(true) - setApiError(undefined) - }) - .catch((error) => { - setApiError(error) + dispatch({ type: "SET_LOADING", loading: true }) + + client.regenerateApiKey() + .then((res) => { + if (res.error) { + dispatch({ type: "SET_ERROR", error: res.error.message }) + } else if (res.data) { + reduxDispatch({ type: "SET_ACCOUNT", account: res.data }) + document.cookie = `token=${res.data.apiKey}` + dispatch({ type: "SET_REVEAL", reveal: true }) + } }) + .catch((error) => dispatch({ type: "SET_ERROR", error: error.message })) + .finally(() => dispatch({ type: "SET_LOADING", loading: false })) } const syncAccountHandler = () => { - syncAccount() - .then((data) => { - dispatch({ action: "SET_DATA", data }) - setApiError(undefined) - }) - .catch((error) => { - setApiError(error) + dispatch({ type: "SET_LOADING", loading: true }) + + client.syncAccount() + .then((res) => { + if (res.error) { + dispatch({ type: "SET_ERROR", error: res.error.message }) + } else if (res.data) { + reduxDispatch({ type: "SET_ACCOUNT", account: res.data }) + } }) + .catch((error) => dispatch({ type: "SET_ERROR", error: error.message })) + .finally(() => dispatch({ type: "SET_LOADING", loading: false })) } return ( @@ -40,47 +83,44 @@ export default () => {
- {error && ( -

Error: {error.message}...

+ {state.error && ( +
+ {state.error} +
)} - - {apiError && ( -
- {apiError.message} -
- )} - - - - - - - - - - -
-
- -
-
- -
-
- - - -
-
+ + + + + + + + + + + + + + + + + + + + + + + +
) diff --git a/src/components/App.tsx b/src/components/App.tsx index 53335af..0de75e8 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -1,24 +1,81 @@ -import React from "react" +import React, { useEffect, useState } from "react" import { Redirect, Route, Switch } from "react-router-dom" -import { Navbar, Footer } from "./Layout" +import { Footer, Navbar } from "./Layout" import { ListContainer, NewContainer } from "./Container" import { ListImage } from "./Image" import { Account } from "./Account" +import { useDispatch } from "redux-react-hook" +import client from "../client" -export default () => ( - <> - - - - - - - - - - -
- © Expected.sh - All Rights Reserved 2019 -
- -) +export default () => { + const [loading, setLoading] = useState(true) + const [error, setError] = useState() + const dispatch = useDispatch() + + useEffect(() => { + let _cancelled = false + + client.getAccount() + .then((res) => { + if (_cancelled) { + return + } + if (res.error) { + if (res.status === 403) { + window.location.href = process.env.AUTH_URL || "http://localhost:3002/oauth/github" + } else { + setError(res.error.message) + } + } else { + dispatch({ type: "SET_ACCOUNT", account: res.data }) + } + }) + .catch((error) => { + if (_cancelled) { + return + } + setError(error.message) + }) + .finally(() => { + if (_cancelled) { + return + } + setLoading(false) + }) + return () => { + _cancelled = true + } + }, []) + + if (loading) { + return ( +

+ Loading... +

+ ) + } + + if (error) { + return ( +

Error: {error}

+ ) + } + + return ( + <> + + + + + + + + + + +
+ © Expected.sh - All Rights Reserved 2019 +
+ + ) +} diff --git a/src/components/Card/CardTable.tsx b/src/components/Card/CardTable.tsx index 08b2406..26fe8cc 100644 --- a/src/components/Card/CardTable.tsx +++ b/src/components/Card/CardTable.tsx @@ -1,17 +1,18 @@ import React, { ReactNode } from "react" import { styled } from "../../style" -interface IColumn { - title: string - key: string - align?: "left" | "center" | "right" - render?: (data: any) => ReactNode -} +type IColumn = + { + title: string + align?: "left" | "center" | "right" + key?: string + render?: (data: T) => ReactNode + } interface IProps { onRowClick?: (data: T) => any dataSource: T[] | undefined - columns: IColumn[] + columns: IColumn[] } const Table = styled.table` @@ -57,14 +58,14 @@ export default ({ columns, dataSource = [], onRowClick }: IProps) => { - {dataSource.map((data: any, index) => ( + {dataSource.map((data, index) => ( - {columns.map(({ key, align, render }, index) => ( + {columns.map((column, index) => ( - {render ? render(data[key]) : data[key]} + {column.render ? column.render(data) : (data as any)[column.key as any]} ))} diff --git a/src/components/Container/ListContainer.tsx b/src/components/Container/ListContainer.tsx index 5dea309..2cdf1fc 100644 --- a/src/components/Container/ListContainer.tsx +++ b/src/components/Container/ListContainer.tsx @@ -1,7 +1,6 @@ -import React from "react" +import React, { useEffect, useReducer } from "react" import TimeAgo from "react-timeago" -import { usePromise } from "../../hooks" -import { getContainers, IContainer } from "../../client" +import client from "../../client" import { Header } from "../Layout" import { Container } from "../Responsive" import { Card, CardTable } from "../Card" @@ -20,23 +19,11 @@ const Tag = styled.div` ` const columns = [ - // { - // render: () => ( - // - // ), - // }, { title: "Name", - key: "name", - render: (name: any) => ( + render: (data: client.Container) => ( <> - {name} + {data.name} ), }, @@ -46,23 +33,20 @@ const columns = [ }, { title: "Created", - key: "createdAt", - render: (createdAt: any) => , + render: (data: client.Container) => , }, { title: "Tags", - key: "tags", - render: (tags: any) => ( + render: (data: client.Container) => ( <> - {tags.map((tag: string, index: number) => ( - {tag} + {data.tags.map((tag: any, i: number) => ( + {tag} ))} ), }, { title: "", - key: "", render: () => { const overlay = () => ( @@ -83,8 +67,59 @@ const columns = [ }, ] +type Action = + { type: "SET_CONTAINERS", containers: client.Container[] } | + { type: "SET_LOADING", loading: boolean } | + { type: "SET_ERROR", error: string } + +interface IState { + loading: boolean + containers: client.Container[] + error?: string +} + +const reducer = (state: IState, action: Action) => { + switch (action.type) { + case "SET_LOADING": + return { + ...state, + loading: action.loading, + } + case "SET_CONTAINERS": + return { + ...state, + containers: action.containers, + } + case "SET_ERROR": + return { + ...state, + error: action.error, + } + default: + return state + } +} + export default () => { - const { loading, data, error } = usePromise(() => getContainers(), []) + const [state, dispatch] = useReducer(reducer, { + loading: true, + containers: [], + }) + + useEffect(() => { + dispatch({ type: "SET_LOADING", loading: true }) + + client.getContainers() + .then((res) => { + if (res.error) { + dispatch({ type: "SET_ERROR", error: res.error.message }) + } else if (res.data) { + dispatch({ type: "SET_CONTAINERS", containers: res.data }) + } + }) + .catch((error) => dispatch({ type: "SET_ERROR", error: error.message })) + .finally(() => dispatch({ type: "SET_LOADING", loading: false })) + }, []) return ( <> @@ -95,14 +130,14 @@ export default () => {
- {error && ( -

Error: {error.message}...

+ {state.error && ( +

Error: {state.error}...

)} - - {data && ( + + {state.containers && ( - columns={columns} dataSource={data} - onRowClick={(data) => console.log(data)}/> + columns={columns} dataSource={state.containers} + onRowClick={(data) => console.log(data)}/> )} diff --git a/src/components/Container/NewContainer.tsx b/src/components/Container/NewContainer.tsx index 046fc21..e4b4078 100644 --- a/src/components/Container/NewContainer.tsx +++ b/src/components/Container/NewContainer.tsx @@ -1,60 +1,132 @@ -import React from "react" +import React, { FormEvent, useReducer } from "react" import { Header } from "../Layout" -import { Button, Form, FormGroup, FormSection, Input, Select } from "../Form" +import { AutocompleteInput, Button, Form, FormGroup, FormSection, Input, TagInput } from "../Form" import { Col, Container, Row } from "../Responsive" -import { useForm } from "../../hooks" +import { PlanTable } from "./Plan" +import client from "../../client" -const test = () => new Promise((resolve, reject) => setTimeout(resolve, 4000)) +interface IContainerPlan { +} + +type Action = + { type: "SET_LOADING", loading: boolean } | + { type: "SET_FORM_VALUE", key: string, value: any } | + { type: "SET_PLANS", plans: IContainerPlan[] } + +interface IState { + loading: boolean + form: client.CreateContainerRequest + plans: IContainerPlan[] + error?: string + formError?: { + name: string + image: string + tags: string + } +} + +const reducer = (state: IState, action: Action) => { + switch (action.type) { + case "SET_LOADING": + return { + ...state, + loading: action.loading, + } + case "SET_FORM_VALUE": + return { + ...state, + form: { + ...state.form, + [action.key]: action.value, + }, + } + case "SET_PLANS": + return { + ...state, + plans: action.plans, + } + default: + return state + } +} export default () => { - const { loading, error, handleChange, handleSubmit, dispatch, values } = useForm({ - name: "", - image: "", - size: "", - tags: "", - }, async (values) => { - await test() + const [state, dispatch] = useReducer(reducer, { + loading: true, + form: { + name: "", + image: "", + size: "64", + tags: [], + }, + plans: [], }) + // useEffect(() => { + // container.() + // .then(plans => dispatch({ type: "SET_PLANS", plans })) + // .finally(() => dispatch({ type: "SET_LOADING", loading: false })) + // // .catch(error => ) + // }, []) + + const handleSubmit = (event: FormEvent) => { + event.preventDefault() + + dispatch({ type: "SET_LOADING", loading: true }) + client.createContainer(state.form) + .then(console.log) + .catch(console.error) + .finally(() => dispatch({ type: "SET_LOADING", loading: false })) + } + return ( <>
-
+ + {state.error &&

{state.error}

} - + + onChange={(event) => dispatch({ type: "SET_FORM_VALUE", key: "name", value: event.target.value })} + autoComplete="off"/> - - + + dispatch({ + type: "SET_FORM_VALUE", + key: "image", + value: event.target.value, + })}/> - - + dispatch({ type: "SET_FORM_VALUE", key: "tags", value: tags })}/> - - - + + + + + - @@ -64,3 +136,12 @@ export default () => { ) } + +// {state.plans && state.plans.map((plan, index) => ( +// +// {plan.name} +// {plan.cpu} vCPU +// {plan.memory}MB +// ${plan.price} +// +// ))} diff --git a/src/components/Container/Plan.tsx b/src/components/Container/Plan.tsx index 738699a..350cf5c 100644 --- a/src/components/Container/Plan.tsx +++ b/src/components/Container/Plan.tsx @@ -1,5 +1,4 @@ import React from "react" -import { Col } from "../Responsive" import { styled } from "../../style" interface IProps { @@ -8,41 +7,29 @@ interface IProps { memory: number } -const Plan = styled.div` - background: ${props => props.theme.color.light}; +export const PlanTable = styled.table` + width: 100%; +` + +export const Plan = styled.tr` border: 1px solid ${props => props.theme.color.grey}; - border-radius: 5px; - padding: 1rem; - width: 250px; - - h3 { - text-align: center; - margin-bottom: 1.2rem; - } - - ul { - list-style: none; - padding: 0; - - li { - border-top: 1px solid ${props => props.theme.color.grey}; - padding: 0.4rem 0.8rem; - } + border-radius: 0.25rem; + + td { + padding: 0.5rem 1rem; } ` -export default ({ name, cpu, memory }: IProps) => { - return ( - <> - -

{name}

- -
    -
  • {cpu} virtual cpu
  • -
  • {memory}MB
  • -
  • Unlimited bandwidth
  • -
-
- - ) -} +// export const Plan = ({ name, cpu, memory }: IProps) => { +// return ( +// +//

{name}

+// +//
    +//
  • {cpu} virtual cpu
  • +//
  • {memory}MB
  • +//
  • Unlimited bandwidth
  • +//
+//
+// ) +// } diff --git a/src/components/Dropdown/Dropdown.tsx b/src/components/Dropdown/Dropdown.tsx index ddc77c6..42bc20c 100644 --- a/src/components/Dropdown/Dropdown.tsx +++ b/src/components/Dropdown/Dropdown.tsx @@ -1,20 +1,22 @@ -import React, { ReactNode, useState } from "react" +import React, { CSSProperties, ReactNode, useState } from "react" import { styled } from "../../style" interface IProps { overlay: () => ReactNode children: ReactNode + style?: CSSProperties + className?: string } const Dropdown = styled.div` position: relative; ` -export default ({ overlay, children }: IProps) => { +export default ({ overlay, children, style = {}, className }: IProps) => { const [toggle, setToggle] = useState(false) return ( - setToggle(false)}> + setToggle(false)} style={style} className={className}>
setToggle(!toggle)}> {children}
diff --git a/src/components/Dropdown/DropdownContent.tsx b/src/components/Dropdown/DropdownContent.tsx index 4001895..b514e97 100644 --- a/src/components/Dropdown/DropdownContent.tsx +++ b/src/components/Dropdown/DropdownContent.tsx @@ -3,8 +3,8 @@ import { styled } from "../../style" export default styled.div` position: absolute; will-change: transform; - top: -10px; - left: -10px; + top: 0; + left: 0; transform: translate3d(0, 38px, 0); border: 1px solid #edf2f9; box-shadow: 0 0.75rem 1.5rem rgba(18, 38, 63, 0.03); diff --git a/src/components/Dropdown/DropdownItem.tsx b/src/components/Dropdown/DropdownItem.tsx index 8ed1db3..a80d684 100644 --- a/src/components/Dropdown/DropdownItem.tsx +++ b/src/components/Dropdown/DropdownItem.tsx @@ -11,4 +11,12 @@ export default styled.div` white-space: nowrap; background-color: transparent; border: 0; + + a:hover { + text-decoration: none; + } + + &:hover { + background: ${props => props.theme.color.greyLight}; + } ` diff --git a/src/components/Form/AutocompleteInput.tsx b/src/components/Form/AutocompleteInput.tsx new file mode 100644 index 0000000..714d4d9 --- /dev/null +++ b/src/components/Form/AutocompleteInput.tsx @@ -0,0 +1,142 @@ +import { styled, theme } from "../../style" +import React, { ChangeEvent, ReactNode, useEffect, useState } from "react" +import { Input } from "./index" + +const AutocompleteInput = styled(Input)`` + +const AutocompleteItems = styled.div` + margin-top: 10px; + border-radius: 0.25rem; + position: absolute; + display: block; + z-index: 99; + border: 1px solid ${theme.color.grey}; + width: 100%; +` + +const AutocompleteItem = styled.div` + cursor: pointer; + background: white; + padding: 0.3rem 0.8rem 0.3rem 0.8rem; + :hover{ + background: ${theme.color.grey}; + } +` + +interface IProps { + suggestions: string[] + onChange: (event: ChangeEvent) => void + name: string, + placeholder?: string, + defaultTags?: string[], + suggestionRender?: (suggest: string) => ReactNode +} + +const defaultSuggestionRender = (suggest: string) => { + return <>{suggest} +} + +export default ({ name, onChange, suggestions, placeholder = "", suggestionRender = defaultSuggestionRender }: IProps) => { + + const [completions, setCompletions] = useState([]) + const [value, setValue] = useState("") + const [completionIndex, setCompletionIndex] = useState(-1) + + const [id] = useState('_' + Math.random().toString(36).substr(2, 9)) + + useEffect(() => { + // const focusOut = (event: any) => { + // hideCompletion() + // event.preventDefault() + // } + // document.addEventListener("click", focusOut) + // window.addEventListener("resize", focusOut) + // return () => { + // document.removeEventListener("click", focusOut) + // window.removeEventListener("resize", focusOut) + // } + }) + + + const change = (e: ChangeEvent) => { + setValue(e.target.value) + setCompletions(suggestions + .filter(x => e.target.value.length > 0 && x.startsWith(e.target.value)) + .slice(0, 5)) + onChange(e) + } + + const clean = () => { + setValue("") + } + + const select = (val: string) => { + setValue(val) + onChange({target: { value: val, name}} as any) + hideCompletion() + } + + const hideCompletion = () => { + setCompletionIndex(-1) + setCompletions([]) + } + + const clickCompletion = (e: any, completion: string) => { + e.preventDefault() + select(completion) + clean() + hideCompletion() + focusInput() + } + + const focusInput = () => { + const input = document.getElementById(id) + if (input) return input.focus() + } + + + const key = (e: any) => { + + const completionDown = () => setCompletionIndex(completionIndex + 1 > completions.length - 1 ? 0 : completionIndex + 1) + const completionUp = () => setCompletionIndex(completionIndex - 1 < 0 ? completions.length - 1 : completionIndex - 1) + + if (e.keyCode === 13) e.preventDefault() + + if (e.keyCode === 27 || e.keyCode === 9) { + hideCompletion() + } else if (e.keyCode === 40) completionDown() + else if (e.keyCode === 38) completionUp() + else if (e.keyCode === 13 && completionIndex !== -1) select(completions[completionIndex]) + else if (e.keyCode === 13 && value.trim().length > 0 && completionIndex === -1) { + select(value) + clean() + } + } + + const sizeOfInput = () => { + const d = document.getElementById(id) + if (d) return d.offsetWidth + "px" + else return '100%' + } + + return ( +
+ + { + completions.length > 0 + ? + + {completions.map((val, index) => + ( clickCompletion(e, val)} + key={index}>{suggestionRender(val)})) + } + + : <> + } +
+ ) +} + diff --git a/src/components/Form/Button.tsx b/src/components/Form/Button.tsx index 6f32033..d7e5b37 100644 --- a/src/components/Form/Button.tsx +++ b/src/components/Form/Button.tsx @@ -19,7 +19,7 @@ export const Button = styled.button` user-select: none; background-color: transparent; border: 1px solid transparent; - padding: 0.375rem 0.75rem; + padding: 0.5rem 0.75rem; font-size: 1rem; line-height: 1.5; border-radius: 0.25rem; diff --git a/src/components/Form/Form.tsx b/src/components/Form/Form.tsx index 80ff820..43f7995 100644 --- a/src/components/Form/Form.tsx +++ b/src/components/Form/Form.tsx @@ -3,7 +3,7 @@ import { styled } from "../../style" import { Loader } from "../Loader" interface IProps { - onSubmit?: (event: FormEvent) => void + onSubmit?: (event: FormEvent) => any loading?: boolean children: ReactNode } diff --git a/src/components/Form/FormGroup.tsx b/src/components/Form/FormGroup.tsx index 91814d4..c20ecff 100644 --- a/src/components/Form/FormGroup.tsx +++ b/src/components/Form/FormGroup.tsx @@ -1,14 +1,16 @@ import React, { ReactNode } from "react" import { styled } from "../../style" -interface IProps { - name?: string - description?: string - children: ReactNode +interface IFormGroupProps { + error?: boolean } -const FormGroup = styled.div` +const FormGroup = styled.div` margin-bottom: 1rem; + + input, input:focus { + border-color: ${props => props.error ? props.theme.color.red : ""}; + } ` const Label = styled.label` @@ -26,8 +28,22 @@ const Description = styled.small` font-weight: 400; ` -export default ({ name, description, children }: IProps) => ( - +const Error = styled.div` + width: 100%; + margin-top: .25rem; + font-size: 80%; + color: ${props => props.theme.color.red}; +` + +interface IProps { + name?: string + description?: string + error?: string + children: ReactNode +} + +export default ({ name, description, error, children }: IProps) => ( + {name && ( ) diff --git a/src/components/Form/Input.tsx b/src/components/Form/Input.tsx index 455d452..9726856 100644 --- a/src/components/Form/Input.tsx +++ b/src/components/Form/Input.tsx @@ -1,16 +1,14 @@ import { styled } from "../../style" - export const Input = styled.input` display: block; width: 100%; - height: calc(1.5em + 0.75rem + 2px); - padding: 0.375rem 0.75rem; + padding: 0.575rem 0.75rem; font-size: .9375rem; font-weight: 400; line-height: 1.5; color: ${props => props.theme.text.normal}; - background-color: #fff; + background: ${props => props.theme.color.light}; background-clip: padding-box; border: 1px solid ${props => props.theme.color.grey}; border-radius: 0.25rem; @@ -27,4 +25,6 @@ export const Input = styled.input` } ` -export const Select = Input.withComponent("select") +export const Select = styled(Input.withComponent("select"))` + height: 2.656rem; +` diff --git a/src/components/Form/TagInput.tsx b/src/components/Form/TagInput.tsx new file mode 100644 index 0000000..d14a5bb --- /dev/null +++ b/src/components/Form/TagInput.tsx @@ -0,0 +1,217 @@ +import { styled, theme } from "../../style" +import React, { KeyboardEvent, useEffect, useReducer, useState } from "react" + +const Input = styled.input` + width: auto; + flex-grow: 1; + flex-shrink: 0; + border: none; + outline: none; + box-shadow: none; + font-size: .9375rem; + font-weight: 400; + line-height: 1.5; + height: calc(2.6rem); + background-clip: padding-box; +` + +const Tag = styled.div` + display: inline; + background: ${props => props.theme.color.dark}; + color: ${props => props.theme.color.light}; + padding: 0.2rem 0.8rem; + margin-right: 5px; + border-radius: 15px; + cursor: pointer; +` + +interface IAutocompleteProps { + focus?: boolean +} + +const Autocomplete = styled.div` + display: flex; + flex-flow: row wrap; + width: 100%; + color: ${props => props.theme.text.normal}; + background-color: ${props => props.theme.color.light}; + border: 1px solid ${props => props.focus ? props.theme.color.blue : props.theme.color.grey}; + border-radius: 0.25rem; + transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; + padding: 0.575rem 0.75rem; + + &:disabled, &[readonly] { + background: ${props => props.theme.color.grey}; + } +` + +const SuggestionList = styled.div` + position: absolute; + will-change: transform; + top: 0; + left: 0; + width: 100%; + transform: translate3d(0, 38px, 0); + border: 1px solid #edf2f9; + box-shadow: 0 0.75rem 1.5rem rgba(18, 38, 63, 0.03); + background-color: ${props => props.theme.color.light}; + border-radius: .25rem; + padding: .4rem 0; + z-index: 1000; + float: left; +` + +const TagInput = styled.div` + position: relative; +` + +const Suggestion = styled.div` + cursor: pointer; + background: white; + padding: 0.3rem 0.8rem 0.3rem 0.8rem; + + :hover{ + background: ${theme.color.greyLight}; + } +` + +interface IState { + value: string + tags: string[] + suggestions: string[] + currentSuggestions: string[] + suggestionOffset: number + suggestionIndex: number + tagIndex: number +} + +type Action = + { action: "SET_VALUE", value: string } | + { action: "ADD_TAG", value: string } | + { action: "DELETE_TAG", value: string } | + { action: "SET_TAG_INDEX", value: number } | + { action: "SET_SUGGESTION_INDEX", value: number } + +const reducer = (state: IState, action: Action) => { + switch (action.action) { + case "SET_VALUE": + return { + ...state, + value: action.value, + currentSuggestions: action.value.trim() ? state.suggestions + .filter(s => s.startsWith(action.value) && !state.tags.includes(s)).slice(0, 5) : [], + tagIndex: -1, + suggestionIndex: -1, + } + case "ADD_TAG": + return { + ...state, + value: "", + tags: state.tags.includes(action.value) || !action.value.trim() ? + state.tags : [...state.tags, action.value], + currentSuggestions: [], + tagIndex: -1, + suggestionIndex: -1, + } + case "DELETE_TAG": + return { + ...state, + tags: state.tags.filter(tag => tag !== action.value), + tagIndex: -1, + suggestionIndex: -1, + } + case "SET_TAG_INDEX": + return { + ...state, + tagIndex: action.value < 0 || action.value >= state.tags.length ? + (action.value < 0 ? state.tags.length - 1 : 0) : action.value, + } + case "SET_SUGGESTION_INDEX": + return { + ...state, + suggestionIndex: action.value < 0 || action.value >= state.currentSuggestions.length ? + (action.value < 0 ? state.tags.length - 1 : 0) : action.value, + } + } + return state +} + +interface IProps { + suggestions: string[] + value?: string[] + placeholder?: string + onChange?: (value: string[]) => any +} + +export default ({ onChange, suggestions, placeholder = "", value = [] }: IProps) => { + const [state, dispatch] = useReducer(reducer, { + value: "", + tags: value, + suggestions, + currentSuggestions: [], + suggestionOffset: 0, + suggestionIndex: -1, + tagIndex: -1, + }) + const [focus, setFocus] = useState(false) + + + useEffect(() => { + if (onChange && state.tags.length) { + onChange(state.tags) + } + }, [state.tags]) + + const handleInputKey = (event: KeyboardEvent) => { + if ([" ", "Enter", ";", ","].includes(event.key)) { + event.preventDefault() + dispatch({ + action: "ADD_TAG", + value: state.suggestionIndex !== -1 ? state.currentSuggestions[state.suggestionIndex] : state.value, + }) + } else if (["ArrowLeft", "ArrowRight"].includes(event.key) && (state.tagIndex !== -1 || !state.value.trim())) { + event.preventDefault() + dispatch({ + action: "SET_TAG_INDEX", + value: event.key === "ArrowLeft" ? state.tagIndex - 1 : state.tagIndex + 1, + }) + } else if (["ArrowUp", "ArrowDown"].includes(event.key)) { + event.preventDefault() + dispatch({ + action: "SET_SUGGESTION_INDEX", + value: event.key === "ArrowUp" ? state.suggestionIndex - 1 : state.suggestionIndex + 1, + }) + } else if (event.key === "Backspace" && (state.tagIndex !== -1 || !state.value.trim())) { + event.preventDefault() + dispatch({ + action: "DELETE_TAG", + value: state.tags[state.tagIndex !== -1 ? state.tagIndex : state.tags.length - 1], + }) + } + } + + return ( + setFocus(true)} onBlur={() => setFocus(false)}> + + {state.tags.map((tag, index) => ( + dispatch({ action: "DELETE_TAG", value: tag })} + style={index !== state.tagIndex ? {} : { background: theme.color.dark }}> + {tag} + + ))} + dispatch({ action: "SET_VALUE", value: e.target.value })}/> + + {(state.currentSuggestions.length > 0 && focus) && ( + + {state.currentSuggestions.map((value, index) => ( + dispatch({ action: "ADD_TAG", value })} + style={index === state.suggestionIndex ? { background: theme.color.greyLight } : {}}> + {value} + + ))} + + )} + + ) +} diff --git a/src/components/Form/index.ts b/src/components/Form/index.ts index 1bb93bd..ba0b0e8 100644 --- a/src/components/Form/index.ts +++ b/src/components/Form/index.ts @@ -2,6 +2,8 @@ import { Input, Select } from "./Input" import { Button, ButtonLink } from "./Button" export { default as Form } from "./Form" +export { default as TagInput } from "./TagInput" +export { default as AutocompleteInput } from "./AutocompleteInput" export { default as FormGroup } from "./FormGroup" export { default as FormSection } from "./FormSection" export { Input, Select, Button, ButtonLink } diff --git a/src/components/Image/ListImage.tsx b/src/components/Image/ListImage.tsx index 8bcea9c..f6212bc 100644 --- a/src/components/Image/ListImage.tsx +++ b/src/components/Image/ListImage.tsx @@ -1,9 +1,162 @@ -import React from "react" +import React, { useEffect, useReducer } from "react" +import TimeAgo from "react-timeago" +import { Header } from "../Layout" +import { Loader } from "../Loader" +import { Card, CardBody, CardTable } from "../Card" +import { Container } from "../Responsive" +import { styled } from "../../style" +import { Dropdown, DropdownButton, DropdownContent, DropdownItem } from "../Dropdown" +import { useMappedState } from "redux-react-hook" +import client from "../../client" + +const NoImage = styled(CardBody)` + h3 { + margin-bottom: 2rem; + } + + pre { + width: fit-content; + color: ${props => props.theme.color.light}; + background: ${props => props.theme.color.dark}; + padding: 0.5rem 1.25rem; + border-radius: 0.25rem; + font-size: 1rem; + + div { + padding: 0.5rem 0; + } + } +` + +const columns = [ + { + title: "Name", + render: (data: image.ImageSummary) => ( + <> + {data.name}:{data.tag} + + ), + }, + { + title: "URL", + render: (data: image.ImageSummary) => ( + <> + registry.expected.sh/{data.namespaceId}/{data.name}:{data.tag} + + ), + }, + { + title: "Last push", + render: (data: image.ImageSummary) => , + }, + { + title: "", + render: () => { + const overlay = () => ( + + Action + Another action + Something else here + + ) + + return ( + + + More + + + ) + }, + }, +] + +type Action = + { type: "SET_IMAGES", images: client.ImageSummary[] } | + { type: "SET_LOADING", loading: boolean } | + { type: "SET_ERROR", error: string } + +interface IState { + loading: boolean + images: client.ImageSummary[] + error?: string +} + +const reducer = (state: IState, action: Action) => { + switch (action.type) { + case "SET_LOADING": + return { + ...state, + loading: action.loading, + } + case "SET_IMAGES": + return { + ...state, + images: action.images, + } + case "SET_ERROR": + return { + ...state, + error: action.error, + } + default: + return state + } +} export default () => { + const account = useMappedState(state => state.account.account) + const [state, dispatch] = useReducer(reducer, { + loading: true, + images: [], + }) + + useEffect(() => { + dispatch({ type: "SET_LOADING", loading: true }) + + client.getImages() + .then((res) => { + if (res.error) { + dispatch({ type: "SET_ERROR", error: res.error.message }) + } else if (res.data) { + dispatch({ type: "SET_IMAGES", images: res.data }) + } + }) + .catch((error) => dispatch({ type: "SET_ERROR", error: error.message })) + .finally(() => dispatch({ type: "SET_LOADING", loading: false })) + }, []) + return ( <> +
+ + {state.error && ( +

Error: {state.error}...

+ )} + + {state.images && ( + + {state.images.length ? ( + columns={columns} dataSource={state.images} + onRowClick={(data) => console.log(data)}/> + ) : ( + +

Push your first image

+
+                    
+ docker login registry.expected.sh -u {account.email} -p {account.apiKey} +
+
+ docker push registry.expected.sh/{account.id}/{"<"}your image{">"} +
+
+
+ )} +
+ )} +
+
) } diff --git a/src/components/Layout/Navbar.tsx b/src/components/Layout/Navbar.tsx index 4358010..7a6aa59 100644 --- a/src/components/Layout/Navbar.tsx +++ b/src/components/Layout/Navbar.tsx @@ -2,6 +2,8 @@ import React from "react" import { Link, Route } from "react-router-dom" import { styled } from "../../style" import { Container } from "../Responsive" +import { useMappedState } from "redux-react-hook" +import { Dropdown, DropdownButton, DropdownContent, DropdownItem } from "../Dropdown" const Navbar = styled.div` background: ${props => props.theme.color.dark}; @@ -86,18 +88,41 @@ const NavLink = ({ to, exact, name }: INavLinkProps) => { ) } -const Profile = styled.div` +const ProfileDropdown = styled(Dropdown)` align-self: center; +` + +const Profile = styled(DropdownButton.withComponent("div"))` + cursor: pointer; color: rgba(255, 255, 255, 0.5); + user-select: none; img { - margin-left: 15px; + margin-left: 1rem; height: 32px; - border-radius: 5px; + border-radius: 0.25rem; + } + + &::after { + margin-left: .4em; + vertical-align: 0; } ` export default () => { + const account = useMappedState(state => state.account.account) + + const overlay = () => ( + + + Account + + + Billing + + + ) + return ( @@ -106,10 +131,12 @@ export default () => { - - Rémi Caumette - Avatar - + + + {account.name} + Avatar + + ) diff --git a/src/components/Loader/Loader.tsx b/src/components/Loader/Loader.tsx index 7825748..271fc1a 100644 --- a/src/components/Loader/Loader.tsx +++ b/src/components/Loader/Loader.tsx @@ -9,7 +9,7 @@ interface IProps { const Loader = styled.div` position: absolute; - top: 50%; + top: calc(50% - 80px); left: 50%; z-index: 10000; height: 5em; @@ -59,7 +59,7 @@ const Spinner = styled.div` export default ({ loading = true, children }: IProps) => { return ( -
+
{loading && ( diff --git a/src/components/Responsive/Row.tsx b/src/components/Responsive/Row.tsx index 39aeb8e..ce78f36 100644 --- a/src/components/Responsive/Row.tsx +++ b/src/components/Responsive/Row.tsx @@ -12,4 +12,5 @@ export default styled.div(props => css` flex-wrap: wrap; align-content: ${props.alignContent || "inherit"}; justify-content: ${props.justifyContent || "inherit"}; + margin: 0 -15px; `) diff --git a/src/index.tsx b/src/index.tsx index 906a150..7cddc5d 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -3,16 +3,22 @@ import ReactDOM from "react-dom" import { Router } from "react-router" import { createBrowserHistory } from "history" import { ThemeProvider } from "emotion-theming" +import { StoreContext } from "redux-react-hook" +import { createStore } from "redux" import { theme } from "./style" +import reducers from "./reducers" import App from "./components/App" import "./index.css" const history = createBrowserHistory() +const store = createStore(reducers) ReactDOM.render(( - - - - - + + + + + + + ), document.getElementById("root")) diff --git a/src/reducers/account.ts b/src/reducers/account.ts new file mode 100644 index 0000000..0b0eb95 --- /dev/null +++ b/src/reducers/account.ts @@ -0,0 +1,24 @@ +import { account } from "../client" + +type Action = + { type: "SET_ACCOUNT", account: account.Account } + +interface IState { + account: account.Account | undefined +} + +const INITIAL_STATE: IState = { + account: undefined, +} + +export default (state: IState = INITIAL_STATE, action: Action) => { + switch (action.type) { + case "SET_ACCOUNT": + return { + ...state, + account: action.account, + } + default: + return state + } +} diff --git a/src/reducers/index.ts b/src/reducers/index.ts new file mode 100644 index 0000000..6f96129 --- /dev/null +++ b/src/reducers/index.ts @@ -0,0 +1,4 @@ +import { combineReducers } from "redux" +import account from "./account" + +export default combineReducers({ account })