Compare commits

...

302 Commits

Author SHA1 Message Date
Alejandro Celaya
984c1ea716 Added v2.5.0 to changelog 2020-05-31 12:00:50 +02:00
Alejandro Celaya
df38cf6ca9 Merge pull request #275 from acelaya-forks/feature/remove-class-components
Feature/remove class components
2020-05-31 11:51:08 +02:00
Alejandro Celaya
1b60b0e2a8 Updated SortableBarGraph to be a functional component 2020-05-31 11:36:56 +02:00
Alejandro Celaya
11f9c7c507 Updated TagsSelector to be a functional component 2020-05-31 11:19:53 +02:00
Alejandro Celaya
ebe649aaac Updated EditTagModal to be a functional component 2020-05-31 11:06:23 +02:00
Alejandro Celaya
656b68d422 Updated DeleteTagConfirmModal to be a functional component 2020-05-31 10:45:18 +02:00
Alejandro Celaya
cd1f186e28 Updated EditTagsModal to be a functional component 2020-05-31 10:31:00 +02:00
Alejandro Celaya
d0b3edaa2f Updated DeleteShortUrlModal to be a functional component 2020-05-31 10:23:13 +02:00
Alejandro Celaya
2268b85ade Updated CreateShortUrlResult to be a functional component 2020-05-31 10:16:09 +02:00
Alejandro Celaya
d7e3b7b912 Updated ImportServersBtn to be a functional component 2020-05-31 09:58:27 +02:00
Alejandro Celaya
4bd83eecfb Updated ScrollToTop to be a functional component 2020-05-31 09:50:00 +02:00
Alejandro Celaya
b7fd2308ad Merge pull request #274 from acelaya-forks/feature/stryker3
Updated to stryker 3
2020-05-31 09:34:02 +02:00
Alejandro Celaya
a6958941ad Updated to stryker 3 2020-05-31 09:27:42 +02:00
Alejandro Celaya
c98b28ff0f Merge pull request #273 from acelaya-forks/feature/charts-improvements
Feature/charts improvements
2020-05-31 09:19:02 +02:00
Alejandro Celaya
6a372badfa Improved labels displayed in charts when visits are highlighted 2020-05-31 09:07:21 +02:00
Alejandro Celaya
b6ab9a1bdd Improved memoization of grouped visits for line chart 2020-05-31 08:55:52 +02:00
Alejandro Celaya
daf9e7cf64 Merge pull request #272 from acelaya-forks/feature/line-chart-improvements
Some improvements on LineChartCard
2020-05-30 17:52:55 +02:00
Alejandro Celaya
ef42dcd666 Simplified code and removed duplication 2020-05-30 17:43:13 +02:00
Alejandro Celaya
1b6028ae6d Some improvements on LineChartCard 2020-05-30 17:39:08 +02:00
Alejandro Celaya
9340512980 Merge pull request #271 from acelaya-forks/feature/line-chart
Feature/line chart
2020-05-30 10:54:27 +02:00
Alejandro Celaya
9d0b4cc065 Updated changelog 2020-05-30 10:43:18 +02:00
Alejandro Celaya
c5cb0dcb26 Added test for LineChartCard 2020-05-30 10:41:46 +02:00
Alejandro Celaya
a42f5ab13e Set fixed height for time-based line chart 2020-05-30 10:26:52 +02:00
Alejandro Celaya
68b0577526 Added dynamic grouping to time-based line chart 2020-05-30 09:57:21 +02:00
Alejandro Celaya
61867366e7 Created first version of the time-based visits chart 2020-05-30 09:25:15 +02:00
Alejandro Celaya
c670d86955 Merge pull request #270 from acelaya-forks/feature/parallel-docker-build
Feature/parallel docker build
2020-05-17 10:37:34 +02:00
Alejandro Celaya
4565a64cd8 Rolled back node version for scrutinizer 2020-05-17 10:28:49 +02:00
Alejandro Celaya
f36e42d9c1 Fixed travis config error 2020-05-17 10:16:53 +02:00
Alejandro Celaya
0a3a97242b Simplified docker image building, as it will now be run in a different job as the dist file release 2020-05-17 10:13:33 +02:00
Alejandro Celaya
68253c3bc4 Disabled multi-arch building until everything is compatible 2020-05-17 10:12:39 +02:00
Alejandro Celaya
544384d85e Updated runtimne versions 2020-05-17 10:04:05 +02:00
Alejandro Celaya
91daec852f Added parallel docker image multi-arch building 2020-05-17 10:02:36 +02:00
Alejandro Celaya
dcc5b9cc8c Merge pull request #268 from acelaya-forks/feature/tag-visits
Feature/tag visits
2020-05-13 20:41:39 +02:00
Alejandro Celaya
1d26cd93fb Added real time updates to tags list page 2020-05-13 18:32:27 +02:00
Alejandro Celaya
e47dfaf36f Changed paginable charts so that they use 50 items per page by default 2020-05-11 19:44:27 +02:00
Alejandro Celaya
09e2c69e46 Ensured visits by tag route does not work for old Shlink servers 2020-05-11 19:40:19 +02:00
Alejandro Celaya
07d3567244 Added progress bar to visits page when loading a lot of visits 2020-05-11 19:32:42 +02:00
Alejandro Celaya
9bdbe90716 Ensured state is properly reset when starting, finisihing or failing to load visits 2020-05-11 18:55:35 +02:00
Alejandro Celaya
02a4380f7c Updated changelog 2020-05-10 20:36:03 +02:00
Alejandro Celaya
4e483dc5d4 Created TagVisits test 2020-05-10 20:30:19 +02:00
Alejandro Celaya
52631e629e Created TagVisitsHeader test 2020-05-10 20:23:45 +02:00
Alejandro Celaya
3a53298417 Improved visits pages titles 2020-05-10 20:17:17 +02:00
Alejandro Celaya
fb0f14fc16 Created header for visits by tag section 2020-05-10 19:49:58 +02:00
Alejandro Celaya
7a94b1730d Created common component for visits header 2020-05-10 19:37:00 +02:00
Alejandro Celaya
f856bc218a Created tagVisits reducer test 2020-05-10 19:12:18 +02:00
Alejandro Celaya
bfbb21e1cc Created page for tag visit stats 2020-05-10 19:02:58 +02:00
Alejandro Celaya
18e18f533b Extracted visits charts elements into reusable component 2020-05-10 17:49:55 +02:00
Alejandro Celaya
6eead70511 Merge pull request #266 from acelaya-forks/feature/tags-list-improvements
Feature/tags list improvements
2020-05-10 11:34:48 +02:00
Alejandro Celaya
6fd30ed51a Improved how tags are exposed by the ApiClient when listing tags 2020-05-10 11:20:40 +02:00
Alejandro Celaya
67c674f073 Updated changelog 2020-05-10 11:15:24 +02:00
Alejandro Celaya
289d8784c0 Converted TagCard component into functional component 2020-05-10 11:12:22 +02:00
Alejandro Celaya
18e026e4ca Updated tags list to display visits and short URLs when remote shlink version allows it 2020-05-10 10:57:49 +02:00
Alejandro Celaya
8741f42fe8 Merge pull request #264 from acelaya-forks/feature/charts-precision
Feature/charts precision
2020-05-07 11:21:51 +02:00
Alejandro Celaya
665d6209d9 Updated changelog 2020-05-07 11:10:10 +02:00
Alejandro Celaya
59fda29894 Added precision 0 to charts, to avoid having decimals 2020-05-07 11:06:14 +02:00
Alejandro Celaya
61c027f9a1 Merge pull request #263 from acelaya-forks/feature/minor-improvements
Minor improvements
2020-05-03 20:23:24 +02:00
Alejandro Celaya
241c9b73b0 Minor improvements 2020-05-03 20:16:21 +02:00
Alejandro Celaya
85dc1d0825 Merge pull request #260 from acelaya-forks/feature/responsive-visits-table
Feature/responsive visits table
2020-04-28 19:08:20 +02:00
Alejandro Celaya
e38887aa26 Ensured side menu is not swippoed when horizontally scrolling visits table 2020-04-28 18:54:58 +02:00
Alejandro Celaya
54fec79945 First minor improvements to the visits table responsiveness 2020-04-27 18:04:24 +02:00
Alejandro Celaya
fad0bf1c9d Merge pull request #258 from acelaya-forks/feature/redux-localstorage
Feature/redux localstorage
2020-04-27 13:49:05 +02:00
Alejandro Celaya
be2f86050f Updated changelog 2020-04-27 13:37:54 +02:00
Alejandro Celaya
a7f941e8e4 Deleted no-longer-needed ServersService 2020-04-27 13:21:07 +02:00
Alejandro Celaya
b08c6748c7 Moved remote servers loading to separated action 2020-04-27 12:54:52 +02:00
Alejandro Celaya
bdd7932e07 Refactored ServersDropdown into functional component 2020-04-27 12:30:17 +02:00
Alejandro Celaya
bcf5dcf180 Converted server handling actions into regular actions 2020-04-27 11:30:51 +02:00
Alejandro Celaya
8b2cbf7aea Some minor refactorings 2020-04-27 10:52:19 +02:00
Alejandro Celaya
277b5e43f8 Flatten model holding list of servers 2020-04-27 10:49:55 +02:00
Alejandro Celaya
7dd6a31609 Deleted SettingsService, as the task is not transparently handled by a redux middleware 2020-04-26 19:07:47 +02:00
Alejandro Celaya
86bf1515d4 Added redux middleware to save parts of the store in the local storage transparently 2020-04-26 19:04:17 +02:00
Alejandro Celaya
bbc47b387e Created single reducer to handle settings 2020-04-26 13:00:27 +02:00
Alejandro Celaya
3953e98a77 Merge pull request #257 from acelaya-forks/feature/navigate-back
Feature/navigate back
2020-04-26 12:02:54 +02:00
Alejandro Celaya
09b8bd501d Converted TagsList component into functional component 2020-04-26 11:48:08 +02:00
Alejandro Celaya
6bddaaa055 Added cancel button to edit server page 2020-04-26 10:56:27 +02:00
Alejandro Celaya
dd728d4d13 Added back button to visits stats page 2020-04-26 10:43:00 +02:00
Alejandro Celaya
9ba8bc8f3d Merge pull request #256 from acelaya-forks/feature/settings-page
Feature/settings page
2020-04-25 11:19:19 +02:00
Alejandro Celaya
16dee3664b Ensured mercure updates are not set even if supported, when they have been disabled 2020-04-25 10:37:50 +02:00
Alejandro Celaya
6fcf588bfd Updated changelog 2020-04-25 10:23:51 +02:00
Alejandro Celaya
6a6c427b0e Added unit tests for settings business logic elements 2020-04-25 10:22:17 +02:00
Alejandro Celaya
41f885d8ec Created settings page and reducers to handle real-time updates config 2020-04-25 09:49:54 +02:00
Alejandro Celaya
7516ca8dd9 Created settings page and converted MainHeader into functional component 2020-04-18 20:58:35 +02:00
Alejandro Celaya
aa59a95f91 Merge pull request #251 from acelaya-forks/feature/real-time-updates
Feature/real time updates
2020-04-18 20:57:07 +02:00
Alejandro Celaya
8a5161c0e8 Updated changelog 2020-04-18 20:36:49 +02:00
Alejandro Celaya
d8ae69e861 Added test for mercure info helpers 2020-04-18 12:49:03 +02:00
Alejandro Celaya
a485d0b507 Added token expired handling to mercure binding 2020-04-18 12:26:00 +02:00
Alejandro Celaya
ed40b79c8d Added more tests covering new use cases 2020-04-18 12:09:51 +02:00
Alejandro Celaya
91488ae294 Fixed visits count not handling separated tooltiups 2020-04-18 11:03:49 +02:00
Alejandro Celaya
a22a1938c1 Added automatic refresh on mercure events 2020-04-18 10:50:01 +02:00
Alejandro Celaya
0f73cb9f8c Converted short URLs list in functional component 2020-04-17 17:39:30 +02:00
Alejandro Celaya
f3129399de Added EventSource connection to mercure hub possible 2020-04-17 17:11:52 +02:00
Alejandro Celaya
37e6c27461 Created mercure info reducer and loaded info when server is reachable 2020-04-17 15:51:18 +02:00
Alejandro Celaya
d231ed3ede Merge pull request #247 from acelaya-forks/feature/user-agent-improvements
Feature/user agent improvements
2020-04-10 20:06:57 +02:00
Alejandro Celaya
cf6f9028f2 Some more improvements on how chart height is calculated 2020-04-10 19:57:33 +02:00
Alejandro Celaya
7cf49d2c1a Increased minimum charts height 2020-04-10 19:47:42 +02:00
Alejandro Celaya
e37fb1b4bd Updated changelog 2020-04-10 19:29:57 +02:00
Alejandro Celaya
faf5d0bf7b Unified function parsing user agent for browser and os 2020-04-10 19:22:13 +02:00
Alejandro Celaya
6fede88072 Added dependency on bowser to have a more accurate browser and OS detection 2020-04-10 19:16:44 +02:00
Alejandro Celaya
87ffbefa61 Merge pull request #246 from acelaya-forks/feature/create-improvements
Feature/create improvements
2020-04-10 18:50:09 +02:00
Alejandro Celaya
f33ae17781 Updated changelog 2020-04-10 18:43:16 +02:00
Alejandro Celaya
2a2bae6d1a Improved short URL creation 2020-04-10 18:42:08 +02:00
Alejandro Celaya
eb65e99024 Merge pull request #244 from acelaya-forks/feature/chart-visit-highlighting
Feature/chart visit highlighting
2020-04-10 15:21:07 +02:00
Alejandro Celaya
52dbeb6201 Optimized visits parser to act over the normalized list of visits 2020-04-10 14:59:12 +02:00
Alejandro Celaya
fafe920b7b Ensured highlighted stats are properly sorted and paginated on charts that support that 2020-04-10 14:38:31 +02:00
Alejandro Celaya
9d1e48ee90 Updated main list paginator to be sticky 2020-04-10 13:42:21 +02:00
Alejandro Celaya
3851342e1b Added button to reset visits selection 2020-04-10 13:27:01 +02:00
Alejandro Celaya
b863c2e19d Used cursor pointer in bar charts 2020-04-10 13:04:39 +02:00
Alejandro Celaya
ed584d19e5 Ensured charts datasets have a unique label 2020-04-10 12:57:14 +02:00
Alejandro Celaya
73256dcf5b Handled toggling between highlighted chart bars 2020-04-10 12:53:54 +02:00
Alejandro Celaya
c67a23c988 Added support to disable date inputs 2020-04-10 12:25:06 +02:00
Alejandro Celaya
8f42e65ccd Allowed visits to be selected on charts so that they get highlighted on the rest of the charts 2020-04-10 11:59:53 +02:00
Alejandro Celaya
05deb1aff0 Merge pull request #242 from acelaya-forks/feature/visits-table
Feature/visits table
2020-04-09 11:23:51 +02:00
Alejandro Celaya
a74b7cdfad Updated changelog 2020-04-09 11:00:27 +02:00
Alejandro Celaya
1c3119ee76 Allowed multiple selection on visits table 2020-04-09 10:56:54 +02:00
Alejandro Celaya
ca52911e42 Added VisitsTable test 2020-04-09 10:21:38 +02:00
Alejandro Celaya
9177bc7cef Tested how hilghlighted data behaves on GraphCards 2020-04-09 09:44:14 +02:00
Alejandro Celaya
310831a26a Converted ShortUrlVisits in functional component 2020-04-07 22:33:41 +02:00
Alejandro Celaya
8a486d991b Implemented some improvements and fixes on how visits table is split and calculated 2020-04-05 18:04:15 +02:00
Alejandro Celaya
b79333393b Converted SearchField component into funcitonal component 2020-04-05 16:18:08 +02:00
Alejandro Celaya
cb7062bb95 Created fake border with before and after pseudoelements for sticky table cells 2020-04-05 16:02:42 +02:00
Alejandro Celaya
94c5b2c471 Improved useToggle hook so that it also returns enabler and disabler 2020-04-05 12:18:41 +02:00
Alejandro Celaya
66bf26f1dc Improved highlighted data calculation so that it works with values different than 1 2020-04-05 11:57:39 +02:00
Alejandro Celaya
f5cc1abe75 Ensured info for selected visit in visits table gets highlighted in bar charts 2020-04-04 20:16:20 +02:00
Alejandro Celaya
bd4255108d Improved VisitsTable performance by memoizing visits lists 2020-04-04 12:58:04 +02:00
Alejandro Celaya
06b63d1af2 Improved rendering of visits table on mobile devices 2020-04-04 12:09:17 +02:00
Alejandro Celaya
2bd70fb9e6 Fixed unit tests 2020-04-04 10:36:38 +02:00
Alejandro Celaya
e6034dfb14 Created VisitsTable 2020-04-03 23:00:57 +02:00
Alejandro Celaya
c8ba6764c2 Merge pull request #238 from acelaya-forks/feature/edit-long-url
Feature/edit long url
2020-03-30 21:36:20 +02:00
Alejandro Celaya
19337d6c05 Added tests for elements regarding short URL edition 2020-03-30 21:26:30 +02:00
Alejandro Celaya
a6ad3c2d4d Updated changelog 2020-03-30 21:01:54 +02:00
Alejandro Celaya
b0dd885c09 Converted ShortUrlsRowMenu into functional component 2020-03-30 21:01:01 +02:00
Alejandro Celaya
2235592308 Fixed ShortUrlsRowMenu test 2020-03-30 20:50:31 +02:00
Alejandro Celaya
1219a16261 Ensured short URLs list is updated after editing the long URL of a short URL 2020-03-30 20:47:33 +02:00
Alejandro Celaya
7949e224e0 Created modal to edit the loing URL behind a short URL 2020-03-30 20:42:58 +02:00
Alejandro Celaya
ab2f311bb7 Merge pull request #237 from acelaya-forks/feature/short-code-length
Feature/short code length
2020-03-29 19:49:09 +02:00
Alejandro Celaya
a5aab43666 Updated changelog 2020-03-29 19:41:29 +02:00
Alejandro Celaya
74ebd4e572 Converted CreateShortUrl to functional component 2020-03-29 19:36:45 +02:00
Alejandro Celaya
bd29670108 Added short code length field to form to create short URLs 2020-03-29 18:55:41 +02:00
Alejandro Celaya
9a20b4428d Merge pull request #236 from acelaya-forks/feature/progressive-paginator
Feature/progressive paginator
2020-03-28 17:52:35 +01:00
Alejandro Celaya
d7da8521ce Created helper functions to determine the key and if a page is disabled on a progressive paginator 2020-03-28 17:43:09 +01:00
Alejandro Celaya
bab3b252c1 Updated changelog 2020-03-28 17:35:02 +01:00
Alejandro Celaya
7f05c5c2da Split utils module into several helpers modules 2020-03-28 17:33:27 +01:00
Alejandro Celaya
2d5c2779c3 Moved helper functions to render progressive paginators to a common place 2020-03-28 17:25:12 +01:00
Alejandro Celaya
06db4f6556 Used progressive pagination for the short URLs list 2020-03-28 17:19:33 +01:00
Alejandro Celaya
ea5ec63a22 Ensured all branches build the docker image 2020-03-22 09:34:24 +01:00
Alejandro Celaya
f46e737e77 Merge pull request #233 from acelaya-forks/feature/fix-docker-build-condition
Fixed docker build condition so that it's run for any branch or tag a…
2020-03-21 13:39:31 +01:00
Alejandro Celaya
6e63bdaafa Fixed docker build condition so that it's run for any branch or tag as long as it is not a PR 2020-03-21 08:43:35 +01:00
Alejandro Celaya
79ccef9f7e Avoid latest docker to be build when building a tag 2020-03-21 06:59:37 +01:00
Alejandro Celaya
a9653b3674 Merge pull request #232 from acelaya-forks/feature/improve-docker-build
Feature/improve docker build
2020-03-20 09:23:35 +01:00
Alejandro Celaya
b5a188e802 Improved building process so that already generated dist files are reused when building docker image is possible 2020-03-20 09:12:43 +01:00
Alejandro Celaya
38fc402b16 Improved docker build script to avoid duplicating code 2020-03-20 07:12:07 +01:00
Alejandro Celaya
584d1ec1ce Fixed conditional in docker build script 2020-03-19 20:39:34 +01:00
Alejandro Celaya
2ca7faa457 Merge pull request #230 from acelaya-forks/feature/travis-docker-build
Added docker image building as a deployment step for travis
2020-03-19 20:32:00 +01:00
Alejandro Celaya
03806abda0 Changed build steps so that mutation testing a docker build are only run on pull request builds 2020-03-19 20:26:35 +01:00
Alejandro Celaya
18d125430d Added docker image building as a deployment step for travis 2020-03-19 20:04:30 +01:00
Alejandro Celaya
f57f6b7745 Merge pull request #228 from acelaya-forks/feature/memoize-server-version
Feature/memoize server version
2020-03-16 19:01:33 +01:00
Alejandro Celaya
75ff2b8f40 Added app gif to readme 2020-03-16 18:53:06 +01:00
Alejandro Celaya
2ec04c0121 Fixed test by using different serverId every time, preventing memoization 2020-03-16 18:51:04 +01:00
Alejandro Celaya
5145a41dac Memoized the loading of the server version, assuming it will not change at runtime 2020-03-16 13:34:24 +01:00
Alejandro Celaya
25c67f1c3e Merge pull request #227 from acelaya-forks/feature/edit-servers
Feature/edit servers
2020-03-15 14:32:30 +01:00
Alejandro Celaya
77b9181150 Replaced hardcoded color by sass var 2020-03-15 14:23:57 +01:00
Alejandro Celaya
e4f7ded8e2 Updated changelog 2020-03-15 14:04:33 +01:00
Alejandro Celaya
35a62f1fb1 Added link to edit existing servers 2020-03-15 14:03:41 +01:00
Alejandro Celaya
24f2deda46 Moved common code to handle currently selected server to HOC 2020-03-15 13:43:12 +01:00
Alejandro Celaya
5d8af1a0e5 Simplified EditServer component by wrapping ServerForm 2020-03-15 12:02:19 +01:00
Alejandro Celaya
6d44ac1e0c Created common component that can be used both for create and edit servers 2020-03-15 11:59:07 +01:00
Alejandro Celaya
fb0ebddf28 Created component to edit existing servers 2020-03-15 11:29:20 +01:00
Alejandro Celaya
0aebaa4da1 Extracted logic to render horizontal form groups to their own components 2020-03-15 10:50:05 +01:00
Alejandro Celaya
f6baedc655 Converted CreateServer into functional component 2020-03-15 10:33:23 +01:00
Alejandro Celaya
7db222664d Fixed tests 2020-03-15 09:56:16 +01:00
Alejandro Celaya
8223f0fd64 Undone weird changes in package lock file 2020-03-15 09:43:42 +01:00
Alejandro Celaya
f44ec42f51 Added links to delete and edit the server when a server could not be reached 2020-03-15 09:17:33 +01:00
Alejandro Celaya
dab75ab6a9 Updated badges 2020-03-10 21:53:21 +01:00
Alejandro Celaya
01672b88e1 Merge pull request #222 from acelaya-forks/feature/server-not-found
Feature/server not found
2020-03-08 13:17:56 +01:00
Alejandro Celaya
78dc297022 Updated changelog 2020-03-08 13:05:15 +01:00
Alejandro Celaya
c8cf75fa28 Created ServerError test 2020-03-08 13:04:21 +01:00
Alejandro Celaya
b011b4e1d8 Fixed tests 2020-03-08 12:57:01 +01:00
Alejandro Celaya
9804a2d18d Added list of servers connected to store in ServerError component 2020-03-08 12:50:42 +01:00
Alejandro Celaya
d1a5ee43e9 Created components to display errors when loading a server 2020-03-08 12:41:18 +01:00
Alejandro Celaya
febecab33c Migrated Home component to a functional component 2020-03-08 11:35:06 +01:00
Alejandro Celaya
99042c0979 Extracted servers list group from home component to a reusable component 2020-03-08 11:16:57 +01:00
Alejandro Celaya
6395e4e00b Improved NotFount component so that link text is passed as children 2020-03-08 10:28:04 +01:00
Alejandro Celaya
4a69907ca3 Fixed generation of component keys to make them render properly 2020-03-08 10:16:45 +01:00
Alejandro Celaya
c8d682cc98 Handled loading server in just one place, and added error handling for loading servers 2020-03-08 10:00:25 +01:00
Alejandro Celaya
f4cc8d3a0c Fixed default value for vertically aligned items 2020-03-07 12:07:51 +01:00
Alejandro Celaya
6ac89334fd Merge pull request #220 from acelaya-forks/feature/improvements
Feature/improvements
2020-03-06 21:56:20 +01:00
Alejandro Celaya
f55d3a66aa Converted ShortUrlsRow component into a functional component 2020-03-06 21:44:03 +01:00
Alejandro Celaya
972eafab34 Updated changelog 2020-03-06 21:26:19 +01:00
Alejandro Celaya
fba156b271 Moved copy-to-clipboard control next to short URL 2020-03-06 21:25:30 +01:00
Alejandro Celaya
96d538db15 Replaced Unknown by Direct for traffic comming from undetermined referrers 2020-03-06 20:42:22 +01:00
Alejandro Celaya
b89bfa3c1c Merge pull request #215 from acelaya-forks/feature/versions
Feature/versions
2020-03-05 14:20:31 +01:00
Alejandro Celaya
73e3f42614 Added ShlinkVersions test 2020-03-05 13:55:39 +01:00
Alejandro Celaya
e761f5e1bd Updated changelog 2020-03-05 13:45:24 +01:00
Alejandro Celaya
4a6dd66ecd Added scripts to pass version when building docker image 2020-03-05 13:37:07 +01:00
Alejandro Celaya
8e1c6908c6 Updated build script so that it replaces version placeholder when a version is provided 2020-03-05 13:27:57 +01:00
Alejandro Celaya
f59e569e22 Extracted logic to determine app version from function to generate dist file 2020-03-05 13:04:12 +01:00
Alejandro Celaya
be50b24504 Added mechanism to provide a version to shlink-web-client 2020-03-05 12:53:32 +01:00
Alejandro Celaya
c181831a37 Fixed tests 2020-03-05 11:58:35 +01:00
Alejandro Celaya
dbee62ac8c Moved shlink versions component to main container 2020-03-05 11:46:38 +01:00
Alejandro Celaya
1e949b3a22 Added shlink versions to side menu 2020-03-05 11:11:26 +01:00
Alejandro Celaya
b02dcf6c53 Refactored delete server components 2020-03-05 10:18:38 +01:00
Alejandro Celaya
ab7718e335 Removed duplicated code from AsideMenu by creating an AsideMenuItem helper component 2020-03-05 10:03:38 +01:00
Alejandro Celaya
451c77d47f Merge pull request #214 from acelaya-forks/feature/consistent-server-loading
Feature/consistent server loading
2020-03-05 09:32:59 +01:00
Alejandro Celaya
fa0d3d4047 Removed no longer needed async/await when building api client 2020-03-05 09:23:53 +01:00
Alejandro Celaya
397a183f65 Converted MenuLayout into a functional component with hooks 2020-03-05 09:08:50 +01:00
Alejandro Celaya
bc8905ee7f Ensured server is properly loaded before trying to render any children component 2020-03-05 08:59:07 +01:00
Alejandro Celaya
853032ac7f Displayed preloader when a server is being loaded 2020-03-05 08:41:55 +01:00
Alejandro Celaya
3b0e282a52 Merge pull request #211 from acelaya-forks/feature/jest-each
Feature/jest each
2020-02-17 18:32:52 +01:00
Alejandro Celaya
bb28cb3862 Updated changelog 2020-02-17 18:25:21 +01:00
Alejandro Celaya
d0f458bece Uninstalled jest-each and replaced by jest's native each 2020-02-17 18:21:52 +01:00
Alejandro Celaya
da54a72b3e Merge pull request #206 from acelaya-forks/feature/match-domain
Feature/match domain
2020-02-08 11:04:58 +01:00
Alejandro Celaya
86c155d8d1 Updated changelog 2020-02-08 10:47:44 +01:00
Alejandro Celaya
666d2d3065 Ensured domain is dispatched when modifying a short URL somehow 2020-02-08 10:46:11 +01:00
Alejandro Celaya
01e69fb6ca Merge pull request #204 from acelaya-forks/feature/multi-domain-fixes
Feature/multi domain fixes
2020-02-08 10:24:49 +01:00
Alejandro Celaya
30e5253acd Simplified instructions removing redundant vars 2020-02-08 10:07:34 +01:00
Alejandro Celaya
c67ce3918b Removed redundant function call 2020-02-08 10:03:24 +01:00
Alejandro Celaya
58077f2d86 Updated changelog 2020-02-08 09:59:13 +01:00
Alejandro Celaya
098c94bccf Ensured domain is passed when deleting a short URL on a specific domain 2020-02-08 09:57:18 +01:00
Alejandro Celaya
861a3c068f Ensured domain is passed when editing meta for a short URL on a specific domain 2020-02-08 09:52:30 +01:00
Alejandro Celaya
3b95e8ebc0 Ensured domain is passed when editing tags for a short URL on a specific domain 2020-02-08 09:48:35 +01:00
Alejandro Celaya
170e427530 Ensured domain is passed when loading detail for a short URL on a specific domain 2020-02-08 09:38:19 +01:00
Alejandro Celaya
707c9f4ce6 Created VisitStatsLink test 2020-02-08 09:22:17 +01:00
Alejandro Celaya
dc672bf0f0 Ensured domain is passed when loading visits for a short URL on a specific domain 2020-02-08 09:07:55 +01:00
Alejandro Celaya
c682737505 Standardized date-picker selected day color 2020-02-02 09:30:41 +01:00
Alejandro Celaya
46fa3d4345 Merge pull request #201 from acelaya-forks/feature/document-routing-fallback
Updated documentation
2020-01-31 20:51:17 +01:00
Alejandro Celaya
9b7bc4b495 Updated documentation 2020-01-31 20:37:50 +01:00
Alejandro Celaya
4385061499 Merge pull request #200 from acelaya-forks/feature/refactor-edit-tags-modal
Feature/refactor edit tags modal
2020-01-31 20:22:04 +01:00
Alejandro Celaya
e17498e68b Updated changelog 2020-01-31 20:13:18 +01:00
Alejandro Celaya
3e298f010b Simplified DeleteShortUrlModal component and shortUrlDeletion reducer 2020-01-31 20:12:22 +01:00
Alejandro Celaya
30117bd121 Simplified EditTagsModal component and shortUrlTags reducer 2020-01-31 20:06:28 +01:00
Alejandro Celaya
93f33b6218 Fixed some tests after not injecting a component 2020-01-31 20:04:03 +01:00
Alejandro Celaya
535d08a607 Merge pull request #197 from MartinH0/master
Add htaccess to redirect if not found to index
2020-01-31 16:21:30 +01:00
MartinH0
6ac3a49db2 Updated nginx.conf (optimization for future)
1. changed location from "~" (case sensitive!) to "~*" (case insensitive!) to also match uppercase static assets. (http://nginx.org/en/docs/http/ngx_http_core_module.html#location)
2. added regex "jpe?g" to match "jpg" and "jpeg" in one command.
2020-01-31 01:36:17 +01:00
MartinH0
c16f760d79 Update .htaccess
1. removed $ (dollar sign from line 14
2. changed line 8 from ".*" to "(.*)"
2020-01-31 01:29:41 +01:00
MartinH0
965c2b243f Update .htaccess
1. added more comments.
2. added NC Tag for making all the static assets case insensetive ("jpg" now matches "jpg" and "JPG" and so on)
3. transformed "jpe|jpeg" into "jpe?g" as its regex for the same, but shorter
4. changed line 8 from "+" to "*" to match everything, also zero times the wildcard
2020-01-31 01:27:13 +01:00
MartinH0
703addddb9 updated htaccess
deleted second json (just needed once)
2020-01-30 21:05:20 +01:00
MartinH0
ab6dff5c31 Updates htaccess
return 404 error if static assets does not exist
2020-01-30 20:51:23 +01:00
MartinH0
2ef330c62b Updated htaccess 2020-01-30 20:49:46 +01:00
MartinH0
72e71aff40 Updated htaccess to meet required functions.
If a file gets called it will be redirected to index.html

But not if it the requested File does contain a dot (and with this does have a file extension.

If you call:
links.domain.de/notexistingfile.jpg 
It will trigger 404

If you call:
links.domain.de/server/[CODE-CODE-CODE]/list-short-urls/1
It will redirect the call to index.html
2020-01-30 19:06:50 +01:00
MartinH0
cefd6ec752 Add htaccess to redirect if not found to index
If (file not found or directory not found)
then > redirect to index.html
2020-01-30 18:51:38 +01:00
MartinH0
aec3de18aa Deleted .htaccess at wrong directory
Sorry fucked it up, will correct it.
2020-01-30 18:51:32 +01:00
MartinH0
97620cb583 Add htaccess to redirect if not found to index
If file not fount or directory not found redirect to index.html
2020-01-30 18:36:02 +01:00
Alejandro Celaya
cf4e8190a4 Merge pull request #195 from acelaya-forks/feature/server-version-wrapper
Feature/server version wrapper
2020-01-28 19:55:24 +01:00
Alejandro Celaya
8af7436f13 Updated changelog 2020-01-28 19:47:41 +01:00
Alejandro Celaya
c53520ae56 Moved logic to dynamically render components based on server version to a separated component 2020-01-28 19:46:36 +01:00
Alejandro Celaya
3adcaef455 Merge pull request #194 from acelaya-forks/feature/fix-set-empty-max-visits
Fixed maxVisits being set to 0 when trying to reset it
2020-01-28 18:51:18 +01:00
Alejandro Celaya
43cd9722a9 Updated project to node 12.14.1 2020-01-28 18:40:33 +01:00
Alejandro Celaya
f3154e770e Fixed maxVisits being set to 0 when trying to reset it 2020-01-28 18:36:23 +01:00
Alejandro Celaya
44aca4aeda Merge pull request #192 from acelaya-forks/feature/version-constraints
Feature/version constraints
2020-01-19 21:37:56 +01:00
Alejandro Celaya
5762342d6c Ensured edit meta menu item is only displayed when shlink v1.18 or greater is run 2020-01-19 21:30:01 +01:00
Alejandro Celaya
2236ed467e Ensured date range filtering is only displayed if Shlink v1.21 ow higer is run 2020-01-19 21:25:45 +01:00
Alejandro Celaya
d244b830ac Updated date on license file 2020-01-19 21:20:32 +01:00
Alejandro Celaya
e89b68fe1e Merge pull request #190 from acelaya-forks/feature/edit-meta
Feature/edit meta
2020-01-19 21:15:25 +01:00
Alejandro Celaya
1f588c5b13 Updated changelog with v2.3.0 2020-01-19 21:00:31 +01:00
Alejandro Celaya
38cad143a0 Created EditMetaModal test 2020-01-19 20:59:01 +01:00
Alejandro Celaya
f52bcc5389 Ensured state is reset on edit meta modal after closing it 2020-01-19 20:37:12 +01:00
Alejandro Celaya
caa6f7bcd8 Created shortUrlMetaReducer test 2020-01-19 20:21:59 +01:00
Alejandro Celaya
207a8cef20 Updated tests from modified code 2020-01-19 13:20:46 +01:00
Alejandro Celaya
d44a4b260e Finished component to allow metadata to be edited on existing short URLs 2020-01-19 13:07:33 +01:00
Alejandro Celaya
80a8e0b55c Created component to edit short URLs meta 2020-01-17 21:07:59 +01:00
Alejandro Celaya
2d60f830f7 Improved icons on short URL menu 2020-01-15 20:25:58 +01:00
Alejandro Celaya
90751a09f7 Merge pull request #188 from acelaya-forks/feature/visits-amount
Feature/visits amount
2020-01-15 18:42:09 +01:00
Alejandro Celaya
301da4bb2a Recovered behavior to show amount of visits in selected date range on visits detail page 2020-01-15 18:31:28 +01:00
Alejandro Celaya
c90cd46095 Removed old ExternalLink component in favor of external one 2020-01-15 18:16:12 +01:00
Alejandro Celaya
7826000384 Merge pull request #187 from acelaya-forks/feature/date-filter
Feature/date filter
2020-01-14 20:59:01 +01:00
Alejandro Celaya
b48dcdd5e1 Fixed wrong files being picked for mutations 2020-01-14 20:48:01 +01:00
Alejandro Celaya
4f6326b139 Updated changelog 2020-01-14 20:21:14 +01:00
Alejandro Celaya
cff96eeccc Created DateRangeRow test 2020-01-14 20:20:27 +01:00
Alejandro Celaya
5eb4a3adec Fixed tests and typos 2020-01-14 20:12:30 +01:00
Alejandro Celaya
b60908a5e9 Added filtering by date to short URLs list 2020-01-14 19:59:25 +01:00
Alejandro Celaya
124441238b Moved style to the proper scope 2020-01-12 12:08:26 +01:00
Alejandro Celaya
4ec0287a74 Merge pull request #185 from Starbix/patch-2
Update nginx base image
2020-01-12 11:01:38 +01:00
Cédric Laubacher
05c67a5c99 Update nginx base image 2020-01-12 10:50:40 +01:00
Alejandro Celaya
f507a3628c Merge pull request #184 from acelaya-forks/feature/show-max-visits
Feature/show max visits
2020-01-11 20:23:02 +01:00
Alejandro Celaya
89e9d2b2d1 Fixed accidentally refactored string 2020-01-11 20:11:41 +01:00
Alejandro Celaya
595858ac4b Used visits count component in short URL visits view 2020-01-11 20:10:12 +01:00
Alejandro Celaya
3f2162fe62 Extracted visits count component to reuse it in other places 2020-01-11 19:58:04 +01:00
Alejandro Celaya
f2cb30409a Updated changelog 2020-01-11 19:41:41 +01:00
Alejandro Celaya
5c4fec5a2f Displayed amount of max visits on those URLs which have it 2020-01-11 19:40:16 +01:00
Alejandro Celaya
e96c119432 Merge pull request #183 from acelaya-forks/feature/support-shlink-2
Feature/support shlink 2
2020-01-11 14:25:25 +01:00
Alejandro Celaya
0920962d72 Used already unpacked property 2020-01-11 14:16:23 +01:00
Alejandro Celaya
aaeb0fff78 Updated changelog 2020-01-11 14:13:58 +01:00
Alejandro Celaya
de41f50945 Ensured preview menu item is hidden when consuming Shlink 2 2020-01-11 14:12:58 +01:00
Alejandro Celaya
0f51bf95e3 Updated ShlinkApiClient so that it retries API version when v2 is not supported 2020-01-11 13:55:37 +01:00
Alejandro Celaya
ba8cade6fc When handling API errors, use the type prop and fallback to error if not found 2020-01-11 12:24:45 +01:00
Alejandro Celaya
dbefae5a01 Merge pull request #173 from Starbix/patch-1
Update baseimages
2019-11-21 11:35:49 +01:00
Cédric Laubacher
727b219742 Update nginx image to latest version 2019-11-21 07:55:40 +01:00
Alejandro Celaya
fb25e44b58 Used strict version number for nginx base image 2019-11-21 07:52:50 +01:00
Cédric Laubacher
fe2d394831 Update baseimages
Nginx can be set to the latest patch version, as its API is really stable.
2019-11-16 10:24:34 +01:00
Alejandro Celaya
efd08ff1d6 Updated changelog 2019-11-10 13:04:15 +01:00
Alejandro Celaya
4b861a5376 Removed profanity 2019-11-10 08:47:13 +01:00
Alejandro Celaya
2076e7d5e8 Merge pull request #171 from MartinH0/master
Updated FavIcons
2019-11-10 08:44:59 +01:00
MartinH0
37f6f1f90c rename to favicon 2019-11-09 22:34:41 +01:00
MartinH0
81f76e0bd6 SVG FavIcon Fix
Added sizes="any" to the svg FavIcon
2019-11-09 22:32:13 +01:00
MartinH0
69b305cd8a Added SVG FavIcon
Added SVG FavIcon as File in Rootdirectory
2019-11-09 22:29:37 +01:00
MartinH0
45742a066e Added SVG FavIcon 2019-11-09 22:28:44 +01:00
MartinH0
86fb8b3f7c Added FavIcons
Added .gif and .png FavIcon
2019-11-09 19:38:50 +01:00
MartinH0
9c0fc8e1d2 Adjusted to FavIcons
Added MS Icons, Added all Apple Icons, Added all normal Icons and standard FavIcons
2019-11-09 19:37:31 +01:00
MartinH0
10d6302180 Added all FavIcons to Manifest
Added all missing FavIcons to the manifest
2019-11-09 19:22:55 +01:00
MartinH0
da7ed6992f FavIcon
Overwrite FavIcon with 128x128 new optimized FavIcon
2019-11-09 19:17:45 +01:00
MartinH0
32c9375ac8 FavIcons edits
Added new FavIcons and overwrite old ones
2019-11-09 19:16:12 +01:00
MartinH0
7ed1334a51 Updated FavIcons
Added all Versions of all available FavIcons.
Also Added normal FavIcons additionally to the apple ones.
Also added the "msapplication-TileImage" meta for Microsoft.
2019-11-07 21:47:57 +01:00
Alejandro Celaya
d9097896f6 Added github funding 2019-10-22 19:33:59 +02:00
259 changed files with 8472 additions and 3775 deletions

View File

@@ -1,6 +1,7 @@
./.github
./build
./coverage
./dist
./node_modules
./test
./shlink-web-client.gif
./dist

View File

@@ -28,6 +28,8 @@
"no-warning-comments": "off",
"no-magic-numbers": "off",
"no-undefined": "off",
"no-inline-comments": "off",
"lines-around-comment": "off",
"indent": ["error", 2, {
"SwitchCase": 1
}

1
.github/FUNDING.yml vendored
View File

@@ -1 +1,2 @@
github: ['acelaya']
custom: ['https://acel.me/donate']

View File

@@ -1,6 +1,6 @@
build:
environment:
node: v12.11.0
node: v12.14.1
tools:
external_code_coverage:
timeout: 1200

View File

@@ -1,7 +1,21 @@
dist: bionic
language: node_js
node_js:
- "12.11.0"
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:
@@ -11,30 +25,34 @@ services:
- docker
install:
- npm ci
- if [[ ${DOCKER_PUBLISH} == 'true' ]]; then sudo bash ./scripts/docker/install-docker ; fi
- if [[ ${DOCKER_PUBLISH} == 'false' ]]; then npm ci ; fi
before_script:
- echo "Building commit range ${TRAVIS_COMMIT_RANGE}"
- export MUTATION_FILES=$(git diff ${TRAVIS_COMMIT_RANGE:-origin/master} --name-only | grep -E 'src\/(.*).(js|ts|jsx|tsx)' | paste -sd ",")
- 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:
- npm run lint
- npm run test:ci
- if [[ -z $TRAVIS_TAG ]]; then docker build -t shlink-web-client:test . ; fi
- if [[ -z $TRAVIS_TAG ]]; then npm run mutate:ci ; fi
- 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:
- node_modules/.bin/ocular coverage/clover.xml
- if [[ ${DOCKER_PUBLISH} == 'false' ]]; then node_modules/.bin/ocular coverage/clover.xml ; fi
# Before deploying, build dist file for current travis tag
before_deploy:
- npm run build ${TRAVIS_TAG#?}
- 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:
tags: true
- 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

View File

@@ -4,6 +4,139 @@ 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
#### 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.
* If it works, it will setup the necessary `EventSource`s, dispatching redux actions when an event is pushed, which will in turn update the UI.
* If it fails, it will assume it is either not configured or not supported by the Shlink version.
* [#265](https://github.com/shlinkio/shlink-web-client/issues/265) Updated tags section to allow displaying number of short URLs using every tag and number of visits for all short URLs using the tag.
This will work only when using Shlink v2.2.0 or above. For previous versions, the tags page will continue behaving the same.
* [#261](https://github.com/shlinkio/shlink-web-client/issues/261) Added new page to show visit stats by tag.
This new page will return a "not found" error when the server is lower than v2.2.0, as older versions do not support fetching stats by tag.
* [#253](https://github.com/shlinkio/shlink-web-client/issues/253) Created new settings page that will be used to define customizations in the app.
* [#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
* [#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
* *Nothing*
#### Removed
* *Nothing*
#### 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
* [#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.
* [#241](https://github.com/shlinkio/shlink-web-client/issues/241) Added support to select charts bars in order to highlight related stats in other charts.
It also selects the visits in the new table, and you can even combine a selection in the chart and in the table.
* [#213](https://github.com/shlinkio/shlink-web-client/issues/213) The versions of both shlink-web-client and currently consumed Shlink server are now displayed in the footer.
* [#221](https://github.com/shlinkio/shlink-web-client/issues/221) Improved how servers are handled, displaying meaningful errors when a not-found or a not-reachable server is tried to be loaded.
* [#226](https://github.com/shlinkio/shlink-web-client/issues/226) Created servers can now be edited.
* [#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
* [#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
* *Nothing*
#### Removed
* *Nothing*
#### 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
* *Nothing*
#### 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
* *Nothing*
#### Removed
* *Nothing*
#### 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
* [#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
* *Nothing*
#### Deprecated
* *Nothing*
#### Removed
* *Nothing*
#### 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

View File

@@ -1,8 +1,11 @@
FROM node:12.11.0-alpine as node
FROM node:12.16.3-alpine as node
COPY . /shlink-web-client
RUN cd /shlink-web-client && npm install && npm run build
ARG VERSION="latest"
ENV VERSION ${VERSION}
RUN cd /shlink-web-client && \
npm install && npm run build -- ${VERSION} --no-dist
FROM nginx:1.17.4-alpine
FROM nginx:1.17.10-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

View File

@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2018-2019 shlinkio
Copyright (c) 2018-2020 shlinkio
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@@ -1,36 +1,45 @@
# shlink-web-client
[![Build Status](https://img.shields.io/travis/shlinkio/shlink-web-client.svg?style=flat-square)](https://travis-ci.org/shlinkio/shlink-web-client)
[![Docker build status](https://img.shields.io/docker/cloud/build/shlinkio/shlink-web-client.svg?style=flat-square)](https://hub.docker.com/r/shlinkio/shlink-web-client/)
[![Code Coverage](https://img.shields.io/scrutinizer/coverage/g/shlinkio/shlink-web-client.svg?style=flat-square)](https://scrutinizer-ci.com/g/shlinkio/shlink-web-client/?branch=master)
[![Scrutinizer Code Quality](https://img.shields.io/scrutinizer/g/shlinkio/shlink-web-client.svg?style=flat-square)](https://scrutinizer-ci.com/g/shlinkio/shlink-web-client/?branch=master)
[![GitHub release](https://img.shields.io/github/release/shlinkio/shlink-web-client.svg?style=flat-square)](https://github.com/shlinkio/shlink-web-client/releases/latest)
[![Docker pulls](https://img.shields.io/docker/pulls/shlinkio/shlink-web-client.svg?style=flat-square)](https://hub.docker.com/r/shlinkio/shlink-web-client/)
[![GitHub license](https://img.shields.io/github/license/shlinkio/shlink-web-client.svg?style=flat-square)](https://github.com/shlinkio/shlink-web-client/blob/master/LICENSE)
[![Paypal Donate](https://img.shields.io/badge/Donate-paypal-blue.svg?style=flat-square&logo=paypal&colorA=cccccc)](https://acel.me/donate)
[![Paypal Donate](https://img.shields.io/badge/Donate-paypal-blue.svg?style=flat-square&logo=paypal&colorA=cccccc)](https://slnk.to/donate)
A ReactJS-based progressive web application for [Shlink](https://shlink.io).
![shlink-web-client](shlink-web-client.gif)
## Installation
There are three ways in which you can use this application.
* The easiest way to use shlink-web-client is by just going to <https://app.shlink.io>.
### From app.shlink.io
The application runs 100% in the browser, so you can safely access any shlink instance from there.
The easiest way to use shlink-web-client is by just going to <https://app.shlink.io>.
* Self hosting the application yourself.
The application runs 100% in the browser, so you can safely access any shlink instance from there.
Get the [latest release](https://github.com/shlinkio/shlink-web-client/releases/latest) and download the distributable zip file attached to it (`shlink-web-client_X.X.X_dist.zip`).
### Docker image
The package contains static files only, so just put it in a folder and serve it with the web server of your choice.
If you want to deploy shlink-web-client in a container-based cluster (kubernetes, docker swarm, etc), just pick the [shlinkio/shlink-web-client](https://hub.docker.com/r/shlinkio/shlink-web-client/) image and do it.
Provided dist files are configured to be served from the root of your domain. If you need to serve shlink-web-client from a subpath, you will have to build it yourself following [these steps](#serve-shlink-in-subpath).
It's a lightweight [nginx:alpine](https://hub.docker.com/r/library/nginx/) image serving the static app on port 80.
* Using the official [docker image](https://hub.docker.com/r/shlinkio/shlink-web-client/)
### Self-hosted
If you want to deploy shlink-web-client in a container-based cluster (kubernetes, docker swarm, etc), just pick the `shlinkio/shlink-web-client` image and do it.
If you want to self-host it yourself, get the [latest release](https://github.com/shlinkio/shlink-web-client/releases/latest) and download the distributable zip file attached to it (`shlink-web-client_X.X.X_dist.zip`).
It's a lightweight [nginx:alpine](https://hub.docker.com/r/library/nginx/) image serving the static app on port 80.
The package contains static files only, so just put it in a folder and serve it with the web server of your choice.
**Considerations**:
* Provided dist files are configured to be served from the root of your domain. If you need to serve shlink-web-client from a subpath, you will have to build it yourself following [these steps](#serve-shlink-in-subpath).
* The app has a client-side router that handles dynamic paths. Because of that, you need to configure your web server to fall-back to the `index.html` file when requested files do not exist.
* If you use Apache, you are covered, since the project includes an `.htaccess` file which already does this.
* If you use nginx, you can [see how it's done](config/docker/nginx.conf) for the docker image and do the same.
## Pre-configuring servers

View File

@@ -5,7 +5,7 @@ server {
index index.html;
# When requesting static paths with extension, try them, and return a 404 if not found
location ~ .+\.(css|js|html|png|jpg|jpeg|gif|bmp|ico|json|csv|otf|eot|svg|svgz|ttf|woff|woff2|ijmap|pdf|tif|map) {
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;
}

View File

@@ -3,7 +3,7 @@ version: '3'
services:
shlink_web_client_node:
container_name: shlink_web_client_node
image: node:12.11.0-alpine
image: node:12.16.3-alpine
command: /bin/sh -c "cd /home/shlink/www && npm install && npm run start"
volumes:
- ./:/home/shlink/www

2337
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,6 @@
{
"name": "shlink-web-client",
"description": "A React-based progressive web application for shlink",
"version": "2.3.0",
"private": false,
"homepage": "",
"repository": "https://github.com/shlinkio/shlink-web-client",
@@ -19,7 +18,8 @@
"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"
"mutate:ci": "npm run mutate -- --mutate=$MUTATION_FILES",
"check": "npm run test & npm run lint & wait"
},
"dependencies": {
"@fortawesome/fontawesome-free": "^5.11.2",
@@ -33,23 +33,25 @@
"axios": "^0.19.0",
"bootstrap": "^4.3.1",
"bottlejs": "^1.7.2",
"bowser": "^2.9.0",
"chart.js": "^2.8.0",
"classnames": "^2.2.6",
"compare-versions": "^3.5.1",
"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.10.2",
"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.10.2",
"react-dom": "^16.13.1",
"react-external-link": "^1.0.0",
"react-leaflet": "^2.4.0",
"react-moment": "^0.9.5",
@@ -60,15 +62,15 @@
"reactstrap": "^8.0.1",
"redux": "^4.0.4",
"redux-actions": "^2.6.5",
"redux-localstorage-simple": "^2.2.0",
"redux-thunk": "^2.3.0",
"uuid": "^3.3.3"
},
"devDependencies": {
"@babel/core": "^7.6.2",
"@stryker-mutator/core": "^2.1.0",
"@stryker-mutator/html-reporter": "^2.1.0",
"@stryker-mutator/javascript-mutator": "^2.1.0",
"@stryker-mutator/jest-runner": "^2.1.0",
"@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",
@@ -85,8 +87,8 @@
"css-loader": "^3.2.0",
"dotenv": "^8.1.0",
"dotenv-expand": "^5.1.0",
"enzyme": "^3.10.0",
"enzyme-adapter-react-16": "^1.14.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",
@@ -104,7 +106,6 @@
"html-webpack-plugin": "^4.0.0-beta.8",
"identity-obj-proxy": "^3.0.0",
"jest": "^24.9.0",
"jest-each": "^24.9.0",
"jest-pnp-resolver": "^1.2.1",
"jest-resolve": "^24.9.0",
"mini-css-extract-plugin": "^0.8.0",

16
public/.htaccess Normal file
View File

@@ -0,0 +1,16 @@
RewriteEngine on
RewriteBase /
# do not do anything for already existing files
RewriteCond %{REQUEST_FILENAME} -f [OR]
RewriteCond %{REQUEST_FILENAME} -l [OR]
RewriteCond %{REQUEST_FILENAME} -d
RewriteRule (.*) - [L]
# if request is no valid file NOR directory
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
# if static asset do not do anything
RewriteRule (.*)(css|js|html|png|jpe?g|gif|bmp|ico|json|csv|otf|eot|svg|svgz|ttf|woff|woff2|ijmap|pdf|tif|map) - [NC,L,R=404]
# everything else should be redirected to /index.html so it can be routed by it
RewriteRule (.*) /index.html [L]

BIN
public/favicon.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 642 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
public/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

1
public/favicon.svg Normal file
View File

@@ -0,0 +1 @@
<svg width="512pt" height="512pt" viewBox="0 0 512 512" version="1.1" xmlns="http://www.w3.org/2000/svg"><g fill="#4595e3"><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>

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
public/icons/icon-16x16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 287 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.0 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

BIN
public/icons/icon-24x24.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 381 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

BIN
public/icons/icon-32x32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 437 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

BIN
public/icons/icon-40x40.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 466 B

BIN
public/icons/icon-48x48.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 551 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

BIN
public/icons/icon-60x60.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 638 B

BIN
public/icons/icon-64x64.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 684 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 750 B

BIN
public/icons/icon-76x76.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 783 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 984 B

View File

@@ -11,12 +11,74 @@
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json">
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
<link rel="apple-touch-icon" href="%PUBLIC_URL%/icons/shlink-128.png">
<link rel="apple-touch-icon" sizes="64x64" href="%PUBLIC_URL%/icons/shlink-64.png">
<link rel="apple-touch-icon" sizes="32x32" href="%PUBLIC_URL%/icons/shlink-32.png">
<link rel="apple-touch-icon" sizes="24x24" href="%PUBLIC_URL%/icons/shlink-24.png">
<link rel="apple-touch-icon" sizes="16x16" href="%PUBLIC_URL%/icons/shlink-16.png">
<!-- FavIcon itself -->
<link rel="icon" type="image/x-icon" href="%PUBLIC_URL%/favicon.ico">
<link rel="icon" type="image/svg+xml" href="%PUBLIC_URL%/favicon.svg" sizes="any">
<link rel="icon" type="image/png" href="%PUBLIC_URL%/favicon.png">
<link rel="icon" type="image/gif" href="%PUBLIC_URL%/favicon.gif">
<!-- Apple Touch -->
<link rel="apple-touch-icon" sizes="16x16" href="%PUBLIC_URL%/icons/icon-16x16.png">
<link rel="apple-touch-icon" sizes="24x24" href="%PUBLIC_URL%/icons/icon-24x24.png">
<link rel="apple-touch-icon" sizes="32x32" href="%PUBLIC_URL%/icons/icon-32x32.png">
<link rel="apple-touch-icon" sizes="40x40" href="%PUBLIC_URL%/icons/icon-40x40.png">
<link rel="apple-touch-icon" sizes="48x48" href="%PUBLIC_URL%/icons/icon-48x48.png">
<link rel="apple-touch-icon" sizes="60x60" href="%PUBLIC_URL%/icons/icon-60x60.png">
<link rel="apple-touch-icon" sizes="64x64" href="%PUBLIC_URL%/icons/icon-64x64.png">
<link rel="apple-touch-icon" sizes="72x72" href="%PUBLIC_URL%/icons/icon-72x72.png">
<link rel="apple-touch-icon" sizes="76x76" href="%PUBLIC_URL%/icons/icon-76x76.png">
<link rel="apple-touch-icon" sizes="96x96" href="%PUBLIC_URL%/icons/icon-96x96.png">
<link rel="apple-touch-icon" sizes="114x114" href="%PUBLIC_URL%/icons/icon-114x114.png">
<link rel="apple-touch-icon" sizes="120x120" href="%PUBLIC_URL%/icons/icon-120x120.png">
<link rel="apple-touch-icon" sizes="128x128" href="%PUBLIC_URL%/icons/icon-128x128.png">
<link rel="apple-touch-icon" sizes="144x144" href="%PUBLIC_URL%/icons/icon-144x144.png">
<link rel="apple-touch-icon" sizes="150x150" href="%PUBLIC_URL%/icons/icon-150x150.png">
<link rel="apple-touch-icon" sizes="152x152" href="%PUBLIC_URL%/icons/icon-152x152.png">
<link rel="apple-touch-icon" sizes="160x160" href="%PUBLIC_URL%/icons/icon-160x160.png">
<link rel="apple-touch-icon" sizes="167x167" href="%PUBLIC_URL%/icons/icon-167x167.png">
<link rel="apple-touch-icon" sizes="180x180" href="%PUBLIC_URL%/icons/icon-180x180.png">
<link rel="apple-touch-icon" sizes="192x192" href="%PUBLIC_URL%/icons/icon-192x192.png">
<link rel="apple-touch-icon" sizes="196x196" href="%PUBLIC_URL%/icons/icon-196x196.png">
<link rel="apple-touch-icon" sizes="228x228" href="%PUBLIC_URL%/icons/icon-228x228.png">
<link rel="apple-touch-icon" sizes="256x256" href="%PUBLIC_URL%/icons/icon-256x256.png">
<link rel="apple-touch-icon" sizes="310x310" href="%PUBLIC_URL%/icons/icon-310x310.png">
<link rel="apple-touch-icon" sizes="384x384" href="%PUBLIC_URL%/icons/icon-384x384.png">
<link rel="apple-touch-icon" sizes="512x512" href="%PUBLIC_URL%/icons/icon-512x512.png">
<link rel="apple-touch-icon" sizes="1024x1024" href="%PUBLIC_URL%/icons/icon-1024x1024.png">
<!-- Normal -->
<link rel="icon" type="image/png" sizes="1024x1024" href="%PUBLIC_URL%/icons/icon-1024x1024.png">
<link rel="icon" type="image/png" sizes="512x512" href="%PUBLIC_URL%/icons/icon-512x512.png">
<link rel="icon" type="image/png" sizes="384x384" href="%PUBLIC_URL%/icons/icon-384x384.png">
<link rel="icon" type="image/png" sizes="310x310" href="%PUBLIC_URL%/icons/icon-310x310.png">
<link rel="icon" type="image/png" sizes="256x256" href="%PUBLIC_URL%/icons/icon-256x256.png">
<link rel="icon" type="image/png" sizes="228x228" href="%PUBLIC_URL%/icons/icon-228x228.png">
<link rel="icon" type="image/png" sizes="196x196" href="%PUBLIC_URL%/icons/icon-196x196.png">
<link rel="icon" type="image/png" sizes="192x192" href="%PUBLIC_URL%/icons/icon-192x192.png">
<link rel="icon" type="image/png" sizes="180x180" href="%PUBLIC_URL%/icons/icon-180x180.png">
<link rel="icon" type="image/png" sizes="167x167" href="%PUBLIC_URL%/icons/icon-167x167.png">
<link rel="icon" type="image/png" sizes="160x160" href="%PUBLIC_URL%/icons/icon-160x160.png">
<link rel="icon" type="image/png" sizes="152x152" href="%PUBLIC_URL%/icons/icon-152x152.png">
<link rel="icon" type="image/png" sizes="150x150" href="%PUBLIC_URL%/icons/icon-150x150.png">
<link rel="icon" type="image/png" sizes="144x144" href="%PUBLIC_URL%/icons/icon-144x144.png">
<link rel="icon" type="image/png" sizes="128x128" href="%PUBLIC_URL%/icons/icon-128x128.png">
<link rel="icon" type="image/png" sizes="120x120" href="%PUBLIC_URL%/icons/icon-120x120.png">
<link rel="icon" type="image/png" sizes="114x114" href="%PUBLIC_URL%/icons/icon-114x114.png">
<link rel="icon" type="image/png" sizes="96x96" href="%PUBLIC_URL%/icons/icon-96x96.png">
<link rel="icon" type="image/png" sizes="76x76" href="%PUBLIC_URL%/icons/icon-76x76.png">
<link rel="icon" type="image/png" sizes="72x72" href="%PUBLIC_URL%/icons/icon-72x72.png">
<link rel="icon" type="image/png" sizes="64x64" href="%PUBLIC_URL%/icons/icon-64x64.png">
<link rel="icon" type="image/png" sizes="60x60" href="%PUBLIC_URL%/icons/icon-60x60.png">
<link rel="icon" type="image/png" sizes="48x48" href="%PUBLIC_URL%/icons/icon-48x48.png">
<link rel="icon" type="image/png" sizes="40x40" href="%PUBLIC_URL%/icons/icon-40x40.png">
<link rel="icon" type="image/png" sizes="32x32" href="%PUBLIC_URL%/icons/icon-32x32.png">
<link rel="icon" type="image/png" sizes="24x24" href="%PUBLIC_URL%/icons/icon-24x24.png">
<link rel="icon" type="image/png" sizes="16x16" href="%PUBLIC_URL%/icons/icon-16x16.png">
<!-- MS -->
<meta name="msapplication-TileImage" content="%PUBLIC_URL%/icons/icon-144x144.png">
<meta name="msapplication-square70x70logo" content="%PUBLIC_URL%/icons/icon-70x70.png">
<meta name="msapplication-square144x144logo" content="%PUBLIC_URL%/icons/icon-144x144.png">
<meta name="msapplication-square150x150logo" content="%PUBLIC_URL%/icons/icon-150x150.png">
<meta name="msapplication-square310x310logo" content="%PUBLIC_URL%/icons/icon-310x310.png">
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.

View File

@@ -6,16 +6,66 @@
"theme_color": "#4696e5",
"background_color": "#4696e5",
"icons": [
{
"src": "./icons/icon-16x16.png",
"type": "image/png",
"sizes": "16x16"
},
{
"src": "./icons/icon-24x24.png",
"type": "image/png",
"sizes": "24x24"
},
{
"src": "./icons/icon-32x32.png",
"type": "image/png",
"sizes": "32x32"
},
{
"src": "./icons/icon-40x40.png",
"type": "image/png",
"sizes": "40x40"
},
{
"src": "./icons/icon-48x48.png",
"type": "image/png",
"sizes": "48x48"
},
{
"src": "./icons/icon-60x60.png",
"type": "image/png",
"sizes": "60x60"
},
{
"src": "./icons/icon-64x64.png",
"type": "image/png",
"sizes": "64x64"
},
{
"src": "./icons/icon-72x72.png",
"type": "image/png",
"sizes": "72x72"
},
{
"src": "./icons/icon-76x76.png",
"type": "image/png",
"sizes": "76x76"
},
{
"src": "./icons/icon-96x96.png",
"type": "image/png",
"sizes": "96x96"
},
{
"src": "./icons/icon-114x114.png",
"type": "image/png",
"sizes": "114x114"
},
{
"src": "./icons/icon-120x120.png",
"type": "image/png",
"sizes": "120x120"
},
{
"src": "./icons/icon-128x128.png",
"type": "image/png",
@@ -26,20 +76,70 @@
"type": "image/png",
"sizes": "144x144"
},
{
"src": "./icons/icon-150x150.png",
"type": "image/png",
"sizes": "150x150"
},
{
"src": "./icons/icon-152x152.png",
"type": "image/png",
"sizes": "152x152"
},
{
"src": "./icons/icon-160x160.png",
"type": "image/png",
"sizes": "160x160"
},
{
"src": "./icons/icon-167x167.png",
"type": "image/png",
"sizes": "167x167"
},
{
"src": "./icons/icon-180x180.png",
"type": "image/png",
"sizes": "180x180"
},
{
"src": "./icons/icon-192x192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "./icons/icon-196x196.png",
"type": "image/png",
"sizes": "196x196"
},
{
"src": "./icons/icon-228x228.png",
"type": "image/png",
"sizes": "228x228"
},
{
"src": "./icons/icon-256x256.png",
"type": "image/png",
"sizes": "256x256"
},
{
"src": "./icons/icon-310x310.png",
"type": "image/png",
"sizes": "310x310"
},
{
"src": "./icons/icon-384x384.png",
"type": "image/png",
"sizes": "384x384"
},
{
"src": "./icons/icon-512x512.png",
"type": "image/png",
"sizes": "512x512"
},
{
"src": "./icons/icon-1024x1024.png",
"type": "image/png",
"sizes": "1024x1024"
}
]
}

View File

@@ -14,7 +14,6 @@ process.on('unhandledRejection', (err) => {
// Ensure environment variables are read.
require('../config/env');
const path = require('path');
const chalk = require('chalk');
const fs = require('fs-extra');
const webpack = require('webpack');
@@ -22,7 +21,6 @@ const bfj = require('bfj');
const AdmZip = require('adm-zip');
const checkRequiredFiles = require('react-dev-utils/checkRequiredFiles');
const formatWebpackMessages = require('react-dev-utils/formatWebpackMessages');
const printHostingInstructions = require('react-dev-utils/printHostingInstructions');
const FileSizeReporter = require('react-dev-utils/FileSizeReporter');
const printBuildError = require('react-dev-utils/printBuildError');
const { checkBrowsers } = require('react-dev-utils/browsersHelper');
@@ -30,7 +28,6 @@ const paths = require('../config/paths');
const configFactory = require('../config/webpack.config');
const { measureFileSizesBeforeBuild, printFileSizesAfterBuild } = FileSizeReporter;
const useYarn = fs.existsSync(paths.yarnLockFile);
// These sizes are pretty large. We'll warn for bundles exceeding them.
const WARN_AFTER_BUNDLE_GZIP_SIZE = 512 * 1024; // eslint-disable-line
@@ -47,6 +44,8 @@ if (!checkRequiredFiles([ paths.appHtml, paths.appIndexJs ])) {
const argvSliceStart = 2;
const argv = process.argv.slice(argvSliceStart);
const writeStatsJson = argv.indexOf('--stats') !== -1;
const withoutDist = argv.indexOf('--no-dist') !== -1;
const { version, hasVersion } = getVersionFromArgs(argv);
// Generate configuration
const config = configFactory('production');
@@ -85,6 +84,7 @@ checkBrowsers(paths.appPath, isInteractive)
);
} else {
console.log(chalk.green('Compiled successfully.\n'));
hasVersion && replaceVersionPlaceholder(version);
}
console.log('File sizes after gzip:\n');
@@ -96,20 +96,6 @@ checkBrowsers(paths.appPath, isInteractive)
WARN_AFTER_CHUNK_GZIP_SIZE
);
console.log();
const appPackage = require(paths.appPackageJson);
const { publicUrl } = paths;
const { output: { publicPath } } = config;
const buildFolder = path.relative(process.cwd(), paths.appBuild);
printHostingInstructions(
appPackage,
publicUrl,
publicPath,
buildFolder,
useYarn
);
},
(err) => {
console.log(chalk.red('Failed to compile.\n'));
@@ -117,7 +103,7 @@ checkBrowsers(paths.appPath, isInteractive)
process.exit(1);
}
)
.then(zipDist)
.then(() => hasVersion && !withoutDist && zipDist(version))
.catch((err) => {
if (err && err.message) {
console.log(err.message);
@@ -200,15 +186,7 @@ function copyPublicFolder() {
});
}
function zipDist() {
const minArgsToContainVersion = 3;
// If no version was provided, do nothing
if (process.argv.length < minArgsToContainVersion) {
return;
}
const [ , , version ] = process.argv;
function zipDist(version) {
const versionFileName = `./dist/shlink-web-client_${version}_dist.zip`;
console.log(chalk.cyan(`Generating dist file for version ${chalk.bold(version)}...`));
@@ -226,4 +204,24 @@ function zipDist() {
console.log(chalk.red('An error occurred while generating dist file'));
console.log(e);
}
console.log();
}
function getVersionFromArgs(argv) {
const [ version ] = argv;
return { version, hasVersion: !!version };
}
function replaceVersionPlaceholder(version) {
const staticJsFilesPath = './build/static/js';
const versionPlaceholder = '%_VERSION_%';
const isMainFile = (file) => file.startsWith('main.') && file.endsWith('.js');
const [ mainJsFile ] = fs.readdirSync(staticJsFilesPath).filter(isMainFile);
const filePath = `${staticJsFilesPath}/${mainJsFile}`;
const fileContent = fs.readFileSync(filePath, 'utf-8');
const replaced = fileContent.replace(versionPlaceholder, version);
fs.writeFileSync(filePath, replaced, 'utf-8');
}

33
scripts/docker/build Executable file
View File

@@ -0,0 +1,33 @@
#!/bin/bash
set -e
#PLATFORMS="linux/arm/v7,linux/arm64/v8,linux/amd64"
PLATFORMS="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
docker buildx build --push \
--platform ${PLATFORMS} \
-t ${DOCKER_IMAGE}:latest .
else
TAGS="-t ${DOCKER_IMAGE}:${TRAVIS_TAG#?}"
# 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"
docker buildx build --push \
--build-arg SHLINK_VERSION=${TRAVIS_TAG#?} \
--platform ${PLATFORMS} \
${TAGS} .
fi

12
scripts/docker/install-docker Executable file
View File

@@ -0,0 +1,12 @@
#!/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

BIN
shlink-web-client.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 MiB

View File

@@ -1,21 +1,40 @@
import React from 'react';
import React, { useEffect } from 'react';
import PropTypes from 'prop-types';
import { Route, Switch } from 'react-router-dom';
import './App.scss';
import NotFound from './common/NotFound';
import './App.scss';
const App = (MainHeader, Home, MenuLayout, CreateServer) => () => (
<div className="container-fluid app-container">
<MainHeader />
const propTypes = {
fetchServers: PropTypes.func,
servers: PropTypes.object,
};
<div className="app">
<Switch>
<Route exact path="/server/create" component={CreateServer} />
<Route exact path="/" component={Home} />
<Route path="/server/:serverId" component={MenuLayout} />
<Route component={NotFound} />
</Switch>
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>
</div>
);
);
};
App.propTypes = propTypes;
export default App;

View File

@@ -1,16 +1,34 @@
import { faList as listIcon, faLink as createIcon, faTags as tagsIcon } from '@fortawesome/free-solid-svg-icons';
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 classNames from 'classnames';
import { serverType } from '../servers/prop-types';
import './AsideMenu.scss';
const defaultProps = {
className: '',
showOnMobile: false,
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,
@@ -20,43 +38,34 @@ const propTypes = {
const AsideMenu = (DeleteServerButton) => {
const AsideMenu = ({ selectedServer, className, showOnMobile }) => {
const serverId = selectedServer ? selectedServer.id : '';
const asideClass = classnames('aside-menu', className, {
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">
<NavLink
className="aside-menu__item"
activeClassName="aside-menu__item--selected"
to={`/server/${serverId}/list-short-urls/1`}
isActive={shortUrlsIsActive}
>
<AsideMenuItem to={buildPath('/list-short-urls/1')} isActive={shortUrlsIsActive}>
<FontAwesomeIcon icon={listIcon} />
<span className="aside-menu__item-text">List short URLs</span>
</NavLink>
<NavLink
className="aside-menu__item"
activeClassName="aside-menu__item--selected"
to={`/server/${serverId}/create-short-url`}
>
</AsideMenuItem>
<AsideMenuItem to={buildPath('/create-short-url')}>
<FontAwesomeIcon icon={createIcon} flip="horizontal" />
<span className="aside-menu__item-text">Create short URL</span>
</NavLink>
<NavLink
className="aside-menu__item"
activeClassName="aside-menu__item--selected"
to={`/server/${serverId}/manage-tags`}
>
</AsideMenuItem>
<AsideMenuItem to={buildPath('/manage-tags')}>
<FontAwesomeIcon icon={tagsIcon} />
<span className="aside-menu__item-text">Manage tags</span>
</NavLink>
</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>
@@ -64,7 +73,6 @@ const AsideMenu = (DeleteServerButton) => {
);
};
AsideMenu.defaultProps = defaultProps;
AsideMenu.propTypes = propTypes;
return AsideMenu;

View File

@@ -67,6 +67,9 @@ $asideMenuMobileWidth: 280px;
.aside-menu__item--danger {
color: $dangerColor;
}
.aside-menu__item--push {
margin-top: auto;
}

View File

@@ -3,6 +3,7 @@ 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,

View File

@@ -1,52 +1,34 @@
import { faChevronRight as chevronIcon } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import React, { useEffect } from 'react';
import { isEmpty, values } from 'ramda';
import React from 'react';
import { Link } from 'react-router-dom';
import { ListGroup, ListGroupItem } from 'reactstrap';
import PropTypes from 'prop-types';
import './Home.scss';
import ServersListGroup from '../servers/ServersListGroup';
export default class Home extends React.Component {
static propTypes = {
resetSelectedServer: PropTypes.func,
servers: PropTypes.object,
};
const propTypes = {
resetSelectedServer: PropTypes.func,
servers: PropTypes.object,
};
componentDidMount() {
this.props.resetSelectedServer();
}
const Home = ({ resetSelectedServer, servers }) => {
const serversList = values(servers);
const hasServers = !isEmpty(serversList);
render() {
const { servers: { list, loading } } = this.props;
const servers = values(list);
const hasServers = !isEmpty(servers);
useEffect(() => {
resetSelectedServer();
}, []);
return (
<div className="home">
<h1 className="home__title">Welcome to Shlink</h1>
<h5 className="home__intro">
{!loading && hasServers && <span>Please, select a server.</span>}
{!loading && !hasServers && <span>Please, <Link to="/server/create">add a server</Link>.</span>}
{loading && <span>Trying to load servers...</span>}
</h5>
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>
);
};
{!loading && hasServers && (
<ListGroup className="home__servers-list">
{servers.map(({ name, id }) => (
<ListGroupItem
key={id}
tag={Link}
to={`/server/${id}/list-short-urls/1`}
className="home__servers-item"
>
{name}
<FontAwesomeIcon icon={chevronIcon} className="home__servers-item-icon" />
</ListGroupItem>
))}
</ListGroup>
)}
</div>
);
}
}
Home.propTypes = propTypes;
export default Home;

View File

@@ -1,5 +1,4 @@
@import '../utils/base';
@import '../utils/mixins/vertical-align';
.home {
text-align: center;
@@ -17,21 +16,3 @@
font-size: 2.2rem;
}
}
.home__servers-list {
margin-top: 1rem;
width: 100%;
max-width: 400px;
}
.home__servers-item.home__servers-item {
text-align: left;
position: relative;
padding: .75rem 2.5rem .75rem 1rem;
}
.home__servers-item-icon {
@include vertical-align();
right: 1rem;
}

View File

@@ -1,37 +1,28 @@
import { faPlus as plusIcon, faChevronDown as arrowIcon } from '@fortawesome/free-solid-svg-icons';
import { faPlus as plusIcon, faChevronDown as arrowIcon, faCogs as cogsIcon } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import React from 'react';
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 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 MainHeader = (ServersDropdown) => class MainHeader extends React.Component {
static propTypes = {
location: PropTypes.object,
};
const propTypes = {
location: PropTypes.object,
};
state = { isOpen: false };
handleToggle = () => {
this.setState(({ isOpen }) => ({
isOpen: !isOpen,
}));
};
const MainHeader = (ServersDropdown) => {
const MainHeaderComp = ({ location }) => {
const [ isOpen, toggleOpen, , close ] = useToggle();
const { pathname } = location;
componentDidUpdate(prevProps) {
if (this.props.location !== prevProps.location) {
this.setState({ isOpen: false });
}
}
useEffect(close, [ location ]);
render() {
const { location } = this.props;
const createServerPath = '/server/create';
const toggleClass = classnames('main-header__toggle-icon', {
'main-header__toggle-icon--opened': this.state.isOpen,
});
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">
@@ -39,18 +30,19 @@ const MainHeader = (ServersDropdown) => class MainHeader extends React.Component
<img src={shlinkLogo} alt="Shlink" className="main-header__brand-logo" /> Shlink
</NavbarBrand>
<NavbarToggler onClick={this.handleToggle}>
<NavbarToggler onClick={toggleOpen}>
<FontAwesomeIcon icon={arrowIcon} className={toggleClass} />
</NavbarToggler>
<Collapse navbar isOpen={this.state.isOpen}>
<Collapse navbar isOpen={isOpen}>
<Nav navbar className="ml-auto">
<NavItem>
<NavLink
tag={Link}
to={createServerPath}
active={location.pathname === createServerPath}
>
<NavLink tag={Link} to={settingsPath} active={pathname === settingsPath}>
<FontAwesomeIcon icon={cogsIcon} />&nbsp; Settings
</NavLink>
</NavItem>
<NavItem>
<NavLink tag={Link} to={createServerPath} active={pathname === createServerPath}>
<FontAwesomeIcon icon={plusIcon} />&nbsp; Add server
</NavLink>
</NavItem>
@@ -59,7 +51,11 @@ const MainHeader = (ServersDropdown) => class MainHeader extends React.Component
</Collapse>
</Navbar>
);
}
};
MainHeaderComp.propTypes = propTypes;
return MainHeaderComp;
};
export default MainHeader;

View File

@@ -1,110 +1,98 @@
import React from 'react';
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 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 MenuLayout = (TagsList, ShortUrls, AsideMenu, CreateShortUrl, ShortUrlVisits) =>
class MenuLayout extends React.Component {
static propTypes = {
match: PropTypes.object,
selectServer: PropTypes.func,
location: PropTypes.object,
selectedServer: serverType,
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();
};
state = { showSideBar: false };
return (
<React.Fragment>
<FontAwesomeIcon icon={burgerIcon} className={burgerClasses} onClick={toggleSidebar} />
componentDidMount() {
const { match, selectServer } = this.props;
const { params: { serverId } } = match;
selectServer(serverId);
}
componentDidUpdate(prevProps) {
const { location } = this.props;
// Hide sidebar when location changes
if (location !== prevProps.location) {
this.setState({ showSideBar: false });
}
}
render() {
const { selectedServer, match } = this.props;
const { params: { serverId } } = match;
const burgerClasses = classnames('menu-layout__burger-icon', {
'menu-layout__burger-icon--active': this.state.showSideBar,
});
const swipeMenuIfNoModalExists = (showSideBar) => () => {
if (document.querySelector('.modal')) {
return;
}
this.setState({ showSideBar });
};
return (
<React.Fragment>
<FontAwesomeIcon
icon={burgerIcon}
className={burgerClasses}
onClick={() => this.setState(({ showSideBar }) => ({ showSideBar: !showSideBar }))}
/>
<Swipeable
delta={40}
className="menu-layout__swipeable"
onSwipedLeft={swipeMenuIfNoModalExists(false)}
onSwipedRight={swipeMenuIfNoModalExists(true)}
>
<div className="row menu-layout__swipeable-inner">
<AsideMenu
className="col-lg-2 col-md-3"
selectedServer={selectedServer}
showOnMobile={this.state.showSideBar}
/>
<div
className="col-lg-10 offset-lg-2 col-md-9 offset-md-3"
onClick={() => this.setState({ showSideBar: false })}
>
<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
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}
/>
<Route
exact
path="/server/:serverId/manage-tags"
component={TagsList}
/>
<Route
render={() => <NotFound to={`/server/${serverId}/list-short-urls/1`} btnText="List short URLs" />}
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>
</Swipeable>
</React.Fragment>
);
}
</div>
</Swipeable>
</React.Fragment>
);
};
MenuLayoutComp.propTypes = propTypes;
return withSelectedServer(MenuLayoutComp, ServerError);
};
export default MenuLayout;

View File

@@ -32,3 +32,26 @@
.menu-layout__burger-icon--active {
color: white;
}
$footer-height: 2.3rem;
$footer-margin: .8rem;
.menu-layout__container {
padding: 20px 0 ($footer-height + $footer-margin);
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;
}
}

View File

@@ -0,0 +1,13 @@
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;

View File

@@ -0,0 +1,3 @@
.no-menu-wrapper {
padding: 40px 20px;
}

View File

@@ -4,17 +4,18 @@ import * as PropTypes from 'prop-types';
const propTypes = {
to: PropTypes.string,
btnText: PropTypes.string,
children: PropTypes.node,
};
const NotFound = ({ to = '/', btnText = 'Home' }) => (
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.
Use your browser&apos;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">{btnText}</Link>
<Link to={to} className="btn btn-outline-primary btn-lg">{children}</Link>
</div>
);

View File

@@ -1,23 +1,23 @@
import React from 'react';
import { useEffect } from 'react';
import PropTypes from 'prop-types';
const ScrollToTop = ({ scrollTo }) => class ScrollToTop extends React.Component {
static propTypes = {
location: PropTypes.object,
children: PropTypes.node,
const propTypes = {
location: PropTypes.object,
children: PropTypes.node,
};
const ScrollToTop = () => {
const ScrollToTopComp = ({ location, children }) => {
useEffect(() => {
scrollTo(0, 0);
}, [ location ]);
return children;
};
componentDidUpdate({ location: prevLocation }) {
const { location } = this.props;
ScrollToTopComp.propTypes = propTypes;
if (location !== prevLocation) {
scrollTo(0, 0);
}
}
render() {
return this.props.children;
}
return ScrollToTopComp;
};
export default ScrollToTop;

View File

@@ -0,0 +1,29 @@
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;

View File

@@ -1,38 +1,18 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { Pagination, PaginationItem, PaginationLink } from 'reactstrap';
import { range, max, min } from 'ramda';
import { isPageDisabled, keyForPage, progressivePagination } from '../utils/helpers/pagination';
import './SimplePaginator.scss';
const propTypes = {
pagesCount: PropTypes.number.isRequired,
currentPage: PropTypes.number.isRequired,
setCurrentPage: PropTypes.func.isRequired,
centered: PropTypes.bool,
};
export const ellipsis = '...';
const pagination = (currentPage, pageCount) => {
const delta = 2;
const pages = range(
max(delta, currentPage - delta),
min(pageCount - 1, currentPage + delta) + 1
);
if (currentPage - delta > delta) {
pages.unshift(ellipsis);
}
if (currentPage + delta < pageCount - 1) {
pages.push(ellipsis);
}
pages.unshift(1);
pages.push(pageCount);
return pages;
};
const SimplePaginator = ({ pagesCount, currentPage, setCurrentPage }) => {
const SimplePaginator = ({ pagesCount, currentPage, setCurrentPage, centered = true }) => {
if (pagesCount < 2) {
return null;
}
@@ -40,17 +20,17 @@ const SimplePaginator = ({ pagesCount, currentPage, setCurrentPage }) => {
const onClick = (page) => () => setCurrentPage(page);
return (
<Pagination listClassName="flex-wrap justify-content-center mb-0 simple-paginator">
<Pagination listClassName={classNames('flex-wrap mb-0 simple-paginator', { 'justify-content-center': centered })}>
<PaginationItem disabled={currentPage <= 1}>
<PaginationLink previous tag="span" onClick={onClick(currentPage - 1)} />
</PaginationItem>
{pagination(currentPage, pagesCount).map((page, index) => (
{progressivePagination(currentPage, pagesCount).map((pageNumber, index) => (
<PaginationItem
key={page !== ellipsis ? page : `${page}_${index}`}
active={page === currentPage}
disabled={page === ellipsis}
key={keyForPage(pageNumber, index)}
disabled={isPageDisabled(pageNumber)}
active={currentPage === pageNumber}
>
<PaginationLink tag="span" onClick={onClick(page)}>{page}</PaginationLink>
<PaginationLink tag="span" onClick={onClick(pageNumber)}>{pageNumber}</PaginationLink>
</PaginationItem>
))}
<PaginationItem disabled={currentPage >= pagesCount}>

View File

@@ -4,6 +4,7 @@ import Home from '../Home';
import MenuLayout from '../MenuLayout';
import AsideMenu from '../AsideMenu';
import ErrorHandler from '../ErrorHandler';
import ShlinkVersions from '../ShlinkVersions';
const provideServices = (bottle, connect, withRouter) => {
bottle.constant('window', global.window);
@@ -25,13 +26,19 @@ const provideServices = (bottle, connect, withRouter) => {
'ShortUrls',
'AsideMenu',
'CreateShortUrl',
'ShortUrlVisits'
'ShortUrlVisits',
'TagVisits',
'ShlinkVersions',
'ServerError'
);
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('ErrorHandler', ErrorHandler, 'window', 'console');
};

View File

@@ -9,6 +9,8 @@ import provideServersServices from '../servers/services/provideServices';
import provideVisitsServices from '../visits/services/provideServices';
import provideTagsServices from '../tags/services/provideServices';
import provideUtilsServices from '../utils/services/provideServices';
import provideMercureServices from '../mercure/services/provideServices';
import provideSettingsServices from '../settings/services/provideServices';
const bottle = new Bottle();
const { container } = bottle;
@@ -20,13 +22,14 @@ const mapActionService = (map, actionName) => ({
// 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 = (propsFromState, actionServiceNames = []) =>
reduxConnect(
propsFromState ? pick(propsFromState) : null,
actionServiceNames.reduce(mapActionService, {})
);
bottle.serviceFactory('App', App, 'MainHeader', 'Home', 'MenuLayout', 'CreateServer');
bottle.serviceFactory('App', App, 'MainHeader', 'Home', 'MenuLayout', 'CreateServer', 'EditServer', 'Settings');
bottle.decorator('App', connect([ 'servers' ], [ 'fetchServers' ]));
provideCommonServices(bottle, connect, withRouter);
provideShortUrlsServices(bottle, connect);
@@ -34,5 +37,7 @@ provideServersServices(bottle, connect, withRouter);
provideTagsServices(bottle, connect);
provideVisitsServices(bottle, connect);
provideUtilsServices(bottle);
provideMercureServices(bottle);
provideSettingsServices(bottle, connect);
export default container;

View File

@@ -1,13 +1,21 @@
import ReduxThunk from 'redux-thunk';
import { applyMiddleware, compose, createStore } from 'redux';
import { save, load } 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 store = createStore(reducers, composeEnhancers(
applyMiddleware(ReduxThunk)
const localStorageConfig = {
states: [ 'settings', 'servers' ],
namespace: 'shlink',
namespaceSeparator: '.',
debounce: 300,
};
const store = createStore(reducers, load(localStorageConfig), composeEnhancers(
applyMiddleware(save(localStorageConfig), ReduxThunk)
));
export default store;

View File

@@ -10,10 +10,6 @@ body,
outline: none !important;
}
.nowrap {
white-space: nowrap;
}
.bg-main {
background-color: $mainColor !important;
}
@@ -28,14 +24,6 @@ body,
color: inherit !important;
}
.shlink-container {
padding: 20px 0;
@media (min-width: $mdMin) {
padding: 30px 15px;
}
}
.badge-main {
color: #fff;
background-color: $mainColor;
@@ -56,6 +44,24 @@ body,
cursor: pointer;
}
.paddingless {
padding: 0;
.indivisible {
white-space: nowrap;
}
.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;
}

View File

@@ -0,0 +1,24 @@
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();
};

View File

@@ -0,0 +1,41 @@
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 });
}
};

View File

@@ -0,0 +1,8 @@
import { loadMercureInfo } from '../reducers/mercureInfo';
const provideServices = (bottle) => {
// Actions
bottle.serviceFactory('loadMercureInfo', loadMercureInfo, 'buildShlinkApiClient');
};
export default provideServices;

View File

@@ -1,16 +1,21 @@
import { combineReducers } from 'redux';
import serversReducer from '../servers/reducers/server';
import serversReducer from '../servers/reducers/servers';
import selectedServerReducer from '../servers/reducers/selectedServer';
import shortUrlsListReducer from '../short-urls/reducers/shortUrlsList';
import shortUrlsListParamsReducer from '../short-urls/reducers/shortUrlsListParams';
import shortUrlCreationReducer from '../short-urls/reducers/shortUrlCreation';
import shortUrlDeletionReducer from '../short-urls/reducers/shortUrlDeletion';
import shortUrlTagsReducer from '../short-urls/reducers/shortUrlTags';
import shortUrlMetaReducer from '../short-urls/reducers/shortUrlMeta';
import shortUrlEditionReducer from '../short-urls/reducers/shortUrlEdition';
import shortUrlVisitsReducer from '../visits/reducers/shortUrlVisits';
import tagVisitsReducer from '../visits/reducers/tagVisits';
import shortUrlDetailReducer from '../visits/reducers/shortUrlDetail';
import tagsListReducer from '../tags/reducers/tagsList';
import tagDeleteReducer from '../tags/reducers/tagDelete';
import tagEditReducer from '../tags/reducers/tagEdit';
import mercureInfoReducer from '../mercure/reducers/mercureInfo';
import settingsReducer from '../settings/reducers/settings';
export default combineReducers({
servers: serversReducer,
@@ -20,9 +25,14 @@ export default combineReducers({
shortUrlCreationResult: shortUrlCreationReducer,
shortUrlDeletion: shortUrlDeletionReducer,
shortUrlTags: shortUrlTagsReducer,
shortUrlMeta: shortUrlMetaReducer,
shortUrlEdition: shortUrlEditionReducer,
shortUrlVisits: shortUrlVisitsReducer,
tagVisits: tagVisitsReducer,
shortUrlDetail: shortUrlDetailReducer,
tagsList: tagsListReducer,
tagDelete: tagDeleteReducer,
tagEdit: tagEditReducer,
mercureInfo: mercureInfoReducer,
settings: settingsReducer,
});

View File

@@ -1,91 +1,57 @@
import { assoc, dissoc, pipe } from 'ramda';
import React from 'react';
import React, { useEffect } from 'react';
import { v4 as uuid } from 'uuid';
import PropTypes from 'prop-types';
import NoMenuLayout from '../common/NoMenuLayout';
import { ServerForm } from './helpers/ServerForm';
import './CreateServer.scss';
const SHOW_IMPORT_MSG_TIME = 4000;
const propTypes = {
createServer: PropTypes.func,
history: PropTypes.shape({
push: PropTypes.func,
}),
resetSelectedServer: PropTypes.func,
};
const CreateServer = (ImportServersBtn, stateFlagTimeout) => class CreateServer extends React.Component {
static propTypes = {
createServer: PropTypes.func,
history: PropTypes.shape({
push: PropTypes.func,
}),
resetSelectedServer: PropTypes.func,
};
const CreateServer = (ImportServersBtn, useStateFlagTimeout) => {
const CreateServerComp = ({ createServer, history: { push }, resetSelectedServer }) => {
const [ serversImported, setServersImported ] = useStateFlagTimeout(false, SHOW_IMPORT_MSG_TIME);
const handleSubmit = (serverData) => {
const id = uuid();
const server = { id, ...serverData };
state = {
name: '',
url: '',
apiKey: '',
serversImported: false,
};
createServer(server);
push(`/server/${id}/list-short-urls/1`);
};
handleSubmit = (e) => {
e.preventDefault();
const { createServer, history: { push } } = this.props;
const server = pipe(
assoc('id', uuid()),
dissoc('serversImported')
)(this.state);
createServer(server);
push(`/server/${server.id}/list-short-urls/1`);
};
componentDidMount() {
this.props.resetSelectedServer();
}
render() {
const renderInputGroup = (id, placeholder, type = 'text') => (
<div className="form-group row">
<label htmlFor={id} className="col-lg-1 col-md-2 col-form-label create-server__label">
{placeholder}:
</label>
<div className="col-lg-11 col-md-10">
<input
type={type}
className="form-control"
id={id}
placeholder={placeholder}
value={this.state[id]}
required
onChange={(e) => this.setState({ [id]: e.target.value })}
/>
</div>
</div>
);
useEffect(() => {
resetSelectedServer();
}, []);
return (
<div className="create-server">
<form onSubmit={this.handleSubmit}>
{renderInputGroup('name', 'Name')}
{renderInputGroup('url', 'URL', 'url')}
{renderInputGroup('apiKey', 'API key')}
<NoMenuLayout>
<ServerForm onSubmit={handleSubmit}>
<ImportServersBtn onImport={setServersImported} />
<button className="btn btn-outline-primary">Create server</button>
</ServerForm>
<div className="text-right">
<ImportServersBtn
onImport={() => stateFlagTimeout(this.setState.bind(this), 'serversImported', true, SHOW_IMPORT_MSG_TIME)}
/>
<button className="btn btn-outline-primary">Create server</button>
</div>
{this.state.serversImported && (
<div className="row create-server__import-success-msg">
<div className="col-md-10 offset-md-1">
<div className="p-2 mt-3 bg-main text-white text-center">
Servers properly imported. You can now select one from the list :)
</div>
{serversImported && (
<div className="row create-server__import-success-msg">
<div className="col-md-10 offset-md-1">
<div className="p-2 mt-3 bg-main text-white text-center">
Servers properly imported. You can now select one from the list :)
</div>
</div>
)}
</form>
</div>
</div>
)}
</NoMenuLayout>
);
}
};
CreateServerComp.propTypes = propTypes;
return CreateServerComp;
};
export default CreateServer;

View File

@@ -1,9 +1,5 @@
@import '../utils/base';
.create-server {
padding: 40px 20px;
}
.create-server__label {
font-weight: 700;
cursor: pointer;

View File

@@ -1,40 +1,36 @@
import React from 'react';
import { faMinusCircle as deleteIcon } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import React from 'react';
import PropTypes from 'prop-types';
import { useToggle } from '../utils/helpers/hooks';
import { serverType } from './prop-types';
const DeleteServerButton = (DeleteServerModal) => class DeleteServerButton extends React.Component {
static propTypes = {
server: serverType,
className: PropTypes.string,
};
const propTypes = {
server: serverType,
className: PropTypes.string,
textClassName: PropTypes.string,
children: PropTypes.node,
};
state = { isModalOpen: false };
render() {
const { server, className } = this.props;
const DeleteServerButton = (DeleteServerModal) => {
const DeleteServerButtonComp = ({ server, className, children, textClassName }) => {
const [ isModalOpen, , showModal, hideModal ] = useToggle();
return (
<React.Fragment>
<span
className={className}
key="deleteServerBtn"
onClick={() => this.setState({ isModalOpen: true })}
>
<FontAwesomeIcon icon={deleteIcon} />
<span className="aside-menu__item-text">Delete this server</span>
<span className={className} onClick={showModal}>
{!children && <FontAwesomeIcon icon={deleteIcon} />}
<span className={textClassName}>{children || 'Remove this server'}</span>
</span>
<DeleteServerModal
isOpen={this.state.isModalOpen}
toggle={() => this.setState(({ isModalOpen }) => ({ isModalOpen: !isModalOpen }))}
server={server}
key="deleteServerModal"
/>
<DeleteServerModal server={server} isOpen={isModalOpen} toggle={hideModal} />
</React.Fragment>
);
}
};
DeleteServerButtonComp.propTypes = propTypes;
return DeleteServerButtonComp;
};
export default DeleteServerButton;

View File

@@ -22,12 +22,14 @@ const DeleteServerModal = ({ server, toggle, isOpen, deleteServer, history }) =>
return (
<Modal isOpen={isOpen} toggle={toggle} centered>
<ModalHeader toggle={toggle}><span className="text-danger">Delete server</span></ModalHeader>
<ModalHeader toggle={toggle}><span className="text-danger">Remove server</span></ModalHeader>
<ModalBody>
<p>Are you sure you want to delete server <b>{server ? server.name : ''}</b>?</p>
<p>Are you sure you want to remove <b>{server ? server.name : ''}</b>?</p>
<p>
No data will be deleted, only the access to that server will be removed from this host.
You can create it again at any moment.
<i>
No data will be deleted, only the access to this server will be removed from this host.
You can create it again at any moment.
</i>
</p>
</ModalBody>
<ModalFooter>

38
src/servers/EditServer.js Normal file
View File

@@ -0,0 +1,38 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Button } from 'reactstrap';
import NoMenuLayout from '../common/NoMenuLayout';
import { ServerForm } from './helpers/ServerForm';
import { withSelectedServer } from './helpers/withSelectedServer';
import { serverType } from './prop-types';
const propTypes = {
editServer: PropTypes.func,
selectedServer: serverType,
history: PropTypes.shape({
push: PropTypes.func,
goBack: PropTypes.func,
}),
};
export const EditServer = (ServerError) => {
const EditServerComp = ({ editServer, selectedServer, history: { push, goBack } }) => {
const handleSubmit = (serverData) => {
editServer(selectedServer.id, serverData);
push(`/server/${selectedServer.id}/list-short-urls/1`);
};
return (
<NoMenuLayout>
<ServerForm initialValues={selectedServer} onSubmit={handleSubmit}>
<Button outline className="mr-2" onClick={goBack}>Cancel</Button>
<Button outline color="primary">Save</Button>
</ServerForm>
</NoMenuLayout>
);
};
EditServerComp.propTypes = propTypes;
return withSelectedServer(EditServerComp, ServerError);
};

View File

@@ -2,63 +2,54 @@ import { isEmpty, values } from 'ramda';
import React from 'react';
import { DropdownItem, DropdownMenu, DropdownToggle, UncontrolledDropdown } from 'reactstrap';
import PropTypes from 'prop-types';
import { Link } from 'react-router-dom';
import { serverType } from './prop-types';
const ServersDropdown = (serversExporter) => class ServersDropdown extends React.Component {
static propTypes = {
servers: PropTypes.object,
selectedServer: serverType,
selectServer: PropTypes.func,
listServers: PropTypes.func,
history: PropTypes.shape({
push: PropTypes.func,
}),
};
const propTypes = {
servers: PropTypes.object,
selectedServer: serverType,
};
renderServers = () => {
const { servers: { list, loading }, selectedServer, selectServer } = this.props;
const servers = values(list);
const { push } = this.props.history;
const loadServer = (id) => {
selectServer(id)
.then(() => push(`/server/${id}/list-short-urls/1`))
.catch(() => {});
const ServersDropdown = (serversExporter) => {
const ServersDropdownComp = ({ servers, selectedServer }) => {
const serversList = values(servers);
const renderServers = () => {
if (isEmpty(serversList)) {
return <DropdownItem disabled><i>Add a server first...</i></DropdownItem>;
}
return (
<React.Fragment>
{serversList.map(({ name, id }) => (
<DropdownItem
key={id}
tag={Link}
to={`/server/${id}/list-short-urls/1`}
active={selectedServer && selectedServer.id === id}
>
{name}
</DropdownItem>
))}
<DropdownItem divider />
<DropdownItem className="servers-dropdown__export-item" onClick={() => serversExporter.exportServers()}>
Export servers
</DropdownItem>
</React.Fragment>
);
};
if (loading) {
return <DropdownItem disabled><i>Trying to load servers...</i></DropdownItem>;
}
if (isEmpty(servers)) {
return <DropdownItem disabled><i>Add a server first...</i></DropdownItem>;
}
return (
<React.Fragment>
{servers.map(({ name, id }) => (
<DropdownItem key={id} active={selectedServer && selectedServer.id === id} onClick={() => loadServer(id)}>
{name}
</DropdownItem>
))}
<DropdownItem divider />
<DropdownItem
className="servers-dropdown__export-item"
onClick={() => serversExporter.exportServers()}
>
Export servers
</DropdownItem>
</React.Fragment>
<UncontrolledDropdown nav inNavbar>
<DropdownToggle nav caret>Servers</DropdownToggle>
<DropdownMenu right>{renderServers()}</DropdownMenu>
</UncontrolledDropdown>
);
};
componentDidMount = this.props.listServers;
ServersDropdownComp.propTypes = propTypes;
render = () => (
<UncontrolledDropdown nav inNavbar>
<DropdownToggle nav caret>Servers</DropdownToggle>
<DropdownMenu right>{this.renderServers()}</DropdownMenu>
</UncontrolledDropdown>
);
return ServersDropdownComp;
};
export default ServersDropdown;

View File

@@ -0,0 +1,42 @@
import React from 'react';
import PropTypes from 'prop-types';
import { ListGroup, ListGroupItem } from 'reactstrap';
import { Link } from 'react-router-dom';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faChevronRight as chevronIcon } from '@fortawesome/free-solid-svg-icons';
import { serverType } from './prop-types';
import './ServersListGroup.scss';
const propTypes = {
servers: PropTypes.arrayOf(serverType).isRequired,
children: PropTypes.node.isRequired,
};
const ServerListItem = ({ id, name }) => (
<ListGroupItem tag={Link} to={`/server/${id}/list-short-urls/1`} className="servers-list__server-item">
{name}
<FontAwesomeIcon icon={chevronIcon} className="servers-list__server-item-icon" />
</ListGroupItem>
);
ServerListItem.propTypes = {
id: PropTypes.string,
name: PropTypes.string,
};
const ServersListGroup = ({ servers, children }) => (
<React.Fragment>
<div className="container">
<h5>{children}</h5>
</div>
{servers.length > 0 && (
<ListGroup className="servers-list__list-group mt-md-3">
{servers.map(({ id, name }) => <ServerListItem key={id} id={id} name={name} />)}
</ListGroup>
)}
</React.Fragment>
);
ServersListGroup.propTypes = propTypes;
export default ServersListGroup;

View File

@@ -0,0 +1,18 @@
@import '../utils/mixins/vertical-align';
.servers-list__list-group {
width: 100%;
max-width: 400px;
}
.servers-list__server-item.servers-list__server-item {
text-align: left;
position: relative;
padding: .75rem 2.5rem .75rem 1rem;
}
.servers-list__server-item-icon {
@include vertical-align();
right: 1rem;
}

View File

@@ -0,0 +1,30 @@
import React from 'react';
import PropTypes from 'prop-types';
import { serverType } from '../prop-types';
import { versionMatch } from '../../utils/helpers/version';
const propTypes = {
minVersion: PropTypes.string,
maxVersion: PropTypes.string,
selectedServer: serverType,
children: PropTypes.node.isRequired,
};
const ForServerVersion = ({ minVersion, maxVersion, selectedServer, children }) => {
if (!selectedServer) {
return null;
}
const { version } = selectedServer;
const matchesVersion = versionMatch(version, { maxVersion, minVersion });
if (!matchesVersion) {
return null;
}
return <React.Fragment>{children}</React.Fragment>;
};
ForServerVersion.propTypes = propTypes;
export default ForServerVersion;

View File

@@ -1,25 +1,17 @@
import React from 'react';
import React, { useRef } from 'react';
import { UncontrolledTooltip } from 'reactstrap';
import PropTypes from 'prop-types';
const ImportServersBtn = (serversImporter) => class ImportServersBtn extends React.Component {
static defaultProps = {
onImport: () => ({}),
};
static propTypes = {
onImport: PropTypes.func,
createServers: PropTypes.func,
fileRef: PropTypes.oneOfType([ PropTypes.object, PropTypes.node ]),
};
const propTypes = {
onImport: PropTypes.func,
createServers: PropTypes.func,
fileRef: PropTypes.oneOfType([ PropTypes.object, PropTypes.node ]),
};
constructor(props) {
super(props);
this.fileRef = props.fileRef || React.createRef();
}
render() {
const { importServersFromFile } = serversImporter;
const { onImport, createServers } = this.props;
// FIXME Replace with typescript: (ServersImporter)
const ImportServersBtn = ({ importServersFromFile }) => {
const ImportServersBtnComp = ({ createServers, fileRef, onImport = () => {} }) => {
const ref = fileRef || useRef();
const onChange = ({ target }) =>
importServersFromFile(target.files[0])
.then(createServers)
@@ -35,24 +27,22 @@ const ImportServersBtn = (serversImporter) => class ImportServersBtn extends Rea
type="button"
className="btn btn-outline-secondary mr-2"
id="importBtn"
onClick={() => this.fileRef.current.click()}
onClick={() => ref.current.click()}
>
Import from file
</button>
<UncontrolledTooltip placement="top" target="importBtn">
You can create servers by importing a CSV file with columns <b>name</b>, <b>apiKey</b> and <b>url</b>
You can create servers by importing a CSV file with columns <b>name</b>, <b>apiKey</b> and <b>url</b>.
</UncontrolledTooltip>
<input
type="file"
accept="text/csv"
className="create-server__csv-select"
ref={this.fileRef}
onChange={onChange}
/>
<input type="file" accept="text/csv" className="create-server__csv-select" ref={ref} onChange={onChange} />
</React.Fragment>
);
}
};
ImportServersBtnComp.propTypes = propTypes;
return ImportServersBtnComp;
};
export default ImportServersBtn;

View File

@@ -0,0 +1,50 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Link } from 'react-router-dom';
import Message from '../../utils/Message';
import ServersListGroup from '../ServersListGroup';
import { serverType } from '../prop-types';
import './ServerError.scss';
const propTypes = {
servers: PropTypes.object,
selectedServer: serverType,
type: PropTypes.oneOf([ 'not-found', 'not-reachable' ]).isRequired,
};
export const ServerError = (DeleteServerButton) => {
const ServerErrorComp = ({ type, servers, selectedServer }) => (
<div className="server-error__container flex-column">
<div className="row w-100 mb-3 mb-md-5">
<Message type="error">
{type === 'not-found' && 'Could not find this Shlink server.'}
{type === 'not-reachable' && (
<React.Fragment>
<p>Oops! Could not connect to this Shlink server.</p>
Make sure you have internet connection, and the server is properly configured and on-line.
</React.Fragment>
)}
</Message>
</div>
<ServersListGroup servers={Object.values(servers)}>
These are the Shlink servers currently configured. Choose one of
them or <Link to="/server/create">add a new one</Link>.
</ServersListGroup>
{type === 'not-reachable' && (
<div className="container mt-3 mt-md-5">
<h5>
Alternatively, if you think you may have miss-configured this server, you
can <DeleteServerButton server={selectedServer} className="server-error__delete-btn">remove it</DeleteServerButton> or&nbsp;
<Link to={`/server/${selectedServer.id}/edit`}>edit it</Link>.
</h5>
</div>
)}
</div>
);
ServerErrorComp.propTypes = propTypes;
return ServerErrorComp;
};

View File

@@ -0,0 +1,17 @@
@import '../../utils/base';
.server-error__container {
text-align: center;
display: flex;
align-items: center;
justify-content: center;
}
.server-error__delete-btn {
color: $dangerColor;
cursor: pointer;
}
.server-error__delete-btn:hover {
text-decoration: underline;
}

View File

@@ -0,0 +1,41 @@
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { HorizontalFormGroup } from '../../utils/HorizontalFormGroup';
const propTypes = {
onSubmit: PropTypes.func.isRequired,
initialValues: PropTypes.shape({
name: PropTypes.string.isRequired,
url: PropTypes.string.isRequired,
apiKey: PropTypes.string.isRequired,
}),
children: PropTypes.node.isRequired,
};
export const ServerForm = ({ onSubmit, initialValues, children }) => {
const [ name, setName ] = useState('');
const [ url, setUrl ] = useState('');
const [ apiKey, setApiKey ] = useState('');
const handleSubmit = (e) => {
e.preventDefault();
onSubmit({ name, url, apiKey });
};
useEffect(() => {
initialValues && setName(initialValues.name);
initialValues && setUrl(initialValues.url);
initialValues && setApiKey(initialValues.apiKey);
}, [ initialValues ]);
return (
<form onSubmit={handleSubmit}>
<HorizontalFormGroup value={name} onChange={setName}>Name</HorizontalFormGroup>
<HorizontalFormGroup type="url" value={url} onChange={setUrl}>URL</HorizontalFormGroup>
<HorizontalFormGroup value={apiKey} onChange={setApiKey}>API key</HorizontalFormGroup>
<div className="text-right">{children}</div>
</form>
);
};
ServerForm.propTypes = propTypes;

View File

@@ -0,0 +1,35 @@
import React, { useEffect } from 'react';
import PropTypes from 'prop-types';
import Message from '../../utils/Message';
import { serverType } from '../prop-types';
const propTypes = {
selectServer: PropTypes.func,
selectedServer: serverType,
match: PropTypes.object,
};
export const withSelectedServer = (WrappedComponent, ServerError) => {
const Component = (props) => {
const { selectServer, selectedServer, match } = props;
const { params: { serverId } } = match;
useEffect(() => {
selectServer(serverId);
}, [ serverId ]);
if (!selectedServer) {
return <Message loading />;
}
if (selectedServer.serverNotFound) {
return <ServerError type="not-found" />;
}
return <WrappedComponent {...props} />;
};
Component.propTypes = propTypes;
return Component;
};

View File

@@ -1,8 +1,20 @@
import PropTypes from 'prop-types';
export const serverType = PropTypes.shape({
const regularServerType = PropTypes.shape({
id: PropTypes.string,
name: PropTypes.string,
url: PropTypes.string,
apiKey: PropTypes.string,
version: PropTypes.string,
printableVersion: PropTypes.string,
serverNotReachable: PropTypes.bool,
});
const notFoundServerType = PropTypes.shape({
serverNotFound: PropTypes.bool.isRequired,
});
export const serverType = PropTypes.oneOfType([
regularServerType,
notFoundServerType,
]);

View File

@@ -0,0 +1,22 @@
import { pipe, prop } from 'ramda';
import { homepage } from '../../../package.json';
import { createServers } from './servers';
const responseToServersList = pipe(
prop('data'),
(value) => {
if (!Array.isArray(value)) {
throw new Error('Value is not an array');
}
return value;
},
);
export const fetchServers = ({ get }) => () => async (dispatch) => {
const remoteList = await get(`${homepage}/servers.json`)
.then(responseToServersList)
.catch(() => []);
dispatch(createServers(remoteList));
};

View File

@@ -1,6 +1,7 @@
import { createAction, handleActions } from 'redux-actions';
import { identity, memoizeWith, pipe } from 'ramda';
import { resetShortUrlParams } from '../../short-urls/reducers/shortUrlsListParams';
import { versionIsValidSemVer } from '../../utils/utils';
import { versionToPrintable, versionToSemVer as toSemVer } from '../../utils/helpers/version';
/* eslint-disable padding-line-between-statements */
export const SELECT_SERVER = 'shlink/selectedServer/SELECT_SERVER';
@@ -12,26 +13,56 @@ export const LATEST_VERSION_CONSTRAINT = 'latest';
/* eslint-enable padding-line-between-statements */
const initialState = null;
const versionToSemVer = pipe(
(version) => version === LATEST_VERSION_CONSTRAINT ? MAX_FALLBACK_VERSION : version,
toSemVer(MIN_FALLBACK_VERSION)
);
const getServerVersion = memoizeWith(identity, (serverId, health) => health().then(({ version }) => ({
version: versionToSemVer(version),
printableVersion: versionToPrintable(version),
})));
export const resetSelectedServer = createAction(RESET_SELECTED_SERVER);
export const selectServer = ({ findServerById }, buildShlinkApiClient) => (serverId) => async (dispatch) => {
export const selectServer = (buildShlinkApiClient, loadMercureInfo) => (serverId) => async (
dispatch,
getState
) => {
dispatch(resetSelectedServer());
dispatch(resetShortUrlParams());
const selectedServer = findServerById(serverId);
const { health } = await buildShlinkApiClient(selectedServer);
const version = await health()
.then(({ version }) => version === LATEST_VERSION_CONSTRAINT ? MAX_FALLBACK_VERSION : version)
.then((version) => !versionIsValidSemVer(version) ? MIN_FALLBACK_VERSION : version)
.catch(() => MIN_FALLBACK_VERSION);
const { servers } = getState();
const selectedServer = servers[serverId];
dispatch({
type: SELECT_SERVER,
selectedServer: {
...selectedServer,
version,
},
});
if (!selectedServer) {
dispatch({
type: SELECT_SERVER,
selectedServer: { serverNotFound: true },
});
return;
}
try {
const { health } = buildShlinkApiClient(selectedServer);
const { version, printableVersion } = await getServerVersion(serverId, health);
dispatch({
type: SELECT_SERVER,
selectedServer: {
...selectedServer,
version,
printableVersion,
},
});
dispatch(loadMercureInfo());
} catch (e) {
dispatch({
type: SELECT_SERVER,
selectedServer: { ...selectedServer, serverNotReachable: true },
});
}
};
export default handleActions({

View File

@@ -1,61 +0,0 @@
import { handleActions } from 'redux-actions';
import { pipe, isEmpty, assoc, map, prop } from 'ramda';
import { v4 as uuid } from 'uuid';
import { homepage } from '../../../package.json';
/* eslint-disable padding-line-between-statements */
export const FETCH_SERVERS_START = 'shlink/servers/FETCH_SERVERS_START';
export const FETCH_SERVERS = 'shlink/servers/FETCH_SERVERS';
/* eslint-enable padding-line-between-statements */
const initialState = {
list: {},
loading: false,
};
const assocId = (server) => assoc('id', server.id || uuid(), server);
export default handleActions({
[FETCH_SERVERS_START]: (state) => ({ ...state, loading: true }),
[FETCH_SERVERS]: (state, { list }) => ({ list, loading: false }),
}, initialState);
export const listServers = ({ listServers, createServers }, { get }) => () => async (dispatch) => {
dispatch({ type: FETCH_SERVERS_START });
const localList = listServers();
if (!isEmpty(localList)) {
dispatch({ type: FETCH_SERVERS, list: localList });
return;
}
// If local list is empty, try to fetch it remotely (making sure it's an array) and calculate IDs for every server
const getDataAsArrayWithIds = pipe(
prop('data'),
(value) => {
if (!Array.isArray(value)) {
throw new Error('Value is not an array');
}
return value;
},
map(assocId),
);
const remoteList = await get(`${homepage}/servers.json`)
.then(getDataAsArrayWithIds)
.catch(() => []);
createServers(remoteList);
dispatch({ type: FETCH_SERVERS, list: remoteList.reduce((map, server) => ({ ...map, [server.id]: server }), {}) });
};
export const createServer = ({ createServer }, listServersAction) => pipe(createServer, listServersAction);
export const deleteServer = ({ deleteServer }, listServersAction) => pipe(deleteServer, listServersAction);
export const createServers = ({ createServers }, listServersAction) => pipe(
map(assocId),
createServers,
listServersAction
);

View File

@@ -0,0 +1,35 @@
import { handleActions } from 'redux-actions';
import { pipe, assoc, map, reduce, dissoc } from 'ramda';
import { v4 as uuid } from 'uuid';
/* eslint-disable padding-line-between-statements */
export const EDIT_SERVER = 'shlink/servers/EDIT_SERVER';
export const DELETE_SERVER = 'shlink/servers/DELETE_SERVER';
export const CREATE_SERVERS = 'shlink/servers/CREATE_SERVERS';
/* eslint-enable padding-line-between-statements */
const initialState = {};
const assocId = (server) => assoc('id', server.id || uuid(), server);
export default handleActions({
[CREATE_SERVERS]: (state, { newServers }) => ({ ...state, ...newServers }),
[DELETE_SERVER]: (state, { serverId }) => dissoc(serverId, state),
[EDIT_SERVER]: (state, { serverId, serverData }) => !state[serverId]
? state
: assoc(serverId, { ...state[serverId], ...serverData }, state),
}, initialState);
export const createServer = (server) => createServers([ server ]);
const serversListToMap = reduce((acc, server) => assoc(server.id, server, acc), {});
export const createServers = pipe(
map(assocId),
serversListToMap,
(newServers) => ({ type: CREATE_SERVERS, newServers })
);
export const editServer = (serverId, serverData) => ({ type: EDIT_SERVER, serverId, serverData });
export const deleteServer = ({ id }) => ({ type: DELETE_SERVER, serverId: id });

View File

@@ -25,14 +25,14 @@ const saveCsv = (window, csv) => {
};
export default class ServersExporter {
constructor(serversService, window, csvjson) {
this.serversService = serversService;
constructor(storage, window, csvjson) {
this.storage = storage;
this.window = window;
this.csvjson = csvjson;
}
exportServers = async () => {
const servers = values(this.serversService.listServers()).map(dissoc('id'));
const servers = values(this.storage.get('servers') || {}).map(dissoc('id'));
try {
const csv = this.csvjson.toCSV(servers, {

View File

@@ -1,28 +0,0 @@
import { assoc, dissoc, reduce } from 'ramda';
const SERVERS_STORAGE_KEY = 'servers';
export default class ServersService {
constructor(storage) {
this.storage = storage;
}
listServers = () => this.storage.get(SERVERS_STORAGE_KEY) || {};
findServerById = (serverId) => this.listServers()[serverId];
createServer = (server) => this.createServers([ server ]);
createServers = (servers) => {
const allServers = reduce(
(serversObj, server) => assoc(server.id, server, serversObj),
this.listServers(),
servers
);
this.storage.set(SERVERS_STORAGE_KEY, allServers);
};
deleteServer = ({ id }) =>
this.storage.set(SERVERS_STORAGE_KEY, dissoc(id, this.listServers()));
}

View File

@@ -3,21 +3,26 @@ import CreateServer from '../CreateServer';
import ServersDropdown from '../ServersDropdown';
import DeleteServerModal from '../DeleteServerModal';
import DeleteServerButton from '../DeleteServerButton';
import { EditServer } from '../EditServer';
import ImportServersBtn from '../helpers/ImportServersBtn';
import { resetSelectedServer, selectServer } from '../reducers/selectedServer';
import { createServer, createServers, deleteServer, listServers } from '../reducers/server';
import { createServer, createServers, deleteServer, editServer } from '../reducers/servers';
import { fetchServers } from '../reducers/remoteServers';
import ForServerVersion from '../helpers/ForServerVersion';
import { ServerError } from '../helpers/ServerError';
import ServersImporter from './ServersImporter';
import ServersService from './ServersService';
import ServersExporter from './ServersExporter';
const provideServices = (bottle, connect, withRouter) => {
// Components
bottle.serviceFactory('CreateServer', CreateServer, 'ImportServersBtn', 'stateFlagTimeout');
bottle.serviceFactory('CreateServer', CreateServer, 'ImportServersBtn', 'useStateFlagTimeout');
bottle.decorator('CreateServer', connect([ 'selectedServer' ], [ 'createServer', 'resetSelectedServer' ]));
bottle.serviceFactory('EditServer', EditServer, 'ServerError');
bottle.decorator('EditServer', connect([ 'selectedServer' ], [ 'editServer', 'selectServer' ]));
bottle.serviceFactory('ServersDropdown', ServersDropdown, 'ServersExporter');
bottle.decorator('ServersDropdown', withRouter);
bottle.decorator('ServersDropdown', connect([ 'servers', 'selectedServer' ], [ 'listServers', 'selectServer' ]));
bottle.decorator('ServersDropdown', connect([ 'servers', 'selectedServer' ]));
bottle.serviceFactory('DeleteServerModal', () => DeleteServerModal);
bottle.decorator('DeleteServerModal', withRouter);
@@ -28,18 +33,24 @@ const provideServices = (bottle, connect, withRouter) => {
bottle.serviceFactory('ImportServersBtn', ImportServersBtn, 'ServersImporter');
bottle.decorator('ImportServersBtn', connect(null, [ 'createServers' ]));
bottle.serviceFactory('ForServerVersion', () => ForServerVersion);
bottle.decorator('ForServerVersion', connect([ 'selectedServer' ]));
bottle.serviceFactory('ServerError', ServerError, 'DeleteServerButton');
bottle.decorator('ServerError', connect([ 'servers', 'selectedServer' ]));
// Services
bottle.constant('csvjson', csvjson);
bottle.service('ServersImporter', ServersImporter, 'csvjson');
bottle.service('ServersService', ServersService, 'Storage');
bottle.service('ServersExporter', ServersExporter, 'ServersService', 'window', 'csvjson');
bottle.service('ServersExporter', ServersExporter, 'Storage', 'window', 'csvjson');
// Actions
bottle.serviceFactory('selectServer', selectServer, 'ServersService', 'buildShlinkApiClient');
bottle.serviceFactory('createServer', createServer, 'ServersService', 'listServers');
bottle.serviceFactory('createServers', createServers, 'ServersService', 'listServers');
bottle.serviceFactory('deleteServer', deleteServer, 'ServersService', 'listServers');
bottle.serviceFactory('listServers', listServers, 'ServersService', 'axios');
bottle.serviceFactory('selectServer', selectServer, 'buildShlinkApiClient', 'loadMercureInfo');
bottle.serviceFactory('createServer', () => createServer);
bottle.serviceFactory('createServers', () => createServers);
bottle.serviceFactory('deleteServer', () => deleteServer);
bottle.serviceFactory('editServer', () => editServer);
bottle.serviceFactory('fetchServers', fetchServers, 'axios');
bottle.serviceFactory('resetSelectedServer', () => resetSelectedServer);
};

View File

@@ -0,0 +1,25 @@
import React from 'react';
import { Card, CardBody, CardHeader } from 'reactstrap';
import PropTypes from 'prop-types';
import Checkbox from '../utils/Checkbox';
import { SettingsType } from './reducers/settings';
const propTypes = {
settings: SettingsType,
setRealTimeUpdates: PropTypes.func,
};
const RealTimeUpdates = ({ settings: { realTimeUpdates }, setRealTimeUpdates }) => (
<Card>
<CardHeader>Real-time updates</CardHeader>
<CardBody>
<Checkbox checked={realTimeUpdates.enabled} onChange={setRealTimeUpdates}>
Enable or disable real-time updates, when using Shlink v2.2.0 or newer.
</Checkbox>
</CardBody>
</Card>
);
RealTimeUpdates.propTypes = propTypes;
export default RealTimeUpdates;

10
src/settings/Settings.js Normal file
View File

@@ -0,0 +1,10 @@
import React from 'react';
import NoMenuLayout from '../common/NoMenuLayout';
const Settings = (RealTimeUpdates) => () => (
<NoMenuLayout>
<RealTimeUpdates />
</NoMenuLayout>
);
export default Settings;

View File

@@ -0,0 +1,25 @@
import { handleActions } from 'redux-actions';
import PropTypes from 'prop-types';
export const SET_REAL_TIME_UPDATES = 'shlink/realTimeUpdates/SET_REAL_TIME_UPDATES';
export const SettingsType = PropTypes.shape({
realTimeUpdates: PropTypes.shape({
enabled: PropTypes.bool.isRequired,
}),
});
const initialState = {
realTimeUpdates: {
enabled: true,
},
};
export default handleActions({
[SET_REAL_TIME_UPDATES]: (state, { realTimeUpdates }) => ({ ...state, realTimeUpdates }),
}, initialState);
export const setRealTimeUpdates = (enabled) => ({
type: SET_REAL_TIME_UPDATES,
realTimeUpdates: { enabled },
});

View File

@@ -0,0 +1,16 @@
import RealTimeUpdates from '../RealTimeUpdates';
import Settings from '../Settings';
import { setRealTimeUpdates } from '../reducers/settings';
const provideServices = (bottle, connect) => {
// Components
bottle.serviceFactory('Settings', Settings, 'RealTimeUpdates');
bottle.serviceFactory('RealTimeUpdates', () => RealTimeUpdates);
bottle.decorator('RealTimeUpdates', connect([ 'settings' ], [ 'setRealTimeUpdates' ]));
// Actions
bottle.serviceFactory('setRealTimeUpdates', () => setRealTimeUpdates);
};
export default provideServices;

Some files were not shown because too many files have changed in this diff Show More