update all dependencies
This commit is contained in:
parent
9b9dec818a
commit
e532173322
1
.vscode/extensions.json
vendored
1
.vscode/extensions.json
vendored
|
@ -1,7 +1,6 @@
|
||||||
{
|
{
|
||||||
"recommendations": [
|
"recommendations": [
|
||||||
"jakebecker.elixir-ls",
|
"jakebecker.elixir-ls",
|
||||||
"ms-vscode.vscode-typescript-tslint-plugin",
|
|
||||||
"kevinmcgowan.typescriptimport",
|
"kevinmcgowan.typescriptimport",
|
||||||
"msjsdiag.debugger-for-chrome"
|
"msjsdiag.debugger-for-chrome"
|
||||||
]
|
]
|
||||||
|
|
120
backend/mix.lock
120
backend/mix.lock
|
@ -1,66 +1,66 @@
|
||||||
%{
|
%{
|
||||||
"appsignal": {:hex, :appsignal, "1.10.13", "d5df34ac7dc2d937510716f2089cc5f1d45b3f10f38225d19c35f31810b9266d", [:make, :mix], [{:decorator, "~> 1.2.3", [hex: :decorator, repo: "hexpm", optional: false]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: false]}, {:phoenix, ">= 1.2.0", [hex: :phoenix, repo: "hexpm", optional: true]}, {:plug, ">= 1.1.0", [hex: :plug, repo: "hexpm", optional: true]}, {:poison, ">= 1.3.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"},
|
"appsignal": {:hex, :appsignal, "1.10.1", "582238fd95cef54d1440ce9b2ef47c243e7d176200156164b2d924c29c7cef24", [:make, :mix], [{:decorator, "~> 1.2.3", [hex: :decorator, repo: "hexpm", optional: false]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: false]}, {:phoenix, ">= 1.2.0", [hex: :phoenix, repo: "hexpm", optional: true]}, {:plug, ">= 1.1.0", [hex: :plug, repo: "hexpm", optional: true]}, {:poison, ">= 1.3.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm", "686bf26b678d958a366215923de605492f99ca6ad7715a89a4abaf9caf6ea45d"},
|
||||||
"artificery": {:hex, :artificery, "0.4.2", "3ded6e29e13113af52811c72f414d1e88f711410cac1b619ab3a2666bbd7efd4", [:mix], [], "hexpm"},
|
"artificery": {:hex, :artificery, "0.4.2", "3ded6e29e13113af52811c72f414d1e88f711410cac1b619ab3a2666bbd7efd4", [:mix], [], "hexpm"},
|
||||||
"base64url": {:hex, :base64url, "0.0.1", "36a90125f5948e3afd7be97662a1504b934dd5dac78451ca6e9abf85a10286be", [:rebar], [], "hexpm"},
|
"base64url": {:hex, :base64url, "0.0.1", "36a90125f5948e3afd7be97662a1504b934dd5dac78451ca6e9abf85a10286be", [:rebar], [], "hexpm"},
|
||||||
"bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm"},
|
"bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm", "7af5c7e09fe1d40f76c8e4f9dd2be7cebd83909f31fee7cd0e9eadc567da8353"},
|
||||||
"certifi": {:hex, :certifi, "2.5.1", "867ce347f7c7d78563450a18a6a28a8090331e77fa02380b4a21962a65d36ee5", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm"},
|
"certifi": {:hex, :certifi, "2.5.1", "867ce347f7c7d78563450a18a6a28a8090331e77fa02380b4a21962a65d36ee5", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm", "805abd97539caf89ec6d4732c91e62ba9da0cda51ac462380bbd28ee697a8c42"},
|
||||||
"combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm"},
|
"combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"},
|
||||||
"connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], [], "hexpm"},
|
"connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], [], "hexpm", "4a0850c9be22a43af9920a71ab17c051f5f7d45c209e40269a1938832510e4d9"},
|
||||||
"corsica": {:hex, :corsica, "1.1.3", "5f1de40bc9285753aa03afbdd10c364dac79b2ddbf2ba9c5c9c47b397ec06f40", [:mix], [{:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"},
|
"corsica": {:hex, :corsica, "1.1.3", "5f1de40bc9285753aa03afbdd10c364dac79b2ddbf2ba9c5c9c47b397ec06f40", [:mix], [{:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "8156b3a14a114a346262871333a931a1766b2597b56bf994fcfcb65443a348ad"},
|
||||||
"cowboy": {:hex, :cowboy, "2.7.0", "91ed100138a764355f43316b1d23d7ff6bdb0de4ea618cb5d8677c93a7a2f115", [:rebar3], [{:cowlib, "~> 2.8.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.7.1", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm"},
|
"cowboy": {:hex, :cowboy, "2.7.0", "91ed100138a764355f43316b1d23d7ff6bdb0de4ea618cb5d8677c93a7a2f115", [:rebar3], [{:cowlib, "~> 2.8.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.7.1", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "04fd8c6a39edc6aaa9c26123009200fc61f92a3a94f3178c527b70b767c6e605"},
|
||||||
"cowlib": {:hex, :cowlib, "2.8.0", "fd0ff1787db84ac415b8211573e9a30a3ebe71b5cbff7f720089972b2319c8a4", [:rebar3], [], "hexpm"},
|
"cowlib": {:hex, :cowlib, "2.8.0", "fd0ff1787db84ac415b8211573e9a30a3ebe71b5cbff7f720089972b2319c8a4", [:rebar3], [], "hexpm", "79f954a7021b302186a950a32869dbc185523d99d3e44ce430cd1f3289f41ed4"},
|
||||||
"credo": {:hex, :credo, "1.3.2", "08d456dcf3c24da162d02953fb07267e444469d8dad3a2ae47794938ea467b3a", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm"},
|
"credo": {:hex, :credo, "1.4.0", "92339d4cbadd1e88b5ee43d427b639b68a11071b6f73854e33638e30a0ea11f5", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "1fd3b70dce216574ce3c18bdf510b57e7c4c85c2ec9cad4bff854abaf7e58658"},
|
||||||
"crontab": {:hex, :crontab, "1.1.10", "dc9bb1f4299138d47bce38341f5dcbee0aa6c205e864fba7bc847f3b5cb48241", [:mix], [{:ecto, "~> 1.0 or ~> 2.0 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm"},
|
"crontab": {:hex, :crontab, "1.1.10", "dc9bb1f4299138d47bce38341f5dcbee0aa6c205e864fba7bc847f3b5cb48241", [:mix], [{:ecto, "~> 1.0 or ~> 2.0 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm", "1347d889d1a0eda997990876b4894359e34bfbbd688acbb0ba28a2795ca40685"},
|
||||||
"db_connection": {:hex, :db_connection, "2.2.1", "caee17725495f5129cb7faebde001dc4406796f12a62b8949f4ac69315080566", [:mix], [{:connection, "~> 1.0.2", [hex: :connection, repo: "hexpm", optional: false]}], "hexpm"},
|
"db_connection": {:hex, :db_connection, "2.2.2", "3bbca41b199e1598245b716248964926303b5d4609ff065125ce98bcd368939e", [:mix], [{:connection, "~> 1.0.2", [hex: :connection, repo: "hexpm", optional: false]}], "hexpm", "642af240d8a8affb93b4ba5a6fcd2bbcbdc327e1a524b825d383711536f8070c"},
|
||||||
"decimal": {:hex, :decimal, "1.8.1", "a4ef3f5f3428bdbc0d35374029ffcf4ede8533536fa79896dd450168d9acdf3c", [:mix], [], "hexpm"},
|
"decimal": {:hex, :decimal, "1.8.1", "a4ef3f5f3428bdbc0d35374029ffcf4ede8533536fa79896dd450168d9acdf3c", [:mix], [], "hexpm", "3cb154b00225ac687f6cbd4acc4b7960027c757a5152b369923ead9ddbca7aec"},
|
||||||
"decorator": {:hex, :decorator, "1.2.4", "31dfff6143d37f0b68d0bffb3b9f18ace14fea54d4f1b5e4f86ead6f00d9ff6e", [:mix], [], "hexpm"},
|
"decorator": {:hex, :decorator, "1.2.4", "31dfff6143d37f0b68d0bffb3b9f18ace14fea54d4f1b5e4f86ead6f00d9ff6e", [:mix], [], "hexpm", "6c393a3aada02d0eaa6bde725e1816d2b122d7d0fb06c6dd8ebd92d33826396b"},
|
||||||
"distillery": {:hex, :distillery, "2.1.1", "f9332afc2eec8a1a2b86f22429e068ef35f84a93ea1718265e740d90dd367814", [:mix], [{:artificery, "~> 0.2", [hex: :artificery, repo: "hexpm", optional: false]}], "hexpm"},
|
"distillery": {:hex, :distillery, "2.1.1", "f9332afc2eec8a1a2b86f22429e068ef35f84a93ea1718265e740d90dd367814", [:mix], [{:artificery, "~> 0.2", [hex: :artificery, repo: "hexpm", optional: false]}], "hexpm"},
|
||||||
"ecto": {:hex, :ecto, "3.4.2", "6890af71025769bd27ef62b1ed1925cfe23f7f0460bcb3041da4b705215ff23e", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm"},
|
"ecto": {:hex, :ecto, "3.4.4", "a2c881e80dc756d648197ae0d936216c0308370332c5e77a2325a10293eef845", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "cc4bd3ad62abc3b21fb629f0f7a3dab23a192fca837d257dd08449fba7373561"},
|
||||||
"ecto_sql": {:hex, :ecto_sql, "3.4.2", "3d842665a81ba2137b62aa70151afe81dae44824cd09b2076a255937ab4e2dc9", [:mix], [{:db_connection, "~> 2.2", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.4.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.3.0 or ~> 0.4.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.15.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.0", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm"},
|
"ecto_sql": {:hex, :ecto_sql, "3.4.3", "c552aa8a7ccff2b64024f835503b3155d8e73452c180298527fbdbcd6e79710b", [:mix], [{:db_connection, "~> 2.2", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.4.3", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.3.0 or ~> 0.4.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.15.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.0", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ec9e59d6fa3f8cfda9963ada371e9e6659167c2338a997bd7ea23b10b245842b"},
|
||||||
"elasticsearch": {:hex, :elasticsearch, "1.0.0", "626d3fb8e7554d9c93eb18817ae2a3d22c2a4191cc903c4644b1334469b15374", [:mix], [{:httpoison, ">= 0.0.0", [hex: :httpoison, repo: "hexpm", optional: false]}, {:poison, ">= 0.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:sigaws, "~> 0.7", [hex: :sigaws, repo: "hexpm", optional: true]}, {:vex, "~> 0.6.0", [hex: :vex, repo: "hexpm", optional: false]}], "hexpm"},
|
"elasticsearch": {:hex, :elasticsearch, "1.0.0", "626d3fb8e7554d9c93eb18817ae2a3d22c2a4191cc903c4644b1334469b15374", [:mix], [{:httpoison, ">= 0.0.0", [hex: :httpoison, repo: "hexpm", optional: false]}, {:poison, ">= 0.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:sigaws, "~> 0.7", [hex: :sigaws, repo: "hexpm", optional: true]}, {:vex, "~> 0.6.0", [hex: :vex, repo: "hexpm", optional: false]}], "hexpm", "9fa0b717ad57a54c28451b3eb10c5121211c29a7b33615d2bcc7e2f3c9418b2e"},
|
||||||
"ex_twilio": {:hex, :ex_twilio, "0.7.0", "d7ce624ef4661311ae28c3e3aa060ecb66a9f4843184d7400c29072f7d3f5a4a", [:mix], [{:httpoison, ">= 0.9.0", [hex: :httpoison, repo: "hexpm", optional: false]}, {:inflex, "~> 1.0", [hex: :inflex, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:joken, "~> 2.0", [hex: :joken, repo: "hexpm", optional: false]}, {:poison, "~> 3.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"},
|
"ex_twilio": {:hex, :ex_twilio, "0.7.0", "d7ce624ef4661311ae28c3e3aa060ecb66a9f4843184d7400c29072f7d3f5a4a", [:mix], [{:httpoison, ">= 0.9.0", [hex: :httpoison, repo: "hexpm", optional: false]}, {:inflex, "~> 1.0", [hex: :inflex, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:joken, "~> 2.0", [hex: :joken, repo: "hexpm", optional: false]}, {:poison, "~> 3.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm", "6be84f1508ed47d443d18cdc4ea0561f8ad4095b69791dd9be5f2fe14b1dafc5"},
|
||||||
"gen_stage": {:hex, :gen_stage, "1.0.0", "51c8ae56ff54f9a2a604ca583798c210ad245f415115453b773b621c49776df5", [:mix], [], "hexpm"},
|
"gen_stage": {:hex, :gen_stage, "1.0.0", "51c8ae56ff54f9a2a604ca583798c210ad245f415115453b773b621c49776df5", [:mix], [], "hexpm", "1d9fc978db5305ac54e6f5fec7adf80cd893b1000cf78271564c516aa2af7706"},
|
||||||
"gen_state_machine": {:hex, :gen_state_machine, "2.1.0", "a38b0e53fad812d29ec149f0d354da5d1bc0d7222c3711f3a0bd5aa608b42992", [:mix], [], "hexpm"},
|
"gen_state_machine": {:hex, :gen_state_machine, "2.1.0", "a38b0e53fad812d29ec149f0d354da5d1bc0d7222c3711f3a0bd5aa608b42992", [:mix], [], "hexpm", "ae367038808db25cee2f2c4b8d0531522ea587c4995eb6f96ee73410a60fa06b"},
|
||||||
"gettext": {:hex, :gettext, "0.17.4", "f13088e1ec10ce01665cf25f5ff779e7df3f2dc71b37084976cf89d1aa124d5c", [:mix], [], "hexpm"},
|
"gettext": {:hex, :gettext, "0.18.0", "406d6b9e0e3278162c2ae1de0a60270452c553536772167e2d701f028116f870", [:mix], [], "hexpm", "c3f850be6367ebe1a08616c2158affe4a23231c70391050bf359d5f92f66a571"},
|
||||||
"gollum": {:hex, :gollum, "0.3.3", "25ebb47700b9236bc4e5382bf91b72e4cdaf9bae3556172eff27e770735a198f", [:mix], [{:httpoison, "~> 1.5.1", [hex: :httpoison, repo: "hexpm", optional: false]}], "hexpm"},
|
"gollum": {:hex, :gollum, "0.3.3", "25ebb47700b9236bc4e5382bf91b72e4cdaf9bae3556172eff27e770735a198f", [:mix], [{:httpoison, "~> 1.5.1", [hex: :httpoison, repo: "hexpm", optional: false]}], "hexpm", "39268eeaf4f0adb6fdebe4f8c36b10a277881ab2eee3419c9b6727759e2f5a5d"},
|
||||||
"hackney": {:hex, :hackney, "1.15.2", "07e33c794f8f8964ee86cebec1a8ed88db5070e52e904b8f12209773c1036085", [:rebar3], [{:certifi, "2.5.1", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.5", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm"},
|
"hackney": {:hex, :hackney, "1.15.2", "07e33c794f8f8964ee86cebec1a8ed88db5070e52e904b8f12209773c1036085", [:rebar3], [{:certifi, "2.5.1", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.5", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm", "e0100f8ef7d1124222c11ad362c857d3df7cb5f4204054f9f0f4a728666591fc"},
|
||||||
"honeydew": {:hex, :honeydew, "1.4.5", "03818730602274ef0119652d664b92ddf733256e857d29899ce6841e01345bd1", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm"},
|
"honeydew": {:hex, :honeydew, "1.4.5", "03818730602274ef0119652d664b92ddf733256e857d29899ce6841e01345bd1", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm", "62633858ce7e82f67689b4d2b4024bd87fa00dc6a11a227614b816d868a1529d"},
|
||||||
"httpoison": {:hex, :httpoison, "1.5.1", "0f55b5b673b03c5c327dac7015a67cb571b99b631acc0bc1b0b98dcd6b9f2104", [:mix], [{:hackney, "~> 1.8", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"},
|
"httpoison": {:hex, :httpoison, "1.5.1", "0f55b5b673b03c5c327dac7015a67cb571b99b631acc0bc1b0b98dcd6b9f2104", [:mix], [{:hackney, "~> 1.8", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "191a3b6329c917de4e7ca68431919a59bf19e60694b313a69bc1f56a4cb160bf"},
|
||||||
"hunter": {:hex, :hunter, "0.5.1", "374dc4a800e2c340659657f8875e466075c7ea532e0d7a7787665f272b410150", [:mix], [{:httpoison, "~> 1.5", [hex: :httpoison, repo: "hexpm", optional: false]}, {:poison, "~> 4.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"},
|
"hunter": {:hex, :hunter, "0.5.1", "374dc4a800e2c340659657f8875e466075c7ea532e0d7a7787665f272b410150", [:mix], [{:httpoison, "~> 1.5", [hex: :httpoison, repo: "hexpm", optional: false]}, {:poison, "~> 4.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm", "209b2cca7e4d51d5ff7ee4a0ab6cdc4c6ad23ddd61c9e12ceeee6f7ffbeae9c8"},
|
||||||
"idna": {:hex, :idna, "5.1.2", "e21cb58a09f0228a9e0b95eaa1217f1bcfc31a1aaa6e1fdf2f53a33f7dbd9494", [:rebar3], [{:unicode_util_compat, "0.3.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm"},
|
"idna": {:hex, :idna, "5.1.2", "e21cb58a09f0228a9e0b95eaa1217f1bcfc31a1aaa6e1fdf2f53a33f7dbd9494", [:rebar3], [{:unicode_util_compat, "0.3.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "8fddb3aec4692c71647d67de72536254bce9069851754e370a99f2aae69fbdf4"},
|
||||||
"inflex": {:hex, :inflex, "1.10.0", "8366a7696e70e1813aca102e61274addf85d99f4a072b2f9c7984054ea1b9d29", [:mix], [], "hexpm"},
|
"inflex": {:hex, :inflex, "1.10.0", "8366a7696e70e1813aca102e61274addf85d99f4a072b2f9c7984054ea1b9d29", [:mix], [], "hexpm", "7b5ccb9b720c26516f5962dc4565fc26f083ca107b0f6c167048506a125d2df3"},
|
||||||
"jason": {:hex, :jason, "1.2.0", "10043418c42d2493d0ee212d3fddd25d7ffe484380afad769a0a38795938e448", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm"},
|
"jason": {:hex, :jason, "1.2.1", "12b22825e22f468c02eb3e4b9985f3d0cb8dc40b9bd704730efa11abd2708c44", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "b659b8571deedf60f79c5a608e15414085fa141344e2716fbd6988a084b5f993"},
|
||||||
"joken": {:hex, :joken, "2.2.0", "2daa1b12be05184aff7b5ace1d43ca1f81345962285fff3f88db74927c954d3a", [:mix], [{:jose, "~> 1.9", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm"},
|
"joken": {:hex, :joken, "2.2.0", "2daa1b12be05184aff7b5ace1d43ca1f81345962285fff3f88db74927c954d3a", [:mix], [{:jose, "~> 1.9", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm", "b4f92e30388206f869dd25d1af628a1d99d7586e5cf0672f64d4df84c4d2f5e9"},
|
||||||
"jose": {:hex, :jose, "1.10.1", "16d8e460dae7203c6d1efa3f277e25b5af8b659febfc2f2eb4bacf87f128b80a", [:mix, :rebar3], [], "hexpm"},
|
"jose": {:hex, :jose, "1.10.1", "16d8e460dae7203c6d1efa3f277e25b5af8b659febfc2f2eb4bacf87f128b80a", [:mix, :rebar3], [], "hexpm", "3c7ddc8a9394b92891db7c2771da94bf819834a1a4c92e30857b7d582e2f8257"},
|
||||||
"libring": {:hex, :libring, "1.5.0", "44313eb6862f5c9168594a061e9d5f556a9819da7c6444706a9e2da533396d70", [:mix], [], "hexpm"},
|
"libring": {:hex, :libring, "1.5.0", "44313eb6862f5c9168594a061e9d5f556a9819da7c6444706a9e2da533396d70", [:mix], [], "hexpm", "04e843d4fdcff49a62d8e03778d17c6cb2a03fe2d14020d3825a1761b55bd6cc"},
|
||||||
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm"},
|
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
|
||||||
"mime": {:hex, :mime, "1.3.1", "30ce04ab3175b6ad0bdce0035cba77bba68b813d523d1aac73d9781b4d193cf8", [:mix], [], "hexpm"},
|
"mime": {:hex, :mime, "1.3.1", "30ce04ab3175b6ad0bdce0035cba77bba68b813d523d1aac73d9781b4d193cf8", [:mix], [], "hexpm", "6cbe761d6a0ca5a31a0931bf4c63204bceb64538e664a8ecf784a9a6f3b875f1"},
|
||||||
"mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm"},
|
"mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"},
|
||||||
"nebulex": {:hex, :nebulex, "1.1.1", "4117e18e614ecbd078e19558b7b9c58f11d666c4dca584b9382b02913f13ad8a", [:mix], [{:shards, "~> 0.6", [hex: :shards, repo: "hexpm", optional: false]}], "hexpm"},
|
"nebulex": {:hex, :nebulex, "1.1.1", "4117e18e614ecbd078e19558b7b9c58f11d666c4dca584b9382b02913f13ad8a", [:mix], [{:shards, "~> 0.6", [hex: :shards, repo: "hexpm", optional: false]}], "hexpm", "cf3a04f9bfb8fcb8f070ab049c3fab54dd31c72d13430360a1c908c3cacb9196"},
|
||||||
"paginator": {:hex, :paginator, "0.6.0", "bc2c01abdd98281ff39b6a7439cf540091122a7927bdaabc167c61d4508f9cbb", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.0", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.13", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm"},
|
"paginator": {:hex, :paginator, "0.6.0", "bc2c01abdd98281ff39b6a7439cf540091122a7927bdaabc167c61d4508f9cbb", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.0", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.13", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm"},
|
||||||
"parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm"},
|
"parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm", "17ef63abde837ad30680ea7f857dd9e7ced9476cdd7b0394432af4bfc241b960"},
|
||||||
"phoenix": {:hex, :phoenix, "1.4.16", "2cbbe0c81e6601567c44cc380c33aa42a1372ac1426e3de3d93ac448a7ec4308", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 1.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.8.1 or ~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 1.0 or ~> 2.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm"},
|
"phoenix": {:hex, :phoenix, "1.4.17", "1b1bd4cff7cfc87c94deaa7d60dd8c22e04368ab95499483c50640ef3bd838d8", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 1.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.8.1 or ~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 1.0 or ~> 2.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "3a8e5d7a3d76d452bb5fb86e8b7bd115f737e4f8efe202a463d4aeb4a5809611"},
|
||||||
"phoenix_ecto": {:hex, :phoenix_ecto, "4.1.0", "a044d0756d0464c5a541b4a0bf4bcaf89bffcaf92468862408290682c73ae50d", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.9", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"},
|
"phoenix_ecto": {:hex, :phoenix_ecto, "4.1.0", "a044d0756d0464c5a541b4a0bf4bcaf89bffcaf92468862408290682c73ae50d", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.9", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "c5e666a341ff104d0399d8f0e4ff094559b2fde13a5985d4cb5023b2c2ac558b"},
|
||||||
"phoenix_pubsub": {:hex, :phoenix_pubsub, "1.1.2", "496c303bdf1b2e98a9d26e89af5bba3ab487ba3a3735f74bf1f4064d2a845a3e", [:mix], [], "hexpm"},
|
"phoenix_pubsub": {:hex, :phoenix_pubsub, "1.1.2", "496c303bdf1b2e98a9d26e89af5bba3ab487ba3a3735f74bf1f4064d2a845a3e", [:mix], [], "hexpm", "1f13f9f0f3e769a667a6b6828d29dec37497a082d195cc52dbef401a9b69bf38"},
|
||||||
"plug": {:hex, :plug, "1.10.0", "6508295cbeb4c654860845fb95260737e4a8838d34d115ad76cd487584e2fc4d", [:mix], [{:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm"},
|
"plug": {:hex, :plug, "1.10.1", "c56a6d9da7042d581159bcbaef873ba9d87f15dce85420b0d287bca19f40f9bd", [:mix], [{:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "b5cd52259817eb8a31f2454912ba1cff4990bca7811918878091cb2ab9e52cb8"},
|
||||||
"plug_cowboy": {:hex, :plug_cowboy, "2.1.3", "38999a3e85e39f0e6bdfdf820761abac61edde1632cfebbacc445cdcb6ae1333", [:mix], [{:cowboy, "~> 2.5", [hex: :cowboy, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"},
|
"plug_cowboy": {:hex, :plug_cowboy, "2.2.1", "fcf58aa33227a4322a050e4783ee99c63c031a2e7f9a2eb7340d55505e17f30f", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "3b43de24460d87c0971887286e7a20d40462e48eb7235954681a20cee25ddeb6"},
|
||||||
"plug_crypto": {:hex, :plug_crypto, "1.1.2", "bdd187572cc26dbd95b87136290425f2b580a116d3fb1f564216918c9730d227", [:mix], [], "hexpm"},
|
"plug_crypto": {:hex, :plug_crypto, "1.1.2", "bdd187572cc26dbd95b87136290425f2b580a116d3fb1f564216918c9730d227", [:mix], [], "hexpm", "6b8b608f895b6ffcfad49c37c7883e8df98ae19c6a28113b02aa1e9c5b22d6b5"},
|
||||||
"poison": {:hex, :poison, "4.0.1", "bcb755a16fac91cad79bfe9fc3585bb07b9331e50cfe3420a24bcc2d735709ae", [:mix], [], "hexpm"},
|
"poison": {:hex, :poison, "4.0.1", "bcb755a16fac91cad79bfe9fc3585bb07b9331e50cfe3420a24bcc2d735709ae", [:mix], [], "hexpm", "ba8836feea4b394bb718a161fc59a288fe0109b5006d6bdf97b6badfcf6f0f25"},
|
||||||
"postgrex": {:hex, :postgrex, "0.15.3", "5806baa8a19a68c4d07c7a624ccdb9b57e89cbc573f1b98099e3741214746ae4", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm"},
|
"postgrex": {:hex, :postgrex, "0.15.4", "5d691c25fc79070705a2ff0e35ce0822b86a0ee3c6fdb7a4fb354623955e1aed", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "306515b9d975fcb2478dc337a1d27dc3bf8af7cd71017c333fe9db3a3d211b0a"},
|
||||||
"public_suffix": {:hex, :public_suffix, "0.6.0", "100cfe86f13f9f6f0cf67e743b1b83c78dd1223a2c422fa03ebf4adff514cbc3", [:mix], [{:idna, ">= 1.2.0 and < 6.0.0", [hex: :idna, repo: "hexpm", optional: false]}], "hexpm"},
|
"public_suffix": {:hex, :public_suffix, "0.6.0", "100cfe86f13f9f6f0cf67e743b1b83c78dd1223a2c422fa03ebf4adff514cbc3", [:mix], [{:idna, ">= 1.2.0 and < 6.0.0", [hex: :idna, repo: "hexpm", optional: false]}], "hexpm", "663f29209e7930680cb1656cf144cc7484b37fe261f4417fec280d9a20363bfc"},
|
||||||
"quantum": {:hex, :quantum, "2.4.0", "f2ad4b20988f848455d35ed0e884ba0c7629a27ee86cbec6a6e0fc214b6e69cf", [:mix], [{:calendar, "~> 0.17", [hex: :calendar, repo: "hexpm", optional: true]}, {:crontab, "~> 1.1", [hex: :crontab, repo: "hexpm", optional: false]}, {:gen_stage, "~> 0.12 or ~> 1.0", [hex: :gen_stage, repo: "hexpm", optional: false]}, {:swarm, "~> 3.3", [hex: :swarm, repo: "hexpm", optional: false]}, {:timex, "~> 3.1", [hex: :timex, repo: "hexpm", optional: true]}, {:tzdata, "~> 1.0", [hex: :tzdata, repo: "hexpm", optional: true]}], "hexpm"},
|
"quantum": {:hex, :quantum, "2.4.0", "f2ad4b20988f848455d35ed0e884ba0c7629a27ee86cbec6a6e0fc214b6e69cf", [:mix], [{:calendar, "~> 0.17", [hex: :calendar, repo: "hexpm", optional: true]}, {:crontab, "~> 1.1", [hex: :crontab, repo: "hexpm", optional: false]}, {:gen_stage, "~> 0.12 or ~> 1.0", [hex: :gen_stage, repo: "hexpm", optional: false]}, {:swarm, "~> 3.3", [hex: :swarm, repo: "hexpm", optional: false]}, {:timex, "~> 3.1", [hex: :timex, repo: "hexpm", optional: true]}, {:tzdata, "~> 1.0", [hex: :tzdata, repo: "hexpm", optional: true]}], "hexpm", "a125a9e65a5af740a1198f3b05c1a736fce3942f5e0dc2901e0f9be5745bea99"},
|
||||||
"ranch": {:hex, :ranch, "1.7.1", "6b1fab51b49196860b733a49c07604465a47bdb78aa10c1c16a3d199f7f8c881", [:rebar3], [], "hexpm"},
|
"ranch": {:hex, :ranch, "1.7.1", "6b1fab51b49196860b733a49c07604465a47bdb78aa10c1c16a3d199f7f8c881", [:rebar3], [], "hexpm", "451d8527787df716d99dc36162fca05934915db0b6141bbdac2ea8d3c7afc7d7"},
|
||||||
"recase": {:hex, :recase, "0.6.0", "1dd2dd2f4e06603b74977630e739f08b7fedbb9420cc14de353666c2fc8b99f4", [:mix], [], "hexpm"},
|
"recase": {:hex, :recase, "0.6.0", "1dd2dd2f4e06603b74977630e739f08b7fedbb9420cc14de353666c2fc8b99f4", [:mix], [], "hexpm", "8712e318420a228eb2e6366ada230148ed3a4316a798319edd5512f64d78c990"},
|
||||||
"scrivener": {:hex, :scrivener, "2.7.0", "fa94cdea21fad0649921d8066b1833d18d296217bfdf4a5389a2f45ee857b773", [:mix], [], "hexpm"},
|
"scrivener": {:hex, :scrivener, "2.7.0", "fa94cdea21fad0649921d8066b1833d18d296217bfdf4a5389a2f45ee857b773", [:mix], [], "hexpm", "30da36a427f2519cf75993271fb7c5aad1759682a70f90d880a85c3d743d2c57"},
|
||||||
"scrivener_ecto": {:hex, :scrivener_ecto, "2.3.0", "057f9dd3c77315f0a470263c3565353860d0294404aed611b3524c6df9044189", [:mix], [{:ecto, "~> 3.3", [hex: :ecto, repo: "hexpm", optional: false]}, {:scrivener, "~> 2.4", [hex: :scrivener, repo: "hexpm", optional: false]}], "hexpm"},
|
"scrivener_ecto": {:hex, :scrivener_ecto, "2.3.0", "057f9dd3c77315f0a470263c3565353860d0294404aed611b3524c6df9044189", [:mix], [{:ecto, "~> 3.3", [hex: :ecto, repo: "hexpm", optional: false]}, {:scrivener, "~> 2.4", [hex: :scrivener, repo: "hexpm", optional: false]}], "hexpm", "dfa43ca660651da63239e5d4acbfd9c57c5759bbf3a2bdc16cd70777c9bc7e0d"},
|
||||||
"shards": {:hex, :shards, "0.6.2", "e05d05537883220c3b8a8f9d40d5c8ba7ff6064c63ebb6b23046972f6863b2d1", [:make, :rebar3], [], "hexpm"},
|
"shards": {:hex, :shards, "0.6.2", "e05d05537883220c3b8a8f9d40d5c8ba7ff6064c63ebb6b23046972f6863b2d1", [:make, :rebar3], [], "hexpm", "58afa3712f1f1256a2a15e39fa95b7cd758087aaa7a25beaf786daabd87890f0"},
|
||||||
"sobelow": {:hex, :sobelow, "0.10.1", "7ddd72eacd3cff0d8ebaaa7825a11718652d264a8c1b97fe802d09634c955db5", [:mix], [], "hexpm"},
|
"sobelow": {:hex, :sobelow, "0.10.2", "00e91208046d3b434f9f08779fe0ca7c6d6595b7fa33b289e792dffa6dde8081", [:mix], [], "hexpm", "e30fc994330cf6f485c1c4f2fb7c4b2d403557d0e101c6e5329fd17a58e55a7e"},
|
||||||
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.5", "6eaf7ad16cb568bb01753dbbd7a95ff8b91c7979482b95f38443fe2c8852a79b", [:make, :mix, :rebar3], [], "hexpm"},
|
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.5", "6eaf7ad16cb568bb01753dbbd7a95ff8b91c7979482b95f38443fe2c8852a79b", [:make, :mix, :rebar3], [], "hexpm", "13104d7897e38ed7f044c4de953a6c28597d1c952075eb2e328bc6d6f2bfc496"},
|
||||||
"swarm": {:hex, :swarm, "3.4.0", "64f8b30055d74640d2186c66354b33b999438692a91be275bb89cdc7e401f448", [:mix], [{:gen_state_machine, "~> 2.0", [hex: :gen_state_machine, repo: "hexpm", optional: false]}, {:libring, "~> 1.0", [hex: :libring, repo: "hexpm", optional: false]}], "hexpm"},
|
"swarm": {:hex, :swarm, "3.4.0", "64f8b30055d74640d2186c66354b33b999438692a91be275bb89cdc7e401f448", [:mix], [{:gen_state_machine, "~> 2.0", [hex: :gen_state_machine, repo: "hexpm", optional: false]}, {:libring, "~> 1.0", [hex: :libring, repo: "hexpm", optional: false]}], "hexpm", "94884f84783fc1ba027aba8fe8a7dae4aad78c98e9f9c76667ec3471585c08c6"},
|
||||||
"swoosh": {:hex, :swoosh, "0.23.5", "bfd9404bbf5069b1be2ffd317923ce57e58b332e25dbca2a35dedd7820dfee5a", [:mix], [{:cowboy, "~> 1.0.1 or ~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}], "hexpm"},
|
"swoosh": {:hex, :swoosh, "0.23.5", "bfd9404bbf5069b1be2ffd317923ce57e58b332e25dbca2a35dedd7820dfee5a", [:mix], [{:cowboy, "~> 1.0.1 or ~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}], "hexpm", "e3928e1d2889a308aaf3e42755809ac21cffd77cb58eef01cbfdab4ce2fd1e21"},
|
||||||
"telemetry": {:hex, :telemetry, "0.4.1", "ae2718484892448a24470e6aa341bc847c3277bfb8d4e9289f7474d752c09c7f", [:rebar3], [], "hexpm"},
|
"telemetry": {:hex, :telemetry, "0.4.1", "ae2718484892448a24470e6aa341bc847c3277bfb8d4e9289f7474d752c09c7f", [:rebar3], [], "hexpm", "4738382e36a0a9a2b6e25d67c960e40e1a2c95560b9f936d8e29de8cd858480f"},
|
||||||
"timex": {:hex, :timex, "3.6.1", "efdf56d0e67a6b956cc57774353b0329c8ab7726766a11547e529357ffdc1d56", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 0.1.8 or ~> 0.5 or ~> 1.0.0", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm"},
|
"timex": {:hex, :timex, "3.6.1", "efdf56d0e67a6b956cc57774353b0329c8ab7726766a11547e529357ffdc1d56", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 0.1.8 or ~> 0.5 or ~> 1.0.0", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "f354efb2400dd7a80fd9eb6c8419068c4f632da4ac47f3d8822d6e33f08bc852"},
|
||||||
"tzdata": {:hex, :tzdata, "1.0.3", "73470ad29dde46e350c60a66e6b360d3b99d2d18b74c4c349dbebbc27a09a3eb", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"},
|
"tzdata": {:hex, :tzdata, "1.0.3", "73470ad29dde46e350c60a66e6b360d3b99d2d18b74c4c349dbebbc27a09a3eb", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "a6e1ee7003c4d04ecbd21dd3ec690d4c6662db5d3bbdd7262d53cdf5e7c746c1"},
|
||||||
"unicode_util_compat": {:hex, :unicode_util_compat, "0.3.1", "a1f612a7b512638634a603c8f401892afbf99b8ce93a45041f8aaca99cadb85e", [:rebar3], [], "hexpm"},
|
"unicode_util_compat": {:hex, :unicode_util_compat, "0.3.1", "a1f612a7b512638634a603c8f401892afbf99b8ce93a45041f8aaca99cadb85e", [:rebar3], [], "hexpm", "da1d9bef8a092cc7e1e51f1298037a5ddfb0f657fe862dfe7ba4c5807b551c29"},
|
||||||
"vex": {:hex, :vex, "0.6.0", "4e79b396b2ec18cd909eed0450b19108d9631842598d46552dc05031100b7a56", [:mix], [], "hexpm"},
|
"vex": {:hex, :vex, "0.6.0", "4e79b396b2ec18cd909eed0450b19108d9631842598d46552dc05031100b7a56", [:mix], [], "hexpm", "7e4d9b50dd72cf931b52aba3470513686007f2ad54832de37cdb659cc85ba73e"},
|
||||||
}
|
}
|
||||||
|
|
4
frontend/.eslintignore
Normal file
4
frontend/.eslintignore
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
build
|
||||||
|
coverage
|
24
frontend/.eslintrc.js
Normal file
24
frontend/.eslintrc.js
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
module.exports = {
|
||||||
|
root: true,
|
||||||
|
parser: "@typescript-eslint/parser",
|
||||||
|
parserOptions: {
|
||||||
|
tsconfigRootDir: __dirname,
|
||||||
|
project: ["./tsconfig.json"],
|
||||||
|
},
|
||||||
|
plugins: ["@typescript-eslint", "prettier"],
|
||||||
|
extends: [
|
||||||
|
"plugin:@typescript-eslint/recommended-requiring-type-checking",
|
||||||
|
"plugin:@typescript-eslint/eslint-recommended",
|
||||||
|
"plugin:@typescript-eslint/recommended",
|
||||||
|
"plugin:react/recommended",
|
||||||
|
"plugin:react-hooks/recommended",
|
||||||
|
"prettier/@typescript-eslint",
|
||||||
|
"prettier",
|
||||||
|
],
|
||||||
|
rules: {
|
||||||
|
"@typescript-eslint/no-explicit-any": 0,
|
||||||
|
"@typescript-eslint/explicit-function-return-type": 0,
|
||||||
|
"react/prop-types": 0,
|
||||||
|
"@typescript-eslint/no-non-null-assertion": 0
|
||||||
|
},
|
||||||
|
};
|
3
frontend/.prettierrc.js
Normal file
3
frontend/.prettierrc.js
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
module.exports = {
|
||||||
|
printWidth: 100
|
||||||
|
}
|
|
@ -6,7 +6,7 @@
|
||||||
"start": "NODE_ENV=development react-scripts start",
|
"start": "NODE_ENV=development react-scripts start",
|
||||||
"build": "react-scripts build",
|
"build": "react-scripts build",
|
||||||
"typecheck": "tsc --noemit",
|
"typecheck": "tsc --noemit",
|
||||||
"lint": "yarn typecheck && tslint -p tsconfig.json -c tslint.json \"src/**/*.{ts,tsx}\"",
|
"lint": "yarn typecheck && yarn eslint src/ --ext .js,.jsx,.ts,.tsx",
|
||||||
"lint:fix": "yarn lint --fix",
|
"lint:fix": "yarn lint --fix",
|
||||||
"pretty": "prettier --write \"src/**/*.{ts,tsx}\"",
|
"pretty": "prettier --write \"src/**/*.{ts,tsx}\"",
|
||||||
"test": "yarn lint && react-scripts test --ci",
|
"test": "yarn lint && react-scripts test --ci",
|
||||||
|
@ -20,62 +20,67 @@
|
||||||
"lint-staged": {
|
"lint-staged": {
|
||||||
"src/**/*.{ts,tsx}": [
|
"src/**/*.{ts,tsx}": [
|
||||||
"yarn pretty",
|
"yarn pretty",
|
||||||
"yarn lint:fix",
|
"yarn lint:fix"
|
||||||
"git add"
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"prettier": {
|
"prettier": {
|
||||||
"printWidth": 120
|
"printWidth": 120
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@blueprintjs/core": "^3.19.1",
|
"@blueprintjs/core": "^3.26.1",
|
||||||
"@blueprintjs/icons": "^3.11.0",
|
"@blueprintjs/icons": "^3.16.0",
|
||||||
"@blueprintjs/select": "^3.11.1",
|
"@blueprintjs/select": "^3.12.3",
|
||||||
"classnames": "^2.2.6",
|
"classnames": "^2.2.6",
|
||||||
"connected-react-router": "^6.5.2",
|
"connected-react-router": "^6.5.2",
|
||||||
"cross-fetch": "^3.0.4",
|
"cross-fetch": "^3.0.4",
|
||||||
"cytoscape": "^3.11.0",
|
"cytoscape": "^3.15.0",
|
||||||
"cytoscape-popper": "^1.0.4",
|
"cytoscape-popper": "^1.0.7",
|
||||||
"inflection": "^1.12.0",
|
"inflection": "^1.12.0",
|
||||||
"lodash": "^4.17.15",
|
"lodash": "^4.17.15",
|
||||||
"moment": "^2.22.2",
|
"moment": "^2.25.3",
|
||||||
"normalize.css": "^8.0.0",
|
"normalize.css": "^8.0.0",
|
||||||
"numeral": "^2.0.6",
|
"numeral": "^2.0.6",
|
||||||
"react": "^16.10.2",
|
"react": "^16.10.2",
|
||||||
"react-dom": "^16.10.2",
|
"react-dom": "^16.10.2",
|
||||||
"react-redux": "^7.1.1",
|
"react-redux": "^7.1.1",
|
||||||
"react-router-dom": "^5.1.2",
|
"react-router-dom": "^5.2.0",
|
||||||
"react-scripts": "^3.2.0",
|
"react-scripts": "3.4.1",
|
||||||
"react-sigma": "^1.2.30",
|
"react-sigma": "^1.2.30",
|
||||||
"react-virtualized": "^9.21.1",
|
"react-virtualized": "^9.21.1",
|
||||||
"redux": "^4.0.4",
|
"redux": "^4.0.4",
|
||||||
"redux-thunk": "^2.3.0",
|
"redux-thunk": "^2.3.0",
|
||||||
"sanitize-html": "^1.20.1",
|
"sanitize-html": "^1.20.1",
|
||||||
"styled-components": "^4.4.0",
|
"styled-components": "^5.1.0",
|
||||||
"tippy.js": "^4.3.5"
|
"tippy.js": "^4.3.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@blueprintjs/tslint-config": "^1.9.0",
|
|
||||||
"@types/classnames": "^2.2.9",
|
"@types/classnames": "^2.2.9",
|
||||||
"@types/cytoscape": "^3.8.3",
|
"@types/cytoscape": "^3.8.3",
|
||||||
"@types/inflection": "^1.5.28",
|
"@types/inflection": "^1.5.28",
|
||||||
"@types/jest": "^24.0.19",
|
"@types/jest": "^25.2.3",
|
||||||
"@types/lodash": "^4.14.144",
|
"@types/lodash": "^4.14.151",
|
||||||
"@types/node": "^12.7.12",
|
"@types/node": "^14.0.1",
|
||||||
"@types/numeral": "^0.0.26",
|
"@types/numeral": "^0.0.28",
|
||||||
"@types/react": "^16.9.6",
|
"@types/react": "^16.9.35",
|
||||||
"@types/react-dom": "^16.9.2",
|
"@types/react-dom": "^16.9.8",
|
||||||
"@types/react-redux": "^7.1.4",
|
"@types/react-redux": "^7.1.8",
|
||||||
"@types/react-router-dom": "^5.1.0",
|
"@types/react-router-dom": "^5.1.5",
|
||||||
"@types/sanitize-html": "^1.20.2",
|
"@types/sanitize-html": "^1.23.0",
|
||||||
"@types/styled-components": "4.1.19",
|
"@types/styled-components": "5.1.0",
|
||||||
"husky": "^3.0.9",
|
"@typescript-eslint/eslint-plugin": "^2.24.0",
|
||||||
"lint-staged": "^9.4.2",
|
"@typescript-eslint/parser": "^2.34.0",
|
||||||
|
"eslint-config-airbnb-typescript": "^7.2.1",
|
||||||
|
"eslint-config-prettier": "^6.11.0",
|
||||||
|
"eslint-plugin-import": "^2.20.1",
|
||||||
|
"eslint-plugin-jsx-a11y": "^6.2.3",
|
||||||
|
"eslint-plugin-prettier": "^3.1.3",
|
||||||
|
"eslint-plugin-react": "^7.20.0",
|
||||||
|
"eslint-plugin-react-hooks": "^4.0.2",
|
||||||
|
"husky": "^4.2.5",
|
||||||
|
"lint-staged": "^10.2.4",
|
||||||
|
"prettier": "^2.0.5",
|
||||||
"react-axe": "^3.3.0",
|
"react-axe": "^3.3.0",
|
||||||
"tslint": "^5.20.0",
|
"typescript": "^3.9.2"
|
||||||
"tslint-config-security": "^1.16.0",
|
|
||||||
"tslint-eslint-rules": "^5.4.0",
|
|
||||||
"typescript": "^3.6.4"
|
|
||||||
},
|
},
|
||||||
"browserslist": [
|
"browserslist": [
|
||||||
">0.2%",
|
">0.2%",
|
||||||
|
|
|
@ -4,26 +4,26 @@ import { Classes } from "@blueprintjs/core";
|
||||||
|
|
||||||
import { ConnectedRouter } from "connected-react-router";
|
import { ConnectedRouter } from "connected-react-router";
|
||||||
import { Route } from "react-router-dom";
|
import { Route } from "react-router-dom";
|
||||||
import { Nav } from "./components/organisms/";
|
import { Nav } from "./components/organisms";
|
||||||
import {
|
import {
|
||||||
AboutScreen,
|
AboutScreen,
|
||||||
AdminScreen,
|
AdminScreen,
|
||||||
GraphScreen,
|
GraphScreen,
|
||||||
LoginScreen,
|
LoginScreen,
|
||||||
TableScreen,
|
TableScreen,
|
||||||
VerifyLoginScreen
|
VerifyLoginScreen,
|
||||||
} from "./components/screens/";
|
} from "./components/screens";
|
||||||
import { history } from "./index";
|
import { history } from "./index";
|
||||||
|
|
||||||
const AppRouter: React.FC = () => (
|
const AppRouter: React.FC = () => (
|
||||||
<ConnectedRouter history={history}>
|
<ConnectedRouter history={history}>
|
||||||
<div className={`${Classes.DARK} App`}>
|
<div className={`${Classes.DARK} App`}>
|
||||||
<Nav />
|
<Nav />
|
||||||
<Route path="/instances" exact={true} component={TableScreen} />
|
<Route path="/instances" exact component={TableScreen} />
|
||||||
<Route path="/about" exact={true} component={AboutScreen} />
|
<Route path="/about" exact component={AboutScreen} />
|
||||||
<Route path="/admin/login" exact={true} component={LoginScreen} />
|
<Route path="/admin/login" exact component={LoginScreen} />
|
||||||
<Route path="/admin/verify" exact={true} component={VerifyLoginScreen} />
|
<Route path="/admin/verify" exact component={VerifyLoginScreen} />
|
||||||
<Route path="/admin" exact={true} component={AdminScreen} />
|
<Route path="/admin" exact component={AdminScreen} />
|
||||||
{/* We always want the GraphScreen to be rendered (since un- and re-mounting it is expensive */}
|
{/* We always want the GraphScreen to be rendered (since un- and re-mounting it is expensive */}
|
||||||
<GraphScreen />
|
<GraphScreen />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -11,7 +11,7 @@ const FloatingCardElement = styled(Card)`
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const FloatingCard: React.FC<ICardProps> = props => (
|
const FloatingCard: React.FC<ICardProps> = (props) => (
|
||||||
<FloatingCardRow>
|
<FloatingCardRow>
|
||||||
<FloatingCardElement elevation={Elevation.ONE} {...props} />
|
<FloatingCardElement elevation={Elevation.ONE} {...props} />
|
||||||
</FloatingCardRow>
|
</FloatingCardRow>
|
||||||
|
|
|
@ -7,11 +7,11 @@ const StyledSwitch = styled(Switch)`
|
||||||
margin: 0;
|
margin: 0;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
interface IGraphHideEdgesButtonProps {
|
interface GraphHideEdgesButtonProps {
|
||||||
isShowingEdges: boolean;
|
isShowingEdges: boolean;
|
||||||
toggleEdges: () => void;
|
toggleEdges: () => void;
|
||||||
}
|
}
|
||||||
const GraphHideEdgesButton: React.FC<IGraphHideEdgesButtonProps> = ({ isShowingEdges, toggleEdges }) => (
|
const GraphHideEdgesButton: React.FC<GraphHideEdgesButtonProps> = ({ isShowingEdges, toggleEdges }) => (
|
||||||
<FloatingCard>
|
<FloatingCard>
|
||||||
<StyledSwitch checked={isShowingEdges} label="Show connections" onChange={toggleEdges} tabIndex={-1} />
|
<StyledSwitch checked={isShowingEdges} label="Show connections" onChange={toggleEdges} tabIndex={-1} />
|
||||||
</FloatingCard>
|
</FloatingCard>
|
||||||
|
|
|
@ -6,9 +6,9 @@ import React from "react";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import { FloatingCard, InstanceType } from ".";
|
import { FloatingCard, InstanceType } from ".";
|
||||||
import { QUANTITATIVE_COLOR_SCHEME } from "../../constants";
|
import { QUANTITATIVE_COLOR_SCHEME } from "../../constants";
|
||||||
import { IColorScheme } from "../../types";
|
import { ColorScheme } from "../../types";
|
||||||
|
|
||||||
const ColorSchemeSelect = Select.ofType<IColorScheme>();
|
const ColorSchemeSelect = Select.ofType<ColorScheme>();
|
||||||
|
|
||||||
const StyledLi = styled.li`
|
const StyledLi = styled.li`
|
||||||
margin-top: 2px;
|
margin-top: 2px;
|
||||||
|
@ -27,12 +27,12 @@ const ColorBarContainer = styled.div`
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
`;
|
`;
|
||||||
interface IColorBarProps {
|
interface ColorBarProps {
|
||||||
color: string;
|
color: string;
|
||||||
}
|
}
|
||||||
const ColorBar = styled.div<IColorBarProps>`
|
const ColorBar = styled.div<ColorBarProps>`
|
||||||
width: 10px;
|
width: 10px;
|
||||||
background-color: ${props => props.color};
|
background-color: ${(props) => props.color};
|
||||||
flex: 1;
|
flex: 1;
|
||||||
`;
|
`;
|
||||||
const TextContainer = styled.div`
|
const TextContainer = styled.div`
|
||||||
|
@ -41,13 +41,46 @@ const TextContainer = styled.div`
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
interface IGraphKeyProps {
|
const renderItem: ItemRenderer<ColorScheme> = (colorScheme, { handleClick, modifiers }) => {
|
||||||
current?: IColorScheme;
|
if (!modifiers.matchesPredicate) {
|
||||||
colorSchemes: IColorScheme[];
|
return null;
|
||||||
|
}
|
||||||
|
return <MenuItem active={modifiers.active} key={colorScheme.name} onClick={handleClick} text={colorScheme.name} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderQualitativeKey = (values: string[]) => (
|
||||||
|
<ul className={Classes.LIST_UNSTYLED}>
|
||||||
|
{values.map((v) => (
|
||||||
|
<StyledLi key={v}>
|
||||||
|
<InstanceType type={v} />
|
||||||
|
</StyledLi>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderQuantitativeKey = (range: number[]) => {
|
||||||
|
const [min, max] = range;
|
||||||
|
return (
|
||||||
|
<ColorKeyContainer>
|
||||||
|
<ColorBarContainer>
|
||||||
|
{QUANTITATIVE_COLOR_SCHEME.map((color) => (
|
||||||
|
<ColorBar color={color} key={color} />
|
||||||
|
))}
|
||||||
|
</ColorBarContainer>
|
||||||
|
<TextContainer>
|
||||||
|
<span className={Classes.TEXT_SMALL}>{numeral.default(min).format("0")}</span>
|
||||||
|
<span className={Classes.TEXT_SMALL}>{numeral.default(max).format("0")}</span>
|
||||||
|
</TextContainer>
|
||||||
|
</ColorKeyContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
interface GraphKeyProps {
|
||||||
|
current?: ColorScheme;
|
||||||
|
colorSchemes: ColorScheme[];
|
||||||
ranges?: { [key: string]: [number, number] };
|
ranges?: { [key: string]: [number, number] };
|
||||||
onItemSelect: (colorScheme?: IColorScheme) => void;
|
onItemSelect: (colorScheme?: ColorScheme) => void;
|
||||||
}
|
}
|
||||||
const GraphKey: React.FC<IGraphKeyProps> = ({ current, colorSchemes, ranges, onItemSelect }) => {
|
const GraphKey: React.FC<GraphKeyProps> = ({ current, colorSchemes, ranges, onItemSelect }) => {
|
||||||
const unsetColorScheme = () => {
|
const unsetColorScheme = () => {
|
||||||
onItemSelect(undefined);
|
onItemSelect(undefined);
|
||||||
};
|
};
|
||||||
|
@ -76,13 +109,7 @@ const GraphKey: React.FC<IGraphKeyProps> = ({ current, colorSchemes, ranges, onI
|
||||||
rightIcon={IconNames.CARET_DOWN}
|
rightIcon={IconNames.CARET_DOWN}
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button icon={IconNames.SMALL_CROSS} minimal onClick={unsetColorScheme} disabled={!current} tabIndex={-1} />
|
||||||
icon={IconNames.SMALL_CROSS}
|
|
||||||
minimal={true}
|
|
||||||
onClick={unsetColorScheme}
|
|
||||||
disabled={!current}
|
|
||||||
tabIndex={-1}
|
|
||||||
/>
|
|
||||||
</ColorSchemeSelect>
|
</ColorSchemeSelect>
|
||||||
<br />
|
<br />
|
||||||
{!!current && !!key && (
|
{!!current && !!key && (
|
||||||
|
@ -95,38 +122,4 @@ const GraphKey: React.FC<IGraphKeyProps> = ({ current, colorSchemes, ranges, onI
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderItem: ItemRenderer<IColorScheme> = (colorScheme, { handleClick, modifiers }) => {
|
|
||||||
if (!modifiers.matchesPredicate) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return <MenuItem active={modifiers.active} key={colorScheme.name} onClick={handleClick} text={colorScheme.name} />;
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderQualitativeKey = (values: string[]) => (
|
|
||||||
<ul className={Classes.LIST_UNSTYLED}>
|
|
||||||
{values.map(v => (
|
|
||||||
<StyledLi key={v}>
|
|
||||||
<InstanceType type={v} />
|
|
||||||
</StyledLi>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
);
|
|
||||||
|
|
||||||
const renderQuantitativeKey = (range: number[]) => {
|
|
||||||
const [min, max] = range;
|
|
||||||
return (
|
|
||||||
<ColorKeyContainer>
|
|
||||||
<ColorBarContainer>
|
|
||||||
{QUANTITATIVE_COLOR_SCHEME.map((color, idx) => (
|
|
||||||
<ColorBar color={color} key={color} />
|
|
||||||
))}
|
|
||||||
</ColorBarContainer>
|
|
||||||
<TextContainer>
|
|
||||||
<span className={Classes.TEXT_SMALL}>{numeral.default(min).format("0")}</span>
|
|
||||||
<span className={Classes.TEXT_SMALL}>{numeral.default(max).format("0")}</span>
|
|
||||||
</TextContainer>
|
|
||||||
</ColorKeyContainer>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default GraphKey;
|
export default GraphKey;
|
||||||
|
|
|
@ -2,10 +2,10 @@ import { Button } from "@blueprintjs/core";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import FloatingCard from "./FloatingCard";
|
import FloatingCard from "./FloatingCard";
|
||||||
|
|
||||||
interface IGraphResetButtonProps {
|
interface GraphResetButtonProps {
|
||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
}
|
}
|
||||||
const GraphResetButton: React.FC<IGraphResetButtonProps> = ({ onClick }) => (
|
const GraphResetButton: React.FC<GraphResetButtonProps> = ({ onClick }) => (
|
||||||
<FloatingCard>
|
<FloatingCard>
|
||||||
<Button icon="compass" title="Reset graph view" onClick={onClick} tabIndex={-1} />
|
<Button icon="compass" title="Reset graph view" onClick={onClick} tabIndex={-1} />
|
||||||
</FloatingCard>
|
</FloatingCard>
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { QUALITATIVE_COLOR_SCHEME } from "../../constants";
|
||||||
import { typeColorScheme } from "../../types";
|
import { typeColorScheme } from "../../types";
|
||||||
import { getTypeDisplayString } from "../../util";
|
import { getTypeDisplayString } from "../../util";
|
||||||
|
|
||||||
interface IInstanceTypeProps {
|
interface InstanceTypeProps {
|
||||||
type: string;
|
type: string;
|
||||||
colorAfterName?: boolean;
|
colorAfterName?: boolean;
|
||||||
}
|
}
|
||||||
|
@ -13,9 +13,9 @@ interface IInstanceTypeProps {
|
||||||
* By default, renders the color followed by the name of the instance type.
|
* By default, renders the color followed by the name of the instance type.
|
||||||
* You can change this by passing `colorAfterName={true}`.
|
* You can change this by passing `colorAfterName={true}`.
|
||||||
*/
|
*/
|
||||||
const InstanceType: React.FC<IInstanceTypeProps> = ({ type, colorAfterName }) => {
|
const InstanceType: React.FC<InstanceTypeProps> = ({ type, colorAfterName }) => {
|
||||||
const idx = typeColorScheme.values.indexOf(type);
|
const idx = typeColorScheme.values.indexOf(type);
|
||||||
const name = " " + getTypeDisplayString(type);
|
const name = ` ${getTypeDisplayString(type)}`;
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{!!colorAfterName && name}
|
{!!colorAfterName && name}
|
||||||
|
|
|
@ -11,19 +11,19 @@ const Backdrop = styled.div`
|
||||||
z-index: 3;
|
z-index: 3;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
interface IContainerProps {
|
interface ContainerProps {
|
||||||
fullWidth?: boolean;
|
fullWidth?: boolean;
|
||||||
}
|
}
|
||||||
const Container = styled.div<IContainerProps>`
|
const Container = styled.div<ContainerProps>`
|
||||||
max-width: ${props => (props.fullWidth ? "100%" : "800px")};
|
max-width: ${(props) => (props.fullWidth ? "100%" : "800px")};
|
||||||
margin: auto;
|
margin: auto;
|
||||||
padding: 2em;
|
padding: 2em;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
interface IPageProps {
|
interface PageProps {
|
||||||
fullWidth?: boolean;
|
fullWidth?: boolean;
|
||||||
}
|
}
|
||||||
const Page: React.FC<IPageProps> = ({ children, fullWidth }) => (
|
const Page: React.FC<PageProps> = ({ children, fullWidth }) => (
|
||||||
<Backdrop>
|
<Backdrop>
|
||||||
<Container fullWidth={fullWidth}>{children}</Container>
|
<Container fullWidth={fullWidth}>{children}</Container>
|
||||||
</Backdrop>
|
</Backdrop>
|
||||||
|
|
|
@ -10,9 +10,9 @@ import {
|
||||||
QUALITATIVE_COLOR_SCHEME,
|
QUALITATIVE_COLOR_SCHEME,
|
||||||
QUANTITATIVE_COLOR_SCHEME,
|
QUANTITATIVE_COLOR_SCHEME,
|
||||||
SEARCH_RESULT_COLOR,
|
SEARCH_RESULT_COLOR,
|
||||||
SELECTED_NODE_COLOR
|
SELECTED_NODE_COLOR,
|
||||||
} from "../../constants";
|
} from "../../constants";
|
||||||
import { IColorScheme } from "../../types";
|
import { ColorScheme } from "../../types";
|
||||||
import { getBuckets, getTypeDisplayString } from "../../util";
|
import { getBuckets, getTypeDisplayString } from "../../util";
|
||||||
|
|
||||||
const CytoscapeContainer = styled.div`
|
const CytoscapeContainer = styled.div`
|
||||||
|
@ -21,8 +21,8 @@ const CytoscapeContainer = styled.div`
|
||||||
flex: 1;
|
flex: 1;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
interface ICytoscapeProps {
|
interface CytoscapeProps {
|
||||||
colorScheme?: IColorScheme;
|
colorScheme?: ColorScheme;
|
||||||
currentNodeId: string | null;
|
currentNodeId: string | null;
|
||||||
elements: cytoscape.ElementsDefinition;
|
elements: cytoscape.ElementsDefinition;
|
||||||
hoveringOver?: string;
|
hoveringOver?: string;
|
||||||
|
@ -32,10 +32,11 @@ interface ICytoscapeProps {
|
||||||
navigateToInstancePath?: (domain: string) => void;
|
navigateToInstancePath?: (domain: string) => void;
|
||||||
navigateToRoot?: () => void;
|
navigateToRoot?: () => void;
|
||||||
}
|
}
|
||||||
class Cytoscape extends React.PureComponent<ICytoscapeProps> {
|
class Cytoscape extends React.PureComponent<CytoscapeProps> {
|
||||||
private cy?: cytoscape.Core;
|
private cy?: cytoscape.Core;
|
||||||
|
|
||||||
public componentDidMount() {
|
public componentDidMount() {
|
||||||
|
// eslint-disable-next-line react/no-find-dom-node
|
||||||
const container = ReactDOM.findDOMNode(this);
|
const container = ReactDOM.findDOMNode(this);
|
||||||
this.cy = cytoscape({
|
this.cy = cytoscape({
|
||||||
autoungrabify: true,
|
autoungrabify: true,
|
||||||
|
@ -44,16 +45,16 @@ class Cytoscape extends React.PureComponent<ICytoscapeProps> {
|
||||||
hideEdgesOnViewport: true,
|
hideEdgesOnViewport: true,
|
||||||
hideLabelsOnViewport: true,
|
hideLabelsOnViewport: true,
|
||||||
layout: {
|
layout: {
|
||||||
name: "preset"
|
name: "preset",
|
||||||
},
|
},
|
||||||
maxZoom: 2,
|
maxZoom: 2,
|
||||||
minZoom: 0.01,
|
minZoom: 0.01,
|
||||||
pixelRatio: 1.0,
|
pixelRatio: 1.0,
|
||||||
selectionType: "single"
|
selectionType: "single",
|
||||||
});
|
});
|
||||||
|
|
||||||
// Setup node tooltip on hover
|
// Setup node tooltip on hover
|
||||||
this.cy.nodes().forEach(n => {
|
this.cy.nodes().forEach((n) => {
|
||||||
const tooltipContent = `${n.data("id")} (${getTypeDisplayString(n.data("type"))})`;
|
const tooltipContent = `${n.data("id")} (${getTypeDisplayString(n.data("type"))})`;
|
||||||
const ref = (n as any).popperRef();
|
const ref = (n as any).popperRef();
|
||||||
const t = tippy(ref, {
|
const t = tippy(ref, {
|
||||||
|
@ -61,12 +62,12 @@ class Cytoscape extends React.PureComponent<ICytoscapeProps> {
|
||||||
animation: "fade",
|
animation: "fade",
|
||||||
content: tooltipContent,
|
content: tooltipContent,
|
||||||
duration: 100,
|
duration: 100,
|
||||||
trigger: "manual"
|
trigger: "manual",
|
||||||
});
|
});
|
||||||
n.on("mouseover", e => {
|
n.on("mouseover", () => {
|
||||||
(t as Instance).show();
|
(t as Instance).show();
|
||||||
});
|
});
|
||||||
n.on("mouseout", e => {
|
n.on("mouseout", () => {
|
||||||
(t as Instance).hide();
|
(t as Instance).hide();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -78,25 +79,25 @@ class Cytoscape extends React.PureComponent<ICytoscapeProps> {
|
||||||
.style({
|
.style({
|
||||||
"curve-style": "haystack", // fast edges
|
"curve-style": "haystack", // fast edges
|
||||||
"line-color": DEFAULT_NODE_COLOR,
|
"line-color": DEFAULT_NODE_COLOR,
|
||||||
width: "mapData(weight, 0, 0.5, 1, 20)"
|
width: "mapData(weight, 0, 0.5, 1, 20)",
|
||||||
})
|
})
|
||||||
.selector("node[label]")
|
.selector("node[label]")
|
||||||
.style({
|
.style({
|
||||||
color: DEFAULT_NODE_COLOR,
|
color: DEFAULT_NODE_COLOR,
|
||||||
"font-size": "mapData(size, 1, 6, 10, 100)",
|
"font-size": "mapData(size, 1, 6, 10, 100)",
|
||||||
"min-zoomed-font-size": 16
|
"min-zoomed-font-size": 16,
|
||||||
})
|
})
|
||||||
.selector(".hidden") // used to hide nodes not in the neighborhood of the selected, or to hide edges
|
.selector(".hidden") // used to hide nodes not in the neighborhood of the selected, or to hide edges
|
||||||
.style({
|
.style({
|
||||||
display: "none"
|
display: "none",
|
||||||
})
|
})
|
||||||
.selector(".thickEdge") // when a node is selected, make edges thicker so you can actually see them
|
.selector(".thickEdge") // when a node is selected, make edges thicker so you can actually see them
|
||||||
.style({
|
.style({
|
||||||
width: 2
|
width: 2,
|
||||||
});
|
});
|
||||||
this.resetNodeColorScheme(style); // this function also called `update()`
|
this.resetNodeColorScheme(style); // this function also called `update()`
|
||||||
|
|
||||||
this.cy.nodes().on("select", e => {
|
this.cy.nodes().on("select", (e) => {
|
||||||
const instanceId = e.target.data("id");
|
const instanceId = e.target.data("id");
|
||||||
if (instanceId && instanceId !== this.props.currentNodeId) {
|
if (instanceId && instanceId !== this.props.currentNodeId) {
|
||||||
if (this.props.navigateToInstancePath) {
|
if (this.props.navigateToInstancePath) {
|
||||||
|
@ -110,21 +111,19 @@ class Cytoscape extends React.PureComponent<ICytoscapeProps> {
|
||||||
this.cy!.nodes().removeClass("hidden");
|
this.cy!.nodes().removeClass("hidden");
|
||||||
this.cy!.edges().removeClass("thickEdge");
|
this.cy!.edges().removeClass("thickEdge");
|
||||||
// Then hide everything except neighborhood
|
// Then hide everything except neighborhood
|
||||||
this.cy!.nodes()
|
this.cy!.nodes().diff(neighborhood).left.addClass("hidden");
|
||||||
.diff(neighborhood)
|
|
||||||
.left.addClass("hidden");
|
|
||||||
neighborhood.connectedEdges().addClass("thickEdge");
|
neighborhood.connectedEdges().addClass("thickEdge");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
this.cy.nodes().on("unselect", e => {
|
this.cy.nodes().on("unselect", () => {
|
||||||
this.cy!.batch(() => {
|
this.cy!.batch(() => {
|
||||||
this.cy!.nodes().removeClass("hidden");
|
this.cy!.nodes().removeClass("hidden");
|
||||||
this.cy!.edges().removeClass("thickEdge");
|
this.cy!.edges().removeClass("thickEdge");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
this.cy.on("click", e => {
|
this.cy.on("click", (e) => {
|
||||||
// Clicking on the background should also deselect
|
// Clicking on the background should also deselect
|
||||||
const target = e.target;
|
const { target } = e;
|
||||||
if (!target || target === this.cy || target.isEdge()) {
|
if (!target || target === this.cy || target.isEdge()) {
|
||||||
if (this.props.navigateToRoot) {
|
if (this.props.navigateToRoot) {
|
||||||
// Go to the URL "/"
|
// Go to the URL "/"
|
||||||
|
@ -136,7 +135,7 @@ class Cytoscape extends React.PureComponent<ICytoscapeProps> {
|
||||||
this.setNodeSelection();
|
this.setNodeSelection();
|
||||||
}
|
}
|
||||||
|
|
||||||
public componentDidUpdate(prevProps: ICytoscapeProps) {
|
public componentDidUpdate(prevProps: CytoscapeProps) {
|
||||||
this.setNodeSelection(prevProps.currentNodeId);
|
this.setNodeSelection(prevProps.currentNodeId);
|
||||||
if (prevProps.colorScheme !== this.props.colorScheme) {
|
if (prevProps.colorScheme !== this.props.colorScheme) {
|
||||||
this.updateColorScheme();
|
this.updateColorScheme();
|
||||||
|
@ -174,12 +173,12 @@ class Cytoscape extends React.PureComponent<ICytoscapeProps> {
|
||||||
if (currentNodeId) {
|
if (currentNodeId) {
|
||||||
this.cy.zoom({
|
this.cy.zoom({
|
||||||
level: 0.2,
|
level: 0.2,
|
||||||
position: this.cy.$id(currentNodeId).position()
|
position: this.cy.$id(currentNodeId).position(),
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this.cy.zoom({
|
this.cy.zoom({
|
||||||
level: 0.2,
|
level: 0.2,
|
||||||
position: { x: 0, y: 0 }
|
position: { x: 0, y: 0 },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -221,7 +220,7 @@ class Cytoscape extends React.PureComponent<ICytoscapeProps> {
|
||||||
// quite good as it is, so...
|
// quite good as it is, so...
|
||||||
height: "mapData(size, 1, 6, 20, 200)",
|
height: "mapData(size, 1, 6, 20, 200)",
|
||||||
label: "data(id)",
|
label: "data(id)",
|
||||||
width: "mapData(size, 1, 6, 20, 200)"
|
width: "mapData(size, 1, 6, 20, 200)",
|
||||||
});
|
});
|
||||||
|
|
||||||
this.setNodeSearchColorScheme(style);
|
this.setNodeSearchColorScheme(style);
|
||||||
|
@ -240,16 +239,16 @@ class Cytoscape extends React.PureComponent<ICytoscapeProps> {
|
||||||
"background-color": SEARCH_RESULT_COLOR,
|
"background-color": SEARCH_RESULT_COLOR,
|
||||||
"border-color": SEARCH_RESULT_COLOR,
|
"border-color": SEARCH_RESULT_COLOR,
|
||||||
"border-opacity": 0.7,
|
"border-opacity": 0.7,
|
||||||
"border-width": 250
|
"border-width": 250,
|
||||||
})
|
})
|
||||||
.selector("node.hovered")
|
.selector("node.hovered")
|
||||||
.style({
|
.style({
|
||||||
"border-color": HOVERED_NODE_COLOR,
|
"border-color": HOVERED_NODE_COLOR,
|
||||||
"border-width": 1000
|
"border-width": 1000,
|
||||||
})
|
})
|
||||||
.selector("node:selected")
|
.selector("node:selected")
|
||||||
.style({
|
.style({
|
||||||
"background-color": SELECTED_NODE_COLOR
|
"background-color": SELECTED_NODE_COLOR,
|
||||||
})
|
})
|
||||||
.update();
|
.update();
|
||||||
};
|
};
|
||||||
|
@ -263,10 +262,11 @@ class Cytoscape extends React.PureComponent<ICytoscapeProps> {
|
||||||
if (!colorScheme) {
|
if (!colorScheme) {
|
||||||
this.resetNodeColorScheme();
|
this.resetNodeColorScheme();
|
||||||
return;
|
return;
|
||||||
} else if (colorScheme.type === "qualitative") {
|
}
|
||||||
|
if (colorScheme.type === "qualitative") {
|
||||||
colorScheme.values.forEach((v, idx) => {
|
colorScheme.values.forEach((v, idx) => {
|
||||||
style = style.selector(`node[${colorScheme.cytoscapeDataKey} = '${v}']`).style({
|
style = style.selector(`node[${colorScheme.cytoscapeDataKey} = '${v}']`).style({
|
||||||
"background-color": QUALITATIVE_COLOR_SCHEME[idx]
|
"background-color": QUALITATIVE_COLOR_SCHEME[idx],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
} else if (colorScheme.type === "quantitative") {
|
} else if (colorScheme.type === "quantitative") {
|
||||||
|
@ -284,7 +284,7 @@ class Cytoscape extends React.PureComponent<ICytoscapeProps> {
|
||||||
const max = idx === QUANTITATIVE_COLOR_SCHEME.length - 1 ? maxVal + 1 : buckets[idx + 1];
|
const max = idx === QUANTITATIVE_COLOR_SCHEME.length - 1 ? maxVal + 1 : buckets[idx + 1];
|
||||||
const selector = `node[${dataKey} >= ${min}][${dataKey} < ${max}]`;
|
const selector = `node[${dataKey} >= ${min}][${dataKey} < ${max}]`;
|
||||||
style = style.selector(selector).style({
|
style = style.selector(selector).style({
|
||||||
"background-color": color
|
"background-color": color,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -304,10 +304,10 @@ class Cytoscape extends React.PureComponent<ICytoscapeProps> {
|
||||||
}
|
}
|
||||||
const { hoveringOver } = this.props;
|
const { hoveringOver } = this.props;
|
||||||
|
|
||||||
if (!!prevHoveredId) {
|
if (prevHoveredId) {
|
||||||
this.cy.$id(prevHoveredId).removeClass("hovered");
|
this.cy.$id(prevHoveredId).removeClass("hovered");
|
||||||
}
|
}
|
||||||
if (!!hoveringOver) {
|
if (hoveringOver) {
|
||||||
this.cy.$id(hoveringOver).addClass("hovered");
|
this.cy.$id(hoveringOver).addClass("hovered");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -322,7 +322,7 @@ class Cytoscape extends React.PureComponent<ICytoscapeProps> {
|
||||||
this.cy!.nodes().removeClass("searchResult");
|
this.cy!.nodes().removeClass("searchResult");
|
||||||
|
|
||||||
if (!!searchResultIds && searchResultIds.length > 0) {
|
if (!!searchResultIds && searchResultIds.length > 0) {
|
||||||
const currentResultSelector = searchResultIds.map(id => `node[id = "${id}"]`).join(", ");
|
const currentResultSelector = searchResultIds.map((id) => `node[id = "${id}"]`).join(", ");
|
||||||
this.cy!.$(currentResultSelector).addClass("searchResult");
|
this.cy!.$(currentResultSelector).addClass("searchResult");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -344,11 +344,11 @@ class Cytoscape extends React.PureComponent<ICytoscapeProps> {
|
||||||
|
|
||||||
/* Helper function to remove edges if source or target node is missing */
|
/* Helper function to remove edges if source or target node is missing */
|
||||||
private cleanElements = (elements: cytoscape.ElementsDefinition): cytoscape.ElementsDefinition => {
|
private cleanElements = (elements: cytoscape.ElementsDefinition): cytoscape.ElementsDefinition => {
|
||||||
const domains = new Set(elements.nodes.map(n => n.data.id));
|
const domains = new Set(elements.nodes.map((n) => n.data.id));
|
||||||
const edges = elements.edges.filter(e => domains.has(e.data.source) && domains.has(e.data.target));
|
const edges = elements.edges.filter((e) => domains.has(e.data.source) && domains.has(e.data.target));
|
||||||
return {
|
return {
|
||||||
edges,
|
edges,
|
||||||
nodes: elements.nodes
|
nodes: elements.nodes,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,11 +2,11 @@ import { NonIdealState } from "@blueprintjs/core";
|
||||||
import { IconNames } from "@blueprintjs/icons";
|
import { IconNames } from "@blueprintjs/icons";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
|
||||||
interface IErrorStateProps {
|
interface ErrorStateProps {
|
||||||
description?: string;
|
description?: string;
|
||||||
}
|
}
|
||||||
const ErrorState: React.FC<IErrorStateProps> = ({ description }) => (
|
const ErrorState: React.FC<ErrorStateProps> = ({ description }) => (
|
||||||
<NonIdealState icon={IconNames.ERROR} title={"Something went wrong."} description={description} />
|
<NonIdealState icon={IconNames.ERROR} title="Something went wrong." description={description} />
|
||||||
);
|
);
|
||||||
|
|
||||||
export default ErrorState;
|
export default ErrorState;
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import { IColorScheme } from "../../types";
|
import { ColorScheme } from "../../types";
|
||||||
import { GraphHideEdgesButton, GraphKey, GraphResetButton } from "../atoms";
|
import { GraphHideEdgesButton, GraphKey, GraphResetButton } from "../atoms";
|
||||||
|
|
||||||
const GraphToolsContainer = styled.div`
|
const GraphToolsContainer = styled.div`
|
||||||
|
@ -11,35 +11,33 @@ const GraphToolsContainer = styled.div`
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
interface IGraphToolsProps {
|
interface GraphToolsProps {
|
||||||
currentColorScheme?: IColorScheme;
|
currentColorScheme?: ColorScheme;
|
||||||
colorSchemes: IColorScheme[];
|
colorSchemes: ColorScheme[];
|
||||||
isShowingEdges: boolean;
|
isShowingEdges: boolean;
|
||||||
ranges?: { [key: string]: [number, number] };
|
ranges?: { [key: string]: [number, number] };
|
||||||
onColorSchemeSelect: (colorScheme?: IColorScheme) => void;
|
onColorSchemeSelect: (colorScheme?: ColorScheme) => void;
|
||||||
onResetButtonClick: () => void;
|
onResetButtonClick: () => void;
|
||||||
toggleEdges: () => void;
|
toggleEdges: () => void;
|
||||||
}
|
}
|
||||||
const GraphTools: React.FC<IGraphToolsProps> = ({
|
const GraphTools: React.FC<GraphToolsProps> = ({
|
||||||
currentColorScheme,
|
currentColorScheme,
|
||||||
colorSchemes,
|
colorSchemes,
|
||||||
isShowingEdges,
|
isShowingEdges,
|
||||||
ranges,
|
ranges,
|
||||||
onColorSchemeSelect,
|
onColorSchemeSelect,
|
||||||
onResetButtonClick,
|
onResetButtonClick,
|
||||||
toggleEdges
|
toggleEdges,
|
||||||
}) => {
|
}) => (
|
||||||
return (
|
<GraphToolsContainer>
|
||||||
<GraphToolsContainer>
|
<GraphResetButton onClick={onResetButtonClick} />
|
||||||
<GraphResetButton onClick={onResetButtonClick} />
|
<GraphHideEdgesButton isShowingEdges={isShowingEdges} toggleEdges={toggleEdges} />
|
||||||
<GraphHideEdgesButton isShowingEdges={isShowingEdges} toggleEdges={toggleEdges} />
|
<GraphKey
|
||||||
<GraphKey
|
current={currentColorScheme}
|
||||||
current={currentColorScheme}
|
colorSchemes={colorSchemes}
|
||||||
colorSchemes={colorSchemes}
|
onItemSelect={onColorSchemeSelect}
|
||||||
onItemSelect={onColorSchemeSelect}
|
ranges={ranges}
|
||||||
ranges={ranges}
|
/>
|
||||||
/>
|
</GraphToolsContainer>
|
||||||
</GraphToolsContainer>
|
);
|
||||||
);
|
|
||||||
};
|
|
||||||
export default GraphTools;
|
export default GraphTools;
|
||||||
|
|
|
@ -4,7 +4,7 @@ import * as numeral from "numeral";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import sanitize from "sanitize-html";
|
import sanitize from "sanitize-html";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import { ISearchResultInstance } from "../../redux/types";
|
import { SearchResultInstance } from "../../redux/types";
|
||||||
import { InstanceType } from "../atoms";
|
import { InstanceType } from "../atoms";
|
||||||
|
|
||||||
const StyledCard = styled(Card)`
|
const StyledCard = styled(Card)`
|
||||||
|
@ -32,18 +32,18 @@ const StyledUserCount = styled.div`
|
||||||
const StyledDescription = styled.div`
|
const StyledDescription = styled.div`
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
`;
|
`;
|
||||||
interface ISearchResultProps {
|
interface SearchResultProps {
|
||||||
result: ISearchResultInstance;
|
result: SearchResultInstance;
|
||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
onMouseEnter: () => void;
|
onMouseEnter: () => void;
|
||||||
onMouseLeave: () => void;
|
onMouseLeave: () => void;
|
||||||
}
|
}
|
||||||
const SearchResult: React.FC<ISearchResultProps> = ({ result, onClick, onMouseEnter, onMouseLeave }) => {
|
const SearchResult: React.FC<SearchResultProps> = ({ result, onClick, onMouseEnter, onMouseLeave }) => {
|
||||||
let shortenedDescription;
|
let shortenedDescription;
|
||||||
if (result.description) {
|
if (result.description) {
|
||||||
shortenedDescription = result.description && sanitize(result.description);
|
shortenedDescription = result.description && sanitize(result.description);
|
||||||
if (shortenedDescription.length > 100) {
|
if (shortenedDescription.length > 100) {
|
||||||
shortenedDescription = shortenedDescription.substring(0, 100) + "...";
|
shortenedDescription = `${shortenedDescription.substring(0, 100)}...`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -59,7 +59,7 @@ const SearchResult: React.FC<ISearchResultProps> = ({ result, onClick, onMouseEn
|
||||||
return (
|
return (
|
||||||
<StyledCard
|
<StyledCard
|
||||||
elevation={Elevation.ONE}
|
elevation={Elevation.ONE}
|
||||||
interactive={true}
|
interactive
|
||||||
key={result.name}
|
key={result.name}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
onMouseEnter={onMouseEnter}
|
onMouseEnter={onMouseEnter}
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
import { Classes, H3 } from "@blueprintjs/core";
|
import { Classes, H3 } from "@blueprintjs/core";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { IFederationRestrictions } from "../../redux/types";
|
import { FederationRestrictions } from "../../redux/types";
|
||||||
|
|
||||||
const maybeGetList = (domains?: string[]) =>
|
const maybeGetList = (domains?: string[]) =>
|
||||||
domains && (
|
domains && (
|
||||||
<ul>
|
<ul>
|
||||||
{domains.sort().map(domain => (
|
{domains.sort().map((domain) => (
|
||||||
<li key={domain}>
|
<li key={domain}>
|
||||||
<Link to={`/instance/${domain}`} className={`${Classes.BUTTON} ${Classes.MINIMAL}`} role="button">
|
<Link to={`/instance/${domain}`} className={`${Classes.BUTTON} ${Classes.MINIMAL}`} role="button">
|
||||||
{domain}
|
{domain}
|
||||||
|
@ -16,10 +16,10 @@ const maybeGetList = (domains?: string[]) =>
|
||||||
</ul>
|
</ul>
|
||||||
);
|
);
|
||||||
|
|
||||||
interface IFederationTabProps {
|
interface FederationTabProps {
|
||||||
restrictions?: IFederationRestrictions;
|
restrictions?: FederationRestrictions;
|
||||||
}
|
}
|
||||||
const FederationTab: React.FC<IFederationTabProps> = ({ restrictions }) => {
|
const FederationTab: React.FC<FederationTabProps> = ({ restrictions }) => {
|
||||||
if (!restrictions) {
|
if (!restrictions) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,33 +6,33 @@ import { push } from "connected-react-router";
|
||||||
import { Dispatch } from "redux";
|
import { Dispatch } from "redux";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import { fetchGraph } from "../../redux/actions";
|
import { fetchGraph } from "../../redux/actions";
|
||||||
import { IAppState, IGraphResponse } from "../../redux/types";
|
import { AppState, GraphResponse } from "../../redux/types";
|
||||||
import { colorSchemes, IColorScheme } from "../../types";
|
import { colorSchemes, ColorScheme } from "../../types";
|
||||||
import { domainMatchSelector } from "../../util";
|
import { domainMatchSelector } from "../../util";
|
||||||
import { Cytoscape, ErrorState, GraphTools } from "../molecules/";
|
import { Cytoscape, ErrorState, GraphTools } from "../molecules";
|
||||||
|
|
||||||
const GraphDiv = styled.div`
|
const GraphDiv = styled.div`
|
||||||
flex: 2;
|
flex: 2;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
interface IGraphProps {
|
interface GraphProps {
|
||||||
currentInstanceName: string | null;
|
currentInstanceName: string | null;
|
||||||
fetchGraph: () => void;
|
fetchGraph: () => void;
|
||||||
graphResponse?: IGraphResponse;
|
graphResponse?: GraphResponse;
|
||||||
graphLoadError: boolean;
|
graphLoadError: boolean;
|
||||||
hoveringOverResult?: string;
|
hoveringOverResult?: string;
|
||||||
isLoadingGraph: boolean;
|
isLoadingGraph: boolean;
|
||||||
searchResultDomains: string[];
|
searchResultDomains: string[];
|
||||||
navigate: (path: string) => void;
|
navigate: (path: string) => void;
|
||||||
}
|
}
|
||||||
interface IGraphState {
|
interface GraphState {
|
||||||
colorScheme?: IColorScheme;
|
colorScheme?: ColorScheme;
|
||||||
isShowingEdges: boolean;
|
isShowingEdges: boolean;
|
||||||
}
|
}
|
||||||
class GraphImpl extends React.PureComponent<IGraphProps, IGraphState> {
|
class GraphImpl extends React.PureComponent<GraphProps, GraphState> {
|
||||||
private cytoscapeComponent: React.RefObject<Cytoscape>;
|
private cytoscapeComponent: React.RefObject<Cytoscape>;
|
||||||
|
|
||||||
public constructor(props: IGraphProps) {
|
public constructor(props: GraphProps) {
|
||||||
super(props);
|
super(props);
|
||||||
this.cytoscapeComponent = React.createRef();
|
this.cytoscapeComponent = React.createRef();
|
||||||
this.state = { colorScheme: undefined, isShowingEdges: true };
|
this.state = { colorScheme: undefined, isShowingEdges: true };
|
||||||
|
@ -76,7 +76,7 @@ class GraphImpl extends React.PureComponent<IGraphProps, IGraphState> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return <GraphDiv aria-hidden={true}>{content}</GraphDiv>;
|
return <GraphDiv aria-hidden>{content}</GraphDiv>;
|
||||||
}
|
}
|
||||||
|
|
||||||
private loadGraph = () => {
|
private loadGraph = () => {
|
||||||
|
@ -95,7 +95,7 @@ class GraphImpl extends React.PureComponent<IGraphProps, IGraphState> {
|
||||||
this.setState({ isShowingEdges: !this.state.isShowingEdges });
|
this.setState({ isShowingEdges: !this.state.isShowingEdges });
|
||||||
};
|
};
|
||||||
|
|
||||||
private setColorScheme = (colorScheme?: IColorScheme) => {
|
private setColorScheme = (colorScheme?: ColorScheme) => {
|
||||||
this.setState({ colorScheme });
|
this.setState({ colorScheme });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -107,7 +107,7 @@ class GraphImpl extends React.PureComponent<IGraphProps, IGraphState> {
|
||||||
this.props.navigate("/");
|
this.props.navigate("/");
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const mapStateToProps = (state: IAppState) => {
|
const mapStateToProps = (state: AppState) => {
|
||||||
const match = domainMatchSelector(state);
|
const match = domainMatchSelector(state);
|
||||||
return {
|
return {
|
||||||
currentInstanceName: match && match.params.domain,
|
currentInstanceName: match && match.params.domain,
|
||||||
|
@ -115,15 +115,12 @@ const mapStateToProps = (state: IAppState) => {
|
||||||
graphResponse: state.data.graphResponse,
|
graphResponse: state.data.graphResponse,
|
||||||
hoveringOverResult: state.search.hoveringOverResult,
|
hoveringOverResult: state.search.hoveringOverResult,
|
||||||
isLoadingGraph: state.data.isLoadingGraph,
|
isLoadingGraph: state.data.isLoadingGraph,
|
||||||
searchResultDomains: state.search.results.map(r => r.name)
|
searchResultDomains: state.search.results.map((r) => r.name),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
const mapDispatchToProps = (dispatch: Dispatch) => ({
|
const mapDispatchToProps = (dispatch: Dispatch) => ({
|
||||||
fetchGraph: () => dispatch(fetchGraph() as any),
|
fetchGraph: () => dispatch(fetchGraph() as any),
|
||||||
navigate: (path: string) => dispatch(push(path))
|
navigate: (path: string) => dispatch(push(path)),
|
||||||
});
|
});
|
||||||
const Graph = connect(
|
const Graph = connect(mapStateToProps, mapDispatchToProps)(GraphImpl);
|
||||||
mapStateToProps,
|
|
||||||
mapDispatchToProps
|
|
||||||
)(GraphImpl);
|
|
||||||
export default Graph;
|
export default Graph;
|
||||||
|
|
|
@ -8,7 +8,7 @@ import { connect } from "react-redux";
|
||||||
import { Dispatch } from "redux";
|
import { Dispatch } from "redux";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import { loadInstanceList } from "../../redux/actions";
|
import { loadInstanceList } from "../../redux/actions";
|
||||||
import { IAppState, IInstanceListResponse, IInstanceSort, SortField } from "../../redux/types";
|
import { AppState, InstanceListResponse, InstanceSort, SortField, InstanceDetails } from "../../redux/types";
|
||||||
import { InstanceType } from "../atoms";
|
import { InstanceType } from "../atoms";
|
||||||
import { ErrorState } from "../molecules";
|
import { ErrorState } from "../molecules";
|
||||||
|
|
||||||
|
@ -41,15 +41,15 @@ const InsularityColumn = styled.th`
|
||||||
width: 15%;
|
width: 15%;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
interface IInstanceTableProps {
|
interface InstanceTableProps {
|
||||||
loadError: boolean;
|
loadError: boolean;
|
||||||
instancesResponse?: IInstanceListResponse;
|
instancesResponse?: InstanceListResponse;
|
||||||
instanceListSort: IInstanceSort;
|
instanceListSort: InstanceSort;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
loadInstanceList: (page?: number, sort?: IInstanceSort) => void;
|
loadInstanceList: (page?: number, sort?: InstanceSort) => void;
|
||||||
navigate: (path: string) => void;
|
navigate: (path: string) => void;
|
||||||
}
|
}
|
||||||
class InstanceTable extends React.PureComponent<IInstanceTableProps> {
|
class InstanceTable extends React.PureComponent<InstanceTableProps> {
|
||||||
public componentDidMount() {
|
public componentDidMount() {
|
||||||
const { isLoading, instancesResponse, loadError } = this.props;
|
const { isLoading, instancesResponse, loadError } = this.props;
|
||||||
if (!isLoading && !instancesResponse && !loadError) {
|
if (!isLoading && !instancesResponse && !loadError) {
|
||||||
|
@ -61,22 +61,23 @@ class InstanceTable extends React.PureComponent<IInstanceTableProps> {
|
||||||
const { isLoading, instancesResponse, loadError } = this.props;
|
const { isLoading, instancesResponse, loadError } = this.props;
|
||||||
if (loadError) {
|
if (loadError) {
|
||||||
return <ErrorState />;
|
return <ErrorState />;
|
||||||
} else if (isLoading || !instancesResponse) {
|
}
|
||||||
|
if (isLoading || !instancesResponse) {
|
||||||
return <NonIdealState icon={<Spinner />} />;
|
return <NonIdealState icon={<Spinner />} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { instances, pageNumber: currentPage, totalPages, totalEntries, pageSize } = instancesResponse!;
|
const { instances, pageNumber: currentPage, totalPages, totalEntries, pageSize } = instancesResponse;
|
||||||
const pagesToDisplay = this.getPagesToDisplay(totalPages, currentPage);
|
const pagesToDisplay = this.getPagesToDisplay(totalPages, currentPage);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<StyledTable striped={true} bordered={true} interactive={true}>
|
<StyledTable striped bordered interactive>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<InstanceColumn>
|
<InstanceColumn>
|
||||||
Instance
|
Instance
|
||||||
<Button
|
<Button
|
||||||
minimal={true}
|
minimal
|
||||||
icon={this.getSortIcon("domain")}
|
icon={this.getSortIcon("domain")}
|
||||||
onClick={this.sortByFactory("domain")}
|
onClick={this.sortByFactory("domain")}
|
||||||
intent={this.getSortIntent("domain")}
|
intent={this.getSortIntent("domain")}
|
||||||
|
@ -87,7 +88,7 @@ class InstanceTable extends React.PureComponent<IInstanceTableProps> {
|
||||||
<UserCountColumn>
|
<UserCountColumn>
|
||||||
Users
|
Users
|
||||||
<Button
|
<Button
|
||||||
minimal={true}
|
minimal
|
||||||
icon={this.getSortIcon("userCount")}
|
icon={this.getSortIcon("userCount")}
|
||||||
onClick={this.sortByFactory("userCount")}
|
onClick={this.sortByFactory("userCount")}
|
||||||
intent={this.getSortIntent("userCount")}
|
intent={this.getSortIntent("userCount")}
|
||||||
|
@ -96,7 +97,7 @@ class InstanceTable extends React.PureComponent<IInstanceTableProps> {
|
||||||
<StatusCountColumn>
|
<StatusCountColumn>
|
||||||
Statuses
|
Statuses
|
||||||
<Button
|
<Button
|
||||||
minimal={true}
|
minimal
|
||||||
icon={this.getSortIcon("statusCount")}
|
icon={this.getSortIcon("statusCount")}
|
||||||
onClick={this.sortByFactory("statusCount")}
|
onClick={this.sortByFactory("statusCount")}
|
||||||
intent={this.getSortIntent("statusCount")}
|
intent={this.getSortIntent("statusCount")}
|
||||||
|
@ -105,7 +106,7 @@ class InstanceTable extends React.PureComponent<IInstanceTableProps> {
|
||||||
<InsularityColumn>
|
<InsularityColumn>
|
||||||
Insularity
|
Insularity
|
||||||
<Button
|
<Button
|
||||||
minimal={true}
|
minimal
|
||||||
icon={this.getSortIcon("insularity")}
|
icon={this.getSortIcon("insularity")}
|
||||||
onClick={this.sortByFactory("insularity")}
|
onClick={this.sortByFactory("insularity")}
|
||||||
intent={this.getSortIntent("insularity")}
|
intent={this.getSortIntent("insularity")}
|
||||||
|
@ -114,7 +115,7 @@ class InstanceTable extends React.PureComponent<IInstanceTableProps> {
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{instances.map(i => (
|
{instances.map((i: InstanceDetails) => (
|
||||||
<tr key={i.name} onClick={this.goToInstanceFactory(i.name)}>
|
<tr key={i.name} onClick={this.goToInstanceFactory(i.name)}>
|
||||||
<td>{i.name}</td>
|
<td>{i.name}</td>
|
||||||
<td>{i.type && <InstanceType type={i.type} />}</td>
|
<td>{i.type && <InstanceType type={i.type} />}</td>
|
||||||
|
@ -134,7 +135,7 @@ class InstanceTable extends React.PureComponent<IInstanceTableProps> {
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<ButtonGroup>
|
<ButtonGroup>
|
||||||
{zip(pagesToDisplay, pagesToDisplay.slice(1)).map(([page, nextPage], idx) => {
|
{zip(pagesToDisplay, pagesToDisplay.slice(1)).map(([page, nextPage]) => {
|
||||||
if (page === undefined) {
|
if (page === undefined) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -152,8 +153,8 @@ class InstanceTable extends React.PureComponent<IInstanceTableProps> {
|
||||||
{page}
|
{page}
|
||||||
</Button>
|
</Button>
|
||||||
{isEndOfSection && (
|
{isEndOfSection && (
|
||||||
<Button disabled={true} key={"..."}>
|
<Button disabled key="...">
|
||||||
{"..."}
|
...
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
@ -187,20 +188,19 @@ class InstanceTable extends React.PureComponent<IInstanceTableProps> {
|
||||||
const { instanceListSort } = this.props;
|
const { instanceListSort } = this.props;
|
||||||
if (instanceListSort.field !== field) {
|
if (instanceListSort.field !== field) {
|
||||||
return IconNames.SORT;
|
return IconNames.SORT;
|
||||||
} else if (instanceListSort.direction === "asc") {
|
|
||||||
return IconNames.SORT_ASC;
|
|
||||||
} else {
|
|
||||||
return IconNames.SORT_DESC;
|
|
||||||
}
|
}
|
||||||
|
if (instanceListSort.direction === "asc") {
|
||||||
|
return IconNames.SORT_ASC;
|
||||||
|
}
|
||||||
|
return IconNames.SORT_DESC;
|
||||||
};
|
};
|
||||||
|
|
||||||
private getSortIntent = (field: SortField) => {
|
private getSortIntent = (field: SortField) => {
|
||||||
const { instanceListSort } = this.props;
|
const { instanceListSort } = this.props;
|
||||||
if (instanceListSort.field === field) {
|
if (instanceListSort.field === field) {
|
||||||
return Intent.PRIMARY;
|
return Intent.PRIMARY;
|
||||||
} else {
|
|
||||||
return Intent.NONE;
|
|
||||||
}
|
}
|
||||||
|
return Intent.NONE;
|
||||||
};
|
};
|
||||||
|
|
||||||
private getPagesToDisplay = (totalPages: number, currentPage: number) => {
|
private getPagesToDisplay = (totalPages: number, currentPage: number) => {
|
||||||
|
@ -214,24 +214,19 @@ class InstanceTable extends React.PureComponent<IInstanceTableProps> {
|
||||||
|
|
||||||
const pagesToDisplay = firstPages.concat(surroundingPages).concat(lastPages);
|
const pagesToDisplay = firstPages.concat(surroundingPages).concat(lastPages);
|
||||||
|
|
||||||
return sortedUniq(sortBy(pagesToDisplay, n => n));
|
return sortedUniq(sortBy(pagesToDisplay, (n) => n));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapStateToProps = (state: IAppState) => {
|
const mapStateToProps = (state: AppState) => ({
|
||||||
return {
|
instanceListSort: state.data.instanceListSort,
|
||||||
instanceListSort: state.data.instanceListSort,
|
instancesResponse: state.data.instancesResponse,
|
||||||
instancesResponse: state.data.instancesResponse,
|
isLoading: state.data.isLoadingInstanceList,
|
||||||
isLoading: state.data.isLoadingInstanceList,
|
loadError: state.data.instanceListLoadError,
|
||||||
loadError: state.data.instanceListLoadError
|
});
|
||||||
};
|
|
||||||
};
|
|
||||||
const mapDispatchToProps = (dispatch: Dispatch) => ({
|
const mapDispatchToProps = (dispatch: Dispatch) => ({
|
||||||
loadInstanceList: (page?: number, sort?: IInstanceSort) => dispatch(loadInstanceList(page, sort) as any),
|
loadInstanceList: (page?: number, sort?: InstanceSort) => dispatch(loadInstanceList(page, sort) as any),
|
||||||
navigate: (path: string) => dispatch(push(path))
|
navigate: (path: string) => dispatch(push(path)),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(
|
export default connect(mapStateToProps, mapDispatchToProps)(InstanceTable);
|
||||||
mapStateToProps,
|
|
||||||
mapDispatchToProps
|
|
||||||
)(InstanceTable);
|
|
||||||
|
|
|
@ -1,21 +1,19 @@
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
|
||||||
import { Alignment, Navbar } from "@blueprintjs/core";
|
import { Alignment, Navbar, Classes } from "@blueprintjs/core";
|
||||||
import { IconNames } from "@blueprintjs/icons";
|
import { IconNames } from "@blueprintjs/icons";
|
||||||
|
|
||||||
import { Classes } from "@blueprintjs/core";
|
|
||||||
import { match, NavLink } from "react-router-dom";
|
import { match, NavLink } from "react-router-dom";
|
||||||
import { IInstanceDomainPath } from "../../constants";
|
import { InstanceDomainPath } from "../../constants";
|
||||||
|
|
||||||
interface INavState {
|
interface NavState {
|
||||||
aboutIsOpen: boolean;
|
aboutIsOpen: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const graphIsActive = (currMatch: match<IInstanceDomainPath>, location: Location) => {
|
const graphIsActive = (currMatch: match<InstanceDomainPath>, location: Location) =>
|
||||||
return location.pathname === "/" || location.pathname.startsWith("/instance/");
|
location.pathname === "/" || location.pathname.startsWith("/instance/");
|
||||||
};
|
|
||||||
|
|
||||||
class Nav extends React.Component<{}, INavState> {
|
class Nav extends React.Component<{}, NavState> {
|
||||||
constructor(props: any) {
|
constructor(props: any) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = { aboutIsOpen: false };
|
this.state = { aboutIsOpen: false };
|
||||||
|
@ -23,7 +21,7 @@ class Nav extends React.Component<{}, INavState> {
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
return (
|
return (
|
||||||
<Navbar fixedToTop={true}>
|
<Navbar fixedToTop>
|
||||||
<Navbar.Group align={Alignment.LEFT}>
|
<Navbar.Group align={Alignment.LEFT}>
|
||||||
<Navbar.Heading>fediverse.space</Navbar.Heading>
|
<Navbar.Heading>fediverse.space</Navbar.Heading>
|
||||||
<Navbar.Divider />
|
<Navbar.Divider />
|
||||||
|
@ -46,7 +44,7 @@ class Nav extends React.Component<{}, INavState> {
|
||||||
to="/about"
|
to="/about"
|
||||||
className={`${Classes.BUTTON} ${Classes.MINIMAL} bp3-icon-${IconNames.INFO_SIGN}`}
|
className={`${Classes.BUTTON} ${Classes.MINIMAL} bp3-icon-${IconNames.INFO_SIGN}`}
|
||||||
activeClassName={Classes.INTENT_PRIMARY}
|
activeClassName={Classes.INTENT_PRIMARY}
|
||||||
exact={true}
|
exact
|
||||||
>
|
>
|
||||||
About
|
About
|
||||||
</NavLink>
|
</NavLink>
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { IconNames } from "@blueprintjs/icons";
|
||||||
import React, { MouseEvent } from "react";
|
import React, { MouseEvent } from "react";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import { INSTANCE_TYPES } from "../../constants";
|
import { INSTANCE_TYPES } from "../../constants";
|
||||||
import { getSearchFilterDisplayValue, ISearchFilter } from "../../searchFilters";
|
import { getSearchFilterDisplayValue, SearchFilter } from "../../searchFilters";
|
||||||
import { getTypeDisplayString } from "../../util";
|
import { getTypeDisplayString } from "../../util";
|
||||||
|
|
||||||
const SearchFilterContainer = styled.div`
|
const SearchFilterContainer = styled.div`
|
||||||
|
@ -19,30 +19,30 @@ const StyledTag = styled(Tag)`
|
||||||
margin-left: 5px;
|
margin-left: 5px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
interface ISearchFiltersProps {
|
interface SearchFiltersProps {
|
||||||
selectedFilters: ISearchFilter[];
|
selectedFilters: SearchFilter[];
|
||||||
selectFilter: (filter: ISearchFilter) => void;
|
selectFilter: (filter: SearchFilter) => void;
|
||||||
deselectFilter: (e: MouseEvent<HTMLButtonElement>, props: ITagProps) => void;
|
deselectFilter: (e: MouseEvent<HTMLButtonElement>, props: ITagProps) => void;
|
||||||
}
|
}
|
||||||
const SearchFilters: React.FC<ISearchFiltersProps> = ({ selectedFilters, selectFilter, deselectFilter }) => {
|
const SearchFilters: React.FC<SearchFiltersProps> = ({ selectedFilters, selectFilter, deselectFilter }) => {
|
||||||
const hasInstanceTypeFilter = selectedFilters.some(sf => sf.field === "type");
|
const hasInstanceTypeFilter = selectedFilters.some((sf) => sf.field === "type");
|
||||||
|
|
||||||
const handleSelectInstanceType = (e: MouseEvent<HTMLElement>) => {
|
const handleSelectInstanceType = (e: MouseEvent<HTMLElement>) => {
|
||||||
const field = "type";
|
const field = "type";
|
||||||
const relation = "eq";
|
const relation = "eq";
|
||||||
const value = e.currentTarget.innerText.toLowerCase().replace(" ", "");
|
const value = e.currentTarget.innerText.toLowerCase().replace(" ", "");
|
||||||
const filter: ISearchFilter = {
|
const filter: SearchFilter = {
|
||||||
displayValue: getSearchFilterDisplayValue(field, relation, value),
|
displayValue: getSearchFilterDisplayValue(field, relation, value),
|
||||||
field,
|
field,
|
||||||
relation,
|
relation,
|
||||||
value
|
value,
|
||||||
};
|
};
|
||||||
selectFilter(filter);
|
selectFilter(filter);
|
||||||
};
|
};
|
||||||
const renderMenu = () => (
|
const renderMenu = () => (
|
||||||
<Menu>
|
<Menu>
|
||||||
<MenuItem icon={IconNames.SYMBOL_CIRCLE} text="Instance type" disabled={hasInstanceTypeFilter}>
|
<MenuItem icon={IconNames.SYMBOL_CIRCLE} text="Instance type" disabled={hasInstanceTypeFilter}>
|
||||||
{INSTANCE_TYPES.map(t => (
|
{INSTANCE_TYPES.map((t) => (
|
||||||
<MenuItem key={t} text={getTypeDisplayString(t)} onClick={handleSelectInstanceType} />
|
<MenuItem key={t} text={getTypeDisplayString(t)} onClick={handleSelectInstanceType} />
|
||||||
))}
|
))}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
@ -51,15 +51,15 @@ const SearchFilters: React.FC<ISearchFiltersProps> = ({ selectedFilters, selectF
|
||||||
return (
|
return (
|
||||||
<SearchFilterContainer>
|
<SearchFilterContainer>
|
||||||
<TagContainer>
|
<TagContainer>
|
||||||
{selectedFilters.map(filter => (
|
{selectedFilters.map((filter) => (
|
||||||
<StyledTag key={filter.displayValue} minimal={true} onRemove={deselectFilter}>
|
<StyledTag key={filter.displayValue} minimal onRemove={deselectFilter}>
|
||||||
{filter.displayValue}
|
{filter.displayValue}
|
||||||
</StyledTag>
|
</StyledTag>
|
||||||
))}
|
))}
|
||||||
</TagContainer>
|
</TagContainer>
|
||||||
<Popover autoFocus={false} content={renderMenu()} position={Position.BOTTOM}>
|
<Popover autoFocus={false} content={renderMenu()} position={Position.BOTTOM}>
|
||||||
<Button minimal={true} icon={IconNames.FILTER}>
|
<Button minimal icon={IconNames.FILTER}>
|
||||||
{"Add filter"}
|
Add filter
|
||||||
</Button>
|
</Button>
|
||||||
</Popover>
|
</Popover>
|
||||||
</SearchFilterContainer>
|
</SearchFilterContainer>
|
||||||
|
|
|
@ -17,11 +17,9 @@ const StyledCard = styled(Card)`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
`;
|
`;
|
||||||
const SidebarContainer: React.FC = ({ children }) => {
|
const SidebarContainer: React.FC = ({ children }) => (
|
||||||
return (
|
<RightDiv>
|
||||||
<RightDiv>
|
<StyledCard elevation={Elevation.TWO}>{children}</StyledCard>
|
||||||
<StyledCard elevation={Elevation.TWO}>{children}</StyledCard>
|
</RightDiv>
|
||||||
</RightDiv>
|
);
|
||||||
);
|
|
||||||
};
|
|
||||||
export default SidebarContainer;
|
export default SidebarContainer;
|
||||||
|
|
|
@ -2,9 +2,9 @@ import { Classes, Code, H1, H2, H4 } from "@blueprintjs/core";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
// import appsignalLogo from "../../assets/appsignal.svg";
|
// import appsignalLogo from "../../assets/appsignal.svg";
|
||||||
import gitlabLogo from "../../assets/gitlab.png";
|
import * as gitlabLogo from "../../assets/gitlab.png";
|
||||||
import nlnetLogo from "../../assets/nlnet.png";
|
import * as nlnetLogo from "../../assets/nlnet.png";
|
||||||
import { Page } from "../atoms/";
|
import { Page } from "../atoms";
|
||||||
|
|
||||||
const SponsorContainer = styled.div`
|
const SponsorContainer = styled.div`
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
|
@ -36,10 +36,11 @@ const AboutScreen: React.FC = () => (
|
||||||
<br />
|
<br />
|
||||||
<H2>FAQ</H2>
|
<H2>FAQ</H2>
|
||||||
|
|
||||||
<H4>Why can't I see details about my instance?</H4>
|
<H4>Why can't I see details about my instance?</H4>
|
||||||
<p className={Classes.RUNNING_TEXT}>
|
<p className={Classes.RUNNING_TEXT}>
|
||||||
fediverse.space only supports servers using the Mastodon API, the Misskey API, the GNU Social API, or Nodeinfo.
|
fediverse.space only supports servers using the Mastodon API, the Misskey API, the GNU Social API, or Nodeinfo.
|
||||||
Instances with 10 or fewer users won't be scraped -- it's a tool for understanding communities, not individuals.
|
Instances with 10 or fewer users won't be scraped -- it's a tool for understanding communities, not
|
||||||
|
individuals.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<H4>
|
<H4>
|
||||||
|
|
|
@ -17,7 +17,7 @@ const ButtonContainer = styled.div`
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
interface IAdminSettings {
|
interface AdminSettings {
|
||||||
domain: string;
|
domain: string;
|
||||||
optIn: boolean;
|
optIn: boolean;
|
||||||
optOut: boolean;
|
optOut: boolean;
|
||||||
|
@ -25,26 +25,26 @@ interface IAdminSettings {
|
||||||
statusCount: number;
|
statusCount: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IAdminScreenProps {
|
interface AdminScreenProps {
|
||||||
navigate: (path: string) => void;
|
navigate: (path: string) => void;
|
||||||
}
|
}
|
||||||
interface IAdminScreenState {
|
interface AdminScreenState {
|
||||||
settings?: IAdminSettings;
|
settings?: AdminSettings;
|
||||||
isUpdating: boolean;
|
isUpdating: boolean;
|
||||||
}
|
}
|
||||||
class AdminScreen extends React.PureComponent<IAdminScreenProps, IAdminScreenState> {
|
class AdminScreen extends React.PureComponent<AdminScreenProps, AdminScreenState> {
|
||||||
private authToken = getAuthToken();
|
private authToken = getAuthToken();
|
||||||
|
|
||||||
public constructor(props: IAdminScreenProps) {
|
public constructor(props: AdminScreenProps) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = { isUpdating: false };
|
this.state = { isUpdating: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
public componentDidMount() {
|
public componentDidMount() {
|
||||||
// Load instance settings from server
|
// Load instance settings from server
|
||||||
if (!!this.authToken) {
|
if (this.authToken) {
|
||||||
getFromApi(`admin`, this.authToken!)
|
getFromApi(`admin`, this.authToken)
|
||||||
.then(response => {
|
.then((response) => {
|
||||||
this.setState({ settings: response });
|
this.setState({ settings: response });
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
|
@ -52,7 +52,7 @@ class AdminScreen extends React.PureComponent<IAdminScreenProps, IAdminScreenSta
|
||||||
icon: IconNames.ERROR,
|
icon: IconNames.ERROR,
|
||||||
intent: Intent.DANGER,
|
intent: Intent.DANGER,
|
||||||
message: "Failed to load settings.",
|
message: "Failed to load settings.",
|
||||||
timeout: 0
|
timeout: 0,
|
||||||
});
|
});
|
||||||
unsetAuthToken();
|
unsetAuthToken();
|
||||||
});
|
});
|
||||||
|
@ -78,7 +78,7 @@ class AdminScreen extends React.PureComponent<IAdminScreenProps, IAdminScreenSta
|
||||||
<Switch
|
<Switch
|
||||||
id="opt-in-switch"
|
id="opt-in-switch"
|
||||||
checked={!!settings.optIn}
|
checked={!!settings.optIn}
|
||||||
large={true}
|
large
|
||||||
label="Opt in"
|
label="Opt in"
|
||||||
disabled={!!isUpdating}
|
disabled={!!isUpdating}
|
||||||
onChange={this.updateOptIn}
|
onChange={this.updateOptIn}
|
||||||
|
@ -89,7 +89,7 @@ class AdminScreen extends React.PureComponent<IAdminScreenProps, IAdminScreenSta
|
||||||
<Switch
|
<Switch
|
||||||
id="opt-out-switch"
|
id="opt-out-switch"
|
||||||
checked={!!settings.optOut}
|
checked={!!settings.optOut}
|
||||||
large={true}
|
large
|
||||||
label="Opt out"
|
label="Opt out"
|
||||||
disabled={!!isUpdating}
|
disabled={!!isUpdating}
|
||||||
onChange={this.updateOptOut}
|
onChange={this.updateOptOut}
|
||||||
|
@ -116,9 +116,9 @@ class AdminScreen extends React.PureComponent<IAdminScreenProps, IAdminScreenSta
|
||||||
}
|
}
|
||||||
|
|
||||||
private updateOptIn = (e: React.FormEvent<HTMLInputElement>) => {
|
private updateOptIn = (e: React.FormEvent<HTMLInputElement>) => {
|
||||||
const settings = this.state.settings as IAdminSettings;
|
const settings = this.state.settings as AdminSettings;
|
||||||
const optIn = e.currentTarget.checked;
|
const optIn = e.currentTarget.checked;
|
||||||
let optOut = settings.optOut;
|
let { optOut } = settings;
|
||||||
if (optIn) {
|
if (optIn) {
|
||||||
optOut = false;
|
optOut = false;
|
||||||
}
|
}
|
||||||
|
@ -126,9 +126,9 @@ class AdminScreen extends React.PureComponent<IAdminScreenProps, IAdminScreenSta
|
||||||
};
|
};
|
||||||
|
|
||||||
private updateOptOut = (e: React.FormEvent<HTMLInputElement>) => {
|
private updateOptOut = (e: React.FormEvent<HTMLInputElement>) => {
|
||||||
const settings = this.state.settings as IAdminSettings;
|
const settings = this.state.settings as AdminSettings;
|
||||||
const optOut = e.currentTarget.checked;
|
const optOut = e.currentTarget.checked;
|
||||||
let optIn = settings.optIn;
|
let { optIn } = settings;
|
||||||
if (optOut) {
|
if (optOut) {
|
||||||
optIn = false;
|
optIn = false;
|
||||||
}
|
}
|
||||||
|
@ -140,15 +140,15 @@ class AdminScreen extends React.PureComponent<IAdminScreenProps, IAdminScreenSta
|
||||||
this.setState({ isUpdating: true });
|
this.setState({ isUpdating: true });
|
||||||
const body = {
|
const body = {
|
||||||
optIn: this.state.settings!.optIn,
|
optIn: this.state.settings!.optIn,
|
||||||
optOut: this.state.settings!.optOut
|
optOut: this.state.settings!.optOut,
|
||||||
};
|
};
|
||||||
postToApi(`admin`, body, this.authToken!)
|
postToApi(`admin`, body, this.authToken!)
|
||||||
.then(response => {
|
.then((response) => {
|
||||||
this.setState({ settings: response, isUpdating: false });
|
this.setState({ settings: response, isUpdating: false });
|
||||||
AppToaster.show({
|
AppToaster.show({
|
||||||
icon: IconNames.TICK,
|
icon: IconNames.TICK,
|
||||||
intent: Intent.SUCCESS,
|
intent: Intent.SUCCESS,
|
||||||
message: "Successfully updated settings."
|
message: "Successfully updated settings.",
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
|
@ -161,16 +161,13 @@ class AdminScreen extends React.PureComponent<IAdminScreenProps, IAdminScreenSta
|
||||||
unsetAuthToken();
|
unsetAuthToken();
|
||||||
AppToaster.show({
|
AppToaster.show({
|
||||||
icon: IconNames.LOG_OUT,
|
icon: IconNames.LOG_OUT,
|
||||||
message: "Logged out."
|
message: "Logged out.",
|
||||||
});
|
});
|
||||||
this.props.navigate("/admin/login");
|
this.props.navigate("/admin/login");
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch: Dispatch) => ({
|
const mapDispatchToProps = (dispatch: Dispatch) => ({
|
||||||
navigate: (path: string) => dispatch(push(path))
|
navigate: (path: string) => dispatch(push(path)),
|
||||||
});
|
});
|
||||||
export default connect(
|
export default connect(undefined, mapDispatchToProps)(AdminScreen);
|
||||||
undefined,
|
|
||||||
mapDispatchToProps
|
|
||||||
)(AdminScreen);
|
|
||||||
|
|
|
@ -7,9 +7,9 @@ import { Route, RouteComponentProps, Switch, withRouter } from "react-router";
|
||||||
import { InstanceScreen, SearchScreen } from ".";
|
import { InstanceScreen, SearchScreen } from ".";
|
||||||
import { INSTANCE_DOMAIN_PATH } from "../../constants";
|
import { INSTANCE_DOMAIN_PATH } from "../../constants";
|
||||||
import { loadInstance } from "../../redux/actions";
|
import { loadInstance } from "../../redux/actions";
|
||||||
import { IAppState } from "../../redux/types";
|
import { AppState } from "../../redux/types";
|
||||||
import { domainMatchSelector, isSmallScreen } from "../../util";
|
import { domainMatchSelector, isSmallScreen } from "../../util";
|
||||||
import { Graph, SidebarContainer } from "../organisms/";
|
import { Graph, SidebarContainer } from "../organisms";
|
||||||
|
|
||||||
const GraphContainer = styled.div`
|
const GraphContainer = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -24,13 +24,13 @@ const FullDiv = styled.div`
|
||||||
right: 0;
|
right: 0;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
interface IGraphScreenProps extends RouteComponentProps {
|
interface GraphScreenProps extends RouteComponentProps {
|
||||||
currentInstanceName: string | null;
|
currentInstanceName: string | null;
|
||||||
pathname: string;
|
pathname: string;
|
||||||
graphLoadError: boolean;
|
graphLoadError: boolean;
|
||||||
loadInstance: (domain: string | null) => void;
|
loadInstance: (domain: string | null) => void;
|
||||||
}
|
}
|
||||||
interface IGraphScreenState {
|
interface GraphScreenState {
|
||||||
hasBeenViewed: boolean;
|
hasBeenViewed: boolean;
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
|
@ -41,8 +41,8 @@ interface IGraphScreenState {
|
||||||
* However, if it's not the first page viewed (e.g. if someone opens directly on /about) we don't want to render the
|
* However, if it's not the first page viewed (e.g. if someone opens directly on /about) we don't want to render the
|
||||||
* graph since it slows down everything else!
|
* graph since it slows down everything else!
|
||||||
*/
|
*/
|
||||||
class GraphScreenImpl extends React.Component<IGraphScreenProps, IGraphScreenState> {
|
class GraphScreenImpl extends React.Component<GraphScreenProps, GraphScreenState> {
|
||||||
public constructor(props: IGraphScreenProps) {
|
public constructor(props: GraphScreenProps) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = { hasBeenViewed: false };
|
this.state = { hasBeenViewed: false };
|
||||||
}
|
}
|
||||||
|
@ -56,7 +56,7 @@ class GraphScreenImpl extends React.Component<IGraphScreenProps, IGraphScreenSta
|
||||||
this.loadCurrentInstance();
|
this.loadCurrentInstance();
|
||||||
}
|
}
|
||||||
|
|
||||||
public componentDidUpdate(prevProps: IGraphScreenProps) {
|
public componentDidUpdate(prevProps: GraphScreenProps) {
|
||||||
this.setHasBeenViewed();
|
this.setHasBeenViewed();
|
||||||
this.loadCurrentInstance(prevProps.currentInstanceName);
|
this.loadCurrentInstance(prevProps.currentInstanceName);
|
||||||
}
|
}
|
||||||
|
@ -72,7 +72,7 @@ class GraphScreenImpl extends React.Component<IGraphScreenProps, IGraphScreenSta
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private renderRoutes = ({ location }: RouteComponentProps) => (
|
private renderRoutes = () => (
|
||||||
<FullDiv>
|
<FullDiv>
|
||||||
<GraphContainer>
|
<GraphContainer>
|
||||||
{/* Smaller screens never load the entire graph. Instead, `InstanceScreen` shows only the neighborhood. */}
|
{/* Smaller screens never load the entire graph. Instead, `InstanceScreen` shows only the neighborhood. */}
|
||||||
|
@ -80,7 +80,7 @@ class GraphScreenImpl extends React.Component<IGraphScreenProps, IGraphScreenSta
|
||||||
<SidebarContainer>
|
<SidebarContainer>
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route path={INSTANCE_DOMAIN_PATH} component={InstanceScreen} />
|
<Route path={INSTANCE_DOMAIN_PATH} component={InstanceScreen} />
|
||||||
<Route exact={true} path="/" component={SearchScreen} />
|
<Route exact path="/" component={SearchScreen} />
|
||||||
</Switch>
|
</Switch>
|
||||||
</SidebarContainer>
|
</SidebarContainer>
|
||||||
</GraphContainer>
|
</GraphContainer>
|
||||||
|
@ -94,19 +94,16 @@ class GraphScreenImpl extends React.Component<IGraphScreenProps, IGraphScreenSta
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapStateToProps = (state: IAppState) => {
|
const mapStateToProps = (state: AppState) => {
|
||||||
const match = domainMatchSelector(state);
|
const match = domainMatchSelector(state);
|
||||||
return {
|
return {
|
||||||
currentInstanceName: match && match.params.domain,
|
currentInstanceName: match && match.params.domain,
|
||||||
graphLoadError: state.data.graphLoadError,
|
graphLoadError: state.data.graphLoadError,
|
||||||
pathname: state.router.location.pathname
|
pathname: state.router.location.pathname,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
const mapDispatchToProps = (dispatch: Dispatch) => ({
|
const mapDispatchToProps = (dispatch: Dispatch) => ({
|
||||||
loadInstance: (domain: string | null) => dispatch(loadInstance(domain) as any)
|
loadInstance: (domain: string | null) => dispatch(loadInstance(domain) as any),
|
||||||
});
|
});
|
||||||
const GraphScreen = connect(
|
const GraphScreen = connect(mapStateToProps, mapDispatchToProps)(GraphScreenImpl);
|
||||||
mapStateToProps,
|
|
||||||
mapDispatchToProps
|
|
||||||
)(GraphScreenImpl);
|
|
||||||
export default withRouter(GraphScreen);
|
export default withRouter(GraphScreen);
|
||||||
|
|
|
@ -20,7 +20,7 @@ import {
|
||||||
Spinner,
|
Spinner,
|
||||||
Tab,
|
Tab,
|
||||||
Tabs,
|
Tabs,
|
||||||
Tooltip
|
Tooltip,
|
||||||
} from "@blueprintjs/core";
|
} from "@blueprintjs/core";
|
||||||
import { IconNames } from "@blueprintjs/icons";
|
import { IconNames } from "@blueprintjs/icons";
|
||||||
|
|
||||||
|
@ -28,10 +28,10 @@ import { push } from "connected-react-router";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { Dispatch } from "redux";
|
import { Dispatch } from "redux";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import { IAppState, IGraph, IGraphResponse, IInstanceDetails } from "../../redux/types";
|
import { AppState, Graph, GraphResponse, InstanceDetails, Peer } from "../../redux/types";
|
||||||
import { domainMatchSelector, getFromApi, isSmallScreen } from "../../util";
|
import { domainMatchSelector, getFromApi, isSmallScreen } from "../../util";
|
||||||
import { InstanceType } from "../atoms";
|
import { InstanceType } from "../atoms";
|
||||||
import { Cytoscape, ErrorState } from "../molecules/";
|
import { Cytoscape, ErrorState } from "../molecules";
|
||||||
import { FederationTab } from "../organisms";
|
import { FederationTab } from "../organisms";
|
||||||
|
|
||||||
const InstanceScreenContainer = styled.div`
|
const InstanceScreenContainer = styled.div`
|
||||||
|
@ -82,25 +82,25 @@ const StyledGraphContainer = styled.div`
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
`;
|
`;
|
||||||
interface IInstanceScreenProps {
|
interface InstanceScreenProps {
|
||||||
graph?: IGraph;
|
graph?: Graph;
|
||||||
instanceName: string | null;
|
instanceName: string | null;
|
||||||
instanceLoadError: boolean;
|
instanceLoadError: boolean;
|
||||||
instanceDetails: IInstanceDetails | null;
|
instanceDetails: InstanceDetails | null;
|
||||||
isLoadingInstanceDetails: boolean;
|
isLoadingInstanceDetails: boolean;
|
||||||
navigateToRoot: () => void;
|
navigateToRoot: () => void;
|
||||||
navigateToInstance: (domain: string) => void;
|
navigateToInstance: (domain: string) => void;
|
||||||
}
|
}
|
||||||
interface IInstanceScreenState {
|
interface InstanceScreenState {
|
||||||
neighbors?: string[];
|
neighbors?: string[];
|
||||||
isProcessingNeighbors: boolean;
|
isProcessingNeighbors: boolean;
|
||||||
// Local (neighborhood) graph. Used only on small screens (mobile devices).
|
// Local (neighborhood) graph. Used only on small screens (mobile devices).
|
||||||
isLoadingLocalGraph: boolean;
|
isLoadingLocalGraph: boolean;
|
||||||
localGraph?: IGraph;
|
localGraph?: Graph;
|
||||||
localGraphLoadError?: boolean;
|
localGraphLoadError?: boolean;
|
||||||
}
|
}
|
||||||
class InstanceScreenImpl extends React.PureComponent<IInstanceScreenProps, IInstanceScreenState> {
|
class InstanceScreenImpl extends React.PureComponent<InstanceScreenProps, InstanceScreenState> {
|
||||||
public constructor(props: IInstanceScreenProps) {
|
public constructor(props: InstanceScreenProps) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = { isProcessingNeighbors: false, isLoadingLocalGraph: false, localGraphLoadError: false };
|
this.state = { isProcessingNeighbors: false, isLoadingLocalGraph: false, localGraphLoadError: false };
|
||||||
}
|
}
|
||||||
|
@ -116,9 +116,9 @@ class InstanceScreenImpl extends React.PureComponent<IInstanceScreenProps, IInst
|
||||||
!this.props.instanceDetails.status
|
!this.props.instanceDetails.status
|
||||||
) {
|
) {
|
||||||
content = <ErrorState />;
|
content = <ErrorState />;
|
||||||
} else if (this.props.instanceDetails.status.toLowerCase().indexOf("personal instance") > -1) {
|
} else if (this.props.instanceDetails.status.toLowerCase().includes("personal instance")) {
|
||||||
content = this.renderPersonalInstanceErrorState();
|
content = this.renderPersonalInstanceErrorState();
|
||||||
} else if (this.props.instanceDetails.status.toLowerCase().indexOf("robots.txt") > -1) {
|
} else if (this.props.instanceDetails.status.toLowerCase().includes("robots.txt")) {
|
||||||
content = this.renderRobotsTxtState();
|
content = this.renderRobotsTxtState();
|
||||||
} else if (this.props.instanceDetails.status !== "success") {
|
} else if (this.props.instanceDetails.status !== "success") {
|
||||||
content = this.renderMissingDataState();
|
content = this.renderMissingDataState();
|
||||||
|
@ -130,7 +130,7 @@ class InstanceScreenImpl extends React.PureComponent<IInstanceScreenProps, IInst
|
||||||
<HeadingContainer>
|
<HeadingContainer>
|
||||||
<StyledHeadingH2>{this.props.instanceName}</StyledHeadingH2>
|
<StyledHeadingH2>{this.props.instanceName}</StyledHeadingH2>
|
||||||
<StyledHeadingTooltip content="Open link in new tab" position={Position.TOP} className={Classes.DARK}>
|
<StyledHeadingTooltip content="Open link in new tab" position={Position.TOP} className={Classes.DARK}>
|
||||||
<AnchorButton icon={IconNames.LINK} minimal={true} onClick={this.openInstanceLink} />
|
<AnchorButton icon={IconNames.LINK} minimal onClick={this.openInstanceLink} />
|
||||||
</StyledHeadingTooltip>
|
</StyledHeadingTooltip>
|
||||||
<StyledCloseButton icon={IconNames.CROSS} onClick={this.props.navigateToRoot} />
|
<StyledCloseButton icon={IconNames.CROSS} onClick={this.props.navigateToRoot} />
|
||||||
</HeadingContainer>
|
</HeadingContainer>
|
||||||
|
@ -145,7 +145,7 @@ class InstanceScreenImpl extends React.PureComponent<IInstanceScreenProps, IInst
|
||||||
this.processEdgesToFindNeighbors();
|
this.processEdgesToFindNeighbors();
|
||||||
}
|
}
|
||||||
|
|
||||||
public componentDidUpdate(prevProps: IInstanceScreenProps, prevState: IInstanceScreenState) {
|
public componentDidUpdate(prevProps: InstanceScreenProps, prevState: InstanceScreenState) {
|
||||||
const isNewInstance = prevProps.instanceName !== this.props.instanceName;
|
const isNewInstance = prevProps.instanceName !== this.props.instanceName;
|
||||||
const receivedNewEdges = !!this.props.graph && !this.state.isProcessingNeighbors && !this.state.neighbors;
|
const receivedNewEdges = !!this.props.graph && !this.state.isProcessingNeighbors && !this.state.neighbors;
|
||||||
const receivedNewLocalGraph = !!this.state.localGraph && !prevState.localGraph;
|
const receivedNewLocalGraph = !!this.state.localGraph && !prevState.localGraph;
|
||||||
|
@ -164,10 +164,13 @@ class InstanceScreenImpl extends React.PureComponent<IInstanceScreenProps, IInst
|
||||||
}
|
}
|
||||||
this.setState({ isProcessingNeighbors: true });
|
this.setState({ isProcessingNeighbors: true });
|
||||||
|
|
||||||
const graphToUse = !!graph ? graph : localGraph;
|
const graphToUse = graph || localGraph;
|
||||||
const edges = graphToUse!.edges.filter(e => [e.data.source, e.data.target].indexOf(instanceName!) > -1);
|
if (!graphToUse) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const edges = graphToUse.edges.filter((e) => [e.data.source, e.data.target].includes(instanceName));
|
||||||
const neighbors: any[] = [];
|
const neighbors: any[] = [];
|
||||||
edges.forEach(e => {
|
edges.forEach((e) => {
|
||||||
if (e.data.source === instanceName) {
|
if (e.data.source === instanceName) {
|
||||||
neighbors.push({ neighbor: e.data.target, weight: e.data.weight });
|
neighbors.push({ neighbor: e.data.target, weight: e.data.weight });
|
||||||
} else {
|
} else {
|
||||||
|
@ -183,39 +186,38 @@ class InstanceScreenImpl extends React.PureComponent<IInstanceScreenProps, IInst
|
||||||
}
|
}
|
||||||
this.setState({ isLoadingLocalGraph: true });
|
this.setState({ isLoadingLocalGraph: true });
|
||||||
getFromApi(`graph/${this.props.instanceName}`)
|
getFromApi(`graph/${this.props.instanceName}`)
|
||||||
.then((response: IGraphResponse) => {
|
.then((response: GraphResponse) => {
|
||||||
// We do some processing of edges here to make sure that every edge's source and target are in the neighborhood
|
// We do some processing of edges here to make sure that every edge's source and target are in the neighborhood
|
||||||
// We could (and should) be doing this in the backend, but I don't want to mess around with complex SQL
|
// We could (and should) be doing this in the backend, but I don't want to mess around with complex SQL
|
||||||
// queries.
|
// queries.
|
||||||
// TODO: think more about moving the backend to a graph database that would make this easier.
|
// TODO: think more about moving the backend to a graph database that would make this easier.
|
||||||
const graph = response.graph;
|
const { graph } = response;
|
||||||
const nodeIds = new Set(graph.nodes.map(n => n.data.id));
|
const nodeIds = new Set(graph.nodes.map((n) => n.data.id));
|
||||||
const edges = graph.edges.filter(e => nodeIds.has(e.data.source) && nodeIds.has(e.data.target));
|
const edges = graph.edges.filter((e) => nodeIds.has(e.data.source) && nodeIds.has(e.data.target));
|
||||||
this.setState({ isLoadingLocalGraph: false, localGraph: { ...graph, edges } });
|
this.setState({ isLoadingLocalGraph: false, localGraph: { ...graph, edges } });
|
||||||
})
|
})
|
||||||
.catch(() => this.setState({ isLoadingLocalGraph: false, localGraphLoadError: true }));
|
.catch(() => this.setState({ isLoadingLocalGraph: false, localGraphLoadError: true }));
|
||||||
};
|
};
|
||||||
|
|
||||||
private renderTabs = () => {
|
private renderTabs = () => {
|
||||||
|
const { instanceDetails } = this.props;
|
||||||
const hasNeighbors = this.state.neighbors && this.state.neighbors.length > 0;
|
const hasNeighbors = this.state.neighbors && this.state.neighbors.length > 0;
|
||||||
const federationRestrictions = this.props.instanceDetails && this.props.instanceDetails.federationRestrictions;
|
const federationRestrictions = instanceDetails && instanceDetails.federationRestrictions;
|
||||||
|
|
||||||
const hasLocalGraph =
|
const hasLocalGraph =
|
||||||
!!this.state.localGraph && this.state.localGraph.nodes.length > 0 && this.state.localGraph.edges.length > 0;
|
!!this.state.localGraph && this.state.localGraph.nodes.length > 0 && this.state.localGraph.edges.length > 0;
|
||||||
const insularCallout =
|
const insularCallout =
|
||||||
this.props.graph && !this.state.isProcessingNeighbors && !hasNeighbors && !hasLocalGraph ? (
|
this.props.graph && !this.state.isProcessingNeighbors && !hasNeighbors && !hasLocalGraph ? (
|
||||||
<StyledCallout icon={IconNames.INFO_SIGN} title="Insular instance">
|
<StyledCallout icon={IconNames.INFO_SIGN} title="Insular instance">
|
||||||
<p>This instance doesn't have any neighbors that we know of, so it's hidden from the graph.</p>
|
<p>This instance doesn't have any neighbors that we know of, so it's hidden from the graph.</p>
|
||||||
</StyledCallout>
|
</StyledCallout>
|
||||||
) : (
|
) : undefined;
|
||||||
undefined
|
|
||||||
);
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{insularCallout}
|
{insularCallout}
|
||||||
{this.maybeRenderLocalGraph()}
|
{this.maybeRenderLocalGraph()}
|
||||||
<StyledTabs>
|
<StyledTabs>
|
||||||
{this.props.instanceDetails!.description && (
|
{instanceDetails && instanceDetails.description && (
|
||||||
<Tab id="description" title="Description" panel={this.renderDescription()} />
|
<Tab id="description" title="Description" panel={this.renderDescription()} />
|
||||||
)}
|
)}
|
||||||
{this.shouldRenderStats() && <Tab id="stats" title="Details" panel={this.renderVersionAndCounts()} />}
|
{this.shouldRenderStats() && <Tab id="stats" title="Details" panel={this.renderVersionAndCounts()} />}
|
||||||
|
@ -232,7 +234,7 @@ class InstanceScreenImpl extends React.PureComponent<IInstanceScreenProps, IInst
|
||||||
<StyledLinkToFdNetwork>
|
<StyledLinkToFdNetwork>
|
||||||
<AnchorButton
|
<AnchorButton
|
||||||
href={`https://fediverse.network/${this.props.instanceName}`}
|
href={`https://fediverse.network/${this.props.instanceName}`}
|
||||||
minimal={true}
|
minimal
|
||||||
rightIcon={IconNames.SHARE}
|
rightIcon={IconNames.SHARE}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
text="See more statistics at fediverse.network"
|
text="See more statistics at fediverse.network"
|
||||||
|
@ -244,18 +246,17 @@ class InstanceScreenImpl extends React.PureComponent<IInstanceScreenProps, IInst
|
||||||
|
|
||||||
private maybeRenderLocalGraph = () => {
|
private maybeRenderLocalGraph = () => {
|
||||||
const { localGraph } = this.state;
|
const { localGraph } = this.state;
|
||||||
const hasLocalGraph =
|
const hasLocalGraph = !!localGraph && localGraph.nodes.length > 0 && localGraph.edges.length > 0;
|
||||||
!!this.state.localGraph && this.state.localGraph.nodes.length > 0 && this.state.localGraph.edges.length > 0;
|
if (!hasLocalGraph || !localGraph) {
|
||||||
if (!hasLocalGraph) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<StyledGraphContainer aria-hidden={true}>
|
<StyledGraphContainer aria-hidden>
|
||||||
<Cytoscape
|
<Cytoscape
|
||||||
elements={localGraph!}
|
elements={localGraph}
|
||||||
currentNodeId={this.props.instanceName}
|
currentNodeId={this.props.instanceName}
|
||||||
navigateToInstancePath={this.props.navigateToInstance}
|
navigateToInstancePath={this.props.navigateToInstance}
|
||||||
showEdges={true}
|
showEdges
|
||||||
/>
|
/>
|
||||||
<Divider />
|
<Divider />
|
||||||
</StyledGraphContainer>
|
</StyledGraphContainer>
|
||||||
|
@ -268,7 +269,11 @@ class InstanceScreenImpl extends React.PureComponent<IInstanceScreenProps, IInst
|
||||||
};
|
};
|
||||||
|
|
||||||
private renderDescription = () => {
|
private renderDescription = () => {
|
||||||
const description = this.props.instanceDetails!.description;
|
const { instanceDetails } = this.props;
|
||||||
|
if (!instanceDetails) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { description } = instanceDetails;
|
||||||
if (!description) {
|
if (!description) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -288,10 +293,10 @@ class InstanceScreenImpl extends React.PureComponent<IInstanceScreenProps, IInst
|
||||||
insularity,
|
insularity,
|
||||||
type,
|
type,
|
||||||
statusesPerDay,
|
statusesPerDay,
|
||||||
statusesPerUserPerDay
|
statusesPerUserPerDay,
|
||||||
} = this.props.instanceDetails;
|
} = this.props.instanceDetails;
|
||||||
return (
|
return (
|
||||||
<StyledHTMLTable small={true} striped={true}>
|
<StyledHTMLTable small striped>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Version</td>
|
<td>Version</td>
|
||||||
|
@ -299,7 +304,7 @@ class InstanceScreenImpl extends React.PureComponent<IInstanceScreenProps, IInst
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Instance type</td>
|
<td>Instance type</td>
|
||||||
<td>{(type && <InstanceType type={type} colorAfterName={true} />) || "Unknown"}</td>
|
<td>{(type && <InstanceType type={type} colorAfterName />) || "Unknown"}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Users</td>
|
<td>Users</td>
|
||||||
|
@ -311,7 +316,8 @@ class InstanceScreenImpl extends React.PureComponent<IInstanceScreenProps, IInst
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
Insularity{" "}
|
Insularity
|
||||||
|
{" "}
|
||||||
<Tooltip
|
<Tooltip
|
||||||
content={
|
content={
|
||||||
<span>
|
<span>
|
||||||
|
@ -330,7 +336,8 @@ class InstanceScreenImpl extends React.PureComponent<IInstanceScreenProps, IInst
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
Statuses / day{" "}
|
Statuses / day
|
||||||
|
{" "}
|
||||||
<Tooltip
|
<Tooltip
|
||||||
content={
|
content={
|
||||||
<span>
|
<span>
|
||||||
|
@ -349,7 +356,8 @@ class InstanceScreenImpl extends React.PureComponent<IInstanceScreenProps, IInst
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
Statuses / person / day{" "}
|
Statuses / person / day
|
||||||
|
{" "}
|
||||||
<Tooltip
|
<Tooltip
|
||||||
content={
|
content={
|
||||||
<span>
|
<span>
|
||||||
|
@ -372,7 +380,7 @@ class InstanceScreenImpl extends React.PureComponent<IInstanceScreenProps, IInst
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Last updated</td>
|
<td>Last updated</td>
|
||||||
<td>{moment(lastUpdated + "Z").fromNow() || "Unknown"}</td>
|
<td>{moment(`${lastUpdated}Z`).fromNow() || "Unknown"}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</StyledHTMLTable>
|
</StyledHTMLTable>
|
||||||
|
@ -412,7 +420,7 @@ class InstanceScreenImpl extends React.PureComponent<IInstanceScreenProps, IInst
|
||||||
would mean that every single status on {this.props.instanceName} contained a mention of someone on the other
|
would mean that every single status on {this.props.instanceName} contained a mention of someone on the other
|
||||||
instance, and vice versa.
|
instance, and vice versa.
|
||||||
</p>
|
</p>
|
||||||
<StyledHTMLTable small={true} striped={true} interactive={false}>
|
<StyledHTMLTable small striped interactive={false}>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Instance</th>
|
<th>Instance</th>
|
||||||
|
@ -426,11 +434,15 @@ class InstanceScreenImpl extends React.PureComponent<IInstanceScreenProps, IInst
|
||||||
};
|
};
|
||||||
|
|
||||||
private renderPeers = () => {
|
private renderPeers = () => {
|
||||||
const peers = this.props.instanceDetails!.peers;
|
const { instanceDetails } = this.props;
|
||||||
|
if (!instanceDetails) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { peers } = instanceDetails;
|
||||||
if (!peers || peers.length === 0) {
|
if (!peers || peers.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const peerRows = peers.map(instance => (
|
const peerRows = peers.map((instance: Peer) => (
|
||||||
<tr key={instance.name}>
|
<tr key={instance.name}>
|
||||||
<td>
|
<td>
|
||||||
<Link to={`/instance/${instance.name}`} className={`${Classes.BUTTON} ${Classes.MINIMAL}`} role="button">
|
<Link to={`/instance/${instance.name}`} className={`${Classes.BUTTON} ${Classes.MINIMAL}`} role="button">
|
||||||
|
@ -444,7 +456,7 @@ class InstanceScreenImpl extends React.PureComponent<IInstanceScreenProps, IInst
|
||||||
<p className={Classes.TEXT_MUTED}>
|
<p className={Classes.TEXT_MUTED}>
|
||||||
All the instances, past and present, that {this.props.instanceName} knows about.
|
All the instances, past and present, that {this.props.instanceName} knows about.
|
||||||
</p>
|
</p>
|
||||||
<StyledHTMLTable small={true} striped={true} interactive={false} className="fediverse-sidebar-table">
|
<StyledHTMLTable small striped interactive={false} className="fediverse-sidebar-table">
|
||||||
<tbody>{peerRows}</tbody>
|
<tbody>{peerRows}</tbody>
|
||||||
</StyledHTMLTable>
|
</StyledHTMLTable>
|
||||||
</div>
|
</div>
|
||||||
|
@ -453,71 +465,62 @@ class InstanceScreenImpl extends React.PureComponent<IInstanceScreenProps, IInst
|
||||||
|
|
||||||
private renderLoadingState = () => <NonIdealState icon={<Spinner />} />;
|
private renderLoadingState = () => <NonIdealState icon={<Spinner />} />;
|
||||||
|
|
||||||
private renderPersonalInstanceErrorState = () => {
|
private renderPersonalInstanceErrorState = () => (
|
||||||
return (
|
<NonIdealState
|
||||||
<NonIdealState
|
icon={IconNames.BLOCKED_PERSON}
|
||||||
icon={IconNames.BLOCKED_PERSON}
|
title="No data"
|
||||||
title="No data"
|
description="This instance has fewer than 10 users. It was not crawled in order to protect their privacy, but if it's your instance you can opt in."
|
||||||
description="This instance has fewer than 10 users. It was not crawled in order to protect their privacy, but if it's your instance you can opt in."
|
action={
|
||||||
action={
|
<Link to="/admin" className={Classes.BUTTON} role="button">
|
||||||
<Link to={"/admin"} className={Classes.BUTTON} role="button">
|
Opt in
|
||||||
{"Opt in"}
|
</Link>
|
||||||
</Link>
|
}
|
||||||
}
|
/>
|
||||||
/>
|
);
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
private renderMissingDataState = () => {
|
private renderMissingDataState = () => (
|
||||||
return (
|
<>
|
||||||
<>
|
<NonIdealState
|
||||||
<NonIdealState
|
icon={IconNames.ERROR}
|
||||||
icon={IconNames.ERROR}
|
title="No data"
|
||||||
title="No data"
|
description="This instance could not be crawled. Either it was down or it's an instance type we don't support yet."
|
||||||
description="This instance could not be crawled. Either it was down or it's an instance type we don't support yet."
|
/>
|
||||||
/>
|
<span className="sidebar-hidden-instance-status" style={{ display: "none" }}>
|
||||||
<span className="sidebar-hidden-instance-status" style={{ display: "none" }}>
|
{this.props.instanceDetails && this.props.instanceDetails.status}
|
||||||
{this.props.instanceDetails && this.props.instanceDetails.status}
|
</span>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
private renderRobotsTxtState = () => (
|
||||||
|
<NonIdealState
|
||||||
|
icon={
|
||||||
|
<span role="img" aria-label="robot">
|
||||||
|
🤖
|
||||||
</span>
|
</span>
|
||||||
</>
|
}
|
||||||
);
|
title="No data"
|
||||||
};
|
description="This instance was not crawled because its robots.txt did not allow us to."
|
||||||
|
/>
|
||||||
private renderRobotsTxtState = () => {
|
);
|
||||||
return (
|
|
||||||
<NonIdealState
|
|
||||||
icon={
|
|
||||||
<span role="img" aria-label="robot">
|
|
||||||
🤖
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
title="No data"
|
|
||||||
description="This instance was not crawled because its robots.txt did not allow us to."
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
private openInstanceLink = () => {
|
private openInstanceLink = () => {
|
||||||
window.open("https://" + this.props.instanceName, "_blank");
|
window.open(`https://${this.props.instanceName}`, "_blank");
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapStateToProps = (state: IAppState) => {
|
const mapStateToProps = (state: AppState) => {
|
||||||
const match = domainMatchSelector(state);
|
const match = domainMatchSelector(state);
|
||||||
return {
|
return {
|
||||||
graph: state.data.graphResponse && state.data.graphResponse.graph,
|
graph: state.data.graphResponse && state.data.graphResponse.graph,
|
||||||
instanceDetails: state.currentInstance.currentInstanceDetails,
|
instanceDetails: state.currentInstance.currentInstanceDetails,
|
||||||
instanceLoadError: state.currentInstance.error,
|
instanceLoadError: state.currentInstance.error,
|
||||||
instanceName: match && match.params.domain,
|
instanceName: match && match.params.domain,
|
||||||
isLoadingInstanceDetails: state.currentInstance.isLoadingInstanceDetails
|
isLoadingInstanceDetails: state.currentInstance.isLoadingInstanceDetails,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
const mapDispatchToProps = (dispatch: Dispatch) => ({
|
const mapDispatchToProps = (dispatch: Dispatch) => ({
|
||||||
navigateToInstance: (domain: string) => dispatch(push(`/instance/${domain}`)),
|
navigateToInstance: (domain: string) => dispatch(push(`/instance/${domain}`)),
|
||||||
navigateToRoot: () => dispatch(push("/"))
|
navigateToRoot: () => dispatch(push("/")),
|
||||||
});
|
});
|
||||||
const InstanceScreen = connect(
|
const InstanceScreen = connect(mapStateToProps, mapDispatchToProps)(InstanceScreenImpl);
|
||||||
mapStateToProps,
|
|
||||||
mapDispatchToProps
|
|
||||||
)(InstanceScreenImpl);
|
|
||||||
export default InstanceScreen;
|
export default InstanceScreen;
|
||||||
|
|
|
@ -8,11 +8,11 @@ import { getAuthToken, getFromApi, postToApi } from "../../util";
|
||||||
import { Page } from "../atoms";
|
import { Page } from "../atoms";
|
||||||
import { ErrorState } from "../molecules";
|
import { ErrorState } from "../molecules";
|
||||||
|
|
||||||
interface IFormContainerProps {
|
interface FormContainerProps {
|
||||||
error: boolean;
|
error: boolean;
|
||||||
}
|
}
|
||||||
const FormContainer = styled.div<IFormContainerProps>`
|
const FormContainer = styled.div<FormContainerProps>`
|
||||||
${props => (props.error ? "margin: 20px auto 0 auto;" : "margin-top: 20px;")}
|
${(props) => (props.error ? "margin: 20px auto 0 auto;" : "margin-top: 20px;")}
|
||||||
`;
|
`;
|
||||||
const LoginTypeContainer = styled.div`
|
const LoginTypeContainer = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -31,23 +31,28 @@ const StyledIcon = styled(Icon)`
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
interface ILoginTypes {
|
interface LoginTypes {
|
||||||
domain: string;
|
domain: string;
|
||||||
email?: string;
|
email?: string;
|
||||||
fediverseAccount?: string;
|
fediverseAccount?: string;
|
||||||
}
|
}
|
||||||
interface ILoginScreenState {
|
interface LoginScreenState {
|
||||||
domain: string;
|
domain: string;
|
||||||
isGettingLoginTypes: boolean;
|
isGettingLoginTypes: boolean;
|
||||||
isSendingLoginRequest: boolean;
|
isSendingLoginRequest: boolean;
|
||||||
loginTypes?: ILoginTypes;
|
loginTypes?: LoginTypes;
|
||||||
selectedLoginType?: "email" | "fediverseAccount";
|
selectedLoginType?: "email" | "fediverseAccount";
|
||||||
error: boolean;
|
error: boolean;
|
||||||
}
|
}
|
||||||
class LoginScreen extends React.PureComponent<{}, ILoginScreenState> {
|
class LoginScreen extends React.PureComponent<{}, LoginScreenState> {
|
||||||
public constructor(props: any) {
|
public constructor(props: any) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = { domain: "", error: false, isGettingLoginTypes: false, isSendingLoginRequest: false };
|
this.state = {
|
||||||
|
domain: "",
|
||||||
|
error: false,
|
||||||
|
isGettingLoginTypes: false,
|
||||||
|
isSendingLoginRequest: false,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
|
@ -59,13 +64,13 @@ class LoginScreen extends React.PureComponent<{}, ILoginScreenState> {
|
||||||
const { error, loginTypes, isSendingLoginRequest, selectedLoginType } = this.state;
|
const { error, loginTypes, isSendingLoginRequest, selectedLoginType } = this.state;
|
||||||
|
|
||||||
let content;
|
let content;
|
||||||
if (!!error) {
|
if (error) {
|
||||||
content = (
|
content = (
|
||||||
<ErrorState description="This could be because the instance is down. If not, please reload the page and try again." />
|
<ErrorState description="This could be because the instance is down. If not, please reload the page and try again." />
|
||||||
);
|
);
|
||||||
} else if (!!selectedLoginType && !isSendingLoginRequest) {
|
} else if (!!selectedLoginType && !isSendingLoginRequest) {
|
||||||
content = this.renderPostLogin();
|
content = this.renderPostLogin();
|
||||||
} else if (!!loginTypes) {
|
} else if (loginTypes) {
|
||||||
content = this.renderChooseLoginType();
|
content = this.renderChooseLoginType();
|
||||||
} else {
|
} else {
|
||||||
content = this.renderChooseInstance();
|
content = this.renderChooseInstance();
|
||||||
|
@ -78,7 +83,7 @@ class LoginScreen extends React.PureComponent<{}, ILoginScreenState> {
|
||||||
You must be the instance admin to manage how fediverse.space interacts with your instance.
|
You must be the instance admin to manage how fediverse.space interacts with your instance.
|
||||||
</p>
|
</p>
|
||||||
<p className={Classes.RUNNING_TEXT}>
|
<p className={Classes.RUNNING_TEXT}>
|
||||||
It's currently only possible to administrate Mastodon and Pleroma instances. If you want to login with a
|
It's currently only possible to administrate Mastodon and Pleroma instances. If you want to login with a
|
||||||
direct message, your instance must federate with mastodon.social and vice versa.
|
direct message, your instance must federate with mastodon.social and vice versa.
|
||||||
</p>
|
</p>
|
||||||
<p className={Classes.RUNNING_TEXT}>
|
<p className={Classes.RUNNING_TEXT}>
|
||||||
|
@ -95,7 +100,7 @@ class LoginScreen extends React.PureComponent<{}, ILoginScreenState> {
|
||||||
const onButtonClick = () => this.getLoginTypes();
|
const onButtonClick = () => this.getLoginTypes();
|
||||||
return (
|
return (
|
||||||
<form onSubmit={this.getLoginTypes}>
|
<form onSubmit={this.getLoginTypes}>
|
||||||
<FormGroup label="Instance domain" labelFor="domain-input" disabled={isGettingLoginTypes} inline={true}>
|
<FormGroup label="Instance domain" labelFor="domain-input" disabled={isGettingLoginTypes} inline>
|
||||||
<InputGroup
|
<InputGroup
|
||||||
disabled={isGettingLoginTypes}
|
disabled={isGettingLoginTypes}
|
||||||
id="domain-input"
|
id="domain-input"
|
||||||
|
@ -104,7 +109,7 @@ class LoginScreen extends React.PureComponent<{}, ILoginScreenState> {
|
||||||
rightElement={
|
rightElement={
|
||||||
<Button
|
<Button
|
||||||
intent={Intent.PRIMARY}
|
intent={Intent.PRIMARY}
|
||||||
minimal={true}
|
minimal
|
||||||
rightIcon={IconNames.ARROW_RIGHT}
|
rightIcon={IconNames.ARROW_RIGHT}
|
||||||
title="submit"
|
title="submit"
|
||||||
loading={isGettingLoginTypes}
|
loading={isGettingLoginTypes}
|
||||||
|
@ -130,18 +135,13 @@ class LoginScreen extends React.PureComponent<{}, ILoginScreenState> {
|
||||||
<H4>Choose an authentication method</H4>
|
<H4>Choose an authentication method</H4>
|
||||||
<LoginTypeContainer>
|
<LoginTypeContainer>
|
||||||
{loginTypes.email && (
|
{loginTypes.email && (
|
||||||
<LoginTypeButton
|
<LoginTypeButton large icon={IconNames.ENVELOPE} onClick={loginWithEmail} loading={!!isSendingLoginRequest}>
|
||||||
large={true}
|
|
||||||
icon={IconNames.ENVELOPE}
|
|
||||||
onClick={loginWithEmail}
|
|
||||||
loading={!!isSendingLoginRequest}
|
|
||||||
>
|
|
||||||
{`Email ${loginTypes.email}`}
|
{`Email ${loginTypes.email}`}
|
||||||
</LoginTypeButton>
|
</LoginTypeButton>
|
||||||
)}
|
)}
|
||||||
{loginTypes.fediverseAccount && (
|
{loginTypes.fediverseAccount && (
|
||||||
<LoginTypeButton
|
<LoginTypeButton
|
||||||
large={true}
|
large
|
||||||
icon={IconNames.GLOBE_NETWORK}
|
icon={IconNames.GLOBE_NETWORK}
|
||||||
onClick={loginWithDm}
|
onClick={loginWithDm}
|
||||||
loading={!!isSendingLoginRequest}
|
loading={!!isSendingLoginRequest}
|
||||||
|
@ -175,7 +175,7 @@ class LoginScreen extends React.PureComponent<{}, ILoginScreenState> {
|
||||||
};
|
};
|
||||||
|
|
||||||
private getLoginTypes = (e?: React.FormEvent<HTMLFormElement>) => {
|
private getLoginTypes = (e?: React.FormEvent<HTMLFormElement>) => {
|
||||||
if (!!e) {
|
if (e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
this.setState({ isGettingLoginTypes: true });
|
this.setState({ isGettingLoginTypes: true });
|
||||||
|
@ -184,8 +184,8 @@ class LoginScreen extends React.PureComponent<{}, ILoginScreenState> {
|
||||||
domain = domain.slice(8);
|
domain = domain.slice(8);
|
||||||
}
|
}
|
||||||
getFromApi(`admin/login/${domain.trim()}`)
|
getFromApi(`admin/login/${domain.trim()}`)
|
||||||
.then(response => {
|
.then((response) => {
|
||||||
if (!!response.error) {
|
if (response.error) {
|
||||||
// Go to catch() below
|
// Go to catch() below
|
||||||
throw new Error(response.error);
|
throw new Error(response.error);
|
||||||
} else {
|
} else {
|
||||||
|
@ -196,7 +196,7 @@ class LoginScreen extends React.PureComponent<{}, ILoginScreenState> {
|
||||||
AppToaster.show({
|
AppToaster.show({
|
||||||
icon: IconNames.ERROR,
|
icon: IconNames.ERROR,
|
||||||
intent: Intent.DANGER,
|
intent: Intent.DANGER,
|
||||||
message: err.message
|
message: err.message,
|
||||||
});
|
});
|
||||||
this.setState({ isGettingLoginTypes: false });
|
this.setState({ isGettingLoginTypes: false });
|
||||||
});
|
});
|
||||||
|
@ -205,7 +205,7 @@ class LoginScreen extends React.PureComponent<{}, ILoginScreenState> {
|
||||||
private login = (type: "email" | "fediverseAccount") => {
|
private login = (type: "email" | "fediverseAccount") => {
|
||||||
this.setState({ isSendingLoginRequest: true, selectedLoginType: type });
|
this.setState({ isSendingLoginRequest: true, selectedLoginType: type });
|
||||||
postToApi("admin/login", { domain: this.state.loginTypes!.domain, type })
|
postToApi("admin/login", { domain: this.state.loginTypes!.domain, type })
|
||||||
.then(response => {
|
.then((response) => {
|
||||||
if ("error" in response || "errors" in response) {
|
if ("error" in response || "errors" in response) {
|
||||||
// Go to catch() below
|
// Go to catch() below
|
||||||
throw new Error();
|
throw new Error();
|
||||||
|
|
|
@ -7,20 +7,20 @@ import { connect } from "react-redux";
|
||||||
import { Dispatch } from "redux";
|
import { Dispatch } from "redux";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import { setResultHover, updateSearch } from "../../redux/actions";
|
import { setResultHover, updateSearch } from "../../redux/actions";
|
||||||
import { IAppState, ISearchResultInstance } from "../../redux/types";
|
import { AppState, SearchResultInstance } from "../../redux/types";
|
||||||
import { ISearchFilter } from "../../searchFilters";
|
import { SearchFilter } from "../../searchFilters";
|
||||||
import { isSmallScreen } from "../../util";
|
import { isSmallScreen } from "../../util";
|
||||||
import { SearchResult } from "../molecules";
|
import { SearchResult } from "../molecules";
|
||||||
import { SearchFilters } from "../organisms";
|
import { SearchFilters } from "../organisms";
|
||||||
|
|
||||||
interface ISearchBarContainerProps {
|
interface SearchBarContainerProps {
|
||||||
hasSearchResults: boolean;
|
hasSearchResults: boolean;
|
||||||
hasError: boolean;
|
hasError: boolean;
|
||||||
}
|
}
|
||||||
const SearchBarContainer = styled.div<ISearchBarContainerProps>`
|
const SearchBarContainer = styled.div<SearchBarContainerProps>`
|
||||||
width: 80%;
|
width: 80%;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin: ${props => (props.hasSearchResults || props.hasError ? "0 auto" : "auto")};
|
margin: ${(props) => (props.hasSearchResults || props.hasError ? "0 auto" : "auto")};
|
||||||
align-self: center;
|
align-self: center;
|
||||||
`;
|
`;
|
||||||
const SearchResults = styled.div`
|
const SearchResults = styled.div`
|
||||||
|
@ -38,22 +38,22 @@ const CalloutContainer = styled.div`
|
||||||
text-align: left;
|
text-align: left;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
interface ISearchScreenProps {
|
interface SearchScreenProps {
|
||||||
error: boolean;
|
error: boolean;
|
||||||
isLoadingResults: boolean;
|
isLoadingResults: boolean;
|
||||||
query: string;
|
query: string;
|
||||||
hasMoreResults: boolean;
|
hasMoreResults: boolean;
|
||||||
results: ISearchResultInstance[];
|
results: SearchResultInstance[];
|
||||||
handleSearch: (query: string, filters: ISearchFilter[]) => void;
|
handleSearch: (query: string, filters: SearchFilter[]) => void;
|
||||||
navigateToInstance: (domain: string) => void;
|
navigateToInstance: (domain: string) => void;
|
||||||
setIsHoveringOver: (domain?: string) => void;
|
setIsHoveringOver: (domain?: string) => void;
|
||||||
}
|
}
|
||||||
interface ISearchScreenState {
|
interface SearchScreenState {
|
||||||
currentQuery: string;
|
currentQuery: string;
|
||||||
searchFilters: ISearchFilter[];
|
searchFilters: SearchFilter[];
|
||||||
}
|
}
|
||||||
class SearchScreen extends React.PureComponent<ISearchScreenProps, ISearchScreenState> {
|
class SearchScreen extends React.PureComponent<SearchScreenProps, SearchScreenState> {
|
||||||
public constructor(props: ISearchScreenProps) {
|
public constructor(props: SearchScreenProps) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = { currentQuery: "", searchFilters: [] };
|
this.state = { currentQuery: "", searchFilters: [] };
|
||||||
}
|
}
|
||||||
|
@ -81,7 +81,7 @@ class SearchScreen extends React.PureComponent<ISearchScreenProps, ISearchScreen
|
||||||
} else if (!!results && results.length > 0) {
|
} else if (!!results && results.length > 0) {
|
||||||
content = (
|
content = (
|
||||||
<SearchResults>
|
<SearchResults>
|
||||||
{results.map(result => (
|
{results.map((result) => (
|
||||||
<SearchResult
|
<SearchResult
|
||||||
result={result}
|
result={result}
|
||||||
key={result.name}
|
key={result.name}
|
||||||
|
@ -92,7 +92,7 @@ class SearchScreen extends React.PureComponent<ISearchScreenProps, ISearchScreen
|
||||||
))}
|
))}
|
||||||
{isLoadingResults && <StyledSpinner size={Spinner.SIZE_SMALL} />}
|
{isLoadingResults && <StyledSpinner size={Spinner.SIZE_SMALL} />}
|
||||||
{!isLoadingResults && hasMoreResults && (
|
{!isLoadingResults && hasMoreResults && (
|
||||||
<Button onClick={this.search} minimal={true}>
|
<Button onClick={this.search} minimal>
|
||||||
Load more results
|
Load more results
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
@ -104,13 +104,11 @@ class SearchScreen extends React.PureComponent<ISearchScreenProps, ISearchScreen
|
||||||
if (isLoadingResults) {
|
if (isLoadingResults) {
|
||||||
rightSearchBarElement = <Spinner size={Spinner.SIZE_SMALL} />;
|
rightSearchBarElement = <Spinner size={Spinner.SIZE_SMALL} />;
|
||||||
} else if (query || error) {
|
} else if (query || error) {
|
||||||
rightSearchBarElement = (
|
rightSearchBarElement = <Button minimal icon={IconNames.CROSS} onClick={this.clearQuery} aria-label="Search" />;
|
||||||
<Button minimal={true} icon={IconNames.CROSS} onClick={this.clearQuery} aria-label="Search" />
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
rightSearchBarElement = (
|
rightSearchBarElement = (
|
||||||
<Button
|
<Button
|
||||||
minimal={true}
|
minimal
|
||||||
icon={IconNames.ARROW_RIGHT}
|
icon={IconNames.ARROW_RIGHT}
|
||||||
intent={Intent.PRIMARY}
|
intent={Intent.PRIMARY}
|
||||||
onClick={this.search}
|
onClick={this.search}
|
||||||
|
@ -127,7 +125,7 @@ class SearchScreen extends React.PureComponent<ISearchScreenProps, ISearchScreen
|
||||||
<InputGroup
|
<InputGroup
|
||||||
leftIcon={IconNames.SEARCH}
|
leftIcon={IconNames.SEARCH}
|
||||||
rightElement={rightSearchBarElement}
|
rightElement={rightSearchBarElement}
|
||||||
large={true}
|
large
|
||||||
placeholder="Search instance names and descriptions"
|
placeholder="Search instance names and descriptions"
|
||||||
aria-label="Search instance names and descriptions"
|
aria-label="Search instance names and descriptions"
|
||||||
type="search"
|
type="search"
|
||||||
|
@ -164,10 +162,10 @@ class SearchScreen extends React.PureComponent<ISearchScreenProps, ISearchScreen
|
||||||
this.setState({ currentQuery: "" }, () => this.props.handleSearch("", []));
|
this.setState({ currentQuery: "" }, () => this.props.handleSearch("", []));
|
||||||
};
|
};
|
||||||
|
|
||||||
private selectSearchFilter = (filter: ISearchFilter) => {
|
private selectSearchFilter = (filter: SearchFilter) => {
|
||||||
const { searchFilters } = this.state;
|
const { searchFilters } = this.state;
|
||||||
// Don't add the same filters twice
|
// Don't add the same filters twice
|
||||||
if (searchFilters.some(sf => isEqual(sf, filter))) {
|
if (searchFilters.some((sf) => isEqual(sf, filter))) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.setState({ searchFilters: [...searchFilters, filter] }, this.search);
|
this.setState({ searchFilters: [...searchFilters, filter] }, this.search);
|
||||||
|
@ -176,9 +174,9 @@ class SearchScreen extends React.PureComponent<ISearchScreenProps, ISearchScreen
|
||||||
private deselectSearchFilter = (e: MouseEvent<HTMLButtonElement>) => {
|
private deselectSearchFilter = (e: MouseEvent<HTMLButtonElement>) => {
|
||||||
const { searchFilters } = this.state;
|
const { searchFilters } = this.state;
|
||||||
const displayValueToRemove = get(e, "currentTarget.parentElement.innerText", "");
|
const displayValueToRemove = get(e, "currentTarget.parentElement.innerText", "");
|
||||||
if (!!displayValueToRemove) {
|
if (displayValueToRemove) {
|
||||||
this.setState(
|
this.setState(
|
||||||
{ searchFilters: searchFilters.filter(sf => sf.displayValue !== displayValueToRemove) },
|
{ searchFilters: searchFilters.filter((sf) => sf.displayValue !== displayValueToRemove) },
|
||||||
this.search
|
this.search
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -207,19 +205,16 @@ class SearchScreen extends React.PureComponent<ISearchScreenProps, ISearchScreen
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapStateToProps = (state: IAppState) => ({
|
const mapStateToProps = (state: AppState) => ({
|
||||||
error: state.search.error,
|
error: state.search.error,
|
||||||
hasMoreResults: !!state.search.next,
|
hasMoreResults: !!state.search.next,
|
||||||
isLoadingResults: state.search.isLoadingResults,
|
isLoadingResults: state.search.isLoadingResults,
|
||||||
query: state.search.query,
|
query: state.search.query,
|
||||||
results: state.search.results
|
results: state.search.results,
|
||||||
});
|
});
|
||||||
const mapDispatchToProps = (dispatch: Dispatch) => ({
|
const mapDispatchToProps = (dispatch: Dispatch) => ({
|
||||||
handleSearch: (query: string, filters: ISearchFilter[]) => dispatch(updateSearch(query, filters) as any),
|
handleSearch: (query: string, filters: SearchFilter[]) => dispatch(updateSearch(query, filters) as any),
|
||||||
navigateToInstance: (domain: string) => dispatch(push(`/instance/${domain}`)),
|
navigateToInstance: (domain: string) => dispatch(push(`/instance/${domain}`)),
|
||||||
setIsHoveringOver: (domain?: string) => dispatch(setResultHover(domain))
|
setIsHoveringOver: (domain?: string) => dispatch(setResultHover(domain)),
|
||||||
});
|
});
|
||||||
export default connect(
|
export default connect(mapStateToProps, mapDispatchToProps)(SearchScreen);
|
||||||
mapStateToProps,
|
|
||||||
mapDispatchToProps
|
|
||||||
)(SearchScreen);
|
|
||||||
|
|
|
@ -6,8 +6,8 @@ import { InstanceTable } from "../organisms";
|
||||||
class TableScreen extends React.PureComponent {
|
class TableScreen extends React.PureComponent {
|
||||||
public render() {
|
public render() {
|
||||||
return (
|
return (
|
||||||
<Page fullWidth={true}>
|
<Page fullWidth>
|
||||||
<H1>{"Instances"}</H1>
|
<H1>Instances</H1>
|
||||||
<InstanceTable />
|
<InstanceTable />
|
||||||
</Page>
|
</Page>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,20 +1,20 @@
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { Redirect } from "react-router";
|
import { Redirect } from "react-router";
|
||||||
import { IAppState } from "../../redux/types";
|
import { AppState } from "../../redux/types";
|
||||||
import { setAuthToken } from "../../util";
|
import { setAuthToken } from "../../util";
|
||||||
import { Page } from "../atoms";
|
import { Page } from "../atoms";
|
||||||
|
|
||||||
interface IVerifyLoginScreenProps {
|
interface VerifyLoginScreenProps {
|
||||||
search: string;
|
search: string;
|
||||||
}
|
}
|
||||||
const VerifyLoginScreen: React.FC<IVerifyLoginScreenProps> = ({ search }) => {
|
const VerifyLoginScreen: React.FC<VerifyLoginScreenProps> = ({ search }) => {
|
||||||
const [didSaveToken, setDidSaveToken] = useState(false);
|
const [didSaveToken, setDidSaveToken] = useState(false);
|
||||||
const token = new URLSearchParams(search).get("token");
|
const token = new URLSearchParams(search).get("token");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Save the auth token
|
// Save the auth token
|
||||||
if (!!token) {
|
if (token) {
|
||||||
setAuthToken(token);
|
setAuthToken(token);
|
||||||
setDidSaveToken(true);
|
setDidSaveToken(true);
|
||||||
}
|
}
|
||||||
|
@ -22,15 +22,14 @@ const VerifyLoginScreen: React.FC<IVerifyLoginScreenProps> = ({ search }) => {
|
||||||
|
|
||||||
if (!token) {
|
if (!token) {
|
||||||
return <Redirect to="/admin/login" />;
|
return <Redirect to="/admin/login" />;
|
||||||
} else if (!didSaveToken) {
|
}
|
||||||
|
if (!didSaveToken) {
|
||||||
return <Page />;
|
return <Page />;
|
||||||
}
|
}
|
||||||
return <Redirect to="/admin" />;
|
return <Redirect to="/admin" />;
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapStateToProps = (state: IAppState) => {
|
const mapStateToProps = (state: AppState) => ({
|
||||||
return {
|
search: state.router.location.search,
|
||||||
search: state.router.location.search
|
});
|
||||||
};
|
|
||||||
};
|
|
||||||
export default connect(mapStateToProps)(VerifyLoginScreen);
|
export default connect(mapStateToProps)(VerifyLoginScreen);
|
||||||
|
|
|
@ -21,7 +21,7 @@ export const QUALITATIVE_COLOR_SCHEME = [
|
||||||
"#0E5A8A",
|
"#0E5A8A",
|
||||||
"#0A6640",
|
"#0A6640",
|
||||||
"#A66321",
|
"#A66321",
|
||||||
"#A82A2A"
|
"#A82A2A",
|
||||||
];
|
];
|
||||||
|
|
||||||
// From https://blueprintjs.com/docs/#core/colors.sequential-color-schemes
|
// From https://blueprintjs.com/docs/#core/colors.sequential-color-schemes
|
||||||
|
@ -35,11 +35,11 @@ export const QUANTITATIVE_COLOR_SCHEME = [
|
||||||
"#C15B3F",
|
"#C15B3F",
|
||||||
"#B64C2F",
|
"#B64C2F",
|
||||||
"#AA3C1F",
|
"#AA3C1F",
|
||||||
"#9E2B0E"
|
"#9E2B0E",
|
||||||
];
|
];
|
||||||
|
|
||||||
export const INSTANCE_DOMAIN_PATH = "/instance/:domain";
|
export const INSTANCE_DOMAIN_PATH = "/instance/:domain";
|
||||||
export interface IInstanceDomainPath {
|
export interface InstanceDomainPath {
|
||||||
domain: string;
|
domain: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -55,5 +55,5 @@ export const INSTANCE_TYPES = [
|
||||||
"friendica",
|
"friendica",
|
||||||
"hubzilla",
|
"hubzilla",
|
||||||
"plume",
|
"plume",
|
||||||
"wordpress"
|
"wordpress",
|
||||||
];
|
];
|
||||||
|
|
|
@ -25,8 +25,7 @@ FocusStyleManager.onlyShowFocusOnTabs();
|
||||||
export const history = createBrowserHistory();
|
export const history = createBrowserHistory();
|
||||||
|
|
||||||
// Initialize redux
|
// Initialize redux
|
||||||
// @ts-ignore
|
const composeEnhancers = (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
|
||||||
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
|
|
||||||
const store = createStore(
|
const store = createStore(
|
||||||
createRootReducer(history),
|
createRootReducer(history),
|
||||||
composeEnhancers(applyMiddleware(routerMiddleware(history), thunk))
|
composeEnhancers(applyMiddleware(routerMiddleware(history), thunk))
|
||||||
|
|
2
frontend/src/react-app-env.d.ts
vendored
2
frontend/src/react-app-env.d.ts
vendored
|
@ -1 +1 @@
|
||||||
/// <reference types="react-scripts" />
|
// / <reference types="react-scripts" />
|
||||||
|
|
|
@ -2,171 +2,145 @@ import { isEqual } from "lodash";
|
||||||
import { Dispatch } from "redux";
|
import { Dispatch } from "redux";
|
||||||
|
|
||||||
import { push } from "connected-react-router";
|
import { push } from "connected-react-router";
|
||||||
import { ISearchFilter } from "../searchFilters";
|
import { SearchFilter } from "../searchFilters";
|
||||||
import { getFromApi } from "../util";
|
import { getFromApi } from "../util";
|
||||||
import { ActionType, IAppState, IGraph, IInstanceDetails, IInstanceSort, ISearchResponse } from "./types";
|
import { ActionType, AppState, Graph, InstanceDetails, InstanceSort, SearchResponse } from "./types";
|
||||||
|
|
||||||
// Instance details
|
// Instance details
|
||||||
const requestInstanceDetails = (instanceName: string) => {
|
const requestInstanceDetails = (instanceName: string) => ({
|
||||||
return {
|
payload: instanceName,
|
||||||
payload: instanceName,
|
type: ActionType.REQUEST_INSTANCE_DETAILS,
|
||||||
type: ActionType.REQUEST_INSTANCE_DETAILS
|
});
|
||||||
};
|
const receiveInstanceDetails = (instanceDetails: InstanceDetails) => ({
|
||||||
};
|
payload: instanceDetails,
|
||||||
const receiveInstanceDetails = (instanceDetails: IInstanceDetails) => {
|
type: ActionType.RECEIVE_INSTANCE_DETAILS,
|
||||||
return {
|
});
|
||||||
payload: instanceDetails,
|
const instanceLoadFailed = () => ({
|
||||||
type: ActionType.RECEIVE_INSTANCE_DETAILS
|
type: ActionType.INSTANCE_LOAD_ERROR,
|
||||||
};
|
});
|
||||||
};
|
const deselectInstance = () => ({
|
||||||
const instanceLoadFailed = () => {
|
type: ActionType.DESELECT_INSTANCE,
|
||||||
return {
|
});
|
||||||
type: ActionType.INSTANCE_LOAD_ERROR
|
|
||||||
};
|
|
||||||
};
|
|
||||||
const deselectInstance = () => {
|
|
||||||
return {
|
|
||||||
type: ActionType.DESELECT_INSTANCE
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
// Graph
|
// Graph
|
||||||
const requestGraph = () => {
|
const requestGraph = () => ({
|
||||||
return {
|
type: ActionType.REQUEST_GRAPH,
|
||||||
type: ActionType.REQUEST_GRAPH
|
});
|
||||||
};
|
const receiveGraph = (graph: Graph) => ({
|
||||||
};
|
payload: graph,
|
||||||
const receiveGraph = (graph: IGraph) => {
|
type: ActionType.RECEIVE_GRAPH,
|
||||||
return {
|
});
|
||||||
payload: graph,
|
const graphLoadFailed = () => ({
|
||||||
type: ActionType.RECEIVE_GRAPH
|
type: ActionType.GRAPH_LOAD_ERROR,
|
||||||
};
|
});
|
||||||
};
|
|
||||||
const graphLoadFailed = () => {
|
|
||||||
return {
|
|
||||||
type: ActionType.GRAPH_LOAD_ERROR
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
// Instance list
|
// Instance list
|
||||||
const requestInstanceList = (sort?: IInstanceSort) => ({
|
const requestInstanceList = (sort?: InstanceSort) => ({
|
||||||
payload: sort,
|
payload: sort,
|
||||||
type: ActionType.REQUEST_INSTANCES
|
type: ActionType.REQUEST_INSTANCES,
|
||||||
});
|
});
|
||||||
const receiveInstanceList = (instances: IInstanceDetails[]) => ({
|
const receiveInstanceList = (instances: InstanceDetails[]) => ({
|
||||||
payload: instances,
|
payload: instances,
|
||||||
type: ActionType.RECEIVE_INSTANCES
|
type: ActionType.RECEIVE_INSTANCES,
|
||||||
});
|
});
|
||||||
const instanceListLoadFailed = () => ({
|
const instanceListLoadFailed = () => ({
|
||||||
type: ActionType.INSTANCE_LIST_LOAD_ERROR
|
type: ActionType.INSTANCE_LIST_LOAD_ERROR,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Search
|
// Search
|
||||||
const requestSearchResult = (query: string, filters: ISearchFilter[]) => {
|
const requestSearchResult = (query: string, filters: SearchFilter[]) => ({
|
||||||
return {
|
payload: { query, filters },
|
||||||
payload: { query, filters },
|
type: ActionType.REQUEST_SEARCH_RESULTS,
|
||||||
type: ActionType.REQUEST_SEARCH_RESULTS
|
});
|
||||||
};
|
const receiveSearchResults = (result: SearchResponse) => ({
|
||||||
};
|
payload: result,
|
||||||
const receiveSearchResults = (result: ISearchResponse) => {
|
type: ActionType.RECEIVE_SEARCH_RESULTS,
|
||||||
return {
|
});
|
||||||
payload: result,
|
const searchFailed = () => ({
|
||||||
type: ActionType.RECEIVE_SEARCH_RESULTS
|
type: ActionType.SEARCH_RESULTS_ERROR,
|
||||||
};
|
});
|
||||||
};
|
|
||||||
const searchFailed = () => {
|
|
||||||
return {
|
|
||||||
type: ActionType.SEARCH_RESULTS_ERROR
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const resetSearch = () => {
|
const resetSearch = () => ({
|
||||||
return {
|
type: ActionType.RESET_SEARCH,
|
||||||
type: ActionType.RESET_SEARCH
|
});
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const setResultHover = (domain?: string) => {
|
export const setResultHover = (domain?: string) => ({
|
||||||
return {
|
payload: domain,
|
||||||
payload: domain,
|
type: ActionType.SET_SEARCH_RESULT_HOVER,
|
||||||
type: ActionType.SET_SEARCH_RESULT_HOVER
|
});
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
/** Async actions: https://redux.js.org/advanced/asyncactions */
|
/** Async actions: https://redux.js.org/advanced/asyncactions */
|
||||||
|
|
||||||
export const loadInstance = (instanceName: string | null) => {
|
export const loadInstance = (instanceName: string | null) => (dispatch: Dispatch, getState: () => AppState) => {
|
||||||
return (dispatch: Dispatch, getState: () => IAppState) => {
|
if (!instanceName) {
|
||||||
if (!instanceName) {
|
dispatch(deselectInstance());
|
||||||
dispatch(deselectInstance());
|
if (getState().router.location.pathname.startsWith("/instance/")) {
|
||||||
if (getState().router.location.pathname.startsWith("/instance/")) {
|
dispatch(push("/"));
|
||||||
dispatch(push("/"));
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
dispatch(requestInstanceDetails(instanceName));
|
return;
|
||||||
return getFromApi("instances/" + instanceName)
|
}
|
||||||
.then(details => dispatch(receiveInstanceDetails(details)))
|
dispatch(requestInstanceDetails(instanceName));
|
||||||
.catch(() => dispatch(instanceLoadFailed()));
|
return getFromApi(`instances/${instanceName}`)
|
||||||
};
|
.then((details) => dispatch(receiveInstanceDetails(details)))
|
||||||
|
.catch(() => dispatch(instanceLoadFailed()));
|
||||||
};
|
};
|
||||||
|
|
||||||
export const updateSearch = (query: string, filters: ISearchFilter[]) => {
|
export const updateSearch = (query: string, filters: SearchFilter[]) => (
|
||||||
return (dispatch: Dispatch, getState: () => IAppState) => {
|
dispatch: Dispatch,
|
||||||
query = query.trim();
|
getState: () => AppState
|
||||||
|
) => {
|
||||||
|
query = query.trim();
|
||||||
|
|
||||||
if (!query) {
|
if (!query) {
|
||||||
dispatch(resetSearch());
|
dispatch(resetSearch());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const prevQuery = getState().search.query;
|
const prevQuery = getState().search.query;
|
||||||
const prevFilters = getState().search.filters;
|
const prevFilters = getState().search.filters;
|
||||||
const isNewQuery = prevQuery !== query || !isEqual(prevFilters, filters);
|
const isNewQuery = prevQuery !== query || !isEqual(prevFilters, filters);
|
||||||
|
|
||||||
const next = getState().search.next;
|
const { next } = getState().search;
|
||||||
let url = `search/?query=${query}`;
|
let url = `search/?query=${query}`;
|
||||||
if (!isNewQuery && next) {
|
if (!isNewQuery && next) {
|
||||||
url += `&after=${next}`;
|
url += `&after=${next}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add filters
|
// Add filters
|
||||||
// The format is e.g. type_eq=mastodon or user_count_gt=1000
|
// The format is e.g. type_eq=mastodon or user_count_gt=1000
|
||||||
filters.forEach(filter => {
|
filters.forEach((filter) => {
|
||||||
url += `&${filter.field}_${filter.relation}=${filter.value}`;
|
url += `&${filter.field}_${filter.relation}=${filter.value}`;
|
||||||
});
|
});
|
||||||
|
|
||||||
dispatch(requestSearchResult(query, filters));
|
dispatch(requestSearchResult(query, filters));
|
||||||
return getFromApi(url)
|
return getFromApi(url)
|
||||||
.then(result => dispatch(receiveSearchResults(result)))
|
.then((result) => dispatch(receiveSearchResults(result)))
|
||||||
.catch(() => dispatch(searchFailed()));
|
.catch(() => dispatch(searchFailed()));
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fetchGraph = () => {
|
export const fetchGraph = () => (dispatch: Dispatch) => {
|
||||||
return (dispatch: Dispatch) => {
|
dispatch(requestGraph());
|
||||||
dispatch(requestGraph());
|
return getFromApi("graph")
|
||||||
return getFromApi("graph")
|
.then((graph) => dispatch(receiveGraph(graph)))
|
||||||
.then(graph => dispatch(receiveGraph(graph)))
|
.catch(() => dispatch(graphLoadFailed()));
|
||||||
.catch(() => dispatch(graphLoadFailed()));
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const loadInstanceList = (page?: number, sort?: IInstanceSort) => {
|
export const loadInstanceList = (page?: number, sort?: InstanceSort) => (
|
||||||
return (dispatch: Dispatch, getState: () => IAppState) => {
|
dispatch: Dispatch,
|
||||||
sort = sort ? sort : getState().data.instanceListSort;
|
getState: () => AppState
|
||||||
dispatch(requestInstanceList(sort));
|
) => {
|
||||||
const params: string[] = [];
|
sort = sort || getState().data.instanceListSort;
|
||||||
if (!!page) {
|
dispatch(requestInstanceList(sort));
|
||||||
params.push(`page=${page}`);
|
const params: string[] = [];
|
||||||
}
|
if (page) {
|
||||||
if (!!sort) {
|
params.push(`page=${page}`);
|
||||||
params.push(`sortField=${sort.field}`);
|
}
|
||||||
params.push(`sortDirection=${sort.direction}`);
|
if (sort) {
|
||||||
}
|
params.push(`sortField=${sort.field}`);
|
||||||
const path = !!params ? `instances?${params.join("&")}` : "instances";
|
params.push(`sortDirection=${sort.direction}`);
|
||||||
return getFromApi(path)
|
}
|
||||||
.then(instancesListResponse => dispatch(receiveInstanceList(instancesListResponse)))
|
const path = params ? `instances?${params.join("&")}` : "instances";
|
||||||
.catch(() => dispatch(instanceListLoadFailed()));
|
return getFromApi(path)
|
||||||
};
|
.then((instancesListResponse) => dispatch(receiveInstanceList(instancesListResponse)))
|
||||||
|
.catch(() => dispatch(instanceListLoadFailed()));
|
||||||
};
|
};
|
||||||
|
|
|
@ -3,34 +3,34 @@ import { isEqual } from "lodash";
|
||||||
import { combineReducers } from "redux";
|
import { combineReducers } from "redux";
|
||||||
|
|
||||||
import { History } from "history";
|
import { History } from "history";
|
||||||
import { ActionType, IAction, ICurrentInstanceState, IDataState, ISearchState } from "./types";
|
import { ActionType, Action, CurrentInstanceState, DataState, SearchState } from "./types";
|
||||||
|
|
||||||
const initialDataState: IDataState = {
|
const initialDataState: DataState = {
|
||||||
graphLoadError: false,
|
graphLoadError: false,
|
||||||
instanceListLoadError: false,
|
instanceListLoadError: false,
|
||||||
instanceListSort: { field: "userCount", direction: "desc" },
|
instanceListSort: { field: "userCount", direction: "desc" },
|
||||||
isLoadingGraph: false,
|
isLoadingGraph: false,
|
||||||
isLoadingInstanceList: false
|
isLoadingInstanceList: false,
|
||||||
};
|
};
|
||||||
const data = (state: IDataState = initialDataState, action: IAction): IDataState => {
|
const data = (state: DataState = initialDataState, action: Action): DataState => {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case ActionType.REQUEST_GRAPH:
|
case ActionType.REQUEST_GRAPH:
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
graphResponse: undefined,
|
graphResponse: undefined,
|
||||||
isLoadingGraph: true
|
isLoadingGraph: true,
|
||||||
};
|
};
|
||||||
case ActionType.RECEIVE_GRAPH:
|
case ActionType.RECEIVE_GRAPH:
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
graphResponse: action.payload,
|
graphResponse: action.payload,
|
||||||
isLoadingGraph: false
|
isLoadingGraph: false,
|
||||||
};
|
};
|
||||||
case ActionType.GRAPH_LOAD_ERROR:
|
case ActionType.GRAPH_LOAD_ERROR:
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
graphLoadError: true,
|
graphLoadError: true,
|
||||||
isLoadingGraph: false
|
isLoadingGraph: false,
|
||||||
};
|
};
|
||||||
case ActionType.REQUEST_INSTANCES:
|
case ActionType.REQUEST_INSTANCES:
|
||||||
return {
|
return {
|
||||||
|
@ -38,71 +38,71 @@ const data = (state: IDataState = initialDataState, action: IAction): IDataState
|
||||||
instanceListLoadError: false,
|
instanceListLoadError: false,
|
||||||
instanceListSort: action.payload,
|
instanceListSort: action.payload,
|
||||||
instancesResponse: undefined,
|
instancesResponse: undefined,
|
||||||
isLoadingInstanceList: true
|
isLoadingInstanceList: true,
|
||||||
};
|
};
|
||||||
case ActionType.RECEIVE_INSTANCES:
|
case ActionType.RECEIVE_INSTANCES:
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
instancesResponse: action.payload,
|
instancesResponse: action.payload,
|
||||||
isLoadingInstanceList: false
|
isLoadingInstanceList: false,
|
||||||
};
|
};
|
||||||
case ActionType.INSTANCE_LIST_LOAD_ERROR:
|
case ActionType.INSTANCE_LIST_LOAD_ERROR:
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
instanceListLoadError: true,
|
instanceListLoadError: true,
|
||||||
isLoadingInstanceList: false
|
isLoadingInstanceList: false,
|
||||||
};
|
};
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const initialCurrentInstanceState: ICurrentInstanceState = {
|
const initialCurrentInstanceState: CurrentInstanceState = {
|
||||||
currentInstanceDetails: null,
|
currentInstanceDetails: null,
|
||||||
error: false,
|
error: false,
|
||||||
isLoadingInstanceDetails: false
|
isLoadingInstanceDetails: false,
|
||||||
};
|
};
|
||||||
const currentInstance = (state = initialCurrentInstanceState, action: IAction): ICurrentInstanceState => {
|
const currentInstance = (state = initialCurrentInstanceState, action: Action): CurrentInstanceState => {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case ActionType.REQUEST_INSTANCE_DETAILS:
|
case ActionType.REQUEST_INSTANCE_DETAILS:
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
error: false,
|
error: false,
|
||||||
isLoadingInstanceDetails: true
|
isLoadingInstanceDetails: true,
|
||||||
};
|
};
|
||||||
case ActionType.RECEIVE_INSTANCE_DETAILS:
|
case ActionType.RECEIVE_INSTANCE_DETAILS:
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
currentInstanceDetails: action.payload,
|
currentInstanceDetails: action.payload,
|
||||||
error: false,
|
error: false,
|
||||||
isLoadingInstanceDetails: false
|
isLoadingInstanceDetails: false,
|
||||||
};
|
};
|
||||||
case ActionType.DESELECT_INSTANCE:
|
case ActionType.DESELECT_INSTANCE:
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
currentInstanceDetails: null,
|
currentInstanceDetails: null,
|
||||||
error: false
|
error: false,
|
||||||
};
|
};
|
||||||
case ActionType.INSTANCE_LOAD_ERROR:
|
case ActionType.INSTANCE_LOAD_ERROR:
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
error: true,
|
error: true,
|
||||||
isLoadingInstanceDetails: false
|
isLoadingInstanceDetails: false,
|
||||||
};
|
};
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const initialSearchState: ISearchState = {
|
const initialSearchState: SearchState = {
|
||||||
error: false,
|
error: false,
|
||||||
filters: [],
|
filters: [],
|
||||||
isLoadingResults: false,
|
isLoadingResults: false,
|
||||||
next: "",
|
next: "",
|
||||||
query: "",
|
query: "",
|
||||||
results: []
|
results: [],
|
||||||
};
|
};
|
||||||
const search = (state = initialSearchState, action: IAction): ISearchState => {
|
const search = (state = initialSearchState, action: Action): SearchState => {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case ActionType.REQUEST_SEARCH_RESULTS:
|
case ActionType.REQUEST_SEARCH_RESULTS:
|
||||||
const { query, filters } = action.payload;
|
const { query, filters } = action.payload;
|
||||||
|
@ -114,7 +114,7 @@ const search = (state = initialSearchState, action: IAction): ISearchState => {
|
||||||
isLoadingResults: true,
|
isLoadingResults: true,
|
||||||
next: isNewQuery ? "" : state.next,
|
next: isNewQuery ? "" : state.next,
|
||||||
query,
|
query,
|
||||||
results: isNewQuery ? [] : state.results
|
results: isNewQuery ? [] : state.results,
|
||||||
};
|
};
|
||||||
case ActionType.RECEIVE_SEARCH_RESULTS:
|
case ActionType.RECEIVE_SEARCH_RESULTS:
|
||||||
return {
|
return {
|
||||||
|
@ -122,7 +122,7 @@ const search = (state = initialSearchState, action: IAction): ISearchState => {
|
||||||
error: false,
|
error: false,
|
||||||
isLoadingResults: false,
|
isLoadingResults: false,
|
||||||
next: action.payload.next,
|
next: action.payload.next,
|
||||||
results: state.results.concat(action.payload.results)
|
results: state.results.concat(action.payload.results),
|
||||||
};
|
};
|
||||||
case ActionType.SEARCH_RESULTS_ERROR:
|
case ActionType.SEARCH_RESULTS_ERROR:
|
||||||
return { ...initialSearchState, error: true };
|
return { ...initialSearchState, error: true };
|
||||||
|
@ -131,7 +131,7 @@ const search = (state = initialSearchState, action: IAction): ISearchState => {
|
||||||
case ActionType.SET_SEARCH_RESULT_HOVER:
|
case ActionType.SET_SEARCH_RESULT_HOVER:
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
hoveringOverResult: action.payload
|
hoveringOverResult: action.payload,
|
||||||
};
|
};
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
|
@ -141,8 +141,7 @@ const search = (state = initialSearchState, action: IAction): ISearchState => {
|
||||||
export default (history: History) =>
|
export default (history: History) =>
|
||||||
combineReducers({
|
combineReducers({
|
||||||
router: connectRouter(history),
|
router: connectRouter(history),
|
||||||
// tslint:disable-next-line:object-literal-sort-keys
|
|
||||||
currentInstance,
|
currentInstance,
|
||||||
data,
|
data,
|
||||||
search
|
search,
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { RouterState } from "connected-react-router";
|
import { RouterState } from "connected-react-router";
|
||||||
import { ISearchFilter } from "../searchFilters";
|
import { SearchFilter } from "../searchFilters";
|
||||||
|
|
||||||
export enum ActionType {
|
export enum ActionType {
|
||||||
// Instance details
|
// Instance details
|
||||||
|
@ -22,33 +22,33 @@ export enum ActionType {
|
||||||
SEARCH_RESULTS_ERROR = "SEARCH_RESULTS_ERROR",
|
SEARCH_RESULTS_ERROR = "SEARCH_RESULTS_ERROR",
|
||||||
RESET_SEARCH = "RESET_SEARCH",
|
RESET_SEARCH = "RESET_SEARCH",
|
||||||
// Search -- hovering over results
|
// Search -- hovering over results
|
||||||
SET_SEARCH_RESULT_HOVER = "SET_SEARCH_RESULT_HOVER"
|
SET_SEARCH_RESULT_HOVER = "SET_SEARCH_RESULT_HOVER",
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IAction {
|
export interface Action {
|
||||||
type: ActionType;
|
type: ActionType;
|
||||||
payload: any;
|
payload: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SortField = "domain" | "userCount" | "statusCount" | "insularity";
|
export type SortField = "domain" | "userCount" | "statusCount" | "insularity";
|
||||||
export type SortDirection = "asc" | "desc";
|
export type SortDirection = "asc" | "desc";
|
||||||
export interface IInstanceSort {
|
export interface InstanceSort {
|
||||||
field: SortField;
|
field: SortField;
|
||||||
direction: SortDirection;
|
direction: SortDirection;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IPeer {
|
export interface Peer {
|
||||||
name: string;
|
name: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ISearchResultInstance {
|
export interface SearchResultInstance {
|
||||||
name: string;
|
name: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
userCount?: number;
|
userCount?: number;
|
||||||
type?: string;
|
type?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IFederationRestrictions {
|
export interface FederationRestrictions {
|
||||||
reportRemoval?: string[];
|
reportRemoval?: string[];
|
||||||
reject?: string[];
|
reject?: string[];
|
||||||
mediaRemoval?: string[];
|
mediaRemoval?: string[];
|
||||||
|
@ -59,7 +59,7 @@ export interface IFederationRestrictions {
|
||||||
accept?: string[];
|
accept?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IInstanceDetails {
|
export interface InstanceDetails {
|
||||||
name: string;
|
name: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
version?: string;
|
version?: string;
|
||||||
|
@ -67,8 +67,8 @@ export interface IInstanceDetails {
|
||||||
insularity?: number;
|
insularity?: number;
|
||||||
statusCount?: number;
|
statusCount?: number;
|
||||||
domainCount?: number;
|
domainCount?: number;
|
||||||
peers?: IPeer[];
|
peers?: Peer[];
|
||||||
federationRestrictions: IFederationRestrictions;
|
federationRestrictions: FederationRestrictions;
|
||||||
lastUpdated?: string;
|
lastUpdated?: string;
|
||||||
status: string;
|
status: string;
|
||||||
type?: string;
|
type?: string;
|
||||||
|
@ -76,7 +76,7 @@ export interface IInstanceDetails {
|
||||||
statusesPerUserPerDay?: number;
|
statusesPerUserPerDay?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IGraphNode {
|
interface GraphNode {
|
||||||
data: {
|
data: {
|
||||||
id: string;
|
id: string;
|
||||||
label: string;
|
label: string;
|
||||||
|
@ -88,7 +88,7 @@ interface IGraphNode {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IGraphEdge {
|
interface GraphEdge {
|
||||||
data: {
|
data: {
|
||||||
source: string;
|
source: string;
|
||||||
target: string;
|
target: string;
|
||||||
|
@ -97,65 +97,65 @@ interface IGraphEdge {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IGraphMetadata {
|
interface GraphMetadata {
|
||||||
ranges: { [key: string]: [number, number] };
|
ranges: { [key: string]: [number, number] };
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IGraph {
|
export interface Graph {
|
||||||
nodes: IGraphNode[];
|
nodes: GraphNode[];
|
||||||
edges: IGraphEdge[];
|
edges: GraphEdge[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IGraphResponse {
|
export interface GraphResponse {
|
||||||
graph: IGraph;
|
graph: Graph;
|
||||||
metadata: IGraphMetadata;
|
metadata: GraphMetadata;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ISearchResponse {
|
export interface SearchResponse {
|
||||||
results: ISearchResultInstance[];
|
results: SearchResultInstance[];
|
||||||
next: string | null;
|
next: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IInstanceListResponse {
|
export interface InstanceListResponse {
|
||||||
pageNumber: number;
|
pageNumber: number;
|
||||||
totalPages: number;
|
totalPages: number;
|
||||||
totalEntries: number;
|
totalEntries: number;
|
||||||
pageSize: number;
|
pageSize: number;
|
||||||
instances: IInstanceDetails[];
|
instances: InstanceDetails[];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Redux state
|
// Redux state
|
||||||
|
|
||||||
// The current instance name is stored in the URL. See state -> router -> location
|
// The current instance name is stored in the URL. See state -> router -> location
|
||||||
export interface ICurrentInstanceState {
|
export interface CurrentInstanceState {
|
||||||
currentInstanceDetails: IInstanceDetails | null;
|
currentInstanceDetails: InstanceDetails | null;
|
||||||
isLoadingInstanceDetails: boolean;
|
isLoadingInstanceDetails: boolean;
|
||||||
error: boolean;
|
error: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IDataState {
|
export interface DataState {
|
||||||
graphResponse?: IGraphResponse;
|
graphResponse?: GraphResponse;
|
||||||
instancesResponse?: IInstanceListResponse;
|
instancesResponse?: InstanceListResponse;
|
||||||
instanceListSort: IInstanceSort;
|
instanceListSort: InstanceSort;
|
||||||
isLoadingGraph: boolean;
|
isLoadingGraph: boolean;
|
||||||
isLoadingInstanceList: boolean;
|
isLoadingInstanceList: boolean;
|
||||||
graphLoadError: boolean;
|
graphLoadError: boolean;
|
||||||
instanceListLoadError: boolean;
|
instanceListLoadError: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ISearchState {
|
export interface SearchState {
|
||||||
error: boolean;
|
error: boolean;
|
||||||
isLoadingResults: boolean;
|
isLoadingResults: boolean;
|
||||||
next: string;
|
next: string;
|
||||||
query: string;
|
query: string;
|
||||||
results: ISearchResultInstance[];
|
results: SearchResultInstance[];
|
||||||
filters: ISearchFilter[];
|
filters: SearchFilter[];
|
||||||
hoveringOverResult?: string;
|
hoveringOverResult?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IAppState {
|
export interface AppState {
|
||||||
router: RouterState;
|
router: RouterState;
|
||||||
currentInstance: ICurrentInstanceState;
|
currentInstance: CurrentInstanceState;
|
||||||
data: IDataState;
|
data: DataState;
|
||||||
search: ISearchState;
|
search: SearchState;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
type ISearchFilterRelation = "eq" | "gt" | "gte" | "lt" | "lte";
|
type SearchFilterRelation = "eq" | "gt" | "gte" | "lt" | "lte";
|
||||||
export interface ISearchFilter {
|
export interface SearchFilter {
|
||||||
// The ES field to filter on
|
// The ES field to filter on
|
||||||
field: string;
|
field: string;
|
||||||
relation: ISearchFilterRelation;
|
relation: SearchFilterRelation;
|
||||||
// The value we want to filter to
|
// The value we want to filter to
|
||||||
value: string;
|
value: string;
|
||||||
// Human-meaningful text that we're showing in the UI
|
// Human-meaningful text that we're showing in the UI
|
||||||
|
@ -10,17 +10,19 @@ export interface ISearchFilter {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Maps to translate this to user-friendly text
|
// Maps to translate this to user-friendly text
|
||||||
|
type SearchFilterField = "type" | "user_count";
|
||||||
const searchFilterFieldTranslations = {
|
const searchFilterFieldTranslations = {
|
||||||
type: "Instance type",
|
type: "Instance type",
|
||||||
user_count: "User count"
|
// eslint-disable-next-line @typescript-eslint/camelcase
|
||||||
|
user_count: "User count",
|
||||||
};
|
};
|
||||||
const searchFilterRelationTranslations = {
|
const searchFilterRelationTranslations = {
|
||||||
eq: "=",
|
eq: "=",
|
||||||
gt: ">",
|
gt: ">",
|
||||||
gte: ">=",
|
gte: ">=",
|
||||||
lt: "<",
|
lt: "<",
|
||||||
lte: "<="
|
lte: "<=",
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getSearchFilterDisplayValue = (field: string, relation: ISearchFilterRelation, value: string) =>
|
export const getSearchFilterDisplayValue = (field: SearchFilterField, relation: SearchFilterRelation, value: string) =>
|
||||||
`${searchFilterFieldTranslations[field]} ${searchFilterRelationTranslations[relation]} ${value}`;
|
`${searchFilterFieldTranslations[field]} ${searchFilterRelationTranslations[relation]} ${value}`;
|
||||||
|
|
|
@ -2,5 +2,5 @@ import { Position, Toaster } from "@blueprintjs/core";
|
||||||
|
|
||||||
export default Toaster.create({
|
export default Toaster.create({
|
||||||
className: "app-toaster",
|
className: "app-toaster",
|
||||||
position: Position.TOP
|
position: Position.TOP,
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { INSTANCE_TYPES } from "./constants";
|
import { INSTANCE_TYPES } from "./constants";
|
||||||
|
|
||||||
interface IColorSchemeBase {
|
interface ColorSchemeBase {
|
||||||
// The name of the coloring, e.g. "Instance type"
|
// The name of the coloring, e.g. "Instance type"
|
||||||
name: string;
|
name: string;
|
||||||
// The name of the key in a cytoscape node's `data` field to color by.
|
// The name of the key in a cytoscape node's `data` field to color by.
|
||||||
|
@ -9,30 +9,30 @@ interface IColorSchemeBase {
|
||||||
description?: string;
|
description?: string;
|
||||||
type: "qualitative" | "quantitative";
|
type: "qualitative" | "quantitative";
|
||||||
}
|
}
|
||||||
interface IQualitativeColorScheme extends IColorSchemeBase {
|
interface QualitativeColorScheme extends ColorSchemeBase {
|
||||||
// The values the color scheme is used for. E.g. ["mastodon", "pleroma", "misskey"].
|
// The values the color scheme is used for. E.g. ["mastodon", "pleroma", "misskey"].
|
||||||
values: string[];
|
values: string[];
|
||||||
type: "qualitative";
|
type: "qualitative";
|
||||||
}
|
}
|
||||||
interface IQuantitativeColorScheme extends IColorSchemeBase {
|
interface QuantitativeColorScheme extends ColorSchemeBase {
|
||||||
type: "quantitative";
|
type: "quantitative";
|
||||||
exponential: boolean;
|
exponential: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type IColorScheme = IQualitativeColorScheme | IQuantitativeColorScheme;
|
export type ColorScheme = QualitativeColorScheme | QuantitativeColorScheme;
|
||||||
|
|
||||||
export const typeColorScheme: IQualitativeColorScheme = {
|
export const typeColorScheme: QualitativeColorScheme = {
|
||||||
cytoscapeDataKey: "type",
|
cytoscapeDataKey: "type",
|
||||||
name: "Instance type",
|
name: "Instance type",
|
||||||
type: "qualitative",
|
type: "qualitative",
|
||||||
values: INSTANCE_TYPES
|
values: INSTANCE_TYPES,
|
||||||
};
|
};
|
||||||
export const activityColorScheme: IQuantitativeColorScheme = {
|
export const activityColorScheme: QuantitativeColorScheme = {
|
||||||
cytoscapeDataKey: "statusesPerDay",
|
cytoscapeDataKey: "statusesPerDay",
|
||||||
description: "The average number of statuses posted per day. This is an exponential scale.",
|
description: "The average number of statuses posted per day. This is an exponential scale.",
|
||||||
exponential: true,
|
exponential: true,
|
||||||
name: "Activity",
|
name: "Activity",
|
||||||
type: "quantitative"
|
type: "quantitative",
|
||||||
};
|
};
|
||||||
|
|
||||||
export const colorSchemes: IColorScheme[] = [typeColorScheme, activityColorScheme];
|
export const colorSchemes: ColorScheme[] = [typeColorScheme, activityColorScheme];
|
||||||
|
|
1
frontend/src/typings/globals.d.ts
vendored
Normal file
1
frontend/src/typings/globals.d.ts
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
declare module "*.png";
|
|
@ -1,41 +1,39 @@
|
||||||
import { createMatchSelector } from "connected-react-router";
|
import { createMatchSelector } from "connected-react-router";
|
||||||
import fetch from "cross-fetch";
|
import fetch from "cross-fetch";
|
||||||
import { range } from "lodash";
|
import { range } from "lodash";
|
||||||
import { DESKTOP_WIDTH_THRESHOLD, IInstanceDomainPath, INSTANCE_DOMAIN_PATH } from "./constants";
|
import { DESKTOP_WIDTH_THRESHOLD, InstanceDomainPath, INSTANCE_DOMAIN_PATH } from "./constants";
|
||||||
import { IAppState } from "./redux/types";
|
import { AppState } from "./redux/types";
|
||||||
|
|
||||||
let API_ROOT = "http://localhost:4000/api/";
|
let API_ROOT = "http://localhost:4000/api/";
|
||||||
if (["true", true, 1, "1"].indexOf(process.env.REACT_APP_STAGING || "") > -1) {
|
if (["true", true, 1, "1"].includes(process.env.REACT_APP_STAGING || "")) {
|
||||||
API_ROOT = "https://phoenix.api-develop.fediverse.space/api/";
|
API_ROOT = "https://phoenix.api-develop.fediverse.space/api/";
|
||||||
} else if (process.env.NODE_ENV === "production") {
|
} else if (process.env.NODE_ENV === "production") {
|
||||||
API_ROOT = "https://phoenix.api.fediverse.space/api/";
|
API_ROOT = "https://phoenix.api.fediverse.space/api/";
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getFromApi = (path: string, token?: string): Promise<any> => {
|
export const getFromApi = (path: string, token?: string): Promise<any> => {
|
||||||
const domain = API_ROOT.endsWith("/") ? API_ROOT : API_ROOT + "/";
|
const domain = API_ROOT.endsWith("/") ? API_ROOT : `${API_ROOT}/`;
|
||||||
const headers = token ? { token } : undefined;
|
const headers = token ? { token } : undefined;
|
||||||
return fetch(encodeURI(domain + path), {
|
return fetch(encodeURI(domain + path), {
|
||||||
headers
|
headers,
|
||||||
}).then(response => response.json());
|
}).then((response) => response.json());
|
||||||
};
|
};
|
||||||
|
|
||||||
export const postToApi = (path: string, body: any, token?: string): Promise<any> => {
|
export const postToApi = (path: string, body: any, token?: string): Promise<any> => {
|
||||||
const domain = API_ROOT.endsWith("/") ? API_ROOT : API_ROOT + "/";
|
const domain = API_ROOT.endsWith("/") ? API_ROOT : `${API_ROOT}/`;
|
||||||
const defaultHeaders = {
|
const defaultHeaders = {
|
||||||
Accept: "application/json",
|
Accept: "application/json",
|
||||||
"Content-Type": "application/json"
|
"Content-Type": "application/json",
|
||||||
};
|
};
|
||||||
const headers = token ? { ...defaultHeaders, token } : defaultHeaders;
|
const headers = token ? { ...defaultHeaders, token } : defaultHeaders;
|
||||||
return fetch(encodeURI(domain + path), {
|
return fetch(encodeURI(domain + path), {
|
||||||
body: JSON.stringify(body),
|
body: JSON.stringify(body),
|
||||||
headers,
|
headers,
|
||||||
method: "POST"
|
method: "POST",
|
||||||
}).then(response => {
|
}).then((response) => response.json());
|
||||||
return response.json();
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const domainMatchSelector = createMatchSelector<IAppState, IInstanceDomainPath>(INSTANCE_DOMAIN_PATH);
|
export const domainMatchSelector = createMatchSelector<AppState, InstanceDomainPath>(INSTANCE_DOMAIN_PATH);
|
||||||
|
|
||||||
export const isSmallScreen = window.innerWidth < DESKTOP_WIDTH_THRESHOLD;
|
export const isSmallScreen = window.innerWidth < DESKTOP_WIDTH_THRESHOLD;
|
||||||
|
|
||||||
|
@ -49,28 +47,25 @@ export const unsetAuthToken = () => {
|
||||||
sessionStorage.removeItem("adminToken");
|
sessionStorage.removeItem("adminToken");
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getAuthToken = () => {
|
export const getAuthToken = () => sessionStorage.getItem("adminToken");
|
||||||
return sessionStorage.getItem("adminToken");
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getBuckets = (min: number, max: number, steps: number, exponential: boolean) => {
|
export const getBuckets = (min: number, max: number, steps: number, exponential: boolean) => {
|
||||||
if (exponential) {
|
if (exponential) {
|
||||||
const logSpace = range(steps).map(i => Math.E ** i);
|
const logSpace = range(steps).map((i) => Math.E ** i);
|
||||||
// Scale the log space to the linear range
|
// Scale the log space to the linear range
|
||||||
const logRange = logSpace[logSpace.length - 1] - logSpace[0];
|
const logRange = logSpace[logSpace.length - 1] - logSpace[0];
|
||||||
const linearRange = max - min;
|
const linearRange = max - min;
|
||||||
const scalingFactor = linearRange / logRange;
|
const scalingFactor = linearRange / logRange;
|
||||||
const translation = min - logSpace[0];
|
const translation = min - logSpace[0];
|
||||||
return logSpace.map(i => (i + translation) * scalingFactor);
|
return logSpace.map((i) => (i + translation) * scalingFactor);
|
||||||
} else {
|
|
||||||
// Linear
|
|
||||||
const bucketSize = (max - min) / steps;
|
|
||||||
return range(min, max, bucketSize);
|
|
||||||
}
|
}
|
||||||
|
// Linear
|
||||||
|
const bucketSize = (max - min) / steps;
|
||||||
|
return range(min, max, bucketSize);
|
||||||
};
|
};
|
||||||
|
|
||||||
const typeToDisplay = {
|
const typeToDisplay: { [field: string]: string } = {
|
||||||
gnusocial: "GNU Social"
|
gnusocial: "GNU Social",
|
||||||
};
|
};
|
||||||
export const getTypeDisplayString = (key: string) => {
|
export const getTypeDisplayString = (key: string) => {
|
||||||
if (key in typeToDisplay) {
|
if (key in typeToDisplay) {
|
||||||
|
|
|
@ -1,40 +1,32 @@
|
||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"allowJs": true,
|
|
||||||
"allowSyntheticDefaultImports": true,
|
|
||||||
"forceConsistentCasingInFileNames": true,
|
|
||||||
"jsx": "preserve",
|
|
||||||
"lib": [
|
|
||||||
"es2015",
|
|
||||||
"dom"
|
|
||||||
],
|
|
||||||
"module": "esnext",
|
|
||||||
"moduleResolution": "node",
|
|
||||||
"noImplicitAny": true,
|
|
||||||
"noImplicitReturns": true,
|
|
||||||
"noImplicitThis": true,
|
|
||||||
"noUnusedLocals": true,
|
|
||||||
"outDir": "build",
|
|
||||||
"rootDir": "src",
|
|
||||||
"sourceMap": true,
|
|
||||||
"strictNullChecks": true,
|
|
||||||
"suppressImplicitAnyIndexErrors": true,
|
|
||||||
"target": "es5",
|
"target": "es5",
|
||||||
|
"lib": [
|
||||||
|
"dom",
|
||||||
|
"dom.iterable",
|
||||||
|
"esnext"
|
||||||
|
],
|
||||||
|
"allowJs": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"noImplicitAny": true,
|
||||||
|
"noImplicitReturns": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"jsx": "react",
|
||||||
"typeRoots": [
|
"typeRoots": [
|
||||||
"./node_modules/@types",
|
"./node_modules/@types",
|
||||||
"./src/typings"
|
"./src/typings"
|
||||||
],
|
],
|
||||||
"resolveJsonModule": true,
|
|
||||||
"isolatedModules": true,
|
|
||||||
"noEmit": true
|
|
||||||
},
|
},
|
||||||
"exclude": [
|
|
||||||
"node_modules",
|
|
||||||
"build"
|
|
||||||
],
|
|
||||||
"include": [
|
"include": [
|
||||||
"src"
|
"src"
|
||||||
]
|
]
|
||||||
|
|
|
@ -1,11 +0,0 @@
|
||||||
{
|
|
||||||
"extends": [
|
|
||||||
"tslint:recommended",
|
|
||||||
"tslint-eslint-rules",
|
|
||||||
"tslint-react",
|
|
||||||
"@blueprintjs/tslint-config/blueprint-rules",
|
|
||||||
"tslint-config-prettier",
|
|
||||||
"tslint-config-security"
|
|
||||||
],
|
|
||||||
"exclude": ["**/*.css"]
|
|
||||||
}
|
|
2053
frontend/yarn.lock
2053
frontend/yarn.lock
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue