Repository: shlinkio/shlink-web-client (ts)

Issue:
Display amount of orphan visits in overview page, when using Shlink >=2.6
<!--
Before opening an issue, just take into account that this is a completely free of charge and open source project.
I'm always happy to help and provide support, but some understanding will be expected.
I do this in my own free time, so expect some delays when implementing new features and fixing bugs, and don't take it personal if an issue gets eventually closed.
You may also be asked to provide tests or ways to reproduce reported bugs.
Try to be polite, and understand it is impossible for an OSS project to cover all use cases.

With that said, please fill in the information requested next. More information might be requested once the issue is open.
-->


Pull request description:
Feature/orphan visits card
Closes #386 

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4abdf179..b9880d0d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,6 +8,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
 ### Added
 * [#379](https://github.com/shlinkio/shlink-web-client/issues/379) and [#384](https://github.com/shlinkio/shlink-web-client/issues/384) Improved QR code modal, including controls to customize size, format and margin, as well as a button to copy the link to the clipboard.
 * [#385](https://github.com/shlinkio/shlink-web-client/issues/385) Added setting to determine if "validate URL" should be enabled or disabled by default.
+* [#386](https://github.com/shlinkio/shlink-web-client/issues/386) Added new card in overview section to display amount of orphan visits when using Shlink 2.6.0 or higher.
 
 ### Changed
 * *Nothing*
diff --git a/src/api/types/index.ts b/src/api/types/index.ts
index c5728797..4ca5f7e3 100644
--- a/src/api/types/index.ts
+++ b/src/api/types/index.ts
@@ -46,6 +46,7 @@ export interface ShlinkVisits {
 
 export interface ShlinkVisitsOverview {
   visitsCount: number;
+  orphanVisitsCount?: number; // Optional only for versions older than 2.6.0
 }
 
 export interface ShlinkVisitsParams {
diff --git a/src/servers/Overview.tsx b/src/servers/Overview.tsx
index 892fe46e..71bc58d6 100644
--- a/src/servers/Overview.tsx
+++ b/src/servers/Overview.tsx
@@ -38,7 +38,7 @@ export const Overview = (
 }: OverviewConnectProps) => {
   const { loading, shortUrls } = shortUrlsList;
   const { loading: loadingTags } = tagsList;
-  const { loading: loadingVisits, visitsCount } = visitsOverview;
+  const { loading: loadingVisits, visitsCount, orphanVisitsCount } = visitsOverview;
   const serverId = isServerWithId(selectedServer) ? selectedServer.id : '';
   const history = useHistory();
 
@@ -51,7 +51,7 @@ export const Overview = (
   return (
     <>
       <div className="row mb-3">
-        <div className="col-md-6 col-lg-4">
+        <div className="col-md-6 col-xl-3">
           <Card className="overview__card mb-2" body>
             <CardTitle tag="h5" className="overview__card-title">Visits</CardTitle>
             <CardText tag="h2">
@@ -64,7 +64,20 @@ export const Overview = (
             </CardText>
           </Card>
         </div>
-        <div className="col-md-6 col-lg-4">
+        <div className="col-md-6 col-xl-3">
+          <Card className="overview__card mb-2" body>
+            <CardTitle tag="h5" className="overview__card-title">Orphan visits</CardTitle>
+            <CardText tag="h2">
+              <ForServerVersion minVersion="2.6.0">
+                {loadingVisits ? 'Loading...' : prettify(orphanVisitsCount ?? 0)}
+              </ForServerVersion>
+              <ForServerVersion maxVersion="2.5.*">
+                <small className="text-muted"><i>Shlink 2.6 is needed</i></small>
+              </ForServerVersion>
+            </CardText>
+          </Card>
+        </div>
+        <div className="col-md-6 col-xl-3">
           <Card className="overview__card mb-2" body>
             <CardTitle tag="h5" className="overview__card-title">Short URLs</CardTitle>
             <CardText tag="h2">
@@ -72,7 +85,7 @@ export const Overview = (
             </CardText>
           </Card>
         </div>
-        <div className="col-md-12 col-lg-4">
+        <div className="col-md-6 col-xl-3">
           <Card className="overview__card mb-2" body>
             <CardTitle tag="h5" className="overview__card-title">Tags</CardTitle>
             <CardText tag="h2">{loadingTags ? 'Loading...' : prettify(tagsList.tags.length)}</CardText>
diff --git a/src/visits/reducers/visitsOverview.ts b/src/visits/reducers/visitsOverview.ts
index e3f78e59..ac51abf8 100644
--- a/src/visits/reducers/visitsOverview.ts
+++ b/src/visits/reducers/visitsOverview.ts
@@ -13,6 +13,7 @@ export const GET_OVERVIEW = 'shlink/visitsOverview/GET_OVERVIEW';
 
 export interface VisitsOverview {
   visitsCount: number;
+  orphanVisitsCount?: number;
   loading: boolean;
   error: boolean;
 }
@@ -21,6 +22,7 @@ export type GetVisitsOverviewAction = ShlinkVisitsOverview & Action<string>;
 
 const initialState: VisitsOverview = {
   visitsCount: 0,
+  orphanVisitsCount: 0,
   loading: false,
   error: false,
 };
@@ -28,7 +30,7 @@ const initialState: VisitsOverview = {
 export default buildReducer<VisitsOverview, GetVisitsOverviewAction & CreateVisitsAction>({
   [GET_OVERVIEW_START]: () => ({ ...initialState, loading: true }),
   [GET_OVERVIEW_ERROR]: () => ({ ...initialState, error: true }),
-  [GET_OVERVIEW]: (_, { visitsCount }) => ({ ...initialState, visitsCount }),
+  [GET_OVERVIEW]: (_, { visitsCount, orphanVisitsCount }) => ({ ...initialState, visitsCount, orphanVisitsCount }),
   [CREATE_VISITS]: ({ visitsCount, ...rest }, { createdVisits }) => ({
     ...rest,
     visitsCount: visitsCount + createdVisits.length,


Test patch:
diff --git a/test/servers/Overview.test.tsx b/test/servers/Overview.test.tsx
index f2ea285f..7827b637 100644
--- a/test/servers/Overview.test.tsx
+++ b/test/servers/Overview.test.tsx
@@ -32,7 +32,7 @@ describe('<Overview />', () => {
         loadVisitsOverview={loadVisitsOverview}
         shortUrlsList={Mock.of<ShortUrlsListState>({ loading, shortUrls })}
         tagsList={Mock.of<TagsList>({ loading, tags: [ 'foo', 'bar', 'baz' ] })}
-        visitsOverview={Mock.of<VisitsOverview>({ loading, visitsCount: 3456 })}
+        visitsOverview={Mock.of<VisitsOverview>({ loading, visitsCount: 3456, orphanVisitsCount: 28 })}
         selectedServer={Mock.of<ReachableServer>({ id: serverId })}
         createNewVisits={jest.fn()}
         loadMercureInfo={jest.fn()}
@@ -49,7 +49,7 @@ describe('<Overview />', () => {
     const wrapper = createWrapper(true);
     const cards = wrapper.find(CardText);
 
-    expect(cards).toHaveLength(3);
+    expect(cards).toHaveLength(4);
     cards.forEach((card) => expect(card.html()).toContain('Loading...'));
   });
 
@@ -57,10 +57,11 @@ describe('<Overview />', () => {
     const wrapper = createWrapper();
     const cards = wrapper.find(CardText);
 
-    expect(cards).toHaveLength(3);
+    expect(cards).toHaveLength(4);
     expect(cards.at(0).html()).toContain(prettify(3456));
-    expect(cards.at(1).html()).toContain(prettify(83710));
-    expect(cards.at(2).html()).toContain(prettify(3));
+    expect(cards.at(1).html()).toContain(prettify(28));
+    expect(cards.at(2).html()).toContain(prettify(83710));
+    expect(cards.at(3).html()).toContain(prettify(3));
   });
 
   test('first card displays warning for old shlink versions', () => {