mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2026-02-24 10:46:36 +00:00
Compare commits
649 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f19746cd58 | ||
|
|
85161915b1 | ||
|
|
a295734c13 | ||
|
|
d00b6165b3 | ||
|
|
0cbba1182f | ||
|
|
785806b7a1 | ||
|
|
15b7fd5c93 | ||
|
|
9b32bd2817 | ||
|
|
8b5b035568 | ||
|
|
f7cc90bb77 | ||
|
|
7b0cda7191 | ||
|
|
9791486341 | ||
|
|
40ef51a348 | ||
|
|
a90287ed02 | ||
|
|
12f6a132bd | ||
|
|
1da7119c5c | ||
|
|
01f6f11ee2 | ||
|
|
57d4db5daa | ||
|
|
c7559e78a2 | ||
|
|
2f76c5381f | ||
|
|
304a7431ad | ||
|
|
691dabcfbc | ||
|
|
2dd35dcd44 | ||
|
|
44930b8c5f | ||
|
|
310913b222 | ||
|
|
b877aa8e5b | ||
|
|
27e3d65143 | ||
|
|
b462169e1e | ||
|
|
dc2f30c73b | ||
|
|
8df1ba4671 | ||
|
|
56a3dbd07f | ||
|
|
856ee6d65c | ||
|
|
9518a5e442 | ||
|
|
3a8c7a7bf4 | ||
|
|
7fb0658349 | ||
|
|
6d79851d18 | ||
|
|
f89e4244ea | ||
|
|
3c23016028 | ||
|
|
27c4bd792b | ||
|
|
1b158b3df4 | ||
|
|
19f0dc2920 | ||
|
|
a15917b1ae | ||
|
|
7e5397dd38 | ||
|
|
382d7b1c9f | ||
|
|
58ee123cef | ||
|
|
039a56f410 | ||
|
|
6780aa623b | ||
|
|
7752140c9d | ||
|
|
8bfd38d861 | ||
|
|
27b6676edc | ||
|
|
66c91722fc | ||
|
|
178f15b7d3 | ||
|
|
0e47f9b502 | ||
|
|
d2ad1cd54b | ||
|
|
91e003153b | ||
|
|
c6cca9c91f | ||
|
|
7330fd85ff | ||
|
|
b61d863356 | ||
|
|
f54460e8f8 | ||
|
|
036c8aafcb | ||
|
|
d55160e8f6 | ||
|
|
0572bc2854 | ||
|
|
aceb2350cf | ||
|
|
923575b38b | ||
|
|
f41a8473f8 | ||
|
|
b94cdb2680 | ||
|
|
0cdae72ebd | ||
|
|
75931edc33 | ||
|
|
d1fcd10c04 | ||
|
|
06f4cff97e | ||
|
|
0804322a9f | ||
|
|
53ba14e6f6 | ||
|
|
ead5f2033b | ||
|
|
74ac122787 | ||
|
|
13785c7beb | ||
|
|
9887cae4fd | ||
|
|
410d372755 | ||
|
|
e7a969a78d | ||
|
|
b1d6f58619 | ||
|
|
f49b74229c | ||
|
|
d88f822125 | ||
|
|
dce1cefd49 | ||
|
|
8e71b2e2b1 | ||
|
|
69cb3bd619 | ||
|
|
bf29158a8a | ||
|
|
a28a4846bc | ||
|
|
5eee86003d | ||
|
|
37a3a2022b | ||
|
|
c6be8bd96f | ||
|
|
5166340779 | ||
|
|
520e52595f | ||
|
|
461c0e0bc9 | ||
|
|
0ecb771b23 | ||
|
|
c89e2b5d25 | ||
|
|
aa8f2a0cbc | ||
|
|
eb90aa2274 | ||
|
|
2b5420a429 | ||
|
|
3484e74559 | ||
|
|
edd536cc1e | ||
|
|
322396a366 | ||
|
|
9f02bc6496 | ||
|
|
590393dcfd | ||
|
|
8029823271 | ||
|
|
4417a17d5c | ||
|
|
b8a7dccf92 | ||
|
|
cbe5f98aa3 | ||
|
|
6c2f5b99ac | ||
|
|
fa64c950ca | ||
|
|
0e4667e59c | ||
|
|
56d9dcf562 | ||
|
|
d5e8f81076 | ||
|
|
69905c4b38 | ||
|
|
08694d7693 | ||
|
|
8045fa8886 | ||
|
|
0789494a40 | ||
|
|
34837f2917 | ||
|
|
9e8c743d53 | ||
|
|
239cc4ab84 | ||
|
|
b3e79f4219 | ||
|
|
7c11a6d1ab | ||
|
|
635ee6c5eb | ||
|
|
f79bd39de7 | ||
|
|
5c6979122d | ||
|
|
402efac12e | ||
|
|
770ba624c2 | ||
|
|
d4236b914d | ||
|
|
2cc92b5b41 | ||
|
|
f0598ba47f | ||
|
|
66c5c7ebf1 | ||
|
|
741bc21a55 | ||
|
|
fb1ced5e3f | ||
|
|
3999d14bab | ||
|
|
99c77622cd | ||
|
|
bc5c25deb0 | ||
|
|
0275908f69 | ||
|
|
4be1a295d8 | ||
|
|
ee65c0c050 | ||
|
|
d718329b52 | ||
|
|
55716a8f7f | ||
|
|
5ef719c592 | ||
|
|
3a57416525 | ||
|
|
5bd57e71fd | ||
|
|
c4ed838510 | ||
|
|
affe2309b0 | ||
|
|
638ce89780 | ||
|
|
a0ab9533cb | ||
|
|
7b80948eea | ||
|
|
1cf96c7212 | ||
|
|
151175dc70 | ||
|
|
a30376344e | ||
|
|
db0c43dcdd | ||
|
|
a3550f8e52 | ||
|
|
3a3babadeb | ||
|
|
e22ad2c822 | ||
|
|
342dda3ec9 | ||
|
|
b7af07c043 | ||
|
|
6b338275d3 | ||
|
|
a72d3b2720 | ||
|
|
18042dba6e | ||
|
|
6e09d1372f | ||
|
|
ce02d29ca3 | ||
|
|
e193c700d6 | ||
|
|
bfeb282aa9 | ||
|
|
5caa648112 | ||
|
|
4546b74b6f | ||
|
|
2fb5507803 | ||
|
|
93329c5a12 | ||
|
|
5a91b668dc | ||
|
|
66aac4771c | ||
|
|
ce04b8eb58 | ||
|
|
e0c20c704e | ||
|
|
d5fadc56af | ||
|
|
bbc3342c00 | ||
|
|
76ebbd318a | ||
|
|
24801b068b | ||
|
|
4c21ad0a89 | ||
|
|
f626f9b046 | ||
|
|
ccffa0fe12 | ||
|
|
d5530b4614 | ||
|
|
7c327099bb | ||
|
|
577d7e79da | ||
|
|
31736fad1e | ||
|
|
6319a81ddb | ||
|
|
0ca6ff6906 | ||
|
|
eb69165781 | ||
|
|
4e3d311bef | ||
|
|
54b7aeed20 | ||
|
|
2ba8db1fd3 | ||
|
|
f74270a767 | ||
|
|
9a245fbf13 | ||
|
|
f16e9565e2 | ||
|
|
e65f9a7b89 | ||
|
|
0141a1e0ed | ||
|
|
937876ce67 | ||
|
|
b52120e0d3 | ||
|
|
62b65334b5 | ||
|
|
76dae535d9 | ||
|
|
23ba140ff4 | ||
|
|
76ff7d81b9 | ||
|
|
66deba29f5 | ||
|
|
e44527e9c9 | ||
|
|
aec629b95c | ||
|
|
fa4664e583 | ||
|
|
2952ac8892 | ||
|
|
cf4fc4fa30 | ||
|
|
2d61748aac | ||
|
|
7f61825768 | ||
|
|
c3d6c83ec4 | ||
|
|
c3e38fd580 | ||
|
|
db778a73f7 | ||
|
|
f0a04ced75 | ||
|
|
d6bb718672 | ||
|
|
6d887ec4a8 | ||
|
|
859cd9e5e3 | ||
|
|
eabd7d9ecb | ||
|
|
205e3ffb90 | ||
|
|
8c7a91c7b8 | ||
|
|
56aab349db | ||
|
|
6628a4059e | ||
|
|
10c9f7dabd | ||
|
|
d703e5e182 | ||
|
|
3ad0c4d009 | ||
|
|
1403538660 | ||
|
|
ca670d810d | ||
|
|
d5e20f445d | ||
|
|
eea76d88c3 | ||
|
|
a019bd30df | ||
|
|
631b46393b | ||
|
|
98aa85ca14 | ||
|
|
ea01d22369 | ||
|
|
ff1d2f63c8 | ||
|
|
71468379bd | ||
|
|
843f646264 | ||
|
|
508623f89f | ||
|
|
482489599e | ||
|
|
03f63e3ee3 | ||
|
|
3f3523b80f | ||
|
|
1594717f33 | ||
|
|
ed92b9c949 | ||
|
|
e76b22b2ae | ||
|
|
e380ddb40f | ||
|
|
426d000a59 | ||
|
|
fee62484b5 | ||
|
|
d3f9650e82 | ||
|
|
ad46927750 | ||
|
|
bd79230007 | ||
|
|
5224e7b4ef | ||
|
|
70ce099913 | ||
|
|
b4c2fb5b8f | ||
|
|
6fbf65c873 | ||
|
|
13d3a95a06 | ||
|
|
56b3523c5b | ||
|
|
8a69adfbc9 | ||
|
|
87a32b412f | ||
|
|
df87ad5867 | ||
|
|
f15bbcd027 | ||
|
|
3c9c0fe994 | ||
|
|
a665e96908 | ||
|
|
fddba80b08 | ||
|
|
caa3a09827 | ||
|
|
fa70520f38 | ||
|
|
b789f64a54 | ||
|
|
ce0fc1094e | ||
|
|
ad0a889548 | ||
|
|
1fe76500e8 | ||
|
|
86544f4b24 | ||
|
|
c8f8416c06 | ||
|
|
3d2228441a | ||
|
|
3f616d5482 | ||
|
|
47fb26368b | ||
|
|
fb2194d2d1 | ||
|
|
8ec49b8cfc | ||
|
|
4d77c3abf9 | ||
|
|
d921c44d3b | ||
|
|
eb0ab92472 | ||
|
|
9904ac757b | ||
|
|
71ee886e24 | ||
|
|
25e53bf627 | ||
|
|
d7edd69e60 | ||
|
|
115038f80f | ||
|
|
5479210366 | ||
|
|
46d012b6ff | ||
|
|
80dcbf0668 | ||
|
|
d0825089d0 | ||
|
|
f653739d50 | ||
|
|
2553b27d7d | ||
|
|
3cd30b61e4 | ||
|
|
ae4921b865 | ||
|
|
c89bcab770 | ||
|
|
f97ef8df83 | ||
|
|
e7466ced18 | ||
|
|
0ee899f309 | ||
|
|
36c97ad804 | ||
|
|
d6633f7555 | ||
|
|
61af43f9d9 | ||
|
|
9523277311 | ||
|
|
9703eba6ec | ||
|
|
83791157ce | ||
|
|
7f6c71e8d7 | ||
|
|
9dbf790cc8 | ||
|
|
f313a39b81 | ||
|
|
53f16ac8b5 | ||
|
|
13c681dc39 | ||
|
|
f35be007c1 | ||
|
|
e2d26e8bdd | ||
|
|
5a373fd7ae | ||
|
|
3c53f7d0fc | ||
|
|
57e3db1e1c | ||
|
|
5afd3869dd | ||
|
|
c3ebb0d10f | ||
|
|
4885088d59 | ||
|
|
872890e674 | ||
|
|
8a2e39a935 | ||
|
|
f8edcda665 | ||
|
|
c95cb144a8 | ||
|
|
f9da22c5a1 | ||
|
|
be085f50e0 | ||
|
|
1122f4e560 | ||
|
|
ecefa22204 | ||
|
|
e2ba63ff58 | ||
|
|
277069a0af | ||
|
|
0c9434b555 | ||
|
|
0fce6dd821 | ||
|
|
4b8e5bf3fc | ||
|
|
3546a17575 | ||
|
|
556495ea7e | ||
|
|
e9cef8a029 | ||
|
|
e577eb48d6 | ||
|
|
d08a69954a | ||
|
|
fe81bfccef | ||
|
|
4869435aca | ||
|
|
0822cebb10 | ||
|
|
01a18f2342 | ||
|
|
a22274f382 | ||
|
|
c0098ac7fd | ||
|
|
ba5a99dc2a | ||
|
|
1927ad2d3a | ||
|
|
0356a0204d | ||
|
|
3bf64bee1e | ||
|
|
da484374a1 | ||
|
|
7b9447b717 | ||
|
|
e583eb2759 | ||
|
|
93b4de60f6 | ||
|
|
16f4f7eac8 | ||
|
|
90d4fe72db | ||
|
|
e1298cfa81 | ||
|
|
6be3a1223f | ||
|
|
81d24432a9 | ||
|
|
1d193f1187 | ||
|
|
c56994c813 | ||
|
|
44862073bb | ||
|
|
9eb9182c21 | ||
|
|
b2abfd543e | ||
|
|
8c6eaf2f1d | ||
|
|
811544d7df | ||
|
|
9fdfdf865e | ||
|
|
6a354c277c | ||
|
|
89f6c6c283 | ||
|
|
d534a4e441 | ||
|
|
4c3772d5c8 | ||
|
|
ee95d5a1b7 | ||
|
|
51379eb2a0 | ||
|
|
f69f791790 | ||
|
|
54b1ab12cd | ||
|
|
18d417e78c | ||
|
|
7a48a06442 | ||
|
|
195aaa8be6 | ||
|
|
94d2f3167b | ||
|
|
344f5e9b0d | ||
|
|
b211a29fc5 | ||
|
|
c25355c531 | ||
|
|
5cf0c86a14 | ||
|
|
852e791c80 | ||
|
|
f5d03ed3a2 | ||
|
|
4642e07fd3 | ||
|
|
83221c1066 | ||
|
|
214b952e84 | ||
|
|
42adbb3739 | ||
|
|
9e63c463ca | ||
|
|
260a6c4940 | ||
|
|
fa949cde12 | ||
|
|
23da0328ec | ||
|
|
7da634e772 | ||
|
|
79f7459d77 | ||
|
|
4002392b12 | ||
|
|
e9e53bb69b | ||
|
|
623deec973 | ||
|
|
3453d4ffd5 | ||
|
|
f9ef7eccf8 | ||
|
|
3cdcffaac3 | ||
|
|
0f23cdcd21 | ||
|
|
9dc6c756f2 | ||
|
|
0491694839 | ||
|
|
f1f3c3f98b | ||
|
|
ec3ad8412c | ||
|
|
d39512732a | ||
|
|
95abf4f898 | ||
|
|
61a1087d91 | ||
|
|
3f245a757e | ||
|
|
4e236a80de | ||
|
|
288f6e2cf8 | ||
|
|
9b6d4a4d97 | ||
|
|
f2a8865679 | ||
|
|
017db18e70 | ||
|
|
19c4a61524 | ||
|
|
f01c9bd5c8 | ||
|
|
2a5fa54ae1 | ||
|
|
7a1b6367a8 | ||
|
|
058860737e | ||
|
|
20f2fd1080 | ||
|
|
16ce1d24af | ||
|
|
a51db38749 | ||
|
|
6090f97347 | ||
|
|
c74355e363 | ||
|
|
a013d40bf1 | ||
|
|
7f7473c348 | ||
|
|
df6f1b984f | ||
|
|
b9905c8bf4 | ||
|
|
32957835b3 | ||
|
|
2efc5feb3f | ||
|
|
526fa14dce | ||
|
|
4d969b994e | ||
|
|
d62edb2249 | ||
|
|
bc82e7e7fd | ||
|
|
1e460d3ef7 | ||
|
|
143a05cab1 | ||
|
|
bf1b59c0d8 | ||
|
|
5ab38027bf | ||
|
|
3e6aee47e5 | ||
|
|
60282281a3 | ||
|
|
2017ee7456 | ||
|
|
e60d241fcf | ||
|
|
43af6fdaba | ||
|
|
f359a16004 | ||
|
|
1b413fb0b7 | ||
|
|
20a9259109 | ||
|
|
8d5f7e942d | ||
|
|
17d5c4327b | ||
|
|
9b30a82a79 | ||
|
|
a0ec3c0293 | ||
|
|
d9e39eee2b | ||
|
|
032e9c53f3 | ||
|
|
dba0ac6442 | ||
|
|
920effb4c6 | ||
|
|
bd6e455cd6 | ||
|
|
b9fc906537 | ||
|
|
1415f196bb | ||
|
|
8f7e356e54 | ||
|
|
0ed88079ad | ||
|
|
5182f9d147 | ||
|
|
4e1579832e | ||
|
|
ff48c0cd45 | ||
|
|
02c7125236 | ||
|
|
dc397d4b82 | ||
|
|
2a206f11b9 | ||
|
|
369fcf2f6a | ||
|
|
983e4db3b1 | ||
|
|
2a7c2474cd | ||
|
|
c890124e67 | ||
|
|
3e21cccb14 | ||
|
|
dafebc3df9 | ||
|
|
6619e7cdb6 | ||
|
|
c54f314424 | ||
|
|
4964f28169 | ||
|
|
dead22c332 | ||
|
|
aba65346b4 | ||
|
|
4621246cec | ||
|
|
f83280068b | ||
|
|
0671fa6567 | ||
|
|
5c80e853c6 | ||
|
|
6c90d7072f | ||
|
|
18bccab27a | ||
|
|
b9213952d3 | ||
|
|
f1ae68a300 | ||
|
|
3f0409f25a | ||
|
|
6f568a16bf | ||
|
|
39ae3b4762 | ||
|
|
14e31ed2c3 | ||
|
|
ff1fb0dd12 | ||
|
|
2e6a35181d | ||
|
|
22cca598ca | ||
|
|
de906bf370 | ||
|
|
498594c31b | ||
|
|
cfbd246cfc | ||
|
|
3f91c556e4 | ||
|
|
4d1622607c | ||
|
|
eacdee293c | ||
|
|
f4b115cffd | ||
|
|
7dcd623513 | ||
|
|
8b00d1aaae | ||
|
|
facfd33e96 | ||
|
|
a841dc7531 | ||
|
|
28ebd55b69 | ||
|
|
3eade5a0c0 | ||
|
|
caf74cd87b | ||
|
|
049510f513 | ||
|
|
b151b7eedb | ||
|
|
4e22e9c092 | ||
|
|
793883148a | ||
|
|
8acb7bea24 | ||
|
|
335cceeb82 | ||
|
|
bf7455ad6e | ||
|
|
421cc5b718 | ||
|
|
78d97a64aa | ||
|
|
749c757cbd | ||
|
|
faf9554286 | ||
|
|
b7a0be3872 | ||
|
|
cff8046ff8 | ||
|
|
af1289752d | ||
|
|
b06d9d3bc7 | ||
|
|
b2904189ef | ||
|
|
5c639d241b | ||
|
|
984e9f32a5 | ||
|
|
59d23b584a | ||
|
|
a7d865228a | ||
|
|
260ff716d7 | ||
|
|
9001a3da37 | ||
|
|
46db1e39f3 | ||
|
|
6bf3fc0fd5 | ||
|
|
a136543551 | ||
|
|
23046c149c | ||
|
|
2951d0d75e | ||
|
|
b52e40edd3 | ||
|
|
51556d76ac | ||
|
|
871868f0a4 | ||
|
|
67495fa302 | ||
|
|
fc9341f631 | ||
|
|
3fea8b5505 | ||
|
|
89e3114ef3 | ||
|
|
4dc5fad8b8 | ||
|
|
2567bdfdf0 | ||
|
|
f36cf1e7b9 | ||
|
|
bd88e56331 | ||
|
|
fe3e08de0f | ||
|
|
cfb165d240 | ||
|
|
fa074f91be | ||
|
|
6fc4963663 | ||
|
|
ad437f655e | ||
|
|
9b45513684 | ||
|
|
5d6d802d64 | ||
|
|
536d49aac9 | ||
|
|
796c9ca140 | ||
|
|
1b1a1f3230 | ||
|
|
98d856700c | ||
|
|
b814f500de | ||
|
|
90abf29db9 | ||
|
|
d064eb5f9e | ||
|
|
58c9ef9b51 | ||
|
|
125b13e059 | ||
|
|
dcdee8b308 | ||
|
|
f33d1fca39 | ||
|
|
7e907ba9b6 | ||
|
|
3cec2efbbd | ||
|
|
d4094e66b3 | ||
|
|
73b854037d | ||
|
|
f2e7a2161d | ||
|
|
260ed3041a | ||
|
|
8a146021dd | ||
|
|
4083592212 | ||
|
|
f9c57ca659 | ||
|
|
d0d664ef79 | ||
|
|
16d96efa4a | ||
|
|
f8ea1ae3d5 | ||
|
|
18883caa6d | ||
|
|
84fc82b74e | ||
|
|
8a9c694fbc | ||
|
|
4b33d39d44 | ||
|
|
c0f5d9c12c | ||
|
|
ef630af154 | ||
|
|
ebd7a76896 | ||
|
|
64a968711c | ||
|
|
aee4c2d02f | ||
|
|
8cc0695ee9 | ||
|
|
f40ad91ea9 | ||
|
|
a96539129d | ||
|
|
dcf72e6818 | ||
|
|
54290d4c9a | ||
|
|
eb3775859a | ||
|
|
83531666de | ||
|
|
f3a2535e2f | ||
|
|
f283dc8569 | ||
|
|
b19bbee7fc | ||
|
|
1b03d04318 | ||
|
|
6696fb13d6 | ||
|
|
f04aece7df | ||
|
|
d8f3952920 | ||
|
|
fefa4e7848 | ||
|
|
0b4a348969 | ||
|
|
3e2fee0df5 | ||
|
|
294888454d | ||
|
|
1b7e1e2b5b | ||
|
|
dc78138066 | ||
|
|
87e64e5899 | ||
|
|
e193a692e8 | ||
|
|
2eba607874 | ||
|
|
62df46d648 | ||
|
|
7c67fa4149 | ||
|
|
2db85c2783 | ||
|
|
39663ba936 | ||
|
|
eefea0c37b | ||
|
|
d65a6ba970 | ||
|
|
524b0a74c6 | ||
|
|
72de9d4ff8 | ||
|
|
a91f1b3bd4 | ||
|
|
343a93b984 | ||
|
|
8be17cce8a | ||
|
|
d2f818c1ea | ||
|
|
a675d60d59 | ||
|
|
2d96c21b50 | ||
|
|
6f6ba9e34d | ||
|
|
e6efda5563 | ||
|
|
b1df1652bf | ||
|
|
474239c151 | ||
|
|
feeb212259 | ||
|
|
90245016a0 | ||
|
|
8c7616c3a7 | ||
|
|
ea84ce9c41 | ||
|
|
c4730ec92d | ||
|
|
76b3d573c0 | ||
|
|
b96f4b7a90 | ||
|
|
2a0def262d | ||
|
|
897e35f0b8 | ||
|
|
1c335506d8 | ||
|
|
d46acdbd70 | ||
|
|
026bb4140e | ||
|
|
d80f3da55d | ||
|
|
f18495a4b1 | ||
|
|
f908da78f6 | ||
|
|
bc16381c90 | ||
|
|
4cb7aa64cf | ||
|
|
da6d7aea8b | ||
|
|
b310d79110 | ||
|
|
e26cdc11c3 | ||
|
|
fa54aa3128 | ||
|
|
e31e70039d | ||
|
|
cb761dea8f | ||
|
|
949e0da105 | ||
|
|
770cc59448 | ||
|
|
72dd2bd0a7 | ||
|
|
54733eaa18 | ||
|
|
52c56f7918 | ||
|
|
c46d5187c1 | ||
|
|
05e3e87653 | ||
|
|
8b9289ff08 | ||
|
|
16ffbcfbc0 | ||
|
|
d825b6e174 | ||
|
|
73e55cc742 | ||
|
|
32cc1cc580 | ||
|
|
e00574553f |
38
.eslintrc
38
.eslintrc
@@ -1,45 +1,21 @@
|
||||
{
|
||||
"extends": [
|
||||
"adidas-env/browser",
|
||||
"adidas-env/module",
|
||||
"adidas-env/node",
|
||||
"adidas-es6",
|
||||
"adidas-babel",
|
||||
"adidas-react"
|
||||
"@shlinkio/js-coding-standard"
|
||||
],
|
||||
"plugins": ["jest"],
|
||||
"env": {
|
||||
"jest/globals": true
|
||||
},
|
||||
"parserOptions": {
|
||||
"tsconfigRootDir": ".",
|
||||
"createDefaultProgram": true
|
||||
},
|
||||
"globals": {
|
||||
"process": true,
|
||||
"setImmediate": true
|
||||
},
|
||||
"settings": {
|
||||
"react": {
|
||||
"version": "16.3"
|
||||
}
|
||||
},
|
||||
"ignorePatterns": ["src/service*.ts"],
|
||||
"rules": {
|
||||
"comma-dangle": ["error", "always-multiline"],
|
||||
"no-invalid-this": "off",
|
||||
"no-console": "warn",
|
||||
"template-curly-spacing": ["error", "never"],
|
||||
"no-warning-comments": "off",
|
||||
"no-magic-numbers": "off",
|
||||
"no-undefined": "off",
|
||||
"no-inline-comments": "off",
|
||||
"lines-around-comment": "off",
|
||||
"indent": ["error", 2, {
|
||||
"SwitchCase": 1
|
||||
}
|
||||
],
|
||||
"react/jsx-curly-spacing": ["error", "never"],
|
||||
"react/jsx-indent-props": ["error", 2],
|
||||
"react/jsx-first-prop-new-line": ["error", "multiline-multiprop"],
|
||||
"react/jsx-closing-bracket-location": ["error", "tag-aligned"],
|
||||
"react/no-array-index-key": "off",
|
||||
"react/no-did-update-set-state": "off",
|
||||
"react/display-name": "off"
|
||||
"complexity": "off"
|
||||
}
|
||||
}
|
||||
|
||||
5
.github/ISSUE_TEMPLATE.md
vendored
5
.github/ISSUE_TEMPLATE.md
vendored
@@ -1,6 +1,7 @@
|
||||
<!--
|
||||
Before opening an issue, just take into account that this is a completely free of charge open source project.
|
||||
I'm always happy to help and provide support, but some understanding will be required.
|
||||
Before opening an issue, just take into account that this is a completely free of charge and open source project.
|
||||
I'm always happy to help and provide support, but some understanding will be expected.
|
||||
I do this in my own free time, so expect some delays when implementing new features and fixing bugs, and don't take it personal if an issue gets eventually closed.
|
||||
You may also be asked to provide tests or ways to reproduce reported bugs.
|
||||
Try to be polite, and understand it is impossible for an OSS project to cover all use cases.
|
||||
-->
|
||||
|
||||
5
.github/ISSUE_TEMPLATE/Bug.md
vendored
5
.github/ISSUE_TEMPLATE/Bug.md
vendored
@@ -5,9 +5,10 @@ labels: bug
|
||||
---
|
||||
|
||||
<!--
|
||||
Before opening an issue, just take into account that this is a completely free of charge open source project.
|
||||
I'm always happy to help and provide support, but some understanding will be required.
|
||||
Before opening an issue, just take into account that this is a completely free of charge and open source project.
|
||||
I'm always happy to help and provide support, but some understanding will be expected.
|
||||
I do this in my own free time, so expect some delays when implementing new features and fixing bugs, and don't take it personal if an issue gets eventually closed.
|
||||
You may also be asked to provide tests or ways to reproduce reported bugs.
|
||||
Try to be polite, and understand it is impossible for an OSS project to cover all use cases.
|
||||
|
||||
With that said, please fill in the information requested next. More information might be requested once the issue is open.
|
||||
|
||||
5
.github/ISSUE_TEMPLATE/Feature_Request.md
vendored
5
.github/ISSUE_TEMPLATE/Feature_Request.md
vendored
@@ -5,9 +5,10 @@ labels: feature
|
||||
---
|
||||
|
||||
<!--
|
||||
Before opening an issue, just take into account that this is a completely free of charge open source project.
|
||||
I'm always happy to help and provide support, but some understanding will be required.
|
||||
Before opening an issue, just take into account that this is a completely free of charge and open source project.
|
||||
I'm always happy to help and provide support, but some understanding will be expected.
|
||||
I do this in my own free time, so expect some delays when implementing new features and fixing bugs, and don't take it personal if an issue gets eventually closed.
|
||||
You may also be asked to provide tests or ways to reproduce reported bugs.
|
||||
Try to be polite, and understand it is impossible for an OSS project to cover all use cases.
|
||||
|
||||
With that said, please fill in the information requested next. More information might be requested once the issue is open.
|
||||
|
||||
5
.github/ISSUE_TEMPLATE/Question_Support.md
vendored
5
.github/ISSUE_TEMPLATE/Question_Support.md
vendored
@@ -5,9 +5,10 @@ labels: question
|
||||
---
|
||||
|
||||
<!--
|
||||
Before opening an issue, just take into account that this is a completely free of charge open source project.
|
||||
I'm always happy to help and provide support, but some understanding will be required.
|
||||
Before opening an issue, just take into account that this is a completely free of charge and open source project.
|
||||
I'm always happy to help and provide support, but some understanding will be expected.
|
||||
I do this in my own free time, so expect some delays when implementing new features and fixing bugs, and don't take it personal if an issue gets eventually closed.
|
||||
You may also be asked to provide tests or ways to reproduce reported bugs.
|
||||
Try to be polite, and understand it is impossible for an OSS project to cover all use cases.
|
||||
|
||||
With that said, please fill in the information requested next. More information might be requested once the issue is open.
|
||||
|
||||
58
.github/workflows/ci.yml
vendored
Normal file
58
.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,58 @@
|
||||
name: Continuous integration
|
||||
|
||||
on:
|
||||
pull_request: null
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
- name: Use node.js 14.15
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 14.15
|
||||
- run: npm ci
|
||||
- run: npm run lint
|
||||
|
||||
unit-tests:
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
- name: Use node.js 14.15
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 14.15
|
||||
- run: npm ci
|
||||
- run: npm run test:ci
|
||||
- name: Publish coverage
|
||||
uses: codecov/codecov-action@v1
|
||||
with:
|
||||
file: ./coverage/clover.xml
|
||||
|
||||
mutation-tests:
|
||||
continue-on-error: true
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0 # needed so that the main branch is also fetched
|
||||
- name: Use node.js 14.15
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 14.15
|
||||
- run: npm ci
|
||||
- run: npm run mutate -- --mutate=$(git diff origin/main --name-only | grep -E 'src\/(.*).(ts|tsx)$' | paste -sd ",")
|
||||
|
||||
build-docker-image:
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
- run: docker build -t shlink-web-client:test .
|
||||
29
.github/workflows/deploy-preview.yml
vendored
Normal file
29
.github/workflows/deploy-preview.yml
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
name: Deploy preview
|
||||
|
||||
on:
|
||||
pull_request_target: null
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-20.04
|
||||
continue-on-error: true
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||
ref: ${{ github.event.pull_request.head.ref }}
|
||||
- name: Use node.js 14.15
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 14.15
|
||||
- name: Build
|
||||
run: |
|
||||
npm ci && \
|
||||
node ./scripts/set-homepage.js /shlink-web-client/${GITHUB_HEAD_REF#refs/heads/} && \
|
||||
rm src/service-worker.ts && \
|
||||
npm run build
|
||||
- name: Deploy preview
|
||||
uses: shlinkio/deploy-preview-action@v1.0.1
|
||||
with:
|
||||
folder: build
|
||||
28
.github/workflows/docker-image-build.yml
vendored
Normal file
28
.github/workflows/docker-image-build.yml
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
name: Build docker image
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- develop
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
with:
|
||||
version: latest
|
||||
- name: Login to docker hub
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
|
||||
- name: Build the image
|
||||
run: bash ./scripts/docker/build
|
||||
27
.github/workflows/publish-release.yml
vendored
Normal file
27
.github/workflows/publish-release.yml
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
name: Publish release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
- name: Use node.js 14.15
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 14.15
|
||||
- name: Generate release assets
|
||||
run: npm ci && npm run build ${GITHUB_REF#refs/tags/v}
|
||||
- name: Publish release with assets
|
||||
uses: docker://antonyurchenko/git-release:latest
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
ALLOW_EMPTY_CHANGELOG: "true"
|
||||
with:
|
||||
args: |
|
||||
dist/shlink-web-client_*_dist.zip
|
||||
@@ -1,6 +0,0 @@
|
||||
build:
|
||||
environment:
|
||||
node: v12.14.1
|
||||
tools:
|
||||
external_code_coverage:
|
||||
timeout: 1200
|
||||
58
.travis.yml
58
.travis.yml
@@ -1,58 +0,0 @@
|
||||
dist: bionic
|
||||
|
||||
language: node_js
|
||||
|
||||
jobs:
|
||||
fast_finish: true
|
||||
include:
|
||||
- name: "Docker publish"
|
||||
node_js: '12.16.3'
|
||||
if: NOT type = pull_request
|
||||
env:
|
||||
- DOCKER_PUBLISH="true"
|
||||
- name: "CI"
|
||||
node_js: '12.16.3'
|
||||
env:
|
||||
- DOCKER_PUBLISH="false"
|
||||
allow_failures:
|
||||
- name: "Docker publish"
|
||||
|
||||
cache:
|
||||
directories:
|
||||
- node_modules
|
||||
|
||||
services:
|
||||
- docker
|
||||
|
||||
install:
|
||||
- if [[ ${DOCKER_PUBLISH} == 'true' ]]; then sudo bash ./scripts/docker/install-docker ; fi
|
||||
- if [[ ${DOCKER_PUBLISH} == 'false' ]]; then npm ci ; fi
|
||||
|
||||
before_script:
|
||||
- if [[ ${DOCKER_PUBLISH} == 'false' ]]; then echo "Building commit range ${TRAVIS_COMMIT_RANGE}" ; fi
|
||||
- if [[ ${DOCKER_PUBLISH} == 'false' ]]; then export MUTATION_FILES=$(git diff ${TRAVIS_COMMIT_RANGE:-origin/master} --name-only | grep -E 'src\/(.*).(js|ts|jsx|tsx)$' | paste -sd ",") ; fi
|
||||
|
||||
script:
|
||||
- if [[ ${DOCKER_PUBLISH} == 'true' ]]; then bash ./scripts/docker/build ; fi
|
||||
- if [[ ${DOCKER_PUBLISH} == 'false' ]]; then npm run lint ; fi
|
||||
- if [[ ${DOCKER_PUBLISH} == 'false' ]]; then npm run test:ci ; fi
|
||||
- if [[ ${DOCKER_PUBLISH} == 'false' ]]; then docker build -t shlink-web-client:test . ; fi
|
||||
- if [[ ${DOCKER_PUBLISH} == 'false' ]]; then npm run mutate:ci ; fi
|
||||
|
||||
after_success:
|
||||
- if [[ ${DOCKER_PUBLISH} == 'false' ]]; then node_modules/.bin/ocular coverage/clover.xml ; fi
|
||||
|
||||
# Before deploying, build dist file for current travis tag
|
||||
before_deploy:
|
||||
- if [[ ! -z $TRAVIS_TAG && ${DOCKER_PUBLISH} == 'false' ]]; then npm run build ${TRAVIS_TAG#?} ; fi
|
||||
|
||||
deploy:
|
||||
- provider: releases
|
||||
api_key:
|
||||
secure: jBvPwC7EAbViaNR83rwMSt5XQDK0Iu9rgvEMa7GoyShbHcvUCCPd73Tu9quNpKi6NKsDY3INHgtch3vgonjGNGDGJ+yDyIBzXcvsAX2x3UcHpRbgY12uiINVmQxBI1+OVQB016Nm+cKC/i5Z36K4EmDbYfo+MrKndngM6AjcQFTwI8EwniIMaQgg4gNes//K8NhP5u0c3gwG+Q6jEGnq6uH3kcRgh6/epIZYpQyxjqWqKwF77sgcYj+X2Nf6XxtB5neuCi301UKLoLx8G0skh/Lm6KAIO4s9iIhIFa3UpoF21Ka0TxLpd2JxalLryCnFGlWWE6lxC9Htmc0TeRowJQlGdJXCskJ37xT9MljKY0fwNMu06VS/FUgykuCv+jP3zQu51pKu7Ew7+WeNPjautoOTu54VkdGyHcf2ThBNEyJQuiEwAQe4u7yAxY6R5ovEdvHBSIg4w1E5/Mxy5SMTCUlIAv6H7QQ1X9Z/zJm9HH5KeKz5tsHvQ/RIdSpgHXq/tC8o4Yup/LCFucXfrgvy/8pJoO1UpOlmvm62974NFfo0EG5YWwv6brUqz3QXpMjb8sWqgjltYMYJX3J7WZ34rIc+zt4NAmfhqgczaOC4pUGCiJ8jX3rMWIaQRn1AJ+5V337jL9fNDpTHny4phQjHrMJ1e0HZuNp0Xb5Q8wgqDPM=
|
||||
file: "./dist/shlink-web-client_${TRAVIS_TAG#?}_dist.zip"
|
||||
skip_cleanup: true
|
||||
on:
|
||||
all_branches: true
|
||||
condition: ${DOCKER_PUBLISH} == 'false'
|
||||
tags: true
|
||||
696
CHANGELOG.md
696
CHANGELOG.md
@@ -4,10 +4,330 @@ All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org).
|
||||
|
||||
## 2.5.0 - 2020-05-31
|
||||
## [3.3.2] - 2021-10-17
|
||||
### Added
|
||||
* *Nothing*
|
||||
|
||||
#### Added
|
||||
### Changed
|
||||
* *Nothing*
|
||||
|
||||
### Deprecated
|
||||
* *Nothing*
|
||||
|
||||
### Removed
|
||||
* *Nothing*
|
||||
|
||||
### Fixed
|
||||
* [#503](https://github.com/shlinkio/shlink-web-client/issues/503) Fixed short URLs title not being resettable after creation.
|
||||
|
||||
|
||||
## [3.3.1] - 2021-09-27
|
||||
### Added
|
||||
* *Nothing*
|
||||
|
||||
### Changed
|
||||
* *Nothing*
|
||||
|
||||
### Deprecated
|
||||
* *Nothing*
|
||||
|
||||
### Removed
|
||||
* *Nothing*
|
||||
|
||||
### Fixed
|
||||
* [#497](https://github.com/shlinkio/shlink-web-client/issues/497) Fixed crash in domains section when one of the domains have more than one dot.
|
||||
|
||||
|
||||
## [3.3.0] - 2021-09-25
|
||||
### Added
|
||||
* [#465](https://github.com/shlinkio/shlink-web-client/issues/465) Added new page to manage domains and their redirects, when consuming Shlink 2.8 or higher.
|
||||
* [#460](https://github.com/shlinkio/shlink-web-client/issues/460) Added dynamic title on hover for tags with a very long title.
|
||||
* [#462](https://github.com/shlinkio/shlink-web-client/issues/462) Now it is possible to paste multiple comma-separated tags in the tags selector, making all of them to be added as individual tags.
|
||||
* [#463](https://github.com/shlinkio/shlink-web-client/issues/463) The strategy to determine which tags to suggest in the TagsSelector during short URL creation, can now be configured:
|
||||
|
||||
* `startsWith`: Suggests tags that start with the input. This is the default behavior for keep it as it was so far.
|
||||
* `includes`: Suggests tags that contain the input.
|
||||
|
||||
* [#464](https://github.com/shlinkio/shlink-web-client/issues/464) Added support to download QR codes. This feature requires an unreleased version of Shlink, so it comes disabled, and will get enabled as soon as Shlink v2.9 is released.
|
||||
* [#469](https://github.com/shlinkio/shlink-web-client/issues/469) Added support `errorCorrection` in QR codes, when consuming Shlink 2.8 or higher.
|
||||
* [#459](https://github.com/shlinkio/shlink-web-client/issues/459) Added new list mode to display tags.
|
||||
|
||||
The mode is optional, and you can toggle between the classic cards mode or the new list mode whenever you want.
|
||||
|
||||
You can also configure the default mode from settings.
|
||||
|
||||
### Changed
|
||||
* [#408](https://github.com/shlinkio/shlink-web-client/issues/408) Updated to Chart.js 3.5
|
||||
* [#486](https://github.com/shlinkio/shlink-web-client/issues/486) Refactored components used to render visits charts, making them easier to maintain and understand.
|
||||
* [#409](https://github.com/shlinkio/shlink-web-client/issues/409) Increased required code coverage and added hard threshold check.
|
||||
|
||||
### Deprecated
|
||||
* *Nothing*
|
||||
|
||||
### Removed
|
||||
* [#491](https://github.com/shlinkio/shlink-web-client/issues/491) Dropped support for Shlink older than v2.4.0.
|
||||
|
||||
### Fixed
|
||||
* *Nothing*
|
||||
|
||||
|
||||
## [3.2.1] - 2021-09-12
|
||||
### Added
|
||||
* *Nothing*
|
||||
|
||||
### Changed
|
||||
* *Nothing*
|
||||
|
||||
### Deprecated
|
||||
* *Nothing*
|
||||
|
||||
### Removed
|
||||
* *Nothing*
|
||||
|
||||
### Fixed
|
||||
* [#478](https://github.com/shlinkio/shlink-web-client/issues/478) Fixed tags including special chars not being properly URL encoded before using them as query params.
|
||||
* [#480](https://github.com/shlinkio/shlink-web-client/issues/480) Fixed servers import on Chromium-based browsers when using windows.
|
||||
* [#482](https://github.com/shlinkio/shlink-web-client/issues/480) Fixed end date not being set to the end of the day when filtering visits using a "smart filter" (last 7 days, last 30 days, etc).
|
||||
|
||||
|
||||
## [3.2.0] - 2021-07-12
|
||||
### Added
|
||||
* [#433](https://github.com/shlinkio/shlink-web-client/pull/433) Added support to provide a default server to connect to via env vars:
|
||||
|
||||
* `SHLINK_SERVER_URL`: The URL of the Shlink server to configure by default.
|
||||
* `SHLINK_SERVER_API_KEY`: The API key of the Shlink server.
|
||||
* `SHLINK_SERVER_NAME`: A name you want to give to this server. Defaults to *Shlink* if not provided.
|
||||
|
||||
* [#432](https://github.com/shlinkio/shlink-web-client/pull/432) Added support to provide the `servers.json` file inside a `conf.d` folder.
|
||||
* [#440](https://github.com/shlinkio/shlink-web-client/issues/440) Added hint of what visits come potentially from a bot, in the visits table, when consuming Shlink >=2.7.
|
||||
* [#431](https://github.com/shlinkio/shlink-web-client/issues/431) Added support to filter out visits from potential bots in visits sections, when consuming Shlink >=2.7.
|
||||
* [#430](https://github.com/shlinkio/shlink-web-client/issues/430) Added support to set new and existing short URLs as crawlable, when consuming Shlink >=2.7.
|
||||
* [#450](https://github.com/shlinkio/shlink-web-client/issues/450) Improved landing page design.
|
||||
* [#449](https://github.com/shlinkio/shlink-web-client/issues/449) Improved PWA update banner, allowing to restart the app directly from it without having to close the tab.
|
||||
|
||||
### Changed
|
||||
* [#442](https://github.com/shlinkio/shlink-web-client/issues/442) Visits filtering now goes through the corresponding reducer.
|
||||
* [#337](https://github.com/shlinkio/shlink-web-client/issues/337) Replaced moment.js with date-fns.
|
||||
* [#360](https://github.com/shlinkio/shlink-web-client/issues/360) Changed component used to generate a tags selector, switching from `react-tagsinput`, which is no longer maintained, to `react-tag-autocomplete`.
|
||||
|
||||
### Deprecated
|
||||
* *Nothing*
|
||||
|
||||
### Removed
|
||||
* *Nothing*
|
||||
|
||||
### Fixed
|
||||
* [#438](https://github.com/shlinkio/shlink-web-client/issues/438) Fixed horizontal scrolling in short URLs list on mobile devices when the long URL didn't have words to break.
|
||||
|
||||
|
||||
## [3.1.2] - 2021-06-06
|
||||
### Added
|
||||
* *Nothing*
|
||||
|
||||
### Changed
|
||||
* [#428](https://github.com/shlinkio/shlink-web-client/issues/428) Updated to StrykerJS 5.
|
||||
|
||||
### Deprecated
|
||||
* *Nothing*
|
||||
|
||||
### Removed
|
||||
* *Nothing*
|
||||
|
||||
### Fixed
|
||||
* [#371](https://github.com/shlinkio/shlink-web-client/issues/371) Recovered PWA functionality.
|
||||
|
||||
|
||||
## [3.1.1] - 2021-05-08
|
||||
### Added
|
||||
* *Nothing*
|
||||
|
||||
### Changed
|
||||
* *Nothing*
|
||||
|
||||
### Deprecated
|
||||
* *Nothing*
|
||||
|
||||
### Removed
|
||||
* *Nothing*
|
||||
|
||||
### Fixed
|
||||
* [#413](https://github.com/shlinkio/shlink-web-client/issues/413) Fixed edit short URL form reflecting outdated info after navigating back from other section.
|
||||
* [#412](https://github.com/shlinkio/shlink-web-client/issues/412) Ensured new visits coming from mercure hub are prepended and not appended, to keep proper sorting.
|
||||
* [#417](https://github.com/shlinkio/shlink-web-client/issues/417) Fixed link spanning out of QR code modal.
|
||||
* [#411](https://github.com/shlinkio/shlink-web-client/issues/411) Added missing feedback when editing a short URL to know if everything went right.
|
||||
|
||||
|
||||
## [3.1.0] - 2021-03-29
|
||||
### Added
|
||||
* [#379](https://github.com/shlinkio/shlink-web-client/issues/379) and [#384](https://github.com/shlinkio/shlink-web-client/issues/384) Improved QR code modal, including controls to customize size, format and margin, as well as a button to copy the link to the clipboard.
|
||||
* [#385](https://github.com/shlinkio/shlink-web-client/issues/385) Added setting to determine if "validate URL" should be enabled or disabled by default.
|
||||
* [#386](https://github.com/shlinkio/shlink-web-client/issues/386) Added new card in overview section to display amount of orphan visits when using Shlink 2.6.0 or higher.
|
||||
* [#177](https://github.com/shlinkio/shlink-web-client/issues/177) Added dark theme.
|
||||
* [#387](https://github.com/shlinkio/shlink-web-client/issues/387) and [#395](https://github.com/shlinkio/shlink-web-client/issues/395) Added a section to see orphan visits stats, when consuming Shlink >=2.6.0.
|
||||
* [#383](https://github.com/shlinkio/shlink-web-client/issues/383) Added title to short URLs list, displayed when consuming Shlink >=2.6.0.
|
||||
* [#368](https://github.com/shlinkio/shlink-web-client/issues/368) Added new settings to define the default interval for visits pages.
|
||||
* [#349](https://github.com/shlinkio/shlink-web-client/issues/349) Added support to export visits to CSV.
|
||||
* [#397](https://github.com/shlinkio/shlink-web-client/issues/397) New section to edit all data for short URLs, including title when using Shlink v2.6 or newer.
|
||||
|
||||
This new section replaces the old modals to edit short URL meta, short URL tags and the long URL. Everything is now together in the same section.
|
||||
|
||||
### Changed
|
||||
* [#382](https://github.com/shlinkio/shlink-web-client/issues/382) Ensured short URL tags are edited through the `PATCH /short-urls/{shortCode}` endpoint when using Shlink 2.6.0 or higher.
|
||||
* [#398](https://github.com/shlinkio/shlink-web-client/issues/398) Improved performance when loading short URL details by avoiding API calls if the short URL is already present in local state.
|
||||
|
||||
### Deprecated
|
||||
* *Nothing*
|
||||
|
||||
### Removed
|
||||
* *Nothing*
|
||||
|
||||
### Fixed
|
||||
* [#335](https://github.com/shlinkio/shlink-web-client/issues/335) Fixed linting errors.
|
||||
|
||||
|
||||
## [3.0.1] - 2020-12-30
|
||||
### Added
|
||||
* *Nothing*
|
||||
|
||||
### Changed
|
||||
* *Nothing*
|
||||
|
||||
### Deprecated
|
||||
* *Nothing*
|
||||
|
||||
### Removed
|
||||
* *Nothing*
|
||||
|
||||
### Fixed
|
||||
* [#364](https://github.com/shlinkio/shlink-web-client/issues/364) Fixed all dropdowns so that they are consistently styled.
|
||||
* [#366](https://github.com/shlinkio/shlink-web-client/issues/366) Fixed text in visits menu jumping to next line in some tablet resolutions.
|
||||
* [#367](https://github.com/shlinkio/shlink-web-client/issues/367) Removed conflicting overflow in visits table for mobile devices.
|
||||
* [#365](https://github.com/shlinkio/shlink-web-client/issues/365) Fixed weird rendering of short URLs list in tablets.
|
||||
* [#372](https://github.com/shlinkio/shlink-web-client/issues/372) Fixed importing servers in Android devices.
|
||||
|
||||
|
||||
## [3.0.0] - 2020-12-22
|
||||
### Added
|
||||
* [#340](https://github.com/shlinkio/shlink-web-client/issues/340) Added new "overview" page, showing basic information of the active server.
|
||||
|
||||
As a side effect, it also introduces improvements in the "create short URL" page, grouping components by context and explaining what they are for.
|
||||
|
||||
* [#309](https://github.com/shlinkio/shlink-web-client/issues/309) Added new domain selector component in create URL form which allows selecting from previously used domains or set a new one.
|
||||
* [#315](https://github.com/shlinkio/shlink-web-client/issues/315) Now you can tell if you want to validate the long URL when using Shlink >=2.4.
|
||||
* [#285](https://github.com/shlinkio/shlink-web-client/issues/285) Improved visits section:
|
||||
|
||||
* Charts are now grouped in tabs, so that only one part of the components is rendered at a time.
|
||||
* Amount of highlighted visits is now displayed.
|
||||
* Date filtering can be now selected through relative times (last 7 days, last 30 days, etc) or absolute dates using date pickers.
|
||||
* Only the visits for last 30 days are loaded by default. You can change that at any moment if required.
|
||||
|
||||
* [#355](https://github.com/shlinkio/shlink-web-client/issues/355) Improved home page, fixing also its scrolling behavior for mobile devices.
|
||||
|
||||
### Changed
|
||||
* [#267](https://github.com/shlinkio/shlink-web-client/issues/267) Added some subtle but important improvements on UI/UX.
|
||||
* [#352](https://github.com/shlinkio/shlink-web-client/issues/352) Moved from Scrutinizer to Codecov as the code coverage backend.
|
||||
* [#217](https://github.com/shlinkio/shlink-web-client/issues/217) Improved how messages are displayed, by centralizing it in the `Message` and `Result` components.
|
||||
* [#219](https://github.com/shlinkio/shlink-web-client/issues/219) Improved error messages when something fails while interacting with Shlink's API.
|
||||
|
||||
### Deprecated
|
||||
* *Nothing*
|
||||
|
||||
### Removed
|
||||
* [#344](https://github.com/shlinkio/shlink-web-client/issues/344) Dropped support for Shlink v1.
|
||||
|
||||
### Fixed
|
||||
* *Nothing*
|
||||
|
||||
|
||||
## [2.6.2] - 2020-11-14
|
||||
### Added
|
||||
* *Nothing*
|
||||
|
||||
### Changed
|
||||
* [#325](https://github.com/shlinkio/shlink-web-client/issues/325) and [#294](https://github.com/shlinkio/shlink-web-client/issues/294) Updated all dependencies, including React 17, Typescript 4, react-datepicker 3 and Stryker 4.
|
||||
|
||||
### Deprecated
|
||||
* *Nothing*
|
||||
|
||||
### Removed
|
||||
* *Nothing*
|
||||
|
||||
### Fixed
|
||||
* [#334](https://github.com/shlinkio/shlink-web-client/issues/334) Fixed color-picker making the app crash when closing the modal without closing the color-picker, and then trying to open the modal again.
|
||||
* [#333](https://github.com/shlinkio/shlink-web-client/issues/333) Fixed visits getting accumulated every time the visits page is opened.
|
||||
|
||||
|
||||
## [2.6.1] - 2020-10-31
|
||||
### Added
|
||||
* *Nothing*
|
||||
|
||||
### Changed
|
||||
* [#292](https://github.com/shlinkio/shlink-web-client/issues/292) Improved a bit how caching works by removing the service worker and adding proper HTTP caching config on nginx inside docker image.
|
||||
|
||||
### Deprecated
|
||||
* *Nothing*
|
||||
|
||||
### Removed
|
||||
* *Nothing*
|
||||
|
||||
### Fixed
|
||||
* [#316](https://github.com/shlinkio/shlink-web-client/issues/316) Fixed manifest.json file not getting downloaded after passing credentials when the app is protected with basic auth.
|
||||
* [#311](https://github.com/shlinkio/shlink-web-client/issues/311) Fixed datepicker showing below other components.
|
||||
* [#306](https://github.com/shlinkio/shlink-web-client/issues/306) Fixed multi-arch docker builds by replacing node-sass with dart-sass.
|
||||
* [#328](https://github.com/shlinkio/shlink-web-client/issues/328) Fixed toggle switches getting broken in mobile resolutions.
|
||||
|
||||
|
||||
## [2.6.0] - 2020-09-20
|
||||
### Added
|
||||
* [#289](https://github.com/shlinkio/shlink-web-client/issues/289) Client and server version constraints are now links to the corresponding project release notes.
|
||||
* [#293](https://github.com/shlinkio/shlink-web-client/issues/293) Shlink versions are now always displayed in footer, hiding the server version when there's no connected server.
|
||||
* [#250](https://github.com/shlinkio/shlink-web-client/issues/250) Added support to group real time updates in fixed intervals.
|
||||
|
||||
The settings page now allows to provide the interval in which the UI should get updated, making that happen at once, with all the updates that have happened during that interval.
|
||||
|
||||
By default updates are immediately applied if real-time updates are enabled, to keep the behavior as it was.
|
||||
|
||||
* [#277](https://github.com/shlinkio/shlink-web-client/issues/277) Added highlighting capabilities to the visits line chart.
|
||||
|
||||
### Changed
|
||||
* [#150](https://github.com/shlinkio/shlink-web-client/issues/150) The list of short URLs is now ordered by the creation date, showing newest results first.
|
||||
* [#248](https://github.com/shlinkio/shlink-web-client/issues/248) Numbers displayed application-wide are now prettified.
|
||||
* [#40](https://github.com/shlinkio/shlink-web-client/issues/40) Migrated project to TypeScript.
|
||||
* [#297](https://github.com/shlinkio/shlink-web-client/issues/297) Moved docker image building to github actions.
|
||||
* [#305](https://github.com/shlinkio/shlink-web-client/issues/305) Split travis build so that every step is run in a parallel job.
|
||||
|
||||
### Deprecated
|
||||
* *Nothing*
|
||||
|
||||
### Removed
|
||||
* *Nothing*
|
||||
|
||||
### Fixed
|
||||
* [#295](https://github.com/shlinkio/shlink-web-client/issues/295) Fixed custom slug field not being disabled when selecting a short code length.
|
||||
* [#301](https://github.com/shlinkio/shlink-web-client/issues/301) Fixed tags visits loading not being cancelled when leaving visits page.
|
||||
|
||||
|
||||
## [2.5.1] - 2020-06-06
|
||||
### Added
|
||||
* *Nothing*
|
||||
|
||||
### Changed
|
||||
* [#254](https://github.com/shlinkio/shlink-web-client/issues/254) Reduced duplication on code to handle mercure topics binding.
|
||||
|
||||
### Deprecated
|
||||
* *Nothing*
|
||||
|
||||
### Removed
|
||||
* *Nothing*
|
||||
|
||||
### Fixed
|
||||
* [#276](https://github.com/shlinkio/shlink-web-client/issues/276) Fixed default grouping used for visits line chart, making it be dynamic depending on how old the short URL is.
|
||||
* [#280](https://github.com/shlinkio/shlink-web-client/issues/280) Fixed shlink-web-client version not being properly passed when building stable tags of the docker image.
|
||||
* [#269](https://github.com/shlinkio/shlink-web-client/issues/269) Fixed doughnut chart legends getting to big and hiding charts on mobile devices.
|
||||
|
||||
|
||||
## [2.5.0] - 2020-05-31
|
||||
### Added
|
||||
* [#148](https://github.com/shlinkio/shlink-web-client/issues/148) Added support for real-time updates when consuming a Shlink version that is integrated with a mercure hub server.
|
||||
|
||||
The integration is transparent. When a server is opened, shlink-web-client will try to get the mercure info from it.
|
||||
@@ -27,28 +347,22 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
|
||||
* [#149](https://github.com/shlinkio/shlink-web-client/issues/149) and [#198](https://github.com/shlinkio/shlink-web-client/issues/198) Added new line chart to visits and tags stats which displays amount of visits during selected time period, grouped by month, week, day or hour.
|
||||
|
||||
#### Changed
|
||||
|
||||
### Changed
|
||||
* [#218](https://github.com/shlinkio/shlink-web-client/issues/218) Added back button to sections not displayed in left menu.
|
||||
* [#255](https://github.com/shlinkio/shlink-web-client/issues/255) Improved how servers and settings are persisted in the local storage.
|
||||
|
||||
#### Deprecated
|
||||
|
||||
### Deprecated
|
||||
* *Nothing*
|
||||
|
||||
#### Removed
|
||||
|
||||
### Removed
|
||||
* *Nothing*
|
||||
|
||||
#### Fixed
|
||||
|
||||
### Fixed
|
||||
* [#262](https://github.com/shlinkio/shlink-web-client/issues/262) Fixed charts displaying decimal numbers, when visits are absolute and that makes no sense.
|
||||
|
||||
|
||||
## 2.4.0 - 2020-04-10
|
||||
|
||||
#### Added
|
||||
|
||||
## [2.4.0] - 2020-04-10
|
||||
### Added
|
||||
* [#199](https://github.com/shlinkio/shlink-web-client/issues/199) Added table to visits page which displays the information in a paginated, sortable and filterable list.
|
||||
|
||||
It also supports selecting multiple visits in the table which makes the corresponding data to be highlighted in the visits charts.
|
||||
@@ -63,432 +377,330 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
* [#234](https://github.com/shlinkio/shlink-web-client/issues/234) Allowed short code length to be edited on any new short URL when using Shlink 2.1 or higher.
|
||||
* [#235](https://github.com/shlinkio/shlink-web-client/issues/235) Allowed editing the long URL for any existing short URL when suing Shlink 2.1 or higher.
|
||||
|
||||
#### Changed
|
||||
|
||||
### Changed
|
||||
* [#205](https://github.com/shlinkio/shlink-web-client/issues/205) Replaced `jest-each` package by jet's native `test.each` function.
|
||||
* [#209](https://github.com/shlinkio/shlink-web-client/issues/209) Replaced `Unknown` by `Direct` for visits from undetermined referrers.
|
||||
* [#212](https://github.com/shlinkio/shlink-web-client/issues/212) Moved copy-to-clipboard next to short URL.
|
||||
* [#208](https://github.com/shlinkio/shlink-web-client/issues/208) Short URLs list paginator is now progressive.
|
||||
|
||||
#### Deprecated
|
||||
|
||||
### Deprecated
|
||||
* *Nothing*
|
||||
|
||||
#### Removed
|
||||
|
||||
### Removed
|
||||
* *Nothing*
|
||||
|
||||
#### Fixed
|
||||
|
||||
### Fixed
|
||||
* [#243](https://github.com/shlinkio/shlink-web-client/issues/243) Fixed loading state and resetting on short URL creation form.
|
||||
* [#239](https://github.com/shlinkio/shlink-web-client/issues/239) Fixed how user agents are parsed, reducing false results.
|
||||
|
||||
|
||||
## 2.3.1 - 2020-02-08
|
||||
|
||||
#### Added
|
||||
|
||||
## [2.3.1] - 2020-02-08
|
||||
### Added
|
||||
* *Nothing*
|
||||
|
||||
#### Changed
|
||||
|
||||
### Changed
|
||||
* [#191](https://github.com/shlinkio/shlink-web-client/issues/191) Created `ForServerVersion` helper component which dynamically renders children if current server conditions are met.
|
||||
* [#189](https://github.com/shlinkio/shlink-web-client/issues/189) Simplified short url tags and short url deletion components and reducers, by removing redundant actions.
|
||||
|
||||
#### Deprecated
|
||||
|
||||
### Deprecated
|
||||
* *Nothing*
|
||||
|
||||
#### Removed
|
||||
|
||||
### Removed
|
||||
* *Nothing*
|
||||
|
||||
#### Fixed
|
||||
|
||||
### Fixed
|
||||
* [#193](https://github.com/shlinkio/shlink-web-client/issues/193) Fixed `maxVisits` being set to 0 when trying to reset it from having a value to `null`.
|
||||
* [#196](https://github.com/shlinkio/shlink-web-client/issues/196) Included apache `.htaccess` file which takes care of falling back to index.html when reloading the page on a client-side handled route.
|
||||
* [#179](https://github.com/shlinkio/shlink-web-client/issues/179) Ensured domain is provided to Shlink server when editing, deleting or fetching short URLs which do not belong to default domain.
|
||||
* [#202](https://github.com/shlinkio/shlink-web-client/issues/202) Fixed domain not passed when dispatching actions that affect a single short URL (edit tags, edit meta and delete), which cased the list not to be properly updated.
|
||||
|
||||
|
||||
## 2.3.0 - 2020-01-19
|
||||
|
||||
#### Added
|
||||
|
||||
## [2.3.0] - 2020-01-19
|
||||
### Added
|
||||
* [#174](https://github.com/shlinkio/shlink-web-client/issues/174) Added complete support for Shlink v2.x together with currently supported Shlink versions.
|
||||
* [#164](https://github.com/shlinkio/shlink-web-client/issues/164) Added max visits control on those URLs which have `maxVisits`.
|
||||
* [#178](https://github.com/shlinkio/shlink-web-client/issues/178) Short URLs list can now be filtered by date range.
|
||||
* [#46](https://github.com/shlinkio/shlink-web-client/issues/46) Allowed short URL's metadata to be edited (`maxVisits`, `validSince` and `validUntil`).
|
||||
|
||||
#### Changed
|
||||
|
||||
### Changed
|
||||
* *Nothing*
|
||||
|
||||
#### Deprecated
|
||||
|
||||
### Deprecated
|
||||
* *Nothing*
|
||||
|
||||
#### Removed
|
||||
|
||||
### Removed
|
||||
* *Nothing*
|
||||
|
||||
#### Fixed
|
||||
|
||||
### Fixed
|
||||
* [#170](https://github.com/shlinkio/shlink-web-client/issues/170) Fixed apple icon referencing to incorrect file names.
|
||||
|
||||
|
||||
## 2.2.2 - 2019-10-21
|
||||
|
||||
#### Added
|
||||
|
||||
## [2.2.2] - 2019-10-21
|
||||
### Added
|
||||
* *Nothing*
|
||||
|
||||
#### Changed
|
||||
|
||||
### Changed
|
||||
* *Nothing*
|
||||
|
||||
#### Deprecated
|
||||
|
||||
### Deprecated
|
||||
* *Nothing*
|
||||
|
||||
#### Removed
|
||||
|
||||
### Removed
|
||||
* *Nothing*
|
||||
|
||||
#### Fixed
|
||||
|
||||
### Fixed
|
||||
* [#167](https://github.com/shlinkio/shlink-web-client/issues/167) Fixed `/servers.json` path not being ignored when returning something other than an array.
|
||||
|
||||
|
||||
## 2.2.1 - 2019-10-18
|
||||
|
||||
#### Added
|
||||
|
||||
## [2.2.1] - 2019-10-18
|
||||
### Added
|
||||
* *Nothing*
|
||||
|
||||
#### Changed
|
||||
|
||||
### Changed
|
||||
* *Nothing*
|
||||
|
||||
#### Deprecated
|
||||
|
||||
### Deprecated
|
||||
* *Nothing*
|
||||
|
||||
#### Removed
|
||||
|
||||
### Removed
|
||||
* *Nothing*
|
||||
|
||||
#### Fixed
|
||||
|
||||
### Fixed
|
||||
* [#165](https://github.com/shlinkio/shlink-web-client/issues/165) Fixed error thrown when opening "create" page while using a Shlink version which does not return a valid SemVer version (like `latest` docker image, or any development instance).
|
||||
|
||||
|
||||
## 2.2.0 - 2019-10-05
|
||||
|
||||
#### Added
|
||||
|
||||
## [2.2.0] - 2019-10-05
|
||||
### Added
|
||||
* [#144](https://github.com/shlinkio/shlink-web-client/issues/144) Added domain input to create domain page.
|
||||
|
||||
#### Changed
|
||||
|
||||
### Changed
|
||||
* [#140](https://github.com/shlinkio/shlink-web-client/issues/140) Updated project dependencies.
|
||||
|
||||
#### Deprecated
|
||||
|
||||
### Deprecated
|
||||
* *Nothing*
|
||||
|
||||
#### Removed
|
||||
|
||||
### Removed
|
||||
* *Nothing*
|
||||
|
||||
#### Fixed
|
||||
|
||||
### Fixed
|
||||
* *Nothing*
|
||||
|
||||
|
||||
## 2.1.1 - 2019-09-22
|
||||
|
||||
#### Added
|
||||
|
||||
## [2.1.1] - 2019-09-22
|
||||
### Added
|
||||
* *Nothing*
|
||||
|
||||
#### Changed
|
||||
|
||||
### Changed
|
||||
* [#142](https://github.com/shlinkio/shlink-web-client/issues/142) Updated to newer versions of base docker images for dev and production.
|
||||
|
||||
#### Deprecated
|
||||
|
||||
### Deprecated
|
||||
* *Nothing*
|
||||
|
||||
#### Removed
|
||||
|
||||
### Removed
|
||||
* *Nothing*
|
||||
|
||||
#### Fixed
|
||||
|
||||
### Fixed
|
||||
* [#151](https://github.com/shlinkio/shlink-web-client/issues/151) Fixed "order by" indicator (caret) still indicate ASC on column header when no order is specified.
|
||||
* [#157](https://github.com/shlinkio/shlink-web-client/issues/157) Fixed pagination control on graphs expanding too much when lots of pages need to be rendered.
|
||||
* [#155](https://github.com/shlinkio/shlink-web-client/issues/155) Fixed client-side paths resolve to 404 when served from nginx in docker image instead of falling back to `index.html`.
|
||||
|
||||
|
||||
## 2.1.0 - 2019-05-19
|
||||
|
||||
#### Added
|
||||
|
||||
## [2.1.0] - 2019-05-19
|
||||
### Added
|
||||
* [#101](https://github.com/shlinkio/shlink-web-client/issues/101) Added checkbox to short URL creation form that allows to determine the value of the `findIfExists` flag introduced in Shlink v1.16.0.
|
||||
* [#105](https://github.com/shlinkio/shlink-web-client/issues/105) Added support to pre-configure servers. See [how to pre-configure servers](README.md#pre-configuring-servers) to get more details on how to do it.
|
||||
|
||||
#### Changed
|
||||
|
||||
### Changed
|
||||
* [#125](https://github.com/shlinkio/shlink-web-client/issues/125) Refactored reducers to replace `switch` statements by `handleActions` from [redux-actions](https://github.com/redux-utilities/redux-actions).
|
||||
* [#116](https://github.com/shlinkio/shlink-web-client/issues/116) Removed sinon in favor of jest mocks.
|
||||
* [#72](https://github.com/shlinkio/shlink-web-client/issues/72) Increased code coverage up to 80%.
|
||||
|
||||
#### Deprecated
|
||||
|
||||
### Deprecated
|
||||
* *Nothing*
|
||||
|
||||
#### Removed
|
||||
|
||||
### Removed
|
||||
* *Nothing*
|
||||
|
||||
#### Fixed
|
||||
|
||||
### Fixed
|
||||
* *Nothing*
|
||||
|
||||
|
||||
## 2.0.3 - 2019-03-16
|
||||
|
||||
#### Added
|
||||
|
||||
## [2.0.3] - 2019-03-16
|
||||
### Added
|
||||
* *Nothing*
|
||||
|
||||
#### Changed
|
||||
|
||||
### Changed
|
||||
* *Nothing*
|
||||
|
||||
#### Deprecated
|
||||
|
||||
### Deprecated
|
||||
* *Nothing*
|
||||
|
||||
#### Removed
|
||||
|
||||
### Removed
|
||||
* *Nothing*
|
||||
|
||||
#### Fixed
|
||||
|
||||
### Fixed
|
||||
* [#120](https://github.com/shlinkio/shlink-web-client/issues/120) Fixed crash when visits page is loaded and there are no visits with known cities.
|
||||
* [#113](https://github.com/shlinkio/shlink-web-client/issues/113) Ensured visits loading is cancelled when the visits page is unmounted. Requests on flight will still finish.
|
||||
* [#118](https://github.com/shlinkio/shlink-web-client/issues/118) Fixed chart crashing when trying to render lots of bars by adding pagination.
|
||||
|
||||
|
||||
## 2.0.2 - 2019-03-04
|
||||
|
||||
#### Added
|
||||
|
||||
## [2.0.2] - 2019-03-04
|
||||
### Added
|
||||
* *Nothing*
|
||||
|
||||
#### Changed
|
||||
|
||||
### Changed
|
||||
* *Nothing*
|
||||
|
||||
#### Deprecated
|
||||
|
||||
### Deprecated
|
||||
* *Nothing*
|
||||
|
||||
#### Removed
|
||||
|
||||
### Removed
|
||||
* *Nothing*
|
||||
|
||||
#### Fixed
|
||||
|
||||
### Fixed
|
||||
* [#103](https://github.com/shlinkio/shlink-web-client/issues/103) Fixed visits page getting freezed when loading large amounts of visits.
|
||||
* [#111](https://github.com/shlinkio/shlink-web-client/issues/111) Fixed crash when trying to load a map modal with only one location.
|
||||
* [#115](https://github.com/shlinkio/shlink-web-client/issues/115) Created `ErrorHandler` component which will prevent crashes in app to make it unusable.
|
||||
|
||||
|
||||
## 2.0.1 - 2019-03-03
|
||||
|
||||
#### Added
|
||||
|
||||
## [2.0.1] - 2019-03-03
|
||||
### Added
|
||||
* *Nothing*
|
||||
|
||||
#### Changed
|
||||
|
||||
### Changed
|
||||
* [#106](https://github.com/shlinkio/shlink-web-client/issues/106) Reduced size of docker image by using a multi-stage build Dockerfile.
|
||||
* [#95](https://github.com/shlinkio/shlink-web-client/issues/95) Tested docker image build during travis executions.
|
||||
|
||||
#### Deprecated
|
||||
|
||||
### Deprecated
|
||||
* *Nothing*
|
||||
|
||||
#### Removed
|
||||
|
||||
### Removed
|
||||
* *Nothing*
|
||||
|
||||
#### Fixed
|
||||
|
||||
### Fixed
|
||||
* [#104](https://github.com/shlinkio/shlink-web-client/issues/104) Fixed blank page being showed when not-found paths are loaded.
|
||||
* [#94](https://github.com/shlinkio/shlink-web-client/issues/94) Fixed initial zoom and center on maps.
|
||||
* [#93](https://github.com/shlinkio/shlink-web-client/issues/93) Prevented side menu to be swipeable while a modal window is displayed.
|
||||
|
||||
|
||||
## 2.0.0 - 2019-01-13
|
||||
|
||||
#### Added
|
||||
|
||||
## [2.0.0] - 2019-01-13
|
||||
### Added
|
||||
* [#54](https://github.com/shlinkio/shlink-web-client/issues/54) Added stats by city graphic in visits page.
|
||||
* [#55](https://github.com/shlinkio/shlink-web-client/issues/55) Added map in visits page locating cities from which visits have occurred.
|
||||
|
||||
#### Changed
|
||||
|
||||
### Changed
|
||||
* [#87](https://github.com/shlinkio/shlink-web-client/issues/87) and [#89](https://github.com/shlinkio/shlink-web-client/issues/89) Updated all dependencies to latest major versions.
|
||||
* [#96](https://github.com/shlinkio/shlink-web-client/issues/96) Updated visits page to load visits in multiple paginated requests of `5000` visits when used shlink server supports it. This will prevent shlink to hang when trying to load big amounts of visits.
|
||||
* [#71](https://github.com/shlinkio/shlink-web-client/issues/71) Improved tests and increased code coverage.
|
||||
|
||||
#### Deprecated
|
||||
|
||||
### Deprecated
|
||||
* *Nothing*
|
||||
|
||||
#### Removed
|
||||
|
||||
### Removed
|
||||
* [#59](https://github.com/shlinkio/shlink-web-client/issues/59) Dropped support for old browsers. Internet explorer and dead browsers are no longer supported.
|
||||
* [#97](https://github.com/shlinkio/shlink-web-client/issues/97) Dropped support for authentication via `Authorization` header with Bearer type and JWT, which will make this version no longer work with shlink earlier than v1.13.0.
|
||||
|
||||
#### Fixed
|
||||
|
||||
### Fixed
|
||||
* *Nothing*
|
||||
|
||||
|
||||
## 1.2.1 - 2018-12-21
|
||||
|
||||
#### Added
|
||||
|
||||
## [1.2.1] - 2018-12-21
|
||||
### Added
|
||||
* *Nothing*
|
||||
|
||||
#### Changed
|
||||
|
||||
### Changed
|
||||
* [#80](https://github.com/shlinkio/shlink-web-client/issues/80) Deeply refactored app to do true dependency injection with an IoC container.
|
||||
* [#79](https://github.com/shlinkio/shlink-web-client/issues/79) Updated to nginx 1.15.7 as the base docker image.
|
||||
* [#75](https://github.com/shlinkio/shlink-web-client/issues/75) Prevented duplicated `yarn build` in travis when a tag exists.
|
||||
|
||||
#### Deprecated
|
||||
|
||||
### Deprecated
|
||||
* *Nothing*
|
||||
|
||||
#### Removed
|
||||
|
||||
### Removed
|
||||
* *Nothing*
|
||||
|
||||
#### Fixed
|
||||
|
||||
### Fixed
|
||||
* [#77](https://github.com/shlinkio/shlink-web-client/issues/77) Sortable graphs ordering is now case insensitive.
|
||||
|
||||
|
||||
## 1.2.0 - 2018-11-01
|
||||
|
||||
#### Added
|
||||
|
||||
## [1.2.0] - 2018-11-01
|
||||
### Added
|
||||
* [#65](https://github.com/shlinkio/shlink-web-client/issues/65) Added sorting to both countries and referrers stats graphs.
|
||||
* [#14](https://github.com/shlinkio/shlink-web-client/issues/14) Documented how to build the project so that it can be served from a subpath.
|
||||
|
||||
#### Changed
|
||||
|
||||
### Changed
|
||||
* [#50](https://github.com/shlinkio/shlink-web-client/issues/50) Improved tests and increased code coverage.
|
||||
|
||||
#### Deprecated
|
||||
|
||||
### Deprecated
|
||||
* *Nothing*
|
||||
|
||||
#### Removed
|
||||
|
||||
### Removed
|
||||
* *Nothing*
|
||||
|
||||
#### Fixed
|
||||
|
||||
### Fixed
|
||||
* [#66](https://github.com/shlinkio/shlink-web-client/issues/66) Fixed tooltips in graphs with too small bars not being displayed.
|
||||
|
||||
|
||||
## 1.1.1 - 2018-10-20
|
||||
|
||||
#### Added
|
||||
|
||||
## [1.1.1] - 2018-10-20
|
||||
### Added
|
||||
* [#57](https://github.com/shlinkio/shlink-web-client/issues/57) Automated release generation in travis build.
|
||||
|
||||
#### Changed
|
||||
|
||||
### Changed
|
||||
* *Nothing*
|
||||
|
||||
#### Deprecated
|
||||
|
||||
### Deprecated
|
||||
* *Nothing*
|
||||
|
||||
#### Removed
|
||||
|
||||
### Removed
|
||||
* *Nothing*
|
||||
|
||||
#### Fixed
|
||||
|
||||
### Fixed
|
||||
* [#63](https://github.com/shlinkio/shlink-web-client/issues/63) Improved how bar charts are rendered in stats page, making them try to calculate a bigger height for big data sets.
|
||||
* [#56](https://github.com/shlinkio/shlink-web-client/issues/56) Ensured `ColorGenerator` matches keys in a case insensitive way.
|
||||
* [#53](https://github.com/shlinkio/shlink-web-client/issues/53) Fixed missing margin between date fields in visits page for mobile devices.
|
||||
|
||||
|
||||
## 1.1.0 - 2018-09-16
|
||||
|
||||
#### Added
|
||||
|
||||
## [1.1.0] - 2018-09-16
|
||||
### Added
|
||||
* [#47](https://github.com/shlinkio/shlink-web-client/issues/47) Added support to delete short URLs (requires [shlink v1.12.0](https://github.com/shlinkio/shlink/releases/tag/v1.12.0) or greater).
|
||||
|
||||
#### Changed
|
||||
|
||||
### Changed
|
||||
* [#35](https://github.com/shlinkio/shlink-web-client/issues/35) Visits component split into two, which makes the header not to be refreshed when filtering by date, and also the visits global counter now reflects the actual number of visits which fulfill current filter.
|
||||
* [#36](https://github.com/shlinkio/shlink-web-client/issues/36) Tags selector now autocompletes existing tag names, to prevent typos and ease reusing existing tags.
|
||||
* [#39](https://github.com/shlinkio/shlink-web-client/issues/39) Defined `propTypes` as static properties in class components.
|
||||
|
||||
#### Deprecated
|
||||
|
||||
### Deprecated
|
||||
* *Nothing*
|
||||
|
||||
#### Removed
|
||||
|
||||
### Removed
|
||||
* *Nothing*
|
||||
|
||||
#### Fixed
|
||||
|
||||
### Fixed
|
||||
* [#49](https://github.com/shlinkio/shlink-web-client/issues/49) Ensured filtering parameters are reseted when list component is unmounted so that params are not mixed when coming back.
|
||||
* [#45](https://github.com/shlinkio/shlink-web-client/issues/45) Ensured graphs x-axis start at `0` and don't use decimals.
|
||||
* [#51](https://github.com/shlinkio/shlink-web-client/issues/51) When editing short URL tags, the value returned form server is used when refreshing the list, which is normalized.
|
||||
|
||||
|
||||
## 1.0.1 - 2018-09-02
|
||||
|
||||
#### Added
|
||||
|
||||
## [1.0.1] - 2018-09-02
|
||||
### Added
|
||||
* *Nothing*
|
||||
|
||||
#### Changed
|
||||
|
||||
### Changed
|
||||
* *Nothing*
|
||||
|
||||
#### Deprecated
|
||||
|
||||
### Deprecated
|
||||
* *Nothing*
|
||||
|
||||
#### Removed
|
||||
|
||||
### Removed
|
||||
* *Nothing*
|
||||
|
||||
#### Fixed
|
||||
|
||||
### Fixed
|
||||
* [#42](https://github.com/shlinkio/shlink-web-client/issues/42) Fixed selected tags lost when navigating between pages in short URLs list.
|
||||
* [#43](https://github.com/shlinkio/shlink-web-client/issues/43) Fixed "List short URLs" menu item only selected when in first page.
|
||||
|
||||
|
||||
## 1.0.0 - 2018-08-26
|
||||
|
||||
#### Added
|
||||
|
||||
## [1.0.0] - 2018-08-26
|
||||
### Added
|
||||
* [#4](https://github.com/shlinkio/shlink-web-client/issues/4) Now it is possible to export and import servers.
|
||||
|
||||
* Export all servers in a CSV file.
|
||||
@@ -505,69 +717,53 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
* [#22](https://github.com/shlinkio/shlink-web-client/issues/22) Improved code coverage.
|
||||
* [#28](https://github.com/shlinkio/shlink-web-client/issues/28) Added integration with [Scrutinizer](https://scrutinizer-ci.com/g/shlinkio/shlink-web-client/).
|
||||
|
||||
#### Changed
|
||||
|
||||
### Changed
|
||||
* [#33](https://github.com/shlinkio/shlink-web-client/issues/33) Changed to [adidas coding style](https://github.com/adidas/js-linter-configs) for Javascript.
|
||||
* [#32](https://github.com/shlinkio/shlink-web-client/issues/32) Changed to [adidas coding style](https://github.com/adidas/js-linter-configs) for stylesheets.
|
||||
* [#26](https://github.com/shlinkio/shlink-web-client/issues/26) The tags input now displays tags using their actual color.
|
||||
|
||||
#### Deprecated
|
||||
|
||||
### Deprecated
|
||||
* *Nothing*
|
||||
|
||||
#### Removed
|
||||
|
||||
### Removed
|
||||
* *Nothing*
|
||||
|
||||
#### Fixed
|
||||
|
||||
### Fixed
|
||||
* *Nothing*
|
||||
|
||||
|
||||
## 0.2.0 - 2018-08-12
|
||||
|
||||
#### Added
|
||||
|
||||
## [0.2.0] - 2018-08-12
|
||||
### Added
|
||||
* [#12](https://github.com/shlinkio/shlink-web-client/issues/12) Improved code coverage
|
||||
* [#20](https://github.com/shlinkio/shlink-web-client/issues/20) Added servers list in welcome page, as well as added link to create one when none exist.
|
||||
|
||||
#### Changed
|
||||
|
||||
### Changed
|
||||
* [#11](https://github.com/shlinkio/shlink-web-client/issues/11) Improved app icons fro progressive web apps.
|
||||
|
||||
#### Deprecated
|
||||
|
||||
### Deprecated
|
||||
* *Nothing*
|
||||
|
||||
#### Removed
|
||||
|
||||
### Removed
|
||||
* *Nothing*
|
||||
|
||||
#### Fixed
|
||||
|
||||
### Fixed
|
||||
* [#19](https://github.com/shlinkio/shlink-web-client/issues/19) Added workaround in tags input so that it is possible to add tags on Android devices.
|
||||
* [#17](https://github.com/shlinkio/shlink-web-client/issues/17) Fixed short URLs list not being sortable in mobile resolutions.
|
||||
* [#13](https://github.com/shlinkio/shlink-web-client/issues/13) Improved visits page on mobile resolutions.
|
||||
|
||||
|
||||
## 0.1.1 - 2018-08-06
|
||||
|
||||
#### Added
|
||||
|
||||
## [0.1.1] - 2018-08-06
|
||||
### Added
|
||||
* [#15](https://github.com/shlinkio/shlink-web-client/issues/15) Added a `Dockerfile` that can be used to generate a distributable docker image
|
||||
|
||||
#### Changed
|
||||
|
||||
### Changed
|
||||
* *Nothing*
|
||||
|
||||
#### Deprecated
|
||||
|
||||
### Deprecated
|
||||
* *Nothing*
|
||||
|
||||
#### Removed
|
||||
|
||||
### Removed
|
||||
* *Nothing*
|
||||
|
||||
#### Fixed
|
||||
|
||||
### Fixed
|
||||
* *Nothing*
|
||||
|
||||
72
CONTRIBUTING.md
Normal file
72
CONTRIBUTING.md
Normal file
@@ -0,0 +1,72 @@
|
||||
# Contributing
|
||||
|
||||
This file will guide you through the process of getting to project up and running, in case you want to provide coding contributions.
|
||||
|
||||
You will also see how to ensure the code fulfills the expected code checks, and how to create a pull request.
|
||||
|
||||
## System dependencies
|
||||
|
||||
The project can be run inside a docker container through provided docker-compose configuration.
|
||||
|
||||
Because of this, the only actual dependencies are [docker](https://docs.docker.com/get-docker/) and [docker-compose](https://docs.docker.com/compose/install/).
|
||||
|
||||
## Setting up the project
|
||||
|
||||
The first thing you need to do is fork the repository, and clone it in your local machine.
|
||||
|
||||
Then you will have to follow these steps:
|
||||
|
||||
* Copy the file `docker-compose.override.yml.dist` by also removing the `dist` extension.
|
||||
* Start-up the project by running `docker-compose up`.
|
||||
|
||||
Once this is finished, you will have the project exposed in port `3000` (http://localhost:3000).
|
||||
|
||||
## Project structure
|
||||
|
||||
This project is a [react](https://reactjs.org/) & [redux](https://redux.js.org/) application, built with [typescript](https://www.typescriptlang.org/), which is distributed as a 100% client-side progressive web application.
|
||||
|
||||
This is the basic project structure:
|
||||
|
||||
```
|
||||
shlink-web-client
|
||||
├── config
|
||||
├── public
|
||||
├── scripts
|
||||
├── src
|
||||
├── test
|
||||
├── package.json
|
||||
└── README.md
|
||||
```
|
||||
|
||||
* `config`: It contains some configuration scripts, used during testing, linting and building of the project.
|
||||
* `public`: Will act as the application document root once built, and contains some static assets (favicons, images, etc).
|
||||
* `scripts`: It has some of the CLI scripts used to run tests or building.
|
||||
* `src`: Contains the main source code of the application, including both web components, SASS stylesheets and files with logic.
|
||||
* `test`: Contains the project tests.
|
||||
|
||||
## Running code checks
|
||||
|
||||
> Note: The `indocker` shell script is a helper used to run commands inside the docker container.
|
||||
|
||||
* `./indocker npm run lint`: Checks coding styles are fulfilled, both in JS/TS files as well as in stylesheets.
|
||||
* `./indocker npm run lint:js`: Checks coding styles are fulfilled in JS/TS files.
|
||||
* `./indocker npm run lint:css`: Checks coding styles are fulfilled in stylesheets.
|
||||
* `./indocker npm run lint:js:fix`: Fixes coding styles in JS/TS files.
|
||||
* `./indocker npm run lint:css:fix`: Fixes coding styles in stylesheets.
|
||||
* `./indocker npm run test`: Runs unit tests with Jest.
|
||||
* `./indocker npm run mutate`: Runs mutation tests with StrykerJS (this command can be very slow).
|
||||
|
||||
## Building the project
|
||||
|
||||
The source code in this project cannot be run directly in a web browser, you need to build it first.
|
||||
|
||||
* `./indocker npm run build`: Builds the project using a combination of `webpack`, `babel` and `tsc`, generating the final static files. The content is placed in the `build` folder, which is automatically created if it does not exist.
|
||||
* `./indocker npm run serve:build`: Serves the static files inside the `build` folder in port 5000 (http://localhost:5000). Useful to test the content built with previous command.
|
||||
|
||||
## Pull request process
|
||||
|
||||
In order to provide pull requests to this project, you should always start by creating a new branch, where you will make all desired changes.
|
||||
|
||||
The base branch should always be `main`, and the target branch for the pull request should also be `main`.
|
||||
|
||||
Before your branch can be merged, all the checks described in [Running code checks](#running-code-checks) have to be passing. You can verify that manually, or wait for the build to be run automatically after the pull request is created.
|
||||
@@ -1,12 +1,13 @@
|
||||
FROM node:12.16.3-alpine as node
|
||||
FROM node:14.17-alpine as node
|
||||
COPY . /shlink-web-client
|
||||
ARG VERSION="latest"
|
||||
ENV VERSION ${VERSION}
|
||||
RUN cd /shlink-web-client && \
|
||||
npm install && npm run build -- ${VERSION} --no-dist
|
||||
|
||||
FROM nginx:1.17.10-alpine
|
||||
FROM nginx:1.21-alpine
|
||||
LABEL maintainer="Alejandro Celaya <alejandro@alejandrocelaya.com>"
|
||||
RUN rm -r /usr/share/nginx/html && rm /etc/nginx/conf.d/default.conf
|
||||
COPY config/docker/nginx.conf /etc/nginx/conf.d/default.conf
|
||||
COPY scripts/docker/servers_from_env.sh /docker-entrypoint.d/30-shlink-servers-json.sh
|
||||
COPY --from=node /shlink-web-client/build /usr/share/nginx/html
|
||||
|
||||
38
README.md
38
README.md
@@ -1,17 +1,18 @@
|
||||
# shlink-web-client
|
||||
|
||||
[](https://travis-ci.org/shlinkio/shlink-web-client)
|
||||
[](https://scrutinizer-ci.com/g/shlinkio/shlink-web-client/?branch=master)
|
||||
[](https://scrutinizer-ci.com/g/shlinkio/shlink-web-client/?branch=master)
|
||||
[](https://github.com/shlinkio/shlink-web-client/actions?query=workflow%3A%22Continuous+integration%22)
|
||||
[](https://app.codecov.io/gh/shlinkio/shlink-web-client)
|
||||
[](https://github.com/shlinkio/shlink-web-client/releases/latest)
|
||||
[](https://hub.docker.com/r/shlinkio/shlink-web-client/)
|
||||
[](https://github.com/shlinkio/shlink-web-client/blob/master/LICENSE)
|
||||
[](https://hub.docker.com/r/shlinkio/shlink-web-client/)
|
||||
[](https://github.com/shlinkio/shlink-web-client/blob/main/LICENSE)
|
||||
[](https://slnk.to/donate)
|
||||
|
||||
A ReactJS-based progressive web application for [Shlink](https://shlink.io).
|
||||
|
||||

|
||||
|
||||
> If you are trying to find out how to run the project in development mode or how to provide contributions, read the [CONTRIBUTING](CONTRIBUTING.md) doc.
|
||||
|
||||
## Installation
|
||||
|
||||
There are three ways in which you can use this application.
|
||||
@@ -67,6 +68,33 @@ Those servers can be exported and imported in other browsers, but if for some re
|
||||
If you are using the shlink-web-client docker image, you can mount the `servers.json` file in a volume inside `/usr/share/nginx/html`, which is the app's document root inside the container.
|
||||
|
||||
docker run --name shlink-web-client -p 8000:80 -v ${PWD}/servers.json:/usr/share/nginx/html/servers.json shlinkio/shlink-web-client
|
||||
|
||||
Alternatively, you can mount a `conf.d` directory, which in turn contains the `servers.json` file, in a volume inside `/usr/share/nginx/html`. *(since shlink-web-client 3.2.0)*.
|
||||
|
||||
docker run --name shlink-web-client -p 8000:80 -v ${PWD}/my-config/:/usr/share/nginx/html/conf.d/ shlinkio/shlink-web-client
|
||||
|
||||
If you want to pre-configure a single server, you can provide its config via env vars. When the container starts up, it will build the `servers.json` file dynamically based on them. *(since shlink-web-client 3.2.0)*.
|
||||
|
||||
* `SHLINK_SERVER_URL`: The fully qualified URL for the Shlink server.
|
||||
* `SHLINK_SERVER_API_KEY`: The API key.
|
||||
* `SHLINK_SERVER_NAME`: The name to be displayed. Defaults to **Shlink** if not provided.
|
||||
|
||||
```shell
|
||||
docker run \
|
||||
--name shlink-web-client \
|
||||
-p 8000:80 \
|
||||
-e SHLINK_SERVER_URL=https://doma.in \
|
||||
-e SHLINK_SERVER_API_KEY=6aeb82c6-e275-4538-a747-31f9abfba63c \
|
||||
shlinkio/shlink-web-client
|
||||
```
|
||||
|
||||
> **Be extremely careful when using this feature.**
|
||||
>
|
||||
> Due to shlink-web-client's client-side nature, the file needs to be accessible from the browser.
|
||||
>
|
||||
> Because of that, make sure you use this only when you self-host shlink-web-client, and you know only trusted people will have access to it.
|
||||
>
|
||||
> Failing to do this could cause your API keys to end up being exposed.
|
||||
|
||||
## Serve project in subpath
|
||||
|
||||
|
||||
@@ -4,11 +4,31 @@ server {
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
# Expire rules for static content
|
||||
# HTML files should never be cached. There's only one here, which is the entry point (index.html)
|
||||
location ~* \.(?:manifest|appcache|html?|xml|json)$ {
|
||||
expires -1;
|
||||
}
|
||||
# Images and other binary assets can be saved for a month
|
||||
location ~* \.(?:jpg|jpeg|gif|png|ico|cur|gz|svg|svgz|mp4|ogg|ogv|webm|htc)$ {
|
||||
expires 1M;
|
||||
add_header Cache-Control "public";
|
||||
}
|
||||
# JS and CSS files can be saved for a year, as they are always hashed. New versions will include a new hash anyway, forcing the download
|
||||
location ~* \.(?:css|js)$ {
|
||||
expires 1y;
|
||||
add_header Cache-Control "public";
|
||||
}
|
||||
|
||||
# servers.json may be on the root, or in conf.d directory
|
||||
location = /servers.json {
|
||||
try_files /servers.json /conf.d/servers.json;
|
||||
}
|
||||
|
||||
# When requesting static paths with extension, try them, and return a 404 if not found
|
||||
location ~* .+\.(css|js|html|png|jpe?g|gif|bmp|ico|json|csv|otf|eot|svg|svgz|ttf|woff|woff2|ijmap|pdf|tif|map) {
|
||||
try_files $uri $uri/ =404;
|
||||
}
|
||||
|
||||
# When requesting a path without extension, try it, and return the index if not found
|
||||
# This allows HTML5 history paths to be handled by the client application
|
||||
location / {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable @typescript-eslint/prefer-nullish-coalescing, @typescript-eslint/promise-function-async, @typescript-eslint/prefer-optional-chain */
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
@@ -10,7 +11,7 @@ const { NODE_ENV } = process.env;
|
||||
|
||||
if (!NODE_ENV) {
|
||||
throw new Error(
|
||||
'The NODE_ENV environment variable is required but was not specified.'
|
||||
'The NODE_ENV environment variable is required but was not specified.',
|
||||
);
|
||||
}
|
||||
|
||||
@@ -36,7 +37,7 @@ dotenvFiles.forEach((dotenvFile) => {
|
||||
require('dotenv-expand')(
|
||||
require('dotenv').config({
|
||||
path: dotenvFile,
|
||||
})
|
||||
}),
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -82,7 +83,7 @@ function getClientEnvironment(publicUrl) {
|
||||
// This should only be used as an escape hatch. Normally you would put
|
||||
// images into the `src` and `import` them in code to get their paths.
|
||||
PUBLIC_URL: publicUrl,
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// Stringify all values so we can feed into Webpack DefinePlugin
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable @typescript-eslint/prefer-nullish-coalescing, @typescript-eslint/promise-function-async, @typescript-eslint/prefer-optional-chain */
|
||||
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
@@ -82,6 +83,7 @@ module.exports = {
|
||||
appNodeModules: resolveApp('node_modules'),
|
||||
publicUrl: getPublicUrl(resolveApp('package.json')),
|
||||
servedPath: getServedPath(resolveApp('package.json')),
|
||||
swSrc: resolveModule(resolveApp, 'src/service-worker'),
|
||||
};
|
||||
|
||||
module.exports.moduleFileExtensions = moduleFileExtensions;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import Enzyme from 'enzyme';
|
||||
import Adapter from 'enzyme-adapter-react-16';
|
||||
import Adapter from '@wojtekmaj/enzyme-adapter-react-17';
|
||||
|
||||
Enzyme.configure({ adapter: new Adapter() });
|
||||
|
||||
@@ -33,6 +33,9 @@ const shouldInlineRuntimeChunk = process.env.INLINE_RUNTIME_CHUNK !== 'false';
|
||||
// Check if TypeScript is setup
|
||||
const useTypeScript = fs.existsSync(paths.appTsConfig);
|
||||
|
||||
// Get the path to the uncompiled service worker (if it exists).
|
||||
const swSrc = paths.swSrc;
|
||||
|
||||
// style files regexes
|
||||
const cssRegex = /\.css$/;
|
||||
const cssModuleRegex = /\.module\.css$/;
|
||||
@@ -75,7 +78,7 @@ module.exports = (webpackEnv) => {
|
||||
loader: MiniCssExtractPlugin.loader,
|
||||
options: Object.assign(
|
||||
{},
|
||||
shouldUseRelativeAssetPaths ? { publicPath: '../../' } : undefined
|
||||
shouldUseRelativeAssetPaths ? { publicPath: '../../' } : undefined,
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -227,7 +230,7 @@ module.exports = (webpackEnv) => {
|
||||
|
||||
// Turned on because emoji and regex is not minified properly using default
|
||||
// https://github.com/facebook/create-react-app/issues/2488
|
||||
ascii_only: true,
|
||||
ascii_only: true, // eslint-disable-line @typescript-eslint/camelcase
|
||||
},
|
||||
},
|
||||
|
||||
@@ -281,7 +284,7 @@ module.exports = (webpackEnv) => {
|
||||
modules: [ 'node_modules' ].concat(
|
||||
|
||||
// It is guaranteed to exist because we tweak it in `env.js`
|
||||
process.env.NODE_PATH.split(path.delimiter).filter(Boolean)
|
||||
process.env.NODE_PATH.split(path.delimiter).filter(Boolean),
|
||||
),
|
||||
|
||||
// These are the reasonable defaults supported by the Node ecosystem.
|
||||
@@ -372,7 +375,7 @@ module.exports = (webpackEnv) => {
|
||||
loader: require.resolve('babel-loader'),
|
||||
options: {
|
||||
customize: require.resolve(
|
||||
'babel-preset-react-app/webpack-overrides'
|
||||
'babel-preset-react-app/webpack-overrides',
|
||||
),
|
||||
|
||||
plugins: [
|
||||
@@ -470,7 +473,7 @@ module.exports = (webpackEnv) => {
|
||||
importLoaders: 2,
|
||||
sourceMap: isEnvProduction && shouldUseSourceMap,
|
||||
},
|
||||
'sass-loader'
|
||||
'sass-loader',
|
||||
),
|
||||
|
||||
// Don't consider CSS imports dead code even if the
|
||||
@@ -491,7 +494,7 @@ module.exports = (webpackEnv) => {
|
||||
modules: true,
|
||||
getLocalIdent: getCSSModuleLocalIdent,
|
||||
},
|
||||
'sass-loader'
|
||||
'sass-loader',
|
||||
),
|
||||
},
|
||||
|
||||
@@ -544,8 +547,8 @@ module.exports = (webpackEnv) => {
|
||||
minifyURLs: true,
|
||||
},
|
||||
}
|
||||
: undefined
|
||||
)
|
||||
: undefined,
|
||||
),
|
||||
),
|
||||
|
||||
// Inlines the webpack runtime script. This script is too small to warrant
|
||||
@@ -612,23 +615,16 @@ module.exports = (webpackEnv) => {
|
||||
new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/),
|
||||
|
||||
// Generate a service worker script that will precache, and keep up to date,
|
||||
// the HTML & assets that are part of the Webpack build.
|
||||
isEnvProduction &&
|
||||
new WorkboxWebpackPlugin.GenerateSW({
|
||||
clientsClaim: true,
|
||||
exclude: [ /\.map$/, /asset-manifest\.json$/ ],
|
||||
importWorkboxFrom: 'cdn',
|
||||
navigateFallback: `${publicUrl}/index.html`,
|
||||
navigateFallbackBlacklist: [
|
||||
|
||||
// Exclude URLs starting with /_, as they're likely an API call
|
||||
new RegExp('^/_'),
|
||||
|
||||
// Exclude URLs containing a dot, as they're likely a resource in
|
||||
// public/ and not a SPA route
|
||||
new RegExp('/[^/]+\\.[^/]+$'),
|
||||
],
|
||||
}),
|
||||
// the HTML & assets that are part of the webpack build.
|
||||
isEnvProduction && fs.existsSync(swSrc) && new WorkboxWebpackPlugin.InjectManifest({
|
||||
swSrc,
|
||||
dontCacheBustURLsMatching: /\.[0-9a-f]{8}\./,
|
||||
exclude: [ /\.map$/, /asset-manifest\.json$/, /LICENSE/ ],
|
||||
// Bump up the default maximum size (2mb) that's precached,
|
||||
// to make lazy-loading failure scenarios less likely.
|
||||
// See https://github.com/cra-template/pwa/issues/13#issuecomment-722667270
|
||||
maximumFileSizeToCacheInBytes: 5 * 1024 * 1024,
|
||||
}),
|
||||
|
||||
// TypeScript type checking
|
||||
useTypeScript &&
|
||||
@@ -668,7 +664,7 @@ module.exports = (webpackEnv) => {
|
||||
fs: 'empty',
|
||||
net: 'empty',
|
||||
tls: 'empty',
|
||||
child_process: 'empty',
|
||||
child_process: 'empty', // eslint-disable-line @typescript-eslint/camelcase
|
||||
},
|
||||
|
||||
// Turn off performance processing because we utilize
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable @typescript-eslint/prefer-nullish-coalescing */
|
||||
|
||||
const fs = require('fs');
|
||||
const errorOverlayMiddleware = require('react-dev-utils/errorOverlayMiddleware');
|
||||
@@ -109,7 +110,7 @@ module.exports = function(proxy, allowedHost) {
|
||||
// We do this in development to avoid hitting the production cache if
|
||||
// it used the same host and port.
|
||||
// https://github.com/facebook/create-react-app/issues/2272#issuecomment-302832432
|
||||
app.use(noopServiceWorkerMiddleware());
|
||||
app.use(noopServiceWorkerMiddleware(paths.publicUrl));
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -3,7 +3,7 @@ version: '3'
|
||||
services:
|
||||
shlink_web_client_node:
|
||||
container_name: shlink_web_client_node
|
||||
image: node:12.16.3-alpine
|
||||
image: node:14.17-alpine
|
||||
command: /bin/sh -c "cd /home/shlink/www && npm install && npm run start"
|
||||
volumes:
|
||||
- ./:/home/shlink/www
|
||||
|
||||
@@ -1,13 +1,20 @@
|
||||
module.exports = {
|
||||
coverageDirectory: '<rootDir>/coverage',
|
||||
collectCoverageFrom: [
|
||||
'src/**/*.js',
|
||||
'!src/registerServiceWorker.js',
|
||||
'!src/index.js',
|
||||
'!src/reducers/index.js',
|
||||
'!src/**/provideServices.js',
|
||||
'!src/container/*.js',
|
||||
'src/**/*.{ts,tsx}',
|
||||
'!src/*.{ts,tsx}',
|
||||
'!src/reducers/index.ts',
|
||||
'!src/**/provideServices.ts',
|
||||
'!src/container/*.ts',
|
||||
],
|
||||
coverageThreshold: {
|
||||
global: {
|
||||
statements: 85,
|
||||
branches: 75,
|
||||
functions: 80,
|
||||
lines: 85,
|
||||
},
|
||||
},
|
||||
resolver: 'jest-pnp-resolver',
|
||||
setupFiles: [
|
||||
'react-app-polyfill/jsdom',
|
||||
@@ -17,9 +24,9 @@ module.exports = {
|
||||
testEnvironment: 'jsdom',
|
||||
testURL: 'http://localhost',
|
||||
transform: {
|
||||
'^.+\\.(js|jsx|mjs)$': '<rootDir>/node_modules/babel-jest',
|
||||
'^.+\\.(ts|tsx|js|jsx|mjs)$': '<rootDir>/node_modules/babel-jest',
|
||||
'^.+\\.css$': '<rootDir>/config/jest/cssTransform.js',
|
||||
'^(?!.*\\.(js|jsx|mjs|css|json)$)': '<rootDir>/config/jest/fileTransform.js',
|
||||
'^(?!.*\\.(ts|tsx|js|jsx|mjs|css|json)$)': '<rootDir>/config/jest/fileTransform.js',
|
||||
},
|
||||
transformIgnorePatterns: [
|
||||
'[/\\\\]node_modules[/\\\\].+\\.(js|jsx|ts|tsx)$',
|
||||
|
||||
24879
package-lock.json
generated
24879
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
239
package.json
239
package.json
@@ -6,144 +6,163 @@
|
||||
"repository": "https://github.com/shlinkio/shlink-web-client",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"lint": "npm run lint:js && npm run lint:css",
|
||||
"lint:js": "eslint src test scripts config",
|
||||
"lint:js:fix": "npm run lint:js -- --fix",
|
||||
"lint": "npm run lint:css && npm run lint:js",
|
||||
"lint:css": "stylelint src/*.scss src/**/*.scss",
|
||||
"lint:js": "eslint --ext .js,.ts,.tsx src test",
|
||||
"lint:fix": "npm run lint:css:fix && npm run lint:js:fix",
|
||||
"lint:css:fix": "npm run lint:css -- --fix",
|
||||
"lint:js:fix": "npm run lint:js -- --fix",
|
||||
"start": "node scripts/start.js",
|
||||
"serve:build": "serve ./build",
|
||||
"build": "node scripts/build.js",
|
||||
"test": "node scripts/test.js --env=jsdom --colors",
|
||||
"test:ci": "npm run test -- --coverage --coverageReporters=text --coverageReporters=text-summary --coverageReporters=clover",
|
||||
"test:pretty": "npm run test -- --coverage --coverageReporters=text --coverageReporters=text-summary --coverageReporters=html",
|
||||
"mutate": "./node_modules/.bin/stryker run",
|
||||
"mutate:ci": "npm run mutate -- --mutate=$MUTATION_FILES",
|
||||
"check": "npm run test & npm run lint & wait"
|
||||
"test": "node scripts/test.js --env=jsdom --colors --verbose",
|
||||
"test:coverage": "npm run test -- --coverage --coverageReporters=text --coverageReporters=text-summary",
|
||||
"test:ci": "npm run test:coverage -- --coverageReporters=clover",
|
||||
"test:pretty": "npm run test:coverage -- --coverageReporters=html",
|
||||
"mutate": "./node_modules/.bin/stryker run --concurrency 4"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-free": "^5.11.2",
|
||||
"@fortawesome/fontawesome-svg-core": "^1.2.25",
|
||||
"@fortawesome/free-regular-svg-icons": "^5.11.2",
|
||||
"@fortawesome/free-solid-svg-icons": "^5.11.2",
|
||||
"@fortawesome/react-fontawesome": "^0.1.5",
|
||||
"array-filter": "^1.0.0",
|
||||
"array-map": "^0.0.0",
|
||||
"array-reduce": "^0.0.0",
|
||||
"axios": "^0.19.0",
|
||||
"bootstrap": "^4.3.1",
|
||||
"bottlejs": "^1.7.2",
|
||||
"bowser": "^2.9.0",
|
||||
"chart.js": "^2.8.0",
|
||||
"@fortawesome/fontawesome-free": "^5.15.2",
|
||||
"@fortawesome/fontawesome-svg-core": "^1.2.34",
|
||||
"@fortawesome/free-regular-svg-icons": "^5.15.2",
|
||||
"@fortawesome/free-solid-svg-icons": "^5.15.2",
|
||||
"@fortawesome/react-fontawesome": "^0.1.14",
|
||||
"axios": "^0.21.1",
|
||||
"bootstrap": "^4.6.0",
|
||||
"bottlejs": "^2.0.0",
|
||||
"bowser": "^2.11.0",
|
||||
"chart.js": "^3.5.1",
|
||||
"classnames": "^2.2.6",
|
||||
"compare-versions": "^3.5.1",
|
||||
"compare-versions": "^3.6.0",
|
||||
"csvjson": "^5.1.0",
|
||||
"event-source-polyfill": "^1.0.12",
|
||||
"leaflet": "^1.5.1",
|
||||
"moment": "^2.24.0",
|
||||
"promise": "^8.0.3",
|
||||
"prop-types": "^15.7.2",
|
||||
"qs": "^6.9.0",
|
||||
"ramda": "^0.26.1",
|
||||
"react": "^16.13.1",
|
||||
"react-autosuggest": "^9.4.3",
|
||||
"react-chartjs-2": "^2.8.0",
|
||||
"react-color": "^2.17.3",
|
||||
"react-copy-to-clipboard": "^5.0.1",
|
||||
"react-datepicker": "~1.5.0",
|
||||
"react-dom": "^16.13.1",
|
||||
"react-external-link": "^1.0.0",
|
||||
"react-leaflet": "^2.4.0",
|
||||
"react-moment": "^0.9.5",
|
||||
"react-redux": "^7.1.1",
|
||||
"react-router-dom": "^5.1.2",
|
||||
"react-swipeable": "^5.4.0",
|
||||
"react-tagsinput": "^3.19.0",
|
||||
"reactstrap": "^8.0.1",
|
||||
"redux": "^4.0.4",
|
||||
"redux-actions": "^2.6.5",
|
||||
"redux-localstorage-simple": "^2.2.0",
|
||||
"date-fns": "^2.22.1",
|
||||
"event-source-polyfill": "^1.0.22",
|
||||
"leaflet": "^1.7.1",
|
||||
"promise": "^8.1.0",
|
||||
"qs": "^6.9.6",
|
||||
"ramda": "^0.27.1",
|
||||
"react": "^17.0.1",
|
||||
"react-chartjs-2": "^3.0.4",
|
||||
"react-color": "^2.19.3",
|
||||
"react-copy-to-clipboard": "^5.0.2",
|
||||
"react-datepicker": "^3.6.0",
|
||||
"react-dom": "^17.0.1",
|
||||
"react-external-link": "^1.2.0",
|
||||
"react-leaflet": "^3.1.0",
|
||||
"react-redux": "^7.2.2",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"react-swipeable": "^6.0.1",
|
||||
"react-tag-autocomplete": "^6.1.0",
|
||||
"reactstrap": "^8.9.0",
|
||||
"redux": "^4.0.5",
|
||||
"redux-localstorage-simple": "^2.4.0",
|
||||
"redux-thunk": "^2.3.0",
|
||||
"uuid": "^3.3.3"
|
||||
"uuid": "^8.3.2",
|
||||
"workbox-core": "^6.1.5",
|
||||
"workbox-expiration": "^6.1.5",
|
||||
"workbox-precaching": "^6.1.5",
|
||||
"workbox-routing": "^6.1.5",
|
||||
"workbox-strategies": "^6.1.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.6.2",
|
||||
"@stryker-mutator/core": "^3.2.4",
|
||||
"@stryker-mutator/javascript-mutator": "^3.2.4",
|
||||
"@stryker-mutator/jest-runner": "^3.2.4",
|
||||
"@svgr/webpack": "^4.3.3",
|
||||
"adm-zip": "^0.4.13",
|
||||
"autoprefixer": "^9.6.3",
|
||||
"@babel/core": "^7.13.8",
|
||||
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.13.8",
|
||||
"@babel/plugin-proposal-optional-chaining": "^7.13.8",
|
||||
"@shlinkio/eslint-config-js-coding-standard": "~1.2.2",
|
||||
"@stryker-mutator/core": "^5.0.0",
|
||||
"@stryker-mutator/jest-runner": "^5.0.0",
|
||||
"@stryker-mutator/typescript-checker": "^5.0.0",
|
||||
"@svgr/webpack": "^5.5.0",
|
||||
"@types/classnames": "^2.2.11",
|
||||
"@types/enzyme": "^3.10.8",
|
||||
"@types/jest": "^26.0.20",
|
||||
"@types/leaflet": "^1.5.23",
|
||||
"@types/qs": "^6.9.5",
|
||||
"@types/ramda": "^0.27.38",
|
||||
"@types/react": "^17.0.2",
|
||||
"@types/react-color": "^3.0.4",
|
||||
"@types/react-copy-to-clipboard": "^5.0.0",
|
||||
"@types/react-datepicker": "^3.1.5",
|
||||
"@types/react-dom": "^17.0.1",
|
||||
"@types/react-leaflet": "^2.5.2",
|
||||
"@types/react-redux": "^7.1.16",
|
||||
"@types/react-router-dom": "^5.1.7",
|
||||
"@types/react-tag-autocomplete": "^6.1.0",
|
||||
"@types/uuid": "^8.3.0",
|
||||
"@wojtekmaj/enzyme-adapter-react-17": "^0.3.1",
|
||||
"adm-zip": "^0.4.16",
|
||||
"autoprefixer": "^10.0.2",
|
||||
"babel-core": "7.0.0-bridge.0",
|
||||
"babel-eslint": "^10.0.3",
|
||||
"babel-jest": "^24.9.0",
|
||||
"babel-loader": "^8.0.6",
|
||||
"babel-plugin-named-asset-import": "^0.3.4",
|
||||
"babel-preset-react-app": "^9.0.2",
|
||||
"babel-jest": "^26.6.3",
|
||||
"babel-loader": "^8.2.1",
|
||||
"babel-plugin-named-asset-import": "^0.3.7",
|
||||
"babel-preset-react-app": "^10.0.0",
|
||||
"babel-runtime": "^6.26.0",
|
||||
"bfj": "^7.0.1",
|
||||
"case-sensitive-paths-webpack-plugin": "^2.2.0",
|
||||
"chalk": "^2.4.2",
|
||||
"css-loader": "^3.2.0",
|
||||
"dotenv": "^8.1.0",
|
||||
"bfj": "^7.0.2",
|
||||
"case-sensitive-paths-webpack-plugin": "^2.3.0",
|
||||
"chalk": "^4.1.0",
|
||||
"css-loader": "^5.0.1",
|
||||
"dart-sass": "^1.25.0",
|
||||
"dotenv": "^8.2.0",
|
||||
"dotenv-expand": "^5.1.0",
|
||||
"enzyme": "^3.11.0",
|
||||
"enzyme-adapter-react-16": "^1.15.2",
|
||||
"eslint": "^5.11.1",
|
||||
"eslint-config-adidas-babel": "^1.1.0",
|
||||
"eslint-config-adidas-env": "^1.1.0",
|
||||
"eslint-config-adidas-es6": "^1.2.0",
|
||||
"eslint-config-adidas-react": "^1.1.1",
|
||||
"eslint-loader": "^3.0.2",
|
||||
"eslint-plugin-import": "^2.18.2",
|
||||
"eslint-plugin-jest": "^22.17.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.2.3",
|
||||
"eslint-plugin-promise": "^4.2.1",
|
||||
"eslint-plugin-react": "^7.16.0",
|
||||
"file-loader": "^4.2.0",
|
||||
"eslint": "^7.13.0",
|
||||
"eslint-loader": "^4.0.2",
|
||||
"file-loader": "^6.2.0",
|
||||
"fork-ts-checker-webpack-plugin-alt": "^0.4.14",
|
||||
"fs-extra": "^8.1.0",
|
||||
"html-webpack-plugin": "^4.0.0-beta.8",
|
||||
"fs-extra": "^9.0.1",
|
||||
"html-webpack-plugin": "^4.5.0",
|
||||
"identity-obj-proxy": "^3.0.0",
|
||||
"jest": "^24.9.0",
|
||||
"jest-pnp-resolver": "^1.2.1",
|
||||
"jest-resolve": "^24.9.0",
|
||||
"mini-css-extract-plugin": "^0.8.0",
|
||||
"node-sass": "^4.12.0",
|
||||
"jest": "^26.6.3",
|
||||
"jest-pnp-resolver": "^1.2.2",
|
||||
"jest-resolve": "^26.6.2",
|
||||
"mini-css-extract-plugin": "^1.3.1",
|
||||
"object-assign": "^4.1.1",
|
||||
"ocular.js": "^0.1.0",
|
||||
"optimize-css-assets-webpack-plugin": "^5.0.3",
|
||||
"pnp-webpack-plugin": "^1.5.0",
|
||||
"postcss": "^7.0.18",
|
||||
"postcss-flexbugs-fixes": "^4.1.0",
|
||||
"optimize-css-assets-webpack-plugin": "^5.0.4",
|
||||
"pnp-webpack-plugin": "^1.6.4",
|
||||
"postcss": "^8.1.7",
|
||||
"postcss-flexbugs-fixes": "^4.2.1",
|
||||
"postcss-loader": "^3.0.0",
|
||||
"postcss-preset-env": "^6.7.0",
|
||||
"postcss-safe-parser": "^4.0.1",
|
||||
"postcss-safe-parser": "^5.0.2",
|
||||
"raf": "^3.4.1",
|
||||
"react-app-polyfill": "^1.0.4",
|
||||
"react-dev-utils": "^9.1.0",
|
||||
"resolve": "^1.12.0",
|
||||
"sass-loader": "^8.0.0",
|
||||
"serve": "^11.2.0",
|
||||
"react-app-polyfill": "^2.0.0",
|
||||
"react-dev-utils": "^11.0.0",
|
||||
"resolve": "^1.19.0",
|
||||
"sass": "^1.29.0",
|
||||
"sass-loader": "^10.1.0",
|
||||
"serve": "^12.0.0",
|
||||
"stryker-cli": "^1.0.0",
|
||||
"style-loader": "^1.0.0",
|
||||
"stylelint": "^9.10.1",
|
||||
"stylelint-config-adidas": "^1.2.1",
|
||||
"style-loader": "^2.0.0",
|
||||
"stylelint": "^13.7.2",
|
||||
"stylelint-config-adidas": "^1.3.0",
|
||||
"stylelint-config-adidas-bem": "^1.2.0",
|
||||
"stylelint-config-recommended-scss": "^4.0.0",
|
||||
"stylelint-scss": "^3.11.1",
|
||||
"sw-precache-webpack-plugin": "^0.11.5",
|
||||
"terser-webpack-plugin": "^2.1.2",
|
||||
"url-loader": "^2.2.0",
|
||||
"webpack": "^4.41.0",
|
||||
"webpack-dev-server": "^3.8.2",
|
||||
"stylelint-config-recommended-scss": "^4.2.0",
|
||||
"stylelint-scss": "^3.18.0",
|
||||
"sw-precache-webpack-plugin": "^1.0.0",
|
||||
"terser-webpack-plugin": "^4.2.3",
|
||||
"ts-jest": "^26.5.2",
|
||||
"ts-mockery": "^1.2.0",
|
||||
"typescript": "^4.2.2",
|
||||
"url-loader": "^4.1.1",
|
||||
"webpack": "^4.44.2",
|
||||
"webpack-dev-server": "^3.11.0",
|
||||
"webpack-manifest-plugin": "^2.2.0",
|
||||
"whatwg-fetch": "^3.0.0",
|
||||
"workbox-webpack-plugin": "^4.3.1"
|
||||
"whatwg-fetch": "^3.5.0",
|
||||
"workbox-webpack-plugin": "^6.1.5"
|
||||
},
|
||||
"babel": {
|
||||
"presets": [
|
||||
"react-app"
|
||||
[
|
||||
"react-app",
|
||||
{
|
||||
"runtime": "automatic"
|
||||
}
|
||||
]
|
||||
],
|
||||
"plugins": [
|
||||
"@babel/plugin-proposal-optional-chaining",
|
||||
"@babel/plugin-proposal-nullish-coalescing-operator"
|
||||
]
|
||||
},
|
||||
"browserslist": [
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
manifest.json provides metadata used when your web app is added to the
|
||||
homescreen on Android. See https://developers.google.com/web/fundamentals/engage-and-retain/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json">
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" crossorigin="use-credentials">
|
||||
|
||||
<!-- FavIcon itself -->
|
||||
<link rel="icon" type="image/x-icon" href="%PUBLIC_URL%/favicon.ico">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/* eslint-disable no-console */
|
||||
/* eslint-disable no-console, @typescript-eslint/prefer-nullish-coalescing, @typescript-eslint/promise-function-async, @typescript-eslint/prefer-optional-chain */
|
||||
|
||||
// Do this as the first thing so that any code reading it knows the right env.
|
||||
process.env.BABEL_ENV = 'production';
|
||||
@@ -43,8 +43,8 @@ if (!checkRequiredFiles([ paths.appHtml, paths.appIndexJs ])) {
|
||||
// Process CLI arguments
|
||||
const argvSliceStart = 2;
|
||||
const argv = process.argv.slice(argvSliceStart);
|
||||
const writeStatsJson = argv.indexOf('--stats') !== -1;
|
||||
const withoutDist = argv.indexOf('--no-dist') !== -1;
|
||||
const writeStatsJson = argv.includes('--stats');
|
||||
const withoutDist = argv.includes('--no-dist');
|
||||
const { version, hasVersion } = getVersionFromArgs(argv);
|
||||
|
||||
// Generate configuration
|
||||
@@ -75,12 +75,12 @@ checkBrowsers(paths.appPath, isInteractive)
|
||||
console.log(
|
||||
`\nSearch for the ${
|
||||
chalk.underline(chalk.yellow('keywords'))
|
||||
} to learn more about each warning.`
|
||||
} to learn more about each warning.`,
|
||||
);
|
||||
console.log(
|
||||
`To ignore, add ${
|
||||
chalk.cyan('// eslint-disable-next-line')
|
||||
} to the line before.\n`
|
||||
} to the line before.\n`,
|
||||
);
|
||||
} else {
|
||||
console.log(chalk.green('Compiled successfully.\n'));
|
||||
@@ -93,7 +93,7 @@ checkBrowsers(paths.appPath, isInteractive)
|
||||
previousFileSizes,
|
||||
paths.appBuild,
|
||||
WARN_AFTER_BUNDLE_GZIP_SIZE,
|
||||
WARN_AFTER_CHUNK_GZIP_SIZE
|
||||
WARN_AFTER_CHUNK_GZIP_SIZE,
|
||||
);
|
||||
console.log();
|
||||
},
|
||||
@@ -101,7 +101,7 @@ checkBrowsers(paths.appPath, isInteractive)
|
||||
console.log(chalk.red('Failed to compile.\n'));
|
||||
printBuildError(err);
|
||||
process.exit(1);
|
||||
}
|
||||
},
|
||||
)
|
||||
.then(() => hasVersion && !withoutDist && zipDist(version))
|
||||
.catch((err) => {
|
||||
@@ -133,7 +133,7 @@ function build(previousFileSizes) {
|
||||
});
|
||||
} else {
|
||||
messages = formatWebpackMessages(
|
||||
stats.toJson({ all: false, warnings: true, errors: true })
|
||||
stats.toJson({ all: false, warnings: true, errors: true }),
|
||||
);
|
||||
}
|
||||
if (messages.errors.length) {
|
||||
@@ -154,8 +154,8 @@ function build(previousFileSizes) {
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
'\nTreating warnings as errors because process.env.CI = true.\n' +
|
||||
'Most CI servers set it automatically.\n'
|
||||
)
|
||||
'Most CI servers set it automatically.\n',
|
||||
),
|
||||
);
|
||||
|
||||
return reject(new Error(messages.warnings.join('\n\n')));
|
||||
|
||||
@@ -1,33 +1,25 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
set -ex
|
||||
|
||||
#PLATFORMS="linux/arm/v7,linux/arm64/v8,linux/amd64"
|
||||
PLATFORMS="linux/amd64"
|
||||
PLATFORMS="linux/arm/v7,linux/arm64/v8,linux/amd64"
|
||||
DOCKER_IMAGE="shlinkio/shlink-web-client"
|
||||
BUILDX_VER=v0.4.1
|
||||
export DOCKER_CLI_EXPERIMENTAL=enabled
|
||||
|
||||
mkdir -vp ~/.docker/cli-plugins/ ~/dockercache
|
||||
curl --silent -L "https://github.com/docker/buildx/releases/download/${BUILDX_VER}/buildx-${BUILDX_VER}.linux-amd64" > ~/.docker/cli-plugins/docker-buildx
|
||||
chmod a+x ~/.docker/cli-plugins/docker-buildx
|
||||
|
||||
docker buildx create --use
|
||||
|
||||
echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin
|
||||
|
||||
if [[ -z $TRAVIS_TAG ]]; then
|
||||
if [[ "$GITHUB_REF" == *"develop"* ]]; then
|
||||
docker buildx build --push \
|
||||
--platform ${PLATFORMS} \
|
||||
-t ${DOCKER_IMAGE}:latest .
|
||||
|
||||
# If ref is not develop, then this is a tag. Build that docker tag and also "stable"
|
||||
else
|
||||
TAGS="-t ${DOCKER_IMAGE}:${TRAVIS_TAG#?}"
|
||||
VERSION=${GITHUB_REF#refs/tags/v}
|
||||
TAGS="-t ${DOCKER_IMAGE}:${VERSION}"
|
||||
|
||||
# Push stable tag only if this is not an alpha or beta release
|
||||
[[ $TRAVIS_TAG != *"alpha"* && $TRAVIS_TAG != *"beta"* ]] && TAGS="${TAGS} -t ${DOCKER_IMAGE}:stable"
|
||||
[[ $GITHUB_REF != *"alpha"* && $GITHUB_REF != *"beta"* ]] && TAGS="${TAGS} -t ${DOCKER_IMAGE}:stable"
|
||||
|
||||
docker buildx build --push \
|
||||
--build-arg SHLINK_VERSION=${TRAVIS_TAG#?} \
|
||||
--build-arg VERSION=${VERSION} \
|
||||
--platform ${PLATFORMS} \
|
||||
${TAGS} .
|
||||
fi
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -ex
|
||||
|
||||
# install latest docker version
|
||||
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
|
||||
add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"
|
||||
apt-get update
|
||||
apt-get -y -o Dpkg::Options::="--force-confnew" install docker-ce
|
||||
|
||||
# enable multiarch execution
|
||||
docker run --rm --privileged multiarch/qemu-user-static --reset -p yes
|
||||
16
scripts/docker/servers_from_env.sh
Executable file
16
scripts/docker/servers_from_env.sh
Executable file
@@ -0,0 +1,16 @@
|
||||
#!/bin/sh
|
||||
|
||||
set -e
|
||||
|
||||
ME=$(basename $0)
|
||||
|
||||
setup_single_shlink_server() {
|
||||
[ -n "$SHLINK_SERVER_URL" ] || return 0
|
||||
[ -n "$SHLINK_SERVER_API_KEY" ] || return 0
|
||||
local name="${SHLINK_SERVER_NAME:-Shlink}"
|
||||
echo "[{\"name\":\"${name}\",\"url\":\"${SHLINK_SERVER_URL}\",\"apiKey\":\"${SHLINK_SERVER_API_KEY}\"}]" > /usr/share/nginx/html/servers.json
|
||||
}
|
||||
|
||||
setup_single_shlink_server
|
||||
|
||||
exit 0
|
||||
13
scripts/set-homepage.js
Normal file
13
scripts/set-homepage.js
Normal file
@@ -0,0 +1,13 @@
|
||||
const argv = process.argv.slice(2);
|
||||
const [ homepage ] = argv;
|
||||
|
||||
if (!homepage) {
|
||||
throw new Error('Homepage has to be provided as the first arg for this script');
|
||||
}
|
||||
|
||||
const packageJsonPath = `${__dirname}/../package.json`;
|
||||
const packageJson = require(packageJsonPath);
|
||||
const fs = require('fs');
|
||||
|
||||
packageJson.homepage = homepage;
|
||||
fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2));
|
||||
@@ -1,4 +1,4 @@
|
||||
/* eslint-disable no-console */
|
||||
/* eslint-disable no-console, @typescript-eslint/prefer-nullish-coalescing, @typescript-eslint/promise-function-async, @typescript-eslint/prefer-optional-chain */
|
||||
|
||||
// Do this as the first thing so that any code reading it knows the right env.
|
||||
process.env.BABEL_ENV = 'development';
|
||||
@@ -49,15 +49,15 @@ if (process.env.HOST) {
|
||||
console.log(
|
||||
chalk.cyan(
|
||||
`Attempting to bind to HOST environment variable: ${chalk.yellow(
|
||||
chalk.bold(process.env.HOST)
|
||||
)}`
|
||||
)
|
||||
chalk.bold(process.env.HOST),
|
||||
)}`,
|
||||
),
|
||||
);
|
||||
console.log(
|
||||
'If this was unintentional, check that you haven\'t mistakenly set it in your shell.'
|
||||
'If this was unintentional, check that you haven\'t mistakenly set it in your shell.',
|
||||
);
|
||||
console.log(
|
||||
`Learn more here: ${chalk.yellow('http://bit.ly/CRA-advanced-config')}`
|
||||
`Learn more here: ${chalk.yellow('http://bit.ly/CRA-advanced-config')}`,
|
||||
);
|
||||
console.log();
|
||||
}
|
||||
@@ -91,7 +91,7 @@ checkBrowsers(paths.appPath, isInteractive)
|
||||
// Serve webpack assets generated by the compiler over a web server.
|
||||
const serverConfig = createDevServerConfig(
|
||||
proxyConfig,
|
||||
urls.lanUrlForConfig
|
||||
urls.lanUrlForConfig,
|
||||
);
|
||||
const devServer = new WebpackDevServer(compiler, serverConfig);
|
||||
|
||||
|
||||
17
shlink-web-client.d.ts
vendored
Normal file
17
shlink-web-client.d.ts
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
declare module 'event-source-polyfill' {
|
||||
declare class EventSourcePolyfill {
|
||||
public onmessage?: ({ data }: { data: string }) => void;
|
||||
public onerror?: ({ status }: { status: number }) => void;
|
||||
public close: () => void;
|
||||
public constructor(hubUrl: URL, options?: any);
|
||||
}
|
||||
}
|
||||
|
||||
declare module 'csvjson' {
|
||||
export declare class CsvJson {
|
||||
public toObject<T>(content: string): T[];
|
||||
public toCSV<T>(data: T[], options: { headers: 'full' | 'none' | 'relative' | 'key' }): string;
|
||||
}
|
||||
}
|
||||
|
||||
declare module '*.png'
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 4.3 MiB After Width: | Height: | Size: 2.3 MiB |
40
src/App.js
40
src/App.js
@@ -1,40 +0,0 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Route, Switch } from 'react-router-dom';
|
||||
import NotFound from './common/NotFound';
|
||||
import './App.scss';
|
||||
|
||||
const propTypes = {
|
||||
fetchServers: PropTypes.func,
|
||||
servers: PropTypes.object,
|
||||
};
|
||||
|
||||
const App = (MainHeader, Home, MenuLayout, CreateServer, EditServer, Settings) => ({ fetchServers, servers }) => {
|
||||
// On first load, try to fetch the remote servers if the list is empty
|
||||
useEffect(() => {
|
||||
if (Object.keys(servers).length === 0) {
|
||||
fetchServers();
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="container-fluid app-container">
|
||||
<MainHeader />
|
||||
|
||||
<div className="app">
|
||||
<Switch>
|
||||
<Route exact path="/" component={Home} />
|
||||
<Route exact path="/settings" component={Settings} />
|
||||
<Route exact path="/server/create" component={CreateServer} />
|
||||
<Route exact path="/server/:serverId/edit" component={EditServer} />
|
||||
<Route path="/server/:serverId" component={MenuLayout} />
|
||||
<Route component={NotFound} />
|
||||
</Switch>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
App.propTypes = propTypes;
|
||||
|
||||
export default App;
|
||||
10
src/App.scss
10
src/App.scss
@@ -1,10 +0,0 @@
|
||||
@import './utils/base';
|
||||
|
||||
.app-container {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.app {
|
||||
padding-top: $headerHeight;
|
||||
height: 100%;
|
||||
}
|
||||
16
src/api/ShlinkApiError.tsx
Normal file
16
src/api/ShlinkApiError.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import { ProblemDetailsError } from './types';
|
||||
import { isInvalidArgumentError } from './utils';
|
||||
|
||||
export interface ShlinkApiErrorProps {
|
||||
errorData?: ProblemDetailsError;
|
||||
fallbackMessage?: string;
|
||||
}
|
||||
|
||||
export const ShlinkApiError = ({ errorData, fallbackMessage }: ShlinkApiErrorProps) => (
|
||||
<>
|
||||
{errorData?.detail ?? fallbackMessage}
|
||||
{isInvalidArgumentError(errorData) &&
|
||||
<p className="mb-0">Invalid elements: [{errorData.invalidElements.join(', ')}]</p>
|
||||
}
|
||||
</>
|
||||
);
|
||||
149
src/api/services/ShlinkApiClient.ts
Normal file
149
src/api/services/ShlinkApiClient.ts
Normal file
@@ -0,0 +1,149 @@
|
||||
import { isEmpty, isNil, reject } from 'ramda';
|
||||
import { AxiosInstance, AxiosResponse, Method } from 'axios';
|
||||
import { ShortUrlsListParams } from '../../short-urls/reducers/shortUrlsListParams';
|
||||
import { ShortUrl, ShortUrlData } from '../../short-urls/data';
|
||||
import { OptionalString } from '../../utils/utils';
|
||||
import {
|
||||
ShlinkHealth,
|
||||
ShlinkMercureInfo,
|
||||
ShlinkShortUrlsResponse,
|
||||
ShlinkTags,
|
||||
ShlinkTagsResponse,
|
||||
ShlinkVisits,
|
||||
ShlinkVisitsParams,
|
||||
ShlinkShortUrlData,
|
||||
ShlinkDomain,
|
||||
ShlinkDomainsResponse,
|
||||
ShlinkVisitsOverview,
|
||||
ShlinkEditDomainRedirects,
|
||||
ShlinkDomainRedirects,
|
||||
} from '../types';
|
||||
import { stringifyQuery } from '../../utils/helpers/query';
|
||||
|
||||
const buildShlinkBaseUrl = (url: string, apiVersion: number) => url ? `${url}/rest/v${apiVersion}` : '';
|
||||
const rejectNilProps = reject(isNil);
|
||||
|
||||
export default class ShlinkApiClient {
|
||||
private apiVersion: number;
|
||||
|
||||
public constructor(
|
||||
private readonly axios: AxiosInstance,
|
||||
private readonly baseUrl: string,
|
||||
private readonly apiKey: string,
|
||||
) {
|
||||
this.apiVersion = 2;
|
||||
}
|
||||
|
||||
public readonly listShortUrls = async (params: ShortUrlsListParams = {}): Promise<ShlinkShortUrlsResponse> =>
|
||||
this.performRequest<{ shortUrls: ShlinkShortUrlsResponse }>('/short-urls', 'GET', params)
|
||||
.then(({ data }) => data.shortUrls);
|
||||
|
||||
public readonly createShortUrl = async (options: ShortUrlData): Promise<ShortUrl> => {
|
||||
const filteredOptions = reject((value) => isEmpty(value) || isNil(value), options as any);
|
||||
|
||||
return this.performRequest<ShortUrl>('/short-urls', 'POST', {}, filteredOptions)
|
||||
.then((resp) => resp.data);
|
||||
};
|
||||
|
||||
public readonly getShortUrlVisits = async (shortCode: string, query?: ShlinkVisitsParams): Promise<ShlinkVisits> =>
|
||||
this.performRequest<{ visits: ShlinkVisits }>(`/short-urls/${shortCode}/visits`, 'GET', query)
|
||||
.then(({ data }) => data.visits);
|
||||
|
||||
public readonly getTagVisits = async (tag: string, query?: Omit<ShlinkVisitsParams, 'domain'>): Promise<ShlinkVisits> =>
|
||||
this.performRequest<{ visits: ShlinkVisits }>(`/tags/${tag}/visits`, 'GET', query)
|
||||
.then(({ data }) => data.visits);
|
||||
|
||||
public readonly getOrphanVisits = async (query?: Omit<ShlinkVisitsParams, 'domain'>): Promise<ShlinkVisits> =>
|
||||
this.performRequest<{ visits: ShlinkVisits }>('/visits/orphan', 'GET', query)
|
||||
.then(({ data }) => data.visits);
|
||||
|
||||
public readonly getVisitsOverview = async (): Promise<ShlinkVisitsOverview> =>
|
||||
this.performRequest<{ visits: ShlinkVisitsOverview }>('/visits', 'GET')
|
||||
.then(({ data }) => data.visits);
|
||||
|
||||
public readonly getShortUrl = async (shortCode: string, domain?: OptionalString): Promise<ShortUrl> =>
|
||||
this.performRequest<ShortUrl>(`/short-urls/${shortCode}`, 'GET', { domain })
|
||||
.then(({ data }) => data);
|
||||
|
||||
public readonly deleteShortUrl = async (shortCode: string, domain?: OptionalString): Promise<void> =>
|
||||
this.performRequest(`/short-urls/${shortCode}`, 'DELETE', { domain })
|
||||
.then(() => {});
|
||||
|
||||
/* @deprecated. If using Shlink 2.6.0 or greater, use updateShortUrl instead */
|
||||
public readonly updateShortUrlTags = async (
|
||||
shortCode: string,
|
||||
domain: OptionalString,
|
||||
tags: string[],
|
||||
): Promise<string[]> =>
|
||||
this.performRequest<{ tags: string[] }>(`/short-urls/${shortCode}/tags`, 'PUT', { domain }, { tags })
|
||||
.then(({ data }) => data.tags);
|
||||
|
||||
public readonly updateShortUrl = async (
|
||||
shortCode: string,
|
||||
domain: OptionalString,
|
||||
data: ShlinkShortUrlData,
|
||||
): Promise<ShortUrl> =>
|
||||
this.performRequest<ShortUrl>(`/short-urls/${shortCode}`, 'PATCH', { domain }, data)
|
||||
.then(({ data }) => data);
|
||||
|
||||
public readonly listTags = async (): Promise<ShlinkTags> =>
|
||||
this.performRequest<{ tags: ShlinkTagsResponse }>('/tags', 'GET', { withStats: 'true' })
|
||||
.then((resp) => resp.data.tags)
|
||||
.then(({ data, stats }) => ({ tags: data, stats }));
|
||||
|
||||
public readonly deleteTags = async (tags: string[]): Promise<{ tags: string[] }> =>
|
||||
this.performRequest('/tags', 'DELETE', { tags })
|
||||
.then(() => ({ tags }));
|
||||
|
||||
public readonly editTag = async (oldName: string, newName: string): Promise<{ oldName: string; newName: string }> =>
|
||||
this.performRequest('/tags', 'PUT', {}, { oldName, newName })
|
||||
.then(() => ({ oldName, newName }));
|
||||
|
||||
public readonly health = async (): Promise<ShlinkHealth> =>
|
||||
this.performRequest<ShlinkHealth>('/health', 'GET')
|
||||
.then((resp) => resp.data);
|
||||
|
||||
public readonly mercureInfo = async (): Promise<ShlinkMercureInfo> =>
|
||||
this.performRequest<ShlinkMercureInfo>('/mercure-info', 'GET')
|
||||
.then((resp) => resp.data);
|
||||
|
||||
public readonly listDomains = async (): Promise<ShlinkDomain[]> =>
|
||||
this.performRequest<{ domains: ShlinkDomainsResponse }>('/domains', 'GET').then(({ data }) => data.domains.data);
|
||||
|
||||
public readonly editDomainRedirects = async (
|
||||
domainRedirects: ShlinkEditDomainRedirects,
|
||||
): Promise<ShlinkDomainRedirects> =>
|
||||
this.performRequest<ShlinkDomainRedirects>('/domains/redirects', 'PATCH', {}, domainRedirects).then(({ data }) => data);
|
||||
|
||||
private readonly performRequest = async <T>(url: string, method: Method = 'GET', query = {}, body = {}): Promise<AxiosResponse<T>> => {
|
||||
try {
|
||||
return await this.axios({
|
||||
method,
|
||||
url: `${buildShlinkBaseUrl(this.baseUrl, this.apiVersion)}${url}`,
|
||||
headers: { 'X-Api-Key': this.apiKey },
|
||||
params: rejectNilProps(query),
|
||||
data: body,
|
||||
paramsSerializer: stringifyQuery,
|
||||
});
|
||||
} catch (e) {
|
||||
const { response } = e;
|
||||
|
||||
// Due to a bug on all previous Shlink versions, requests to non-matching URLs will always result on a CORS error
|
||||
// when performed from the browser (due to the preflight request not returning a 2xx status.
|
||||
// See https://github.com/shlinkio/shlink/issues/614), which will make the "response" prop not to be set here.
|
||||
// The bug will be fixed on upcoming Shlink patches, but for other versions, we can consider this situation as
|
||||
// if a request has been performed to a not supported API version.
|
||||
const apiVersionIsNotSupported = !response;
|
||||
|
||||
// When the request is not invalid or we have already tried both API versions, throw the error and let the
|
||||
// caller handle it
|
||||
if (!apiVersionIsNotSupported || this.apiVersion === 2) {
|
||||
throw e;
|
||||
}
|
||||
|
||||
this.apiVersion = this.apiVersion - 1;
|
||||
|
||||
return await this.performRequest(url, method, query, body);
|
||||
}
|
||||
};
|
||||
}
|
||||
36
src/api/services/ShlinkApiClientBuilder.ts
Normal file
36
src/api/services/ShlinkApiClientBuilder.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { AxiosInstance } from 'axios';
|
||||
import { prop } from 'ramda';
|
||||
import { hasServerData, SelectedServer, ServerWithId } from '../../servers/data';
|
||||
import { GetState } from '../../container/types';
|
||||
import ShlinkApiClient from './ShlinkApiClient';
|
||||
|
||||
const apiClients: Record<string, ShlinkApiClient> = {};
|
||||
|
||||
const isGetState = (getStateOrSelectedServer: GetState | ServerWithId): getStateOrSelectedServer is GetState =>
|
||||
typeof getStateOrSelectedServer === 'function';
|
||||
const getSelectedServerFromState = (getState: GetState): SelectedServer => prop('selectedServer', getState());
|
||||
|
||||
export type ShlinkApiClientBuilder = (getStateOrSelectedServer: GetState | ServerWithId) => ShlinkApiClient;
|
||||
|
||||
const buildShlinkApiClient = (axios: AxiosInstance): ShlinkApiClientBuilder => (
|
||||
getStateOrSelectedServer: GetState | ServerWithId,
|
||||
) => {
|
||||
const server = isGetState(getStateOrSelectedServer)
|
||||
? getSelectedServerFromState(getStateOrSelectedServer)
|
||||
: getStateOrSelectedServer;
|
||||
|
||||
if (!hasServerData(server)) {
|
||||
throw new Error('There\'s no selected server or it is not found');
|
||||
}
|
||||
|
||||
const { url, apiKey } = server;
|
||||
const clientKey = `${url}_${apiKey}`;
|
||||
|
||||
if (!apiClients[clientKey]) {
|
||||
apiClients[clientKey] = new ShlinkApiClient(axios, url, apiKey);
|
||||
}
|
||||
|
||||
return apiClients[clientKey];
|
||||
};
|
||||
|
||||
export default buildShlinkApiClient;
|
||||
8
src/api/services/provideServices.ts
Normal file
8
src/api/services/provideServices.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import Bottle from 'bottlejs';
|
||||
import buildShlinkApiClient from './ShlinkApiClientBuilder';
|
||||
|
||||
const provideServices = (bottle: Bottle) => {
|
||||
bottle.serviceFactory('buildShlinkApiClient', buildShlinkApiClient, 'axios');
|
||||
};
|
||||
|
||||
export default provideServices;
|
||||
6
src/api/types/actions.ts
Normal file
6
src/api/types/actions.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { Action } from 'redux';
|
||||
import { ProblemDetailsError } from './index';
|
||||
|
||||
export interface ApiErrorAction extends Action<string> {
|
||||
errorData?: ProblemDetailsError;
|
||||
}
|
||||
105
src/api/types/index.ts
Normal file
105
src/api/types/index.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
import { Visit } from '../../visits/types';
|
||||
import { OptionalString } from '../../utils/utils';
|
||||
import { ShortUrl, ShortUrlMeta } from '../../short-urls/data';
|
||||
|
||||
export interface ShlinkShortUrlsResponse {
|
||||
data: ShortUrl[];
|
||||
pagination: ShlinkPaginator;
|
||||
}
|
||||
|
||||
export interface ShlinkMercureInfo {
|
||||
token: string;
|
||||
mercureHubUrl: string;
|
||||
}
|
||||
|
||||
export interface ShlinkHealth {
|
||||
status: 'pass' | 'fail';
|
||||
version: string;
|
||||
}
|
||||
|
||||
interface ShlinkTagsStats {
|
||||
tag: string;
|
||||
shortUrlsCount: number;
|
||||
visitsCount: number;
|
||||
}
|
||||
|
||||
export interface ShlinkTags {
|
||||
tags: string[];
|
||||
stats?: ShlinkTagsStats[]; // Is only optional in Shlink older than v2.2
|
||||
}
|
||||
|
||||
export interface ShlinkTagsResponse {
|
||||
data: string[];
|
||||
stats?: ShlinkTagsStats[]; // Is only optional in Shlink older than v2.2
|
||||
}
|
||||
|
||||
export interface ShlinkPaginator {
|
||||
currentPage: number;
|
||||
pagesCount: number;
|
||||
totalItems: number;
|
||||
}
|
||||
|
||||
export interface ShlinkVisits {
|
||||
data: Visit[];
|
||||
pagination: ShlinkPaginator;
|
||||
}
|
||||
|
||||
export interface ShlinkVisitsOverview {
|
||||
visitsCount: number;
|
||||
orphanVisitsCount?: number; // Optional only for versions older than 2.6.0
|
||||
}
|
||||
|
||||
export interface ShlinkVisitsParams {
|
||||
domain?: OptionalString;
|
||||
page?: number;
|
||||
itemsPerPage?: number;
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
excludeBots?: boolean;
|
||||
}
|
||||
|
||||
export interface ShlinkShortUrlData extends ShortUrlMeta {
|
||||
longUrl?: string;
|
||||
title?: string;
|
||||
validateUrl?: boolean;
|
||||
tags?: string[];
|
||||
}
|
||||
|
||||
export interface ShlinkDomainRedirects {
|
||||
baseUrlRedirect: string | null;
|
||||
regular404Redirect: string | null;
|
||||
invalidShortUrlRedirect: string | null;
|
||||
}
|
||||
|
||||
export interface ShlinkEditDomainRedirects extends Partial<ShlinkDomainRedirects> {
|
||||
domain: string;
|
||||
}
|
||||
|
||||
export interface ShlinkDomain {
|
||||
domain: string;
|
||||
isDefault: boolean;
|
||||
redirects?: ShlinkDomainRedirects; // Optional only for Shlink older than 2.8
|
||||
}
|
||||
|
||||
export interface ShlinkDomainsResponse {
|
||||
data: ShlinkDomain[];
|
||||
}
|
||||
|
||||
export interface ProblemDetailsError {
|
||||
type: string;
|
||||
detail: string;
|
||||
title: string;
|
||||
status: number;
|
||||
|
||||
[extraProps: string]: any;
|
||||
}
|
||||
|
||||
export interface InvalidArgumentError extends ProblemDetailsError {
|
||||
type: 'INVALID_ARGUMENT';
|
||||
invalidElements: string[];
|
||||
}
|
||||
|
||||
export interface InvalidShortUrlDeletion extends ProblemDetailsError {
|
||||
type: 'INVALID_SHORTCODE_DELETION';
|
||||
threshold: number;
|
||||
}
|
||||
10
src/api/utils/index.ts
Normal file
10
src/api/utils/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { AxiosError } from 'axios';
|
||||
import { InvalidArgumentError, InvalidShortUrlDeletion, ProblemDetailsError } from '../types';
|
||||
|
||||
export const parseApiError = (e: AxiosError<ProblemDetailsError>) => e.response?.data;
|
||||
|
||||
export const isInvalidArgumentError = (error?: ProblemDetailsError): error is InvalidArgumentError =>
|
||||
error?.type === 'INVALID_ARGUMENT';
|
||||
|
||||
export const isInvalidDeletionError = (error?: ProblemDetailsError): error is InvalidShortUrlDeletion =>
|
||||
error?.type === 'INVALID_SHORTCODE_DELETION';
|
||||
26
src/app/App.scss
Normal file
26
src/app/App.scss
Normal file
@@ -0,0 +1,26 @@
|
||||
@import '../utils/base';
|
||||
|
||||
.app-container {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.app {
|
||||
padding-top: $headerHeight;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.shlink-wrapper {
|
||||
min-height: 100%;
|
||||
padding-bottom: $footer-height + $footer-margin;
|
||||
margin-bottom: -($footer-height + $footer-margin);
|
||||
}
|
||||
|
||||
.shlink-footer {
|
||||
height: $footer-height;
|
||||
margin-top: $footer-margin;
|
||||
padding: 0;
|
||||
|
||||
@media (min-width: $mdMin) {
|
||||
padding: 0 15px;
|
||||
}
|
||||
}
|
||||
63
src/app/App.tsx
Normal file
63
src/app/App.tsx
Normal file
@@ -0,0 +1,63 @@
|
||||
import { useEffect, FC } from 'react';
|
||||
import { Route, Switch } from 'react-router-dom';
|
||||
import NotFound from '../common/NotFound';
|
||||
import { ServersMap } from '../servers/data';
|
||||
import { Settings } from '../settings/reducers/settings';
|
||||
import { changeThemeInMarkup } from '../utils/theme';
|
||||
import { AppUpdateBanner } from '../common/AppUpdateBanner';
|
||||
import { forceUpdate } from '../utils/helpers/sw';
|
||||
import './App.scss';
|
||||
|
||||
interface AppProps {
|
||||
fetchServers: () => void;
|
||||
servers: ServersMap;
|
||||
settings: Settings;
|
||||
resetAppUpdate: () => void;
|
||||
appUpdated: boolean;
|
||||
}
|
||||
|
||||
const App = (
|
||||
MainHeader: FC,
|
||||
Home: FC,
|
||||
MenuLayout: FC,
|
||||
CreateServer: FC,
|
||||
EditServer: FC,
|
||||
Settings: FC,
|
||||
ShlinkVersionsContainer: FC,
|
||||
) => ({ fetchServers, servers, settings, appUpdated, resetAppUpdate }: AppProps) => {
|
||||
useEffect(() => {
|
||||
// On first load, try to fetch the remote servers if the list is empty
|
||||
if (Object.keys(servers).length === 0) {
|
||||
fetchServers();
|
||||
}
|
||||
|
||||
changeThemeInMarkup(settings.ui?.theme ?? 'light');
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="container-fluid app-container">
|
||||
<MainHeader />
|
||||
|
||||
<div className="app">
|
||||
<div className="shlink-wrapper">
|
||||
<Switch>
|
||||
<Route exact path="/" component={Home} />
|
||||
<Route exact path="/settings" component={Settings} />
|
||||
<Route exact path="/server/create" component={CreateServer} />
|
||||
<Route exact path="/server/:serverId/edit" component={EditServer} />
|
||||
<Route path="/server/:serverId" component={MenuLayout} />
|
||||
<Route component={NotFound} />
|
||||
</Switch>
|
||||
</div>
|
||||
|
||||
<div className="shlink-footer">
|
||||
<ShlinkVersionsContainer />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<AppUpdateBanner isOpen={appUpdated} toggle={resetAppUpdate} forceUpdate={forceUpdate} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
||||
18
src/app/reducers/appUpdates.ts
Normal file
18
src/app/reducers/appUpdates.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Action } from 'redux';
|
||||
import { buildActionCreator, buildReducer } from '../../utils/helpers/redux';
|
||||
|
||||
/* eslint-disable padding-line-between-statements */
|
||||
export const APP_UPDATE_AVAILABLE = 'shlink/appUpdates/APP_UPDATE_AVAILABLE';
|
||||
export const RESET_APP_UPDATE = 'shlink/appUpdates/RESET_APP_UPDATE';
|
||||
/* eslint-enable padding-line-between-statements */
|
||||
|
||||
const initialState = false;
|
||||
|
||||
export default buildReducer<boolean, Action<string>>({
|
||||
[APP_UPDATE_AVAILABLE]: () => true,
|
||||
[RESET_APP_UPDATE]: () => false,
|
||||
}, initialState);
|
||||
|
||||
export const appUpdateAvailable = buildActionCreator(APP_UPDATE_AVAILABLE);
|
||||
|
||||
export const resetAppUpdate = buildActionCreator(RESET_APP_UPDATE);
|
||||
26
src/app/services/provideServices.ts
Normal file
26
src/app/services/provideServices.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import Bottle from 'bottlejs';
|
||||
import { appUpdateAvailable, resetAppUpdate } from '../reducers/appUpdates';
|
||||
import App from '../App';
|
||||
import { ConnectDecorator } from '../../container/types';
|
||||
|
||||
const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
|
||||
// Components
|
||||
bottle.serviceFactory(
|
||||
'App',
|
||||
App,
|
||||
'MainHeader',
|
||||
'Home',
|
||||
'MenuLayout',
|
||||
'CreateServer',
|
||||
'EditServer',
|
||||
'Settings',
|
||||
'ShlinkVersionsContainer',
|
||||
);
|
||||
bottle.decorator('App', connect([ 'servers', 'settings', 'appUpdated' ], [ 'fetchServers', 'resetAppUpdate' ]));
|
||||
|
||||
// Actions
|
||||
bottle.serviceFactory('appUpdateAvailable', () => appUpdateAvailable);
|
||||
bottle.serviceFactory('resetAppUpdate', () => resetAppUpdate);
|
||||
};
|
||||
|
||||
export default provideServices;
|
||||
17
src/common/AppUpdateBanner.scss
Normal file
17
src/common/AppUpdateBanner.scss
Normal file
@@ -0,0 +1,17 @@
|
||||
@import '../utils/base';
|
||||
@import '../utils/mixins/horizontal-align';
|
||||
|
||||
.app-update-banner.app-update-banner {
|
||||
@include horizontal-align();
|
||||
|
||||
position: fixed;
|
||||
top: $headerHeight - 25px;
|
||||
padding: 0 4rem 0 0;
|
||||
z-index: 1040;
|
||||
margin: 0;
|
||||
color: var(--text-color);
|
||||
text-align: center;
|
||||
width: 700px;
|
||||
max-width: calc(100% - 30px);
|
||||
box-shadow: 0 0 1rem var(--brand-color);
|
||||
}
|
||||
34
src/common/AppUpdateBanner.tsx
Normal file
34
src/common/AppUpdateBanner.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import { FC, MouseEventHandler } from 'react';
|
||||
import { Alert, Button } from 'reactstrap';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faSyncAlt as reloadIcon } from '@fortawesome/free-solid-svg-icons';
|
||||
import { SimpleCard } from '../utils/SimpleCard';
|
||||
import { useToggle } from '../utils/helpers/hooks';
|
||||
import './AppUpdateBanner.scss';
|
||||
|
||||
interface AppUpdateBannerProps {
|
||||
isOpen: boolean;
|
||||
toggle: MouseEventHandler<any>;
|
||||
forceUpdate: Function;
|
||||
}
|
||||
|
||||
export const AppUpdateBanner: FC<AppUpdateBannerProps> = ({ isOpen, toggle, forceUpdate }) => {
|
||||
const [ isUpdating,, setUpdating ] = useToggle();
|
||||
const update = () => {
|
||||
setUpdating();
|
||||
forceUpdate();
|
||||
};
|
||||
|
||||
return (
|
||||
<Alert className="app-update-banner" isOpen={isOpen} toggle={toggle} tag={SimpleCard} color="secondary">
|
||||
<h4 className="mb-4">This app has just been updated!</h4>
|
||||
<p className="mb-0">
|
||||
Restart it to enjoy the new features.
|
||||
<Button disabled={isUpdating} className="ml-2" color="secondary" size="sm" onClick={update}>
|
||||
{!isUpdating && <>Restart now <FontAwesomeIcon icon={reloadIcon} className="ml-1" /></>}
|
||||
{isUpdating && <>Restarting...</>}
|
||||
</Button>
|
||||
</p>
|
||||
</Alert>
|
||||
);
|
||||
};
|
||||
@@ -1,81 +0,0 @@
|
||||
import {
|
||||
faList as listIcon,
|
||||
faLink as createIcon,
|
||||
faTags as tagsIcon,
|
||||
faPen as editIcon,
|
||||
} from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import React from 'react';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import { serverType } from '../servers/prop-types';
|
||||
import './AsideMenu.scss';
|
||||
|
||||
const AsideMenuItem = ({ children, to, className, ...rest }) => (
|
||||
<NavLink
|
||||
className={classNames('aside-menu__item', className)}
|
||||
activeClassName="aside-menu__item--selected"
|
||||
to={to}
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
</NavLink>
|
||||
);
|
||||
|
||||
AsideMenuItem.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
to: PropTypes.string.isRequired,
|
||||
className: PropTypes.string,
|
||||
};
|
||||
|
||||
const propTypes = {
|
||||
selectedServer: serverType,
|
||||
className: PropTypes.string,
|
||||
showOnMobile: PropTypes.bool,
|
||||
};
|
||||
|
||||
const AsideMenu = (DeleteServerButton) => {
|
||||
const AsideMenu = ({ selectedServer, className, showOnMobile }) => {
|
||||
const serverId = selectedServer ? selectedServer.id : '';
|
||||
const asideClass = classNames('aside-menu', className, {
|
||||
'aside-menu--hidden': !showOnMobile,
|
||||
});
|
||||
const shortUrlsIsActive = (match, location) => location.pathname.match('/list-short-urls');
|
||||
const buildPath = (suffix) => `/server/${serverId}${suffix}`;
|
||||
|
||||
return (
|
||||
<aside className={asideClass}>
|
||||
<nav className="nav flex-column aside-menu__nav">
|
||||
<AsideMenuItem to={buildPath('/list-short-urls/1')} isActive={shortUrlsIsActive}>
|
||||
<FontAwesomeIcon icon={listIcon} />
|
||||
<span className="aside-menu__item-text">List short URLs</span>
|
||||
</AsideMenuItem>
|
||||
<AsideMenuItem to={buildPath('/create-short-url')}>
|
||||
<FontAwesomeIcon icon={createIcon} flip="horizontal" />
|
||||
<span className="aside-menu__item-text">Create short URL</span>
|
||||
</AsideMenuItem>
|
||||
<AsideMenuItem to={buildPath('/manage-tags')}>
|
||||
<FontAwesomeIcon icon={tagsIcon} />
|
||||
<span className="aside-menu__item-text">Manage tags</span>
|
||||
</AsideMenuItem>
|
||||
<AsideMenuItem to={buildPath('/edit')} className="aside-menu__item--push">
|
||||
<FontAwesomeIcon icon={editIcon} />
|
||||
<span className="aside-menu__item-text">Edit this server</span>
|
||||
</AsideMenuItem>
|
||||
<DeleteServerButton
|
||||
className="aside-menu__item aside-menu__item--danger"
|
||||
textClassName="aside-menu__item-text"
|
||||
server={selectedServer}
|
||||
/>
|
||||
</nav>
|
||||
</aside>
|
||||
);
|
||||
};
|
||||
|
||||
AsideMenu.propTypes = propTypes;
|
||||
|
||||
return AsideMenu;
|
||||
};
|
||||
|
||||
export default AsideMenu;
|
||||
@@ -1,10 +1,10 @@
|
||||
@import '../utils/base';
|
||||
@import '../utils/mixins/vertical-align';
|
||||
|
||||
$asideMenuMobileWidth: 280px;
|
||||
|
||||
.aside-menu {
|
||||
background-color: #f7f7f7;
|
||||
width: $asideMenuWidth;
|
||||
background-color: var(--primary-color);
|
||||
box-shadow: rgba(0, 0, 0, .05) 0 8px 15px;
|
||||
position: fixed !important;
|
||||
padding-top: 13px;
|
||||
padding-bottom: 10px;
|
||||
@@ -18,11 +18,9 @@ $asideMenuMobileWidth: 280px;
|
||||
|
||||
@media (min-width: $mdMin) {
|
||||
padding: 30px 15px 15px;
|
||||
border-right: 1px solid #eee;
|
||||
}
|
||||
|
||||
@media (max-width: $smMax) {
|
||||
width: $asideMenuMobileWidth !important;
|
||||
transition: left 300ms;
|
||||
top: $headerHeight - 3px;
|
||||
box-shadow: -10px 0 50px 11px rgba(0, 0, 0, .55);
|
||||
@@ -31,7 +29,7 @@ $asideMenuMobileWidth: 280px;
|
||||
|
||||
.aside-menu--hidden {
|
||||
@media (max-width: $smMax) {
|
||||
left: -($asideMenuMobileWidth + 35px);
|
||||
left: -($asideMenuWidth + 35px);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,24 +42,24 @@ $asideMenuMobileWidth: 280px;
|
||||
margin: 0 -15px;
|
||||
text-decoration: none !important;
|
||||
cursor: pointer;
|
||||
|
||||
@media (max-width: $smMax) {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.aside-menu__item:hover {
|
||||
background-color: $lightHoverColor;
|
||||
}
|
||||
|
||||
.aside-menu__item--selected {
|
||||
color: #fff;
|
||||
background-color: $mainColor;
|
||||
background-color: var(--secondary-color);
|
||||
}
|
||||
|
||||
.aside-menu__item--selected,
|
||||
.aside-menu__item--selected:hover {
|
||||
color: #fff;
|
||||
background-color: $mainColor;
|
||||
color: #ffffff;
|
||||
background-color: var(--brand-color);
|
||||
}
|
||||
|
||||
.aside-menu__item--divider {
|
||||
border-bottom: 1px solid #eee;
|
||||
border-bottom: 1px solid #eeeeee;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
@@ -74,7 +72,7 @@ $asideMenuMobileWidth: 280px;
|
||||
}
|
||||
|
||||
.aside-menu__item--danger:hover {
|
||||
color: #fff;
|
||||
color: #ffffff;
|
||||
background-color: $dangerColor;
|
||||
}
|
||||
|
||||
|
||||
92
src/common/AsideMenu.tsx
Normal file
92
src/common/AsideMenu.tsx
Normal file
@@ -0,0 +1,92 @@
|
||||
import {
|
||||
faList as listIcon,
|
||||
faLink as createIcon,
|
||||
faTags as tagsIcon,
|
||||
faPen as editIcon,
|
||||
faHome as overviewIcon,
|
||||
faGlobe as domainsIcon,
|
||||
} from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { FC } from 'react';
|
||||
import { NavLink, NavLinkProps } from 'react-router-dom';
|
||||
import classNames from 'classnames';
|
||||
import { Location } from 'history';
|
||||
import { DeleteServerButtonProps } from '../servers/DeleteServerButton';
|
||||
import { isServerWithId, SelectedServer } from '../servers/data';
|
||||
import { supportsDomainRedirects } from '../utils/helpers/features';
|
||||
import './AsideMenu.scss';
|
||||
|
||||
export interface AsideMenuProps {
|
||||
selectedServer: SelectedServer;
|
||||
className?: string;
|
||||
showOnMobile?: boolean;
|
||||
}
|
||||
|
||||
interface AsideMenuItemProps extends NavLinkProps {
|
||||
to: string;
|
||||
}
|
||||
|
||||
const AsideMenuItem: FC<AsideMenuItemProps> = ({ children, to, className, ...rest }) => (
|
||||
<NavLink
|
||||
className={classNames('aside-menu__item', className)}
|
||||
activeClassName="aside-menu__item--selected"
|
||||
to={to}
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
</NavLink>
|
||||
);
|
||||
|
||||
const AsideMenu = (DeleteServerButton: FC<DeleteServerButtonProps>) => (
|
||||
{ selectedServer, showOnMobile = false }: AsideMenuProps,
|
||||
) => {
|
||||
const serverId = isServerWithId(selectedServer) ? selectedServer.id : '';
|
||||
const addManageDomainsLink = supportsDomainRedirects(selectedServer);
|
||||
const asideClass = classNames('aside-menu', {
|
||||
'aside-menu--hidden': !showOnMobile,
|
||||
});
|
||||
const shortUrlsIsActive = (_: null, location: Location) => location.pathname.match('/list-short-urls') !== null;
|
||||
const buildPath = (suffix: string) => `/server/${serverId}${suffix}`;
|
||||
|
||||
return (
|
||||
<aside className={asideClass}>
|
||||
<nav className="nav flex-column aside-menu__nav">
|
||||
<AsideMenuItem to={buildPath('/overview')}>
|
||||
<FontAwesomeIcon fixedWidth icon={overviewIcon} />
|
||||
<span className="aside-menu__item-text">Overview</span>
|
||||
</AsideMenuItem>
|
||||
<AsideMenuItem to={buildPath('/list-short-urls/1')} isActive={shortUrlsIsActive}>
|
||||
<FontAwesomeIcon fixedWidth icon={listIcon} />
|
||||
<span className="aside-menu__item-text">List short URLs</span>
|
||||
</AsideMenuItem>
|
||||
<AsideMenuItem to={buildPath('/create-short-url')}>
|
||||
<FontAwesomeIcon fixedWidth icon={createIcon} flip="horizontal" />
|
||||
<span className="aside-menu__item-text">Create short URL</span>
|
||||
</AsideMenuItem>
|
||||
<AsideMenuItem to={buildPath('/manage-tags')}>
|
||||
<FontAwesomeIcon fixedWidth icon={tagsIcon} />
|
||||
<span className="aside-menu__item-text">Manage tags</span>
|
||||
</AsideMenuItem>
|
||||
{addManageDomainsLink && (
|
||||
<AsideMenuItem to={buildPath('/manage-domains')}>
|
||||
<FontAwesomeIcon fixedWidth icon={domainsIcon} />
|
||||
<span className="aside-menu__item-text">Manage domains</span>
|
||||
</AsideMenuItem>
|
||||
)}
|
||||
<AsideMenuItem to={buildPath('/edit')} className="aside-menu__item--push">
|
||||
<FontAwesomeIcon fixedWidth icon={editIcon} />
|
||||
<span className="aside-menu__item-text">Edit this server</span>
|
||||
</AsideMenuItem>
|
||||
{isServerWithId(selectedServer) && (
|
||||
<DeleteServerButton
|
||||
className="aside-menu__item aside-menu__item--danger"
|
||||
textClassName="aside-menu__item-text"
|
||||
server={selectedServer}
|
||||
/>
|
||||
)}
|
||||
</nav>
|
||||
</aside>
|
||||
);
|
||||
};
|
||||
|
||||
export default AsideMenu;
|
||||
@@ -1,43 +0,0 @@
|
||||
import React from 'react';
|
||||
import * as PropTypes from 'prop-types';
|
||||
import './ErrorHandler.scss';
|
||||
import { Button } from 'reactstrap';
|
||||
|
||||
// FIXME Replace with typescript: (window, console)
|
||||
const ErrorHandler = ({ location }, { error }) => class ErrorHandler extends React.Component {
|
||||
static propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { hasError: false };
|
||||
}
|
||||
|
||||
static getDerivedStateFromError() {
|
||||
return { hasError: true };
|
||||
}
|
||||
|
||||
componentDidCatch(e) {
|
||||
if (process.env.NODE_ENV !== 'development') {
|
||||
error(e);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.hasError) {
|
||||
return (
|
||||
<div className="error-handler">
|
||||
<h1>Oops! This is awkward :S</h1>
|
||||
<p>It seems that something went wrong. Try refreshing the page or just click this button.</p>
|
||||
<br />
|
||||
<Button outline color="primary" onClick={() => location.reload()}>Take me back</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return this.props.children;
|
||||
}
|
||||
};
|
||||
|
||||
export default ErrorHandler;
|
||||
@@ -1,9 +0,0 @@
|
||||
@import '../utils/mixins/vertical-align.scss';
|
||||
|
||||
.error-handler {
|
||||
@include vertical-align();
|
||||
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
}
|
||||
46
src/common/ErrorHandler.tsx
Normal file
46
src/common/ErrorHandler.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import { Component, ReactNode } from 'react';
|
||||
import { Button } from 'reactstrap';
|
||||
import { SimpleCard } from '../utils/SimpleCard';
|
||||
|
||||
interface ErrorHandlerState {
|
||||
hasError: boolean;
|
||||
}
|
||||
|
||||
const ErrorHandler = (
|
||||
{ location }: Window,
|
||||
{ error }: Console,
|
||||
) => class ErrorHandler extends Component<any, ErrorHandlerState> {
|
||||
public constructor(props: object) {
|
||||
super(props);
|
||||
this.state = { hasError: false };
|
||||
}
|
||||
|
||||
public static getDerivedStateFromError(): ErrorHandlerState {
|
||||
return { hasError: true };
|
||||
}
|
||||
|
||||
public componentDidCatch(e: Error): void {
|
||||
if (process.env.NODE_ENV !== 'development') {
|
||||
error(e);
|
||||
}
|
||||
}
|
||||
|
||||
public render(): ReactNode {
|
||||
if (this.state.hasError) {
|
||||
return (
|
||||
<div className="home">
|
||||
<SimpleCard className="p-4">
|
||||
<h1>Oops! This is awkward :S</h1>
|
||||
<p>It seems that something went wrong. Try refreshing the page or just click this button.</p>
|
||||
<br />
|
||||
<Button outline color="primary" onClick={() => location.reload()}>Take me back</Button>
|
||||
</SimpleCard>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return this.props.children;
|
||||
}
|
||||
};
|
||||
|
||||
export default ErrorHandler;
|
||||
@@ -1,34 +0,0 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { isEmpty, values } from 'ramda';
|
||||
import { Link } from 'react-router-dom';
|
||||
import PropTypes from 'prop-types';
|
||||
import './Home.scss';
|
||||
import ServersListGroup from '../servers/ServersListGroup';
|
||||
|
||||
const propTypes = {
|
||||
resetSelectedServer: PropTypes.func,
|
||||
servers: PropTypes.object,
|
||||
};
|
||||
|
||||
const Home = ({ resetSelectedServer, servers }) => {
|
||||
const serversList = values(servers);
|
||||
const hasServers = !isEmpty(serversList);
|
||||
|
||||
useEffect(() => {
|
||||
resetSelectedServer();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="home">
|
||||
<h1 className="home__title">Welcome to Shlink</h1>
|
||||
<ServersListGroup servers={serversList}>
|
||||
{hasServers && <span>Please, select a server.</span>}
|
||||
{!hasServers && <span>Please, <Link to="/server/create">add a server</Link>.</span>}
|
||||
</ServersListGroup>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Home.propTypes = propTypes;
|
||||
|
||||
export default Home;
|
||||
@@ -1,18 +1,41 @@
|
||||
@import '../utils/base';
|
||||
@import '../utils/mixins/vertical-align';
|
||||
|
||||
.home {
|
||||
text-align: center;
|
||||
height: calc(100vh - #{$headerHeight});
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-flow: column;
|
||||
position: relative;
|
||||
padding-top: 15px;
|
||||
|
||||
@media (min-width: $mdMin) {
|
||||
padding-top: 0;
|
||||
height: calc(100vh - #{$headerHeight} - #{($footer-height + $footer-margin)});
|
||||
}
|
||||
}
|
||||
|
||||
.home__logo {
|
||||
@include vertical-align();
|
||||
}
|
||||
|
||||
.home__main-card {
|
||||
margin: 0 auto;
|
||||
max-width: 720px;
|
||||
|
||||
@media (min-width: $mdMin) {
|
||||
@include vertical-align();
|
||||
}
|
||||
}
|
||||
|
||||
.home__title {
|
||||
text-align: center;
|
||||
font-size: 1.75rem;
|
||||
margin: 0;
|
||||
|
||||
@media (min-width: $mdMin) {
|
||||
font-size: 2.2rem;
|
||||
}
|
||||
}
|
||||
|
||||
.home__servers-container {
|
||||
@media (min-width: $mdMin) {
|
||||
border-left: 1px solid var(--border-color);
|
||||
}
|
||||
}
|
||||
|
||||
59
src/common/Home.tsx
Normal file
59
src/common/Home.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
import { isEmpty, values } from 'ramda';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Card, Row } from 'reactstrap';
|
||||
import { ExternalLink } from 'react-external-link';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faExternalLinkAlt, faPlus } from '@fortawesome/free-solid-svg-icons';
|
||||
import ServersListGroup from '../servers/ServersListGroup';
|
||||
import { ServersMap } from '../servers/data';
|
||||
import { ShlinkLogo } from './img/ShlinkLogo';
|
||||
import './Home.scss';
|
||||
|
||||
export interface HomeProps {
|
||||
servers: ServersMap;
|
||||
}
|
||||
|
||||
const Home = ({ servers }: HomeProps) => {
|
||||
const serversList = values(servers);
|
||||
const hasServers = !isEmpty(serversList);
|
||||
|
||||
return (
|
||||
<div className="home">
|
||||
<Card className="home__main-card">
|
||||
<Row noGutters>
|
||||
<div className="col-md-5 d-none d-md-block">
|
||||
<div className="p-4">
|
||||
<ShlinkLogo />
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-md-7 home__servers-container">
|
||||
<div className="p-4">
|
||||
<h1 className="home__title">Welcome!</h1>
|
||||
</div>
|
||||
<ServersListGroup embedded servers={serversList}>
|
||||
{!hasServers && (
|
||||
<div className="p-4 text-center">
|
||||
<p className="mb-5">This application will help you manage your Shlink servers.</p>
|
||||
<p>
|
||||
<Link to="/server/create" className="btn btn-outline-primary btn-lg mr-2">
|
||||
<FontAwesomeIcon icon={faPlus} /> <span className="ml-1">Add a server</span>
|
||||
</Link>
|
||||
</p>
|
||||
<p className="mb-0 mt-5">
|
||||
<ExternalLink href="https://shlink.io/documentation">
|
||||
<small>
|
||||
<span className="mr-1">Learn more about Shlink</span> <FontAwesomeIcon icon={faExternalLinkAlt} />
|
||||
</small>
|
||||
</ExternalLink>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</ServersListGroup>
|
||||
</div>
|
||||
</Row>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Home;
|
||||
@@ -1,61 +0,0 @@
|
||||
import { faPlus as plusIcon, faChevronDown as arrowIcon, faCogs as cogsIcon } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import React, { useEffect } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Collapse, Nav, Navbar, NavbarBrand, NavbarToggler, NavItem, NavLink } from 'reactstrap';
|
||||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useToggle } from '../utils/helpers/hooks';
|
||||
import shlinkLogo from './shlink-logo-white.png';
|
||||
import './MainHeader.scss';
|
||||
|
||||
const propTypes = {
|
||||
location: PropTypes.object,
|
||||
};
|
||||
|
||||
const MainHeader = (ServersDropdown) => {
|
||||
const MainHeaderComp = ({ location }) => {
|
||||
const [ isOpen, toggleOpen, , close ] = useToggle();
|
||||
const { pathname } = location;
|
||||
|
||||
useEffect(close, [ location ]);
|
||||
|
||||
const createServerPath = '/server/create';
|
||||
const settingsPath = '/settings';
|
||||
const toggleClass = classNames('main-header__toggle-icon', { 'main-header__toggle-icon--opened': isOpen });
|
||||
|
||||
return (
|
||||
<Navbar color="primary" dark fixed="top" className="main-header" expand="md">
|
||||
<NavbarBrand tag={Link} to="/">
|
||||
<img src={shlinkLogo} alt="Shlink" className="main-header__brand-logo" /> Shlink
|
||||
</NavbarBrand>
|
||||
|
||||
<NavbarToggler onClick={toggleOpen}>
|
||||
<FontAwesomeIcon icon={arrowIcon} className={toggleClass} />
|
||||
</NavbarToggler>
|
||||
|
||||
<Collapse navbar isOpen={isOpen}>
|
||||
<Nav navbar className="ml-auto">
|
||||
<NavItem>
|
||||
<NavLink tag={Link} to={settingsPath} active={pathname === settingsPath}>
|
||||
<FontAwesomeIcon icon={cogsIcon} /> Settings
|
||||
</NavLink>
|
||||
</NavItem>
|
||||
<NavItem>
|
||||
<NavLink tag={Link} to={createServerPath} active={pathname === createServerPath}>
|
||||
<FontAwesomeIcon icon={plusIcon} /> Add server
|
||||
</NavLink>
|
||||
</NavItem>
|
||||
<ServersDropdown />
|
||||
</Nav>
|
||||
</Collapse>
|
||||
</Navbar>
|
||||
);
|
||||
};
|
||||
|
||||
MainHeaderComp.propTypes = propTypes;
|
||||
|
||||
return MainHeaderComp;
|
||||
};
|
||||
|
||||
export default MainHeader;
|
||||
@@ -1,8 +1,8 @@
|
||||
@import '../utils/base';
|
||||
|
||||
.main-header.main-header {
|
||||
background-color: $mainColor !important;
|
||||
color: white;
|
||||
background-color: var(--brand-color) !important;
|
||||
|
||||
.navbar-brand {
|
||||
color: inherit !important;
|
||||
|
||||
45
src/common/MainHeader.tsx
Normal file
45
src/common/MainHeader.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import { faChevronDown as arrowIcon, faCogs as cogsIcon } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { FC, useEffect } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Collapse, Nav, Navbar, NavbarBrand, NavbarToggler, NavItem, NavLink } from 'reactstrap';
|
||||
import classNames from 'classnames';
|
||||
import { RouteComponentProps } from 'react-router';
|
||||
import { useToggle } from '../utils/helpers/hooks';
|
||||
import { ShlinkLogo } from './img/ShlinkLogo';
|
||||
import './MainHeader.scss';
|
||||
|
||||
const MainHeader = (ServersDropdown: FC) => ({ location }: RouteComponentProps) => {
|
||||
const [ isOpen, toggleOpen, , close ] = useToggle();
|
||||
const { pathname } = location;
|
||||
|
||||
useEffect(close, [ location ]);
|
||||
|
||||
const settingsPath = '/settings';
|
||||
const toggleClass = classNames('main-header__toggle-icon', { 'main-header__toggle-icon--opened': isOpen });
|
||||
|
||||
return (
|
||||
<Navbar color="primary" dark fixed="top" className="main-header" expand="md">
|
||||
<NavbarBrand tag={Link} to="/">
|
||||
<ShlinkLogo className="main-header__brand-logo" color="white" /> Shlink
|
||||
</NavbarBrand>
|
||||
|
||||
<NavbarToggler onClick={toggleOpen}>
|
||||
<FontAwesomeIcon icon={arrowIcon} className={toggleClass} />
|
||||
</NavbarToggler>
|
||||
|
||||
<Collapse navbar isOpen={isOpen}>
|
||||
<Nav navbar className="ml-auto">
|
||||
<NavItem>
|
||||
<NavLink tag={Link} to={settingsPath} active={pathname === settingsPath}>
|
||||
<FontAwesomeIcon icon={cogsIcon} /> Settings
|
||||
</NavLink>
|
||||
</NavItem>
|
||||
<ServersDropdown />
|
||||
</Nav>
|
||||
</Collapse>
|
||||
</Navbar>
|
||||
);
|
||||
};
|
||||
|
||||
export default MainHeader;
|
||||
@@ -1,98 +0,0 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { Route, Switch } from 'react-router-dom';
|
||||
import { Swipeable } from 'react-swipeable';
|
||||
import { faBars as burgerIcon } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import classNames from 'classnames';
|
||||
import * as PropTypes from 'prop-types';
|
||||
import { serverType } from '../servers/prop-types';
|
||||
import { withSelectedServer } from '../servers/helpers/withSelectedServer';
|
||||
import { useToggle } from '../utils/helpers/hooks';
|
||||
import { versionMatch } from '../utils/helpers/version';
|
||||
import NotFound from './NotFound';
|
||||
import './MenuLayout.scss';
|
||||
|
||||
const propTypes = {
|
||||
match: PropTypes.object,
|
||||
location: PropTypes.object,
|
||||
selectedServer: serverType,
|
||||
};
|
||||
|
||||
const MenuLayout = (
|
||||
TagsList,
|
||||
ShortUrls,
|
||||
AsideMenu,
|
||||
CreateShortUrl,
|
||||
ShortUrlVisits,
|
||||
TagVisits,
|
||||
ShlinkVersions,
|
||||
ServerError
|
||||
) => {
|
||||
const MenuLayoutComp = ({ match, location, selectedServer }) => {
|
||||
const [ sidebarVisible, toggleSidebar, showSidebar, hideSidebar ] = useToggle();
|
||||
const { params: { serverId } } = match;
|
||||
|
||||
useEffect(() => hideSidebar(), [ location ]);
|
||||
|
||||
if (selectedServer.serverNotReachable) {
|
||||
return <ServerError type="not-reachable" />;
|
||||
}
|
||||
|
||||
const addTagsVisitsRoute = versionMatch(selectedServer.version, { minVersion: '2.2.0' });
|
||||
const burgerClasses = classNames('menu-layout__burger-icon', {
|
||||
'menu-layout__burger-icon--active': sidebarVisible,
|
||||
});
|
||||
const swipeMenuIfNoModalExists = (callback) => (e) => {
|
||||
const swippedOnVisitsTable = e.event.path.some(
|
||||
({ classList }) => classList && classList.contains('visits-table')
|
||||
);
|
||||
|
||||
if (swippedOnVisitsTable || document.querySelector('.modal')) {
|
||||
return;
|
||||
}
|
||||
|
||||
callback();
|
||||
};
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<FontAwesomeIcon icon={burgerIcon} className={burgerClasses} onClick={toggleSidebar} />
|
||||
|
||||
<Swipeable
|
||||
delta={40}
|
||||
className="menu-layout__swipeable"
|
||||
onSwipedLeft={swipeMenuIfNoModalExists(hideSidebar)}
|
||||
onSwipedRight={swipeMenuIfNoModalExists(showSidebar)}
|
||||
>
|
||||
<div className="row menu-layout__swipeable-inner">
|
||||
<AsideMenu className="col-lg-2 col-md-3" selectedServer={selectedServer} showOnMobile={sidebarVisible} />
|
||||
<div className="col-lg-10 offset-lg-2 col-md-9 offset-md-3" onClick={() => hideSidebar()}>
|
||||
<div className="menu-layout__container">
|
||||
<Switch>
|
||||
<Route exact path="/server/:serverId/list-short-urls/:page" component={ShortUrls} />
|
||||
<Route exact path="/server/:serverId/create-short-url" component={CreateShortUrl} />
|
||||
<Route exact path="/server/:serverId/short-code/:shortCode/visits" component={ShortUrlVisits} />
|
||||
{addTagsVisitsRoute && <Route exact path="/server/:serverId/tag/:tag/visits" component={TagVisits} />}
|
||||
<Route exact path="/server/:serverId/manage-tags" component={TagsList} />
|
||||
<Route
|
||||
render={() => <NotFound to={`/server/${serverId}/list-short-urls/1`}>List short URLs</NotFound>}
|
||||
/>
|
||||
</Switch>
|
||||
</div>
|
||||
|
||||
<div className="menu-layout__footer text-center text-md-right">
|
||||
<ShlinkVersions />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Swipeable>
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
MenuLayoutComp.propTypes = propTypes;
|
||||
|
||||
return withSelectedServer(MenuLayoutComp, ServerError);
|
||||
};
|
||||
|
||||
export default MenuLayout;
|
||||
@@ -33,25 +33,11 @@
|
||||
color: white;
|
||||
}
|
||||
|
||||
$footer-height: 2.3rem;
|
||||
$footer-margin: .8rem;
|
||||
|
||||
.menu-layout__container {
|
||||
padding: 20px 0 ($footer-height + $footer-margin);
|
||||
.menu-layout__container.menu-layout__container {
|
||||
padding: 20px 0 0;
|
||||
min-height: 100%;
|
||||
margin-bottom: -($footer-height + $footer-margin);
|
||||
|
||||
@media (min-width: $mdMin) {
|
||||
padding: 30px 15px ($footer-height + $footer-margin);
|
||||
}
|
||||
}
|
||||
|
||||
.menu-layout__footer {
|
||||
height: $footer-height;
|
||||
margin-top: $footer-margin;
|
||||
padding: 0;
|
||||
|
||||
@media (min-width: $mdMin) {
|
||||
padding: 0 15px;
|
||||
padding: 30px 0 0 $asideMenuWidth;
|
||||
}
|
||||
}
|
||||
|
||||
72
src/common/MenuLayout.tsx
Normal file
72
src/common/MenuLayout.tsx
Normal file
@@ -0,0 +1,72 @@
|
||||
import { FC, useEffect } from 'react';
|
||||
import { Redirect, Route, Switch } from 'react-router-dom';
|
||||
import { faBars as burgerIcon } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import classNames from 'classnames';
|
||||
import { withSelectedServer } from '../servers/helpers/withSelectedServer';
|
||||
import { useSwipeable, useToggle } from '../utils/helpers/hooks';
|
||||
import { supportsDomainRedirects, supportsOrphanVisits } from '../utils/helpers/features';
|
||||
import { isReachableServer } from '../servers/data';
|
||||
import NotFound from './NotFound';
|
||||
import { AsideMenuProps } from './AsideMenu';
|
||||
import './MenuLayout.scss';
|
||||
|
||||
const MenuLayout = (
|
||||
TagsList: FC,
|
||||
ShortUrls: FC,
|
||||
AsideMenu: FC<AsideMenuProps>,
|
||||
CreateShortUrl: FC,
|
||||
ShortUrlVisits: FC,
|
||||
TagVisits: FC,
|
||||
OrphanVisits: FC,
|
||||
ServerError: FC,
|
||||
Overview: FC,
|
||||
EditShortUrl: FC,
|
||||
ManageDomains: FC,
|
||||
) => withSelectedServer(({ location, selectedServer }) => {
|
||||
const [ sidebarVisible, toggleSidebar, showSidebar, hideSidebar ] = useToggle();
|
||||
|
||||
useEffect(() => hideSidebar(), [ location ]);
|
||||
|
||||
if (!isReachableServer(selectedServer)) {
|
||||
return <ServerError />;
|
||||
}
|
||||
|
||||
const addOrphanVisitsRoute = supportsOrphanVisits(selectedServer);
|
||||
const addManageDomainsRoute = supportsDomainRedirects(selectedServer);
|
||||
const burgerClasses = classNames('menu-layout__burger-icon', { 'menu-layout__burger-icon--active': sidebarVisible });
|
||||
const swipeableProps = useSwipeable(showSidebar, hideSidebar);
|
||||
|
||||
return (
|
||||
<>
|
||||
<FontAwesomeIcon icon={burgerIcon} className={burgerClasses} onClick={toggleSidebar} />
|
||||
|
||||
<div {...swipeableProps} className="menu-layout__swipeable">
|
||||
<div className="menu-layout__swipeable-inner">
|
||||
<AsideMenu selectedServer={selectedServer} showOnMobile={sidebarVisible} />
|
||||
<div className="menu-layout__container" onClick={() => hideSidebar()}>
|
||||
<div className="container-xl">
|
||||
<Switch>
|
||||
<Redirect exact from="/server/:serverId" to="/server/:serverId/overview" />
|
||||
<Route exact path="/server/:serverId/overview" component={Overview} />
|
||||
<Route exact path="/server/:serverId/list-short-urls/:page" component={ShortUrls} />
|
||||
<Route exact path="/server/:serverId/create-short-url" component={CreateShortUrl} />
|
||||
<Route path="/server/:serverId/short-code/:shortCode/visits" component={ShortUrlVisits} />
|
||||
<Route path="/server/:serverId/short-code/:shortCode/edit" component={EditShortUrl} />
|
||||
<Route path="/server/:serverId/tag/:tag/visits" component={TagVisits} />
|
||||
{addOrphanVisitsRoute && <Route path="/server/:serverId/orphan-visits" component={OrphanVisits} />}
|
||||
<Route exact path="/server/:serverId/manage-tags" component={TagsList} />
|
||||
{addManageDomainsRoute && <Route exact path="/server/:serverId/manage-domains" component={ManageDomains} />}
|
||||
<Route
|
||||
render={() => <NotFound to={`/server/${selectedServer.id}/list-short-urls/1`}>List short URLs</NotFound>}
|
||||
/>
|
||||
</Switch>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}, ServerError);
|
||||
|
||||
export default MenuLayout;
|
||||
@@ -1,13 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import './NoMenuLayout.scss';
|
||||
|
||||
const propTypes = {
|
||||
children: PropTypes.node,
|
||||
};
|
||||
|
||||
const NoMenuLayout = ({ children }) => <div className="no-menu-wrapper">{children}</div>;
|
||||
|
||||
NoMenuLayout.propTypes = propTypes;
|
||||
|
||||
export default NoMenuLayout;
|
||||
@@ -1,3 +1,9 @@
|
||||
@import '../utils/base';
|
||||
|
||||
.no-menu-wrapper {
|
||||
padding: 40px 20px;
|
||||
padding: 15px 0 0;
|
||||
|
||||
@media (min-width: $mdMin) {
|
||||
padding: 30px 20px 20px;
|
||||
}
|
||||
}
|
||||
|
||||
6
src/common/NoMenuLayout.tsx
Normal file
6
src/common/NoMenuLayout.tsx
Normal file
@@ -0,0 +1,6 @@
|
||||
import { FC } from 'react';
|
||||
import './NoMenuLayout.scss';
|
||||
|
||||
const NoMenuLayout: FC = ({ children }) => <div className="no-menu-wrapper container-xl">{children}</div>;
|
||||
|
||||
export default NoMenuLayout;
|
||||
@@ -1,24 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import * as PropTypes from 'prop-types';
|
||||
|
||||
const propTypes = {
|
||||
to: PropTypes.string,
|
||||
children: PropTypes.node,
|
||||
};
|
||||
|
||||
const NotFound = ({ to = '/', children = 'Home' }) => (
|
||||
<div className="home">
|
||||
<h2>Oops! We could not find requested route.</h2>
|
||||
<p>
|
||||
Use your browser's back button to navigate to the page you have previously come from, or just press this
|
||||
button.
|
||||
</p>
|
||||
<br />
|
||||
<Link to={to} className="btn btn-outline-primary btn-lg">{children}</Link>
|
||||
</div>
|
||||
);
|
||||
|
||||
NotFound.propTypes = propTypes;
|
||||
|
||||
export default NotFound;
|
||||
23
src/common/NotFound.tsx
Normal file
23
src/common/NotFound.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import { FC } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { SimpleCard } from '../utils/SimpleCard';
|
||||
|
||||
interface NotFoundProps {
|
||||
to?: string;
|
||||
}
|
||||
|
||||
const NotFound: FC<NotFoundProps> = ({ to = '/', children = 'Home' }) => (
|
||||
<div className="home">
|
||||
<SimpleCard className="p-4">
|
||||
<h2>Oops! We could not find requested route.</h2>
|
||||
<p>
|
||||
Use your browser's back button to navigate to the page you have previously come from, or just press this
|
||||
button.
|
||||
</p>
|
||||
<br />
|
||||
<Link to={to} className="btn btn-outline-primary btn-lg">{children}</Link>
|
||||
</SimpleCard>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default NotFound;
|
||||
@@ -1,23 +0,0 @@
|
||||
import { useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const propTypes = {
|
||||
location: PropTypes.object,
|
||||
children: PropTypes.node,
|
||||
};
|
||||
|
||||
const ScrollToTop = () => {
|
||||
const ScrollToTopComp = ({ location, children }) => {
|
||||
useEffect(() => {
|
||||
scrollTo(0, 0);
|
||||
}, [ location ]);
|
||||
|
||||
return children;
|
||||
};
|
||||
|
||||
ScrollToTopComp.propTypes = propTypes;
|
||||
|
||||
return ScrollToTopComp;
|
||||
};
|
||||
|
||||
export default ScrollToTop;
|
||||
12
src/common/ScrollToTop.tsx
Normal file
12
src/common/ScrollToTop.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import { PropsWithChildren, useEffect } from 'react';
|
||||
import { RouteComponentProps } from 'react-router';
|
||||
|
||||
const ScrollToTop = () => ({ location, children }: PropsWithChildren<RouteComponentProps>) => {
|
||||
useEffect(() => {
|
||||
scrollTo(0, 0);
|
||||
}, [ location ]);
|
||||
|
||||
return <>{children}</>;
|
||||
};
|
||||
|
||||
export default ScrollToTop;
|
||||
@@ -1,29 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import { pipe } from 'ramda';
|
||||
import { serverType } from '../servers/prop-types';
|
||||
import { versionToPrintable, versionToSemVer } from '../utils/helpers/version';
|
||||
|
||||
const SHLINK_WEB_CLIENT_VERSION = '%_VERSION_%';
|
||||
|
||||
const propTypes = {
|
||||
selectedServer: serverType,
|
||||
className: PropTypes.string,
|
||||
clientVersion: PropTypes.string,
|
||||
};
|
||||
|
||||
const ShlinkVersions = ({ selectedServer, className, clientVersion = SHLINK_WEB_CLIENT_VERSION }) => {
|
||||
const { printableVersion: serverVersion } = selectedServer;
|
||||
const normalizedClientVersion = pipe(versionToSemVer(), versionToPrintable)(clientVersion);
|
||||
|
||||
return (
|
||||
<small className={classNames('text-muted', className)}>
|
||||
Client: <b>{normalizedClientVersion}</b> - Server: <b>{serverVersion}</b>
|
||||
</small>
|
||||
);
|
||||
};
|
||||
|
||||
ShlinkVersions.propTypes = propTypes;
|
||||
|
||||
export default ShlinkVersions;
|
||||
33
src/common/ShlinkVersions.tsx
Normal file
33
src/common/ShlinkVersions.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import { pipe } from 'ramda';
|
||||
import { ExternalLink } from 'react-external-link';
|
||||
import { versionToPrintable, versionToSemVer } from '../utils/helpers/version';
|
||||
import { isReachableServer } from '../servers/data';
|
||||
import { ShlinkVersionsContainerProps } from './ShlinkVersionsContainer';
|
||||
|
||||
const SHLINK_WEB_CLIENT_VERSION = '%_VERSION_%';
|
||||
const normalizeVersion = pipe(versionToSemVer(), versionToPrintable);
|
||||
|
||||
export interface ShlinkVersionsProps extends ShlinkVersionsContainerProps {
|
||||
clientVersion?: string;
|
||||
}
|
||||
|
||||
const VersionLink = ({ project, version }: { project: 'shlink' | 'shlink-web-client'; version: string }) => (
|
||||
<ExternalLink href={`https://github.com/shlinkio/${project}/releases/${version}`} className="text-muted">
|
||||
<b>{version}</b>
|
||||
</ExternalLink>
|
||||
);
|
||||
|
||||
const ShlinkVersions = ({ selectedServer, clientVersion = SHLINK_WEB_CLIENT_VERSION }: ShlinkVersionsProps) => {
|
||||
const normalizedClientVersion = normalizeVersion(clientVersion);
|
||||
|
||||
return (
|
||||
<small className="text-muted">
|
||||
{isReachableServer(selectedServer) &&
|
||||
<>Server: <VersionLink project="shlink" version={selectedServer.printableVersion} /> - </>
|
||||
}
|
||||
Client: <VersionLink project="shlink-web-client" version={normalizedClientVersion} />
|
||||
</small>
|
||||
);
|
||||
};
|
||||
|
||||
export default ShlinkVersions;
|
||||
9
src/common/ShlinkVersionsContainer.scss
Normal file
9
src/common/ShlinkVersionsContainer.scss
Normal file
@@ -0,0 +1,9 @@
|
||||
@import '../utils/base';
|
||||
|
||||
.shlink-versions-container--with-server {
|
||||
margin-left: 0;
|
||||
|
||||
@media (min-width: $mdMin) {
|
||||
margin-left: $asideMenuWidth;
|
||||
}
|
||||
}
|
||||
22
src/common/ShlinkVersionsContainer.tsx
Normal file
22
src/common/ShlinkVersionsContainer.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import classNames from 'classnames';
|
||||
import { isReachableServer, SelectedServer } from '../servers/data';
|
||||
import ShlinkVersions from './ShlinkVersions';
|
||||
import './ShlinkVersionsContainer.scss';
|
||||
|
||||
export interface ShlinkVersionsContainerProps {
|
||||
selectedServer: SelectedServer;
|
||||
}
|
||||
|
||||
const ShlinkVersionsContainer = ({ selectedServer }: ShlinkVersionsContainerProps) => {
|
||||
const classes = classNames('text-center', {
|
||||
'shlink-versions-container--with-server': isReachableServer(selectedServer),
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={classes}>
|
||||
<ShlinkVersions selectedServer={selectedServer} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ShlinkVersionsContainer;
|
||||
@@ -1,23 +1,28 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { FC } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { Pagination, PaginationItem, PaginationLink } from 'reactstrap';
|
||||
import { isPageDisabled, keyForPage, progressivePagination } from '../utils/helpers/pagination';
|
||||
import {
|
||||
pageIsEllipsis,
|
||||
keyForPage,
|
||||
NumberOrEllipsis,
|
||||
progressivePagination,
|
||||
prettifyPageNumber,
|
||||
} from '../utils/helpers/pagination';
|
||||
import './SimplePaginator.scss';
|
||||
|
||||
const propTypes = {
|
||||
pagesCount: PropTypes.number.isRequired,
|
||||
currentPage: PropTypes.number.isRequired,
|
||||
setCurrentPage: PropTypes.func.isRequired,
|
||||
centered: PropTypes.bool,
|
||||
};
|
||||
interface SimplePaginatorProps {
|
||||
pagesCount: number;
|
||||
currentPage: number;
|
||||
setCurrentPage: (currentPage: number) => void;
|
||||
centered?: boolean;
|
||||
}
|
||||
|
||||
const SimplePaginator = ({ pagesCount, currentPage, setCurrentPage, centered = true }) => {
|
||||
const SimplePaginator: FC<SimplePaginatorProps> = ({ pagesCount, currentPage, setCurrentPage, centered = true }) => {
|
||||
if (pagesCount < 2) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const onClick = (page) => () => setCurrentPage(page);
|
||||
const onClick = (page: NumberOrEllipsis) => () => !pageIsEllipsis(page) && setCurrentPage(page);
|
||||
|
||||
return (
|
||||
<Pagination listClassName={classNames('flex-wrap mb-0 simple-paginator', { 'justify-content-center': centered })}>
|
||||
@@ -27,10 +32,10 @@ const SimplePaginator = ({ pagesCount, currentPage, setCurrentPage, centered = t
|
||||
{progressivePagination(currentPage, pagesCount).map((pageNumber, index) => (
|
||||
<PaginationItem
|
||||
key={keyForPage(pageNumber, index)}
|
||||
disabled={isPageDisabled(pageNumber)}
|
||||
disabled={pageIsEllipsis(pageNumber)}
|
||||
active={currentPage === pageNumber}
|
||||
>
|
||||
<PaginationLink tag="span" onClick={onClick(pageNumber)}>{pageNumber}</PaginationLink>
|
||||
<PaginationLink tag="span" onClick={onClick(pageNumber)}>{prettifyPageNumber(pageNumber)}</PaginationLink>
|
||||
</PaginationItem>
|
||||
))}
|
||||
<PaginationItem disabled={currentPage >= pagesCount}>
|
||||
@@ -40,6 +45,4 @@ const SimplePaginator = ({ pagesCount, currentPage, setCurrentPage, centered = t
|
||||
);
|
||||
};
|
||||
|
||||
SimplePaginator.propTypes = propTypes;
|
||||
|
||||
export default SimplePaginator;
|
||||
25
src/common/img/ShlinkLogo.tsx
Normal file
25
src/common/img/ShlinkLogo.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import { MAIN_COLOR } from '../../utils/theme';
|
||||
|
||||
export interface ShlinkLogoProps {
|
||||
color?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const ShlinkLogo = ({ color = MAIN_COLOR, className }: ShlinkLogoProps) => (
|
||||
<svg className={className} viewBox="0 0 512 512" version="1.1" xmlns="http://www.w3.org/2000/svg">
|
||||
<g fill={color}>
|
||||
<path
|
||||
d=" M 23.71 85.08 C 17.22 49.81 49.44 14.86 85.08 18.12 C 118.83 19.21 145.72 53.33 139.45 86.37 C 155.64 102.30 171.32 118.83 187.87 134.36 C 198.32 111.73 208.84 89.12 219.57 66.62 C 226.05 53.84 243.47 48.74 255.73 56.27 C 263.76 62.10 270.34 69.69 277.25 76.75 C 286.28 86.61 285.72 102.89 276.31 112.31 C 223.38 165.37 170.38 218.37 117.35 271.34 C 107.72 280.99 91.01 281.25 81.11 271.86 C 74.39 264.94 66.82 258.69 61.24 250.77 C 53.72 238.52 58.85 221.07 71.64 214.62 C 94.11 203.87 116.72 193.38 139.33 182.91 C 123.81 166.36 107.30 150.68 91.37 134.49 C 60.20 140.28 27.37 116.78 23.71 85.08 Z"
|
||||
/>
|
||||
<path
|
||||
d=" M 205.21 201.23 C 225.32 181.36 260.88 181.11 281.14 200.86 C 299.25 218.75 317.37 236.65 335.10 254.93 C 356.73 278.01 352.01 318.70 326.03 336.56 C 320.07 330.47 313.73 324.65 308.12 318.28 C 323.86 309.39 328.76 286.18 316.63 272.39 C 301.73 256.95 286.30 242.03 271.24 226.75 C 264.49 219.65 256.80 212.00 246.37 211.52 C 224.65 208.64 205.52 233.36 214.49 253.58 C 221.09 266.81 234.22 275.12 243.62 286.24 C 240.43 295.96 238.09 306.13 238.29 316.46 C 225.55 304.29 213.16 291.73 200.89 279.09 C 180.97 257.57 183.10 220.45 205.21 201.23 Z"
|
||||
/>
|
||||
<path
|
||||
d=" M 273.90 352.07 C 252.28 328.99 256.98 288.31 282.96 270.46 C 288.93 276.54 295.26 282.36 300.88 288.72 C 285.14 297.62 280.23 320.82 292.38 334.61 C 307.27 350.05 322.70 364.96 337.75 380.25 C 344.51 387.35 352.20 395.00 362.64 395.48 C 384.35 398.37 403.49 373.64 394.51 353.42 C 387.92 340.18 374.78 331.88 365.38 320.76 C 368.56 311.04 370.91 300.86 370.71 290.54 C 383.45 302.70 395.84 315.27 408.11 327.91 C 428.03 349.43 425.90 386.55 403.78 405.77 C 383.68 425.64 348.13 425.89 327.86 406.14 C 309.75 388.25 291.60 370.37 273.90 352.07 Z"
|
||||
/>
|
||||
<path
|
||||
d=" M 422.11 403.83 C 431.96 394.07 441.60 384.06 451.66 374.51 C 460.90 383.74 471.89 392.70 474.89 406.11 C 480.16 429.97 484.08 454.13 488.76 478.12 C 490.00 483.41 484.47 488.29 479.35 486.63 C 454.66 481.52 429.55 478.12 405.14 471.84 C 393.17 467.97 385.20 457.75 376.55 449.27 C 386.39 439.49 396.13 429.60 406.06 419.91 C 416.37 433.45 435.74 414.00 422.11 403.83 Z"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
145
src/common/react-tag-autocomplete.scss
Normal file
145
src/common/react-tag-autocomplete.scss
Normal file
@@ -0,0 +1,145 @@
|
||||
@import '../utils/base';
|
||||
|
||||
.react-tags {
|
||||
position: relative;
|
||||
padding: 5px 0 0 6px;
|
||||
border-radius: .3rem;
|
||||
background-color: var(--input-color);
|
||||
border: 1px solid var(--input-border-color);
|
||||
transition: border-color .15s ease-in-out, box-shadow .15s ease-in-out;
|
||||
|
||||
/* shared font styles */
|
||||
font-size: 1em;
|
||||
line-height: 1.2;
|
||||
|
||||
/* clicking anywhere will focus the input */
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
.react-tags.is-focused {
|
||||
box-shadow: 0 0 0 .2rem rgb(70 150 229 / 25%);
|
||||
}
|
||||
|
||||
.react-tags__tag {
|
||||
font-size: 100%;
|
||||
}
|
||||
|
||||
.react-tags__selected {
|
||||
display: inline;
|
||||
vertical-align: 2px;
|
||||
}
|
||||
|
||||
.react-tags__selected-tag {
|
||||
display: inline-block;
|
||||
box-sizing: border-box;
|
||||
margin: 0 6px 6px 0;
|
||||
padding: 6px 8px;
|
||||
border: 1px solid var(--input-border-color);
|
||||
border-radius: .25rem;
|
||||
background: #f1f1f1;
|
||||
|
||||
/* match the font styles */
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
.react-tags__selected-tag:after {
|
||||
content: '\2715';
|
||||
color: #aaaaaa;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.react-tags__selected-tag:hover,
|
||||
.react-tags__selected-tag:focus {
|
||||
border-color: var(--input-border-color);
|
||||
}
|
||||
|
||||
.react-tags__search {
|
||||
display: inline-block;
|
||||
|
||||
/* match tag layout */
|
||||
padding: 6px 2px;
|
||||
margin-bottom: 5px;
|
||||
|
||||
/* prevent autoresize overflowing the container */
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
@media screen and (min-width: $smMin) {
|
||||
.react-tags__search {
|
||||
/* this will become the offsetParent for suggestions */
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
|
||||
.react-tags__search-input {
|
||||
font-size: 1.25rem;
|
||||
line-height: inherit;
|
||||
color: var(--input-text-color);
|
||||
background-color: var(--input-color);
|
||||
|
||||
/* prevent autoresize overflowing the container */
|
||||
max-width: 100%;
|
||||
|
||||
/* remove styles and layout from this element */
|
||||
margin: 0 0 0 7px;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.react-tags__search-input::-ms-clear {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.react-tags__suggestions {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
@media screen and (min-width: $smMin) {
|
||||
.react-tags__suggestions {
|
||||
width: 240px;
|
||||
}
|
||||
}
|
||||
|
||||
.react-tags__suggestions ul {
|
||||
margin: 4px -1px;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
background: var(--primary-color);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: .25rem;
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, .2);
|
||||
}
|
||||
|
||||
.react-tags__suggestions li {
|
||||
padding: 8px 10px;
|
||||
}
|
||||
|
||||
.react-tags__suggestions li:not(:last-child) {
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.react-tags__suggestions li mark {
|
||||
text-decoration: underline;
|
||||
background: none;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.react-tags__suggestions li:hover {
|
||||
cursor: pointer;
|
||||
background-color: var(--active-color);
|
||||
}
|
||||
|
||||
.react-tags__suggestions li.is-active {
|
||||
background-color: var(--active-color);
|
||||
}
|
||||
|
||||
.react-tags__suggestions li.is-disabled {
|
||||
opacity: .5;
|
||||
cursor: auto;
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
.react-tagsinput {
|
||||
background-color: #fff;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: .25rem;
|
||||
overflow: hidden;
|
||||
min-height: 2.6rem;
|
||||
padding: 6px 0 0 6px;
|
||||
transition: border-color .15s ease-in-out, box-shadow .15s ease-in-out;
|
||||
}
|
||||
|
||||
.react-tagsinput--focused {
|
||||
border-color: #80bdff;
|
||||
box-shadow: 0 0 0 .2rem rgba(0, 123, 255, .25);
|
||||
}
|
||||
|
||||
.react-tagsinput-tag {
|
||||
font-size: 1rem;
|
||||
background-color: #f1f1f1;
|
||||
border-radius: 4px;
|
||||
display: inline-block;
|
||||
font-weight: 400;
|
||||
margin: 0 5px 6px 0;
|
||||
padding: 6px 8px;
|
||||
line-height: 1;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.react-tagsinput-remove {
|
||||
cursor: pointer;
|
||||
font-weight: 700;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.react-tagsinput-tag span:before {
|
||||
content: '\2715';
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.react-tagsinput-input {
|
||||
background: transparent;
|
||||
border: 0;
|
||||
outline: none;
|
||||
padding: 3px 5px;
|
||||
width: 100%;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
13
src/common/services/ImageDownloader.ts
Normal file
13
src/common/services/ImageDownloader.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { AxiosInstance } from 'axios';
|
||||
import { saveUrl } from '../../utils/helpers/files';
|
||||
|
||||
export class ImageDownloader {
|
||||
public constructor(private readonly axios: AxiosInstance, private readonly window: Window) {}
|
||||
|
||||
public async saveImage(imgUrl: string, filename: string): Promise<void> {
|
||||
const { data } = await this.axios.get(imgUrl, { responseType: 'blob' });
|
||||
const url = URL.createObjectURL(data);
|
||||
|
||||
saveUrl(this.window, url, filename);
|
||||
}
|
||||
}
|
||||
@@ -1,22 +1,33 @@
|
||||
import axios from 'axios';
|
||||
import Bottle, { Decorator } from 'bottlejs';
|
||||
import ScrollToTop from '../ScrollToTop';
|
||||
import MainHeader from '../MainHeader';
|
||||
import Home from '../Home';
|
||||
import MenuLayout from '../MenuLayout';
|
||||
import AsideMenu from '../AsideMenu';
|
||||
import ErrorHandler from '../ErrorHandler';
|
||||
import ShlinkVersions from '../ShlinkVersions';
|
||||
import ShlinkVersionsContainer from '../ShlinkVersionsContainer';
|
||||
import { ConnectDecorator } from '../../container/types';
|
||||
import { withoutSelectedServer } from '../../servers/helpers/withoutSelectedServer';
|
||||
import { ImageDownloader } from './ImageDownloader';
|
||||
|
||||
const provideServices = (bottle, connect, withRouter) => {
|
||||
bottle.constant('window', global.window);
|
||||
const provideServices = (bottle: Bottle, connect: ConnectDecorator, withRouter: Decorator) => {
|
||||
// Services
|
||||
bottle.constant('window', (global as any).window);
|
||||
bottle.constant('console', global.console);
|
||||
bottle.constant('axios', axios);
|
||||
|
||||
bottle.serviceFactory('ScrollToTop', ScrollToTop, 'window');
|
||||
bottle.service('ImageDownloader', ImageDownloader, 'axios', 'window');
|
||||
|
||||
// Components
|
||||
bottle.serviceFactory('ScrollToTop', ScrollToTop);
|
||||
bottle.decorator('ScrollToTop', withRouter);
|
||||
|
||||
bottle.serviceFactory('MainHeader', MainHeader, 'ServersDropdown');
|
||||
bottle.decorator('MainHeader', withRouter);
|
||||
|
||||
bottle.serviceFactory('Home', () => Home);
|
||||
bottle.decorator('Home', withoutSelectedServer);
|
||||
bottle.decorator('Home', connect([ 'servers' ], [ 'resetSelectedServer' ]));
|
||||
|
||||
bottle.serviceFactory(
|
||||
@@ -28,16 +39,19 @@ const provideServices = (bottle, connect, withRouter) => {
|
||||
'CreateShortUrl',
|
||||
'ShortUrlVisits',
|
||||
'TagVisits',
|
||||
'ShlinkVersions',
|
||||
'ServerError'
|
||||
'OrphanVisits',
|
||||
'ServerError',
|
||||
'Overview',
|
||||
'EditShortUrl',
|
||||
'ManageDomains',
|
||||
);
|
||||
bottle.decorator('MenuLayout', connect([ 'selectedServer', 'shortUrlsListParams' ], [ 'selectServer' ]));
|
||||
bottle.decorator('MenuLayout', withRouter);
|
||||
|
||||
bottle.serviceFactory('AsideMenu', AsideMenu, 'DeleteServerButton');
|
||||
|
||||
bottle.serviceFactory('ShlinkVersions', () => ShlinkVersions);
|
||||
bottle.decorator('ShlinkVersions', connect([ 'selectedServer' ]));
|
||||
bottle.serviceFactory('ShlinkVersionsContainer', () => ShlinkVersionsContainer);
|
||||
bottle.decorator('ShlinkVersionsContainer', connect([ 'selectedServer' ]));
|
||||
|
||||
bottle.serviceFactory('ErrorHandler', ErrorHandler, 'window', 'console');
|
||||
};
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 8.7 KiB |
@@ -1,8 +1,8 @@
|
||||
import Bottle from 'bottlejs';
|
||||
import Bottle, { IContainer } from 'bottlejs';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import { connect as reduxConnect } from 'react-redux';
|
||||
import { pick } from 'ramda';
|
||||
import App from '../App';
|
||||
import provideApiServices from '../api/services/provideServices';
|
||||
import provideCommonServices from '../common/services/provideServices';
|
||||
import provideShortUrlsServices from '../short-urls/services/provideServices';
|
||||
import provideServersServices from '../servers/services/provideServices';
|
||||
@@ -11,27 +11,31 @@ import provideTagsServices from '../tags/services/provideServices';
|
||||
import provideUtilsServices from '../utils/services/provideServices';
|
||||
import provideMercureServices from '../mercure/services/provideServices';
|
||||
import provideSettingsServices from '../settings/services/provideServices';
|
||||
import provideDomainsServices from '../domains/services/provideServices';
|
||||
import provideAppServices from '../app/services/provideServices';
|
||||
import { ConnectDecorator } from './types';
|
||||
|
||||
type LazyActionMap = Record<string, Function>;
|
||||
|
||||
const bottle = new Bottle();
|
||||
const { container } = bottle;
|
||||
|
||||
const lazyService = (container, serviceName) => (...args) => container[serviceName](...args);
|
||||
const mapActionService = (map, actionName) => ({
|
||||
const lazyService = <T extends Function, K>(container: IContainer, serviceName: string) =>
|
||||
(...args: any[]) => (container[serviceName] as T)(...args) as K;
|
||||
const mapActionService = (map: LazyActionMap, actionName: string): LazyActionMap => ({
|
||||
...map,
|
||||
|
||||
// Wrap actual action service in a function so that it is lazily created the first time it is called
|
||||
[actionName]: lazyService(container, actionName),
|
||||
});
|
||||
const connect = (propsFromState, actionServiceNames = []) =>
|
||||
const connect: ConnectDecorator = (propsFromState: string[] | null, actionServiceNames: string[] = []) =>
|
||||
reduxConnect(
|
||||
propsFromState ? pick(propsFromState) : null,
|
||||
actionServiceNames.reduce(mapActionService, {})
|
||||
actionServiceNames.reduce(mapActionService, {}),
|
||||
);
|
||||
|
||||
bottle.serviceFactory('App', App, 'MainHeader', 'Home', 'MenuLayout', 'CreateServer', 'EditServer', 'Settings');
|
||||
bottle.decorator('App', connect([ 'servers' ], [ 'fetchServers' ]));
|
||||
|
||||
provideAppServices(bottle, connect);
|
||||
provideCommonServices(bottle, connect, withRouter);
|
||||
provideApiServices(bottle);
|
||||
provideShortUrlsServices(bottle, connect);
|
||||
provideServersServices(bottle, connect, withRouter);
|
||||
provideTagsServices(bottle, connect);
|
||||
@@ -39,5 +43,6 @@ provideVisitsServices(bottle, connect);
|
||||
provideUtilsServices(bottle);
|
||||
provideMercureServices(bottle);
|
||||
provideSettingsServices(bottle, connect);
|
||||
provideDomainsServices(bottle, connect);
|
||||
|
||||
export default container;
|
||||
@@ -1,13 +1,12 @@
|
||||
import ReduxThunk from 'redux-thunk';
|
||||
import { applyMiddleware, compose, createStore } from 'redux';
|
||||
import { save, load } from 'redux-localstorage-simple';
|
||||
import { save, load, RLSOptions } from 'redux-localstorage-simple';
|
||||
import reducers from '../reducers';
|
||||
|
||||
const composeEnhancers = process.env.NODE_ENV !== 'production' && window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
|
||||
? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
|
||||
: compose;
|
||||
const isProduction = process.env.NODE_ENV !== 'production';
|
||||
const composeEnhancers: Function = !isProduction && (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
|
||||
|
||||
const localStorageConfig = {
|
||||
const localStorageConfig: RLSOptions = {
|
||||
states: [ 'settings', 'servers' ],
|
||||
namespace: 'shlink',
|
||||
namespaceSeparator: '.',
|
||||
@@ -15,7 +14,7 @@ const localStorageConfig = {
|
||||
};
|
||||
|
||||
const store = createStore(reducers, load(localStorageConfig), composeEnhancers(
|
||||
applyMiddleware(save(localStorageConfig), ReduxThunk)
|
||||
applyMiddleware(save(localStorageConfig), ReduxThunk),
|
||||
));
|
||||
|
||||
export default store;
|
||||
43
src/container/types.ts
Normal file
43
src/container/types.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { MercureInfo } from '../mercure/reducers/mercureInfo';
|
||||
import { SelectedServer, ServersMap } from '../servers/data';
|
||||
import { Settings } from '../settings/reducers/settings';
|
||||
import { ShortUrlCreation } from '../short-urls/reducers/shortUrlCreation';
|
||||
import { ShortUrlDeletion } from '../short-urls/reducers/shortUrlDeletion';
|
||||
import { ShortUrlEdition } from '../short-urls/reducers/shortUrlEdition';
|
||||
import { ShortUrlsListParams } from '../short-urls/reducers/shortUrlsListParams';
|
||||
import { ShortUrlsList } from '../short-urls/reducers/shortUrlsList';
|
||||
import { TagDeletion } from '../tags/reducers/tagDelete';
|
||||
import { TagEdition } from '../tags/reducers/tagEdit';
|
||||
import { TagsList } from '../tags/reducers/tagsList';
|
||||
import { ShortUrlDetail } from '../short-urls/reducers/shortUrlDetail';
|
||||
import { ShortUrlVisits } from '../visits/reducers/shortUrlVisits';
|
||||
import { TagVisits } from '../visits/reducers/tagVisits';
|
||||
import { DomainsList } from '../domains/reducers/domainsList';
|
||||
import { VisitsOverview } from '../visits/reducers/visitsOverview';
|
||||
import { VisitsInfo } from '../visits/types';
|
||||
|
||||
export interface ShlinkState {
|
||||
servers: ServersMap;
|
||||
selectedServer: SelectedServer;
|
||||
shortUrlsList: ShortUrlsList;
|
||||
shortUrlsListParams: ShortUrlsListParams;
|
||||
shortUrlCreationResult: ShortUrlCreation;
|
||||
shortUrlDeletion: ShortUrlDeletion;
|
||||
shortUrlEdition: ShortUrlEdition;
|
||||
shortUrlVisits: ShortUrlVisits;
|
||||
tagVisits: TagVisits;
|
||||
orphanVisits: VisitsInfo;
|
||||
shortUrlDetail: ShortUrlDetail;
|
||||
tagsList: TagsList;
|
||||
tagDelete: TagDeletion;
|
||||
tagEdit: TagEdition;
|
||||
mercureInfo: MercureInfo;
|
||||
settings: Settings;
|
||||
domainsList: DomainsList;
|
||||
visitsOverview: VisitsOverview;
|
||||
appUpdated: boolean;
|
||||
}
|
||||
|
||||
export type ConnectDecorator = (props: string[] | null, actions?: string[]) => any;
|
||||
|
||||
export type GetState = () => ShlinkState;
|
||||
73
src/domains/DomainRow.tsx
Normal file
73
src/domains/DomainRow.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
import { FC } from 'react';
|
||||
import { Button, UncontrolledTooltip } from 'reactstrap';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import {
|
||||
faBan as forbiddenIcon,
|
||||
faCheck as defaultDomainIcon,
|
||||
faEdit as editIcon,
|
||||
} from '@fortawesome/free-solid-svg-icons';
|
||||
import { ShlinkDomain, ShlinkDomainRedirects } from '../api/types';
|
||||
import { useToggle } from '../utils/helpers/hooks';
|
||||
import { OptionalString } from '../utils/utils';
|
||||
import { EditDomainRedirectsModal } from './helpers/EditDomainRedirectsModal';
|
||||
|
||||
interface DomainRowProps {
|
||||
domain: ShlinkDomain;
|
||||
defaultRedirects?: ShlinkDomainRedirects;
|
||||
editDomainRedirects: (domain: string, redirects: Partial<ShlinkDomainRedirects>) => Promise<void>;
|
||||
}
|
||||
|
||||
const Nr: FC<{ fallback: OptionalString }> = ({ fallback }) => (
|
||||
<span className="text-muted">
|
||||
{!fallback && <small>No redirect</small>}
|
||||
{fallback && <>{fallback} <small>(as fallback)</small></>}
|
||||
</span>
|
||||
);
|
||||
const DefaultDomain: FC = () => (
|
||||
<>
|
||||
<FontAwesomeIcon icon={defaultDomainIcon} className="text-primary" id="defaultDomainIcon" />
|
||||
<UncontrolledTooltip target="defaultDomainIcon" placement="right">Default domain</UncontrolledTooltip>
|
||||
</>
|
||||
);
|
||||
|
||||
export const DomainRow: FC<DomainRowProps> = ({ domain, editDomainRedirects, defaultRedirects }) => {
|
||||
const [ isOpen, toggle ] = useToggle();
|
||||
const { domain: authority, isDefault, redirects } = domain;
|
||||
const domainId = `domainEdit${authority.replace(/\./g, '')}`;
|
||||
|
||||
return (
|
||||
<tr className="responsive-table__row">
|
||||
<td className="responsive-table__cell" data-th="Is default domain">{isDefault ? <DefaultDomain /> : ''}</td>
|
||||
<th className="responsive-table__cell" data-th="Domain">{authority}</th>
|
||||
<td className="responsive-table__cell" data-th="Base path redirect">
|
||||
{redirects?.baseUrlRedirect ?? <Nr fallback={defaultRedirects?.baseUrlRedirect} />}
|
||||
</td>
|
||||
<td className="responsive-table__cell" data-th="Regular 404 redirect">
|
||||
{redirects?.regular404Redirect ?? <Nr fallback={defaultRedirects?.regular404Redirect} />}
|
||||
</td>
|
||||
<td className="responsive-table__cell" data-th="Invalid short URL redirect">
|
||||
{redirects?.invalidShortUrlRedirect ?? <Nr fallback={defaultRedirects?.invalidShortUrlRedirect} />}
|
||||
</td>
|
||||
<td className="responsive-table__cell text-right">
|
||||
<span id={domainId}>
|
||||
<Button outline size="sm" disabled={isDefault} onClick={isDefault ? undefined : toggle}>
|
||||
<FontAwesomeIcon icon={isDefault ? forbiddenIcon : editIcon} />
|
||||
</Button>
|
||||
</span>
|
||||
{isDefault && (
|
||||
<UncontrolledTooltip target={domainId} placement="left">
|
||||
Redirects for default domain cannot be edited here.
|
||||
<br />
|
||||
Use config options or env vars directly on the server.
|
||||
</UncontrolledTooltip>
|
||||
)}
|
||||
</td>
|
||||
<EditDomainRedirectsModal
|
||||
domain={domain}
|
||||
isOpen={isOpen}
|
||||
toggle={toggle}
|
||||
editDomainRedirects={editDomainRedirects}
|
||||
/>
|
||||
</tr>
|
||||
);
|
||||
};
|
||||
19
src/domains/DomainSelector.scss
Normal file
19
src/domains/DomainSelector.scss
Normal file
@@ -0,0 +1,19 @@
|
||||
@import '../utils/base';
|
||||
@import '../utils/mixins/vertical-align';
|
||||
|
||||
.domains-dropdown__toggle-btn.domains-dropdown__toggle-btn,
|
||||
.domains-dropdown__toggle-btn.domains-dropdown__toggle-btn:hover,
|
||||
.domains-dropdown__toggle-btn.domains-dropdown__toggle-btn:active {
|
||||
color: $textPlaceholder !important;
|
||||
}
|
||||
|
||||
.domains-dropdown__toggle-btn--active.domains-dropdown__toggle-btn--active,
|
||||
.domains-dropdown__toggle-btn--active.domains-dropdown__toggle-btn--active:hover,
|
||||
.domains-dropdown__toggle-btn--active.domains-dropdown__toggle-btn--active:active {
|
||||
color: var(--input-text-color) !important;
|
||||
}
|
||||
|
||||
.domains-dropdown__back-btn.domains-dropdown__back-btn,
|
||||
.domains-dropdown__back-btn.domains-dropdown__back-btn:hover {
|
||||
border-color: var(--border-color);
|
||||
}
|
||||
75
src/domains/DomainSelector.tsx
Normal file
75
src/domains/DomainSelector.tsx
Normal file
@@ -0,0 +1,75 @@
|
||||
import { useEffect } from 'react';
|
||||
import { Button, DropdownItem, Input, InputGroup, InputGroupAddon, UncontrolledTooltip } from 'reactstrap';
|
||||
import { InputProps } from 'reactstrap/lib/Input';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faUndo } from '@fortawesome/free-solid-svg-icons';
|
||||
import { isEmpty, pipe } from 'ramda';
|
||||
import { DropdownBtn } from '../utils/DropdownBtn';
|
||||
import { useToggle } from '../utils/helpers/hooks';
|
||||
import { DomainsList } from './reducers/domainsList';
|
||||
import './DomainSelector.scss';
|
||||
|
||||
export interface DomainSelectorProps extends Omit<InputProps, 'onChange'> {
|
||||
value?: string;
|
||||
onChange: (domain: string) => void;
|
||||
}
|
||||
|
||||
interface DomainSelectorConnectProps extends DomainSelectorProps {
|
||||
listDomains: Function;
|
||||
domainsList: DomainsList;
|
||||
}
|
||||
|
||||
export const DomainSelector = ({ listDomains, value, domainsList, onChange }: DomainSelectorConnectProps) => {
|
||||
const [ inputDisplayed,, showInput, hideInput ] = useToggle();
|
||||
const { domains } = domainsList;
|
||||
const valueIsEmpty = isEmpty(value);
|
||||
const unselectDomain = () => onChange('');
|
||||
|
||||
useEffect(() => {
|
||||
listDomains();
|
||||
}, []);
|
||||
|
||||
return inputDisplayed ? (
|
||||
<InputGroup>
|
||||
<Input
|
||||
value={value}
|
||||
placeholder="Domain"
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
/>
|
||||
<InputGroupAddon addonType="append">
|
||||
<Button
|
||||
id="backToDropdown"
|
||||
outline
|
||||
type="button"
|
||||
className="domains-dropdown__back-btn"
|
||||
onClick={pipe(unselectDomain, hideInput)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faUndo} />
|
||||
</Button>
|
||||
<UncontrolledTooltip target="backToDropdown" placement="left" trigger="hover">
|
||||
Existing domains
|
||||
</UncontrolledTooltip>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
) : (
|
||||
<DropdownBtn
|
||||
text={valueIsEmpty ? 'Domain' : `Domain: ${value}`}
|
||||
className={!valueIsEmpty ? 'domains-dropdown__toggle-btn--active' : 'domains-dropdown__toggle-btn'}
|
||||
>
|
||||
{domains.map(({ domain, isDefault }) => (
|
||||
<DropdownItem
|
||||
key={domain}
|
||||
active={value === domain || isDefault && valueIsEmpty}
|
||||
onClick={() => onChange(domain)}
|
||||
>
|
||||
{domain}
|
||||
{isDefault && <span className="float-right text-muted">default</span>}
|
||||
</DropdownItem>
|
||||
))}
|
||||
<DropdownItem divider />
|
||||
<DropdownItem onClick={pipe(unselectDomain, showInput)}>
|
||||
<i>New domain</i>
|
||||
</DropdownItem>
|
||||
</DropdownBtn>
|
||||
);
|
||||
};
|
||||
71
src/domains/ManageDomains.tsx
Normal file
71
src/domains/ManageDomains.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
import { FC, useEffect } from 'react';
|
||||
import Message from '../utils/Message';
|
||||
import { Result } from '../utils/Result';
|
||||
import { ShlinkApiError } from '../api/ShlinkApiError';
|
||||
import { SimpleCard } from '../utils/SimpleCard';
|
||||
import SearchField from '../utils/SearchField';
|
||||
import { ShlinkDomainRedirects } from '../api/types';
|
||||
import { DomainsList } from './reducers/domainsList';
|
||||
import { DomainRow } from './DomainRow';
|
||||
|
||||
interface ManageDomainsProps {
|
||||
listDomains: Function;
|
||||
filterDomains: (searchTerm: string) => void;
|
||||
editDomainRedirects: (domain: string, redirects: Partial<ShlinkDomainRedirects>) => Promise<void>;
|
||||
domainsList: DomainsList;
|
||||
}
|
||||
|
||||
const headers = [ '', 'Domain', 'Base path redirect', 'Regular 404 redirect', 'Invalid short URL redirect', '' ];
|
||||
|
||||
export const ManageDomains: FC<ManageDomainsProps> = (
|
||||
{ listDomains, domainsList, filterDomains, editDomainRedirects },
|
||||
) => {
|
||||
const { filteredDomains: domains, loading, error, errorData } = domainsList;
|
||||
const defaultRedirects = domains.find(({ isDefault }) => isDefault)?.redirects;
|
||||
|
||||
useEffect(() => {
|
||||
listDomains();
|
||||
}, []);
|
||||
|
||||
if (loading) {
|
||||
return <Message loading />;
|
||||
}
|
||||
|
||||
const renderContent = () => {
|
||||
if (error) {
|
||||
return (
|
||||
<Result type="error">
|
||||
<ShlinkApiError errorData={errorData} fallbackMessage="Error loading domains :(" />
|
||||
</Result>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<SimpleCard>
|
||||
<table className="table table-hover mb-0">
|
||||
<thead className="responsive-table__header">
|
||||
<tr>{headers.map((column, index) => <th key={index}>{column}</th>)}</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{domains.length < 1 && <tr><td colSpan={headers.length} className="text-center">No results found</td></tr>}
|
||||
{domains.map((domain) => (
|
||||
<DomainRow
|
||||
key={domain.domain}
|
||||
domain={domain}
|
||||
editDomainRedirects={editDomainRedirects}
|
||||
defaultRedirects={defaultRedirects}
|
||||
/>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</SimpleCard>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<SearchField className="mb-3" onChange={filterDomains} />
|
||||
{renderContent()}
|
||||
</>
|
||||
);
|
||||
};
|
||||
72
src/domains/helpers/EditDomainRedirectsModal.tsx
Normal file
72
src/domains/helpers/EditDomainRedirectsModal.tsx
Normal file
@@ -0,0 +1,72 @@
|
||||
import { FC, useState } from 'react';
|
||||
import { Button, Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap';
|
||||
import { ShlinkDomain, ShlinkDomainRedirects } from '../../api/types';
|
||||
import { FormGroupContainer, FormGroupContainerProps } from '../../utils/FormGroupContainer';
|
||||
import { handleEventPreventingDefault, nonEmptyValueOrNull } from '../../utils/utils';
|
||||
import { InfoTooltip } from '../../utils/InfoTooltip';
|
||||
|
||||
interface EditDomainRedirectsModalProps {
|
||||
domain: ShlinkDomain;
|
||||
isOpen: boolean;
|
||||
toggle: () => void;
|
||||
editDomainRedirects: (domain: string, redirects: Partial<ShlinkDomainRedirects>) => Promise<void>;
|
||||
}
|
||||
|
||||
const FormGroup: FC<FormGroupContainerProps & { isLast?: boolean }> = ({ isLast, ...rest }) => (
|
||||
<FormGroupContainer
|
||||
{...rest}
|
||||
required={false}
|
||||
type="url"
|
||||
placeholder="No redirect"
|
||||
className={isLast ? 'mb-0' : ''}
|
||||
/>
|
||||
);
|
||||
|
||||
export const EditDomainRedirectsModal: FC<EditDomainRedirectsModalProps> = (
|
||||
{ isOpen, toggle, domain, editDomainRedirects },
|
||||
) => {
|
||||
const [ baseUrlRedirect, setBaseUrlRedirect ] = useState(domain.redirects?.baseUrlRedirect ?? '');
|
||||
const [ regular404Redirect, setRegular404Redirect ] = useState(domain.redirects?.regular404Redirect ?? '');
|
||||
const [ invalidShortUrlRedirect, setInvalidShortUrlRedirect ] = useState(
|
||||
domain.redirects?.invalidShortUrlRedirect ?? '',
|
||||
);
|
||||
const handleSubmit = handleEventPreventingDefault(async () => editDomainRedirects(domain.domain, {
|
||||
baseUrlRedirect: nonEmptyValueOrNull(baseUrlRedirect),
|
||||
regular404Redirect: nonEmptyValueOrNull(regular404Redirect),
|
||||
invalidShortUrlRedirect: nonEmptyValueOrNull(invalidShortUrlRedirect),
|
||||
}).then(toggle));
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} toggle={toggle} centered>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<ModalHeader toggle={toggle}>Edit redirects for <b>{domain.domain}</b></ModalHeader>
|
||||
<ModalBody>
|
||||
<FormGroup value={baseUrlRedirect} onChange={setBaseUrlRedirect}>
|
||||
<InfoTooltip className="mr-2" placement="bottom">
|
||||
Visitors accessing the base url, as in <b>https://{domain.domain}/</b>, will be redirected to this URL.
|
||||
</InfoTooltip>
|
||||
Base URL
|
||||
</FormGroup>
|
||||
<FormGroup value={regular404Redirect} onChange={setRegular404Redirect}>
|
||||
<InfoTooltip className="mr-2" placement="bottom">
|
||||
Visitors accessing a url not matching a short URL pattern, as in <b>https://{domain.domain}/???/[...]</b>,
|
||||
will be redirected to this URL.
|
||||
</InfoTooltip>
|
||||
Regular 404
|
||||
</FormGroup>
|
||||
<FormGroup value={invalidShortUrlRedirect} isLast onChange={setInvalidShortUrlRedirect}>
|
||||
<InfoTooltip className="mr-2" placement="bottom">
|
||||
Visitors accessing a url matching a short URL pattern, but not matching an existing short code, will be
|
||||
redirected to this URL.
|
||||
</InfoTooltip>
|
||||
Invalid short URL
|
||||
</FormGroup>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button color="link" type="button" onClick={toggle}>Cancel</Button>
|
||||
<Button color="primary">Save</Button>
|
||||
</ModalFooter>
|
||||
</form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
33
src/domains/reducers/domainRedirects.ts
Normal file
33
src/domains/reducers/domainRedirects.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { Action, Dispatch } from 'redux';
|
||||
import { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilder';
|
||||
import { ShlinkDomainRedirects } from '../../api/types';
|
||||
import { GetState } from '../../container/types';
|
||||
import { ApiErrorAction } from '../../api/types/actions';
|
||||
import { parseApiError } from '../../api/utils';
|
||||
|
||||
/* eslint-disable padding-line-between-statements */
|
||||
export const EDIT_DOMAIN_REDIRECTS_START = 'shlink/domainRedirects/EDIT_DOMAIN_REDIRECTS_START';
|
||||
export const EDIT_DOMAIN_REDIRECTS_ERROR = 'shlink/domainRedirects/EDIT_DOMAIN_REDIRECTS_ERROR';
|
||||
export const EDIT_DOMAIN_REDIRECTS = 'shlink/domainRedirects/EDIT_DOMAIN_REDIRECTS';
|
||||
/* eslint-enable padding-line-between-statements */
|
||||
|
||||
export interface EditDomainRedirectsAction extends Action<string> {
|
||||
domain: string;
|
||||
redirects: ShlinkDomainRedirects;
|
||||
}
|
||||
|
||||
export const editDomainRedirects = (buildShlinkApiClient: ShlinkApiClientBuilder) => (
|
||||
domain: string,
|
||||
domainRedirects: Partial<ShlinkDomainRedirects>,
|
||||
) => async (dispatch: Dispatch, getState: GetState) => {
|
||||
dispatch({ type: EDIT_DOMAIN_REDIRECTS_START });
|
||||
const { editDomainRedirects } = buildShlinkApiClient(getState);
|
||||
|
||||
try {
|
||||
const redirects = await editDomainRedirects({ domain, ...domainRedirects });
|
||||
|
||||
dispatch<EditDomainRedirectsAction>({ type: EDIT_DOMAIN_REDIRECTS, domain, redirects });
|
||||
} catch (e) {
|
||||
dispatch<ApiErrorAction>({ type: EDIT_DOMAIN_REDIRECTS_ERROR, errorData: parseApiError(e) });
|
||||
}
|
||||
};
|
||||
79
src/domains/reducers/domainsList.ts
Normal file
79
src/domains/reducers/domainsList.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import { Action, Dispatch } from 'redux';
|
||||
import { ProblemDetailsError, ShlinkDomain, ShlinkDomainRedirects } from '../../api/types';
|
||||
import { buildReducer } from '../../utils/helpers/redux';
|
||||
import { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilder';
|
||||
import { GetState } from '../../container/types';
|
||||
import { parseApiError } from '../../api/utils';
|
||||
import { ApiErrorAction } from '../../api/types/actions';
|
||||
import { EDIT_DOMAIN_REDIRECTS, EditDomainRedirectsAction } from './domainRedirects';
|
||||
|
||||
/* eslint-disable padding-line-between-statements */
|
||||
export const LIST_DOMAINS_START = 'shlink/domainsList/LIST_DOMAINS_START';
|
||||
export const LIST_DOMAINS_ERROR = 'shlink/domainsList/LIST_DOMAINS_ERROR';
|
||||
export const LIST_DOMAINS = 'shlink/domainsList/LIST_DOMAINS';
|
||||
export const FILTER_DOMAINS = 'shlink/domainsList/FILTER_DOMAINS';
|
||||
/* eslint-enable padding-line-between-statements */
|
||||
|
||||
export interface DomainsList {
|
||||
domains: ShlinkDomain[];
|
||||
filteredDomains: ShlinkDomain[];
|
||||
loading: boolean;
|
||||
error: boolean;
|
||||
errorData?: ProblemDetailsError;
|
||||
}
|
||||
|
||||
export interface ListDomainsAction extends Action<string> {
|
||||
domains: ShlinkDomain[];
|
||||
}
|
||||
|
||||
interface FilterDomainsAction extends Action<string> {
|
||||
searchTerm: string;
|
||||
}
|
||||
|
||||
const initialState: DomainsList = {
|
||||
domains: [],
|
||||
filteredDomains: [],
|
||||
loading: false,
|
||||
error: false,
|
||||
};
|
||||
|
||||
export type DomainsCombinedAction = ListDomainsAction
|
||||
& ApiErrorAction
|
||||
& FilterDomainsAction
|
||||
& EditDomainRedirectsAction;
|
||||
|
||||
export const replaceRedirectsOnDomain = (domain: string, redirects: ShlinkDomainRedirects) =>
|
||||
(d: ShlinkDomain): ShlinkDomain => d.domain !== domain ? d : { ...d, redirects };
|
||||
|
||||
export default buildReducer<DomainsList, DomainsCombinedAction>({
|
||||
[LIST_DOMAINS_START]: () => ({ ...initialState, loading: true }),
|
||||
[LIST_DOMAINS_ERROR]: ({ errorData }) => ({ ...initialState, error: true, errorData }),
|
||||
[LIST_DOMAINS]: (_, { domains }) => ({ ...initialState, domains, filteredDomains: domains }),
|
||||
[FILTER_DOMAINS]: (state, { searchTerm }) => ({
|
||||
...state,
|
||||
filteredDomains: state.domains.filter(({ domain }) => domain.toLowerCase().match(searchTerm)),
|
||||
}),
|
||||
[EDIT_DOMAIN_REDIRECTS]: (state, { domain, redirects }) => ({
|
||||
...state,
|
||||
domains: state.domains.map(replaceRedirectsOnDomain(domain, redirects)),
|
||||
filteredDomains: state.filteredDomains.map(replaceRedirectsOnDomain(domain, redirects)),
|
||||
}),
|
||||
}, initialState);
|
||||
|
||||
export const listDomains = (buildShlinkApiClient: ShlinkApiClientBuilder) => () => async (
|
||||
dispatch: Dispatch,
|
||||
getState: GetState,
|
||||
) => {
|
||||
dispatch({ type: LIST_DOMAINS_START });
|
||||
const { listDomains } = buildShlinkApiClient(getState);
|
||||
|
||||
try {
|
||||
const domains = await listDomains();
|
||||
|
||||
dispatch<ListDomainsAction>({ type: LIST_DOMAINS, domains });
|
||||
} catch (e) {
|
||||
dispatch<ApiErrorAction>({ type: LIST_DOMAINS_ERROR, errorData: parseApiError(e) });
|
||||
}
|
||||
};
|
||||
|
||||
export const filterDomains = (searchTerm: string): FilterDomainsAction => ({ type: FILTER_DOMAINS, searchTerm });
|
||||
25
src/domains/services/provideServices.ts
Normal file
25
src/domains/services/provideServices.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import Bottle from 'bottlejs';
|
||||
import { ConnectDecorator } from '../../container/types';
|
||||
import { filterDomains, listDomains } from '../reducers/domainsList';
|
||||
import { DomainSelector } from '../DomainSelector';
|
||||
import { ManageDomains } from '../ManageDomains';
|
||||
import { editDomainRedirects } from '../reducers/domainRedirects';
|
||||
|
||||
const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
|
||||
// Components
|
||||
bottle.serviceFactory('DomainSelector', () => DomainSelector);
|
||||
bottle.decorator('DomainSelector', connect([ 'domainsList' ], [ 'listDomains' ]));
|
||||
|
||||
bottle.serviceFactory('ManageDomains', () => ManageDomains);
|
||||
bottle.decorator('ManageDomains', connect(
|
||||
[ 'domainsList' ],
|
||||
[ 'listDomains', 'filterDomains', 'editDomainRedirects' ],
|
||||
));
|
||||
|
||||
// Actions
|
||||
bottle.serviceFactory('listDomains', listDomains, 'buildShlinkApiClient');
|
||||
bottle.serviceFactory('filterDomains', () => filterDomains);
|
||||
bottle.serviceFactory('editDomainRedirects', editDomainRedirects, 'buildShlinkApiClient');
|
||||
};
|
||||
|
||||
export default provideServices;
|
||||
173
src/index.scss
173
src/index.scss
@@ -1,37 +1,158 @@
|
||||
/* stylelint-disable no-descending-specificity */
|
||||
|
||||
@import './utils/base';
|
||||
@import 'node_modules/bootstrap/scss/bootstrap.scss';
|
||||
@import './common/react-tag-autocomplete.scss';
|
||||
@import './theme/theme';
|
||||
@import './utils/table/ResponsiveTable';
|
||||
@import './utils/StickyCardPaginator';
|
||||
|
||||
* {
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
html,
|
||||
body,
|
||||
#root {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
* {
|
||||
outline: none !important;
|
||||
background: var(--secondary-color);
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.bg-main {
|
||||
background-color: $mainColor !important;
|
||||
}
|
||||
|
||||
.card-body,
|
||||
.card-header,
|
||||
.list-group-item {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
background-color: var(--primary-color-alfa);
|
||||
}
|
||||
|
||||
.card {
|
||||
box-shadow: 0 .125rem .25rem rgba(0, 0, 0, .075);
|
||||
background-color: var(--primary-color);
|
||||
border-color: var(--border-color);
|
||||
}
|
||||
|
||||
.list-group {
|
||||
background-color: var(--primary-color);
|
||||
}
|
||||
|
||||
.modal-content,
|
||||
.page-link,
|
||||
.page-item.disabled .page-link,
|
||||
.dropdown-menu {
|
||||
background-color: var(--primary-color);
|
||||
}
|
||||
|
||||
.modal-header,
|
||||
.modal-footer,
|
||||
.card-header,
|
||||
.card-footer,
|
||||
.table thead th,
|
||||
.table th,
|
||||
.table td,
|
||||
.page-link,
|
||||
.page-link:hover,
|
||||
.page-item.disabled .page-link,
|
||||
.dropdown-divider,
|
||||
.dropdown-menu,
|
||||
.list-group-item,
|
||||
.modal-content,
|
||||
hr {
|
||||
border-color: var(--border-color);
|
||||
}
|
||||
|
||||
.table-bordered,
|
||||
.table-bordered thead th,
|
||||
.table-bordered thead td {
|
||||
border-color: var(--table-border-color);
|
||||
}
|
||||
|
||||
.page-link:hover {
|
||||
background-color: var(--secondary-color);
|
||||
}
|
||||
|
||||
.page-item.active .page-link {
|
||||
background-color: var(--brand-color);
|
||||
border-color: var(--brand-color);
|
||||
}
|
||||
|
||||
.pagination .page-link {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.container-xl {
|
||||
@media (min-width: $xlgMin) {
|
||||
max-width: 1320px;
|
||||
}
|
||||
|
||||
@media (max-width: $smMax) {
|
||||
padding-right: 0;
|
||||
padding-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-item,
|
||||
.dropdown-item-text {
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.dropdown-item:not(:disabled) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.dropdown-item:focus:not(:disabled),
|
||||
.dropdown-item:hover:not(:disabled),
|
||||
.dropdown-item.active:not(:disabled),
|
||||
.dropdown-item:active:not(:disabled) {
|
||||
background-color: $lightGrey !important;
|
||||
color: inherit !important;
|
||||
background-color: var(--active-color) !important;
|
||||
color: var(--text-color) !important;
|
||||
}
|
||||
|
||||
.badge-main {
|
||||
color: #fff;
|
||||
background-color: $mainColor;
|
||||
color: #ffffff;
|
||||
background-color: var(--brand-color);
|
||||
}
|
||||
|
||||
.react-datepicker__input-container,
|
||||
.react-datepicker-wrapper {
|
||||
display: block !important;
|
||||
.close,
|
||||
.close:hover,
|
||||
.table,
|
||||
.table-hover tbody tr:hover {
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.table-hover tbody tr:hover {
|
||||
background-color: var(--secondary-color);
|
||||
}
|
||||
|
||||
.form-control,
|
||||
.form-control:focus {
|
||||
background-color: var(--primary-color);
|
||||
border-color: var(--input-border-color);
|
||||
color: var(--input-text-color);
|
||||
}
|
||||
|
||||
.form-control.disabled,
|
||||
.form-control:disabled {
|
||||
background-color: var(--input-disabled-color);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.card .form-control:not(:disabled),
|
||||
.card .form-control:not(:disabled):hover {
|
||||
background-color: var(--input-color);
|
||||
}
|
||||
|
||||
.table-active,
|
||||
.table-active > th,
|
||||
.table-active > td {
|
||||
background-color: var(--table-highlight-color) !important;
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
@@ -40,28 +161,34 @@ body,
|
||||
}
|
||||
}
|
||||
|
||||
.pagination .page-link {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.indivisible {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.text-ellipsis {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.react-datepicker__day--keyboard-selected {
|
||||
background-color: $mainColor;
|
||||
|
||||
&:hover {
|
||||
background-color: darken($mainColor, 12%);
|
||||
}
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
background-color: $mainColor;
|
||||
}
|
||||
|
||||
.btn-xs-block {
|
||||
@media (max-width: $xsMax) {
|
||||
width: 100%;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-md-block {
|
||||
@media (max-width: $mdMax) {
|
||||
width: 100%;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +1,19 @@
|
||||
import 'bootstrap/dist/css/bootstrap.min.css';
|
||||
import React from 'react';
|
||||
import { render } from 'react-dom';
|
||||
import { Provider } from 'react-redux';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import { homepage } from '../package.json';
|
||||
import registerServiceWorker from './registerServiceWorker';
|
||||
import container from './container';
|
||||
import store from './container/store';
|
||||
import { fixLeafletIcons } from './utils/utils';
|
||||
import { fixLeafletIcons } from './utils/helpers/leaflet';
|
||||
import { register as registerServiceWorker } from './serviceWorkerRegistration';
|
||||
import 'react-datepicker/dist/react-datepicker.css';
|
||||
import 'leaflet/dist/leaflet.css';
|
||||
import './common/react-tagsinput.scss';
|
||||
import './index.scss';
|
||||
|
||||
// This overwrites icons used for leaflet maps, fixing some issues caused by webpack while processing the CSS
|
||||
fixLeafletIcons();
|
||||
|
||||
const { App, ScrollToTop, ErrorHandler } = container;
|
||||
const { App, ScrollToTop, ErrorHandler, appUpdateAvailable } = container;
|
||||
|
||||
render(
|
||||
<Provider store={store}>
|
||||
@@ -28,6 +25,14 @@ render(
|
||||
</ErrorHandler>
|
||||
</BrowserRouter>
|
||||
</Provider>,
|
||||
document.getElementById('root')
|
||||
document.getElementById('root'),
|
||||
);
|
||||
registerServiceWorker();
|
||||
|
||||
// If you want your app to work offline and load faster, you can change
|
||||
// unregister() to register() below. Note this comes with some pitfalls.
|
||||
// Learn more about service workers: https://cra.link/PWA
|
||||
registerServiceWorker({
|
||||
onUpdate() {
|
||||
store.dispatch(appUpdateAvailable()); // eslint-disable-line @typescript-eslint/no-unsafe-call
|
||||
},
|
||||
});
|
||||
7
src/mercure/helpers/Topics.ts
Normal file
7
src/mercure/helpers/Topics.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export class Topics {
|
||||
public static readonly visits = 'https://shlink.io/new-visit';
|
||||
|
||||
public static readonly orphanVisits = 'https://shlink.io/new-orphan-visit';
|
||||
|
||||
public static readonly shortUrlVisits = (shortCode: string) => `https://shlink.io/new-visit/${shortCode}`;
|
||||
}
|
||||
41
src/mercure/helpers/boundToMercureHub.tsx
Normal file
41
src/mercure/helpers/boundToMercureHub.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import { FC, useEffect } from 'react';
|
||||
import { pipe } from 'ramda';
|
||||
import { CreateVisit } from '../../visits/types';
|
||||
import { MercureInfo } from '../reducers/mercureInfo';
|
||||
import { bindToMercureTopic } from './index';
|
||||
|
||||
export interface MercureBoundProps {
|
||||
createNewVisits: (createdVisits: CreateVisit[]) => void;
|
||||
loadMercureInfo: () => void;
|
||||
mercureInfo: MercureInfo;
|
||||
}
|
||||
|
||||
export function boundToMercureHub<T = {}>(
|
||||
WrappedComponent: FC<MercureBoundProps & T>,
|
||||
getTopicsForProps: (props: T) => string[],
|
||||
) {
|
||||
const pendingUpdates = new Set<CreateVisit>();
|
||||
|
||||
return (props: MercureBoundProps & T) => {
|
||||
const { createNewVisits, loadMercureInfo, mercureInfo } = props;
|
||||
const { interval } = mercureInfo;
|
||||
|
||||
useEffect(() => {
|
||||
const onMessage = (visit: CreateVisit) => interval ? pendingUpdates.add(visit) : createNewVisits([ visit ]);
|
||||
const closeEventSource = bindToMercureTopic(mercureInfo, getTopicsForProps(props), onMessage, loadMercureInfo);
|
||||
|
||||
if (!interval) {
|
||||
return closeEventSource;
|
||||
}
|
||||
|
||||
const timer = setInterval(() => {
|
||||
createNewVisits([ ...pendingUpdates ]);
|
||||
pendingUpdates.clear();
|
||||
}, interval * 1000 * 60);
|
||||
|
||||
return pipe(() => clearInterval(timer), () => closeEventSource?.());
|
||||
}, [ mercureInfo ]);
|
||||
|
||||
return <WrappedComponent {...props} />;
|
||||
};
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
import { EventSourcePolyfill as EventSource } from 'event-source-polyfill';
|
||||
|
||||
export const bindToMercureTopic = (mercureInfo, realTimeUpdates, topic, onMessage, onTokenExpired) => () => {
|
||||
const { enabled } = realTimeUpdates;
|
||||
const { mercureHubUrl, token, loading, error } = mercureInfo;
|
||||
|
||||
if (!enabled || loading || error) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const hubUrl = new URL(mercureHubUrl);
|
||||
|
||||
hubUrl.searchParams.append('topic', topic);
|
||||
const es = new EventSource(hubUrl, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
|
||||
es.onmessage = ({ data }) => onMessage(JSON.parse(data));
|
||||
es.onerror = ({ status }) => status === 401 && onTokenExpired();
|
||||
|
||||
return () => es.close();
|
||||
};
|
||||
31
src/mercure/helpers/index.ts
Normal file
31
src/mercure/helpers/index.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { EventSourcePolyfill as EventSource } from 'event-source-polyfill';
|
||||
import { MercureInfo } from '../reducers/mercureInfo';
|
||||
|
||||
export const bindToMercureTopic = <T>(mercureInfo: MercureInfo, topics: string[], onMessage: (message: T) => void, onTokenExpired: () => void) => { // eslint-disable-line max-len
|
||||
const { mercureHubUrl, token, loading, error } = mercureInfo;
|
||||
|
||||
if (loading || error || !mercureHubUrl) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const onEventSourceMessage = ({ data }: { data: string }) => onMessage(JSON.parse(data) as T);
|
||||
const onEventSourceError = ({ status }: { status: number }) => status === 401 && onTokenExpired();
|
||||
|
||||
const subscriptions = topics.map((topic) => {
|
||||
const hubUrl = new URL(mercureHubUrl);
|
||||
|
||||
hubUrl.searchParams.append('topic', topic);
|
||||
const es = new EventSource(hubUrl, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
|
||||
es.onmessage = onEventSourceMessage;
|
||||
es.onerror = onEventSourceError;
|
||||
|
||||
return es;
|
||||
});
|
||||
|
||||
return () => subscriptions.forEach((es) => es.close());
|
||||
};
|
||||
@@ -1,41 +0,0 @@
|
||||
import { handleActions } from 'redux-actions';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
/* eslint-disable padding-line-between-statements */
|
||||
export const GET_MERCURE_INFO_START = 'shlink/mercure/GET_MERCURE_INFO_START';
|
||||
export const GET_MERCURE_INFO_ERROR = 'shlink/mercure/GET_MERCURE_INFO_ERROR';
|
||||
export const GET_MERCURE_INFO = 'shlink/mercure/GET_MERCURE_INFO';
|
||||
/* eslint-enable padding-line-between-statements */
|
||||
|
||||
export const MercureInfoType = PropTypes.shape({
|
||||
token: PropTypes.string,
|
||||
mercureHubUrl: PropTypes.string,
|
||||
loading: PropTypes.bool,
|
||||
error: PropTypes.bool,
|
||||
});
|
||||
|
||||
const initialState = {
|
||||
token: undefined,
|
||||
mercureHubUrl: undefined,
|
||||
loading: true,
|
||||
error: false,
|
||||
};
|
||||
|
||||
export default handleActions({
|
||||
[GET_MERCURE_INFO_START]: (state) => ({ ...state, loading: true, error: false }),
|
||||
[GET_MERCURE_INFO_ERROR]: (state) => ({ ...state, loading: false, error: true }),
|
||||
[GET_MERCURE_INFO]: (state, { token, mercureHubUrl }) => ({ token, mercureHubUrl, loading: false, error: false }),
|
||||
}, initialState);
|
||||
|
||||
export const loadMercureInfo = (buildShlinkApiClient) => () => async (dispatch, getState) => {
|
||||
dispatch({ type: GET_MERCURE_INFO_START });
|
||||
const { mercureInfo } = buildShlinkApiClient(getState);
|
||||
|
||||
try {
|
||||
const result = await mercureInfo();
|
||||
|
||||
dispatch({ type: GET_MERCURE_INFO, ...result });
|
||||
} catch (e) {
|
||||
dispatch({ type: GET_MERCURE_INFO_ERROR });
|
||||
}
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user