Skip to content

Commit

Permalink
Merge pull request #817 from acelaya-forks/feature/tags-stats
Browse files Browse the repository at this point in the history
Feature/tags stats
  • Loading branch information
acelaya committed Mar 18, 2023
2 parents ddaec7c + 96c20b3 commit 8fa61a6
Show file tree
Hide file tree
Showing 6 changed files with 46 additions and 6 deletions.
7 changes: 5 additions & 2 deletions CHANGELOG.md
Expand Up @@ -10,9 +10,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
* [#808](https://github.com/shlinkio/shlink-web-client/issues/808) Respect settings on excluding bots in the overview section, for visits cards.

### Changed
* Update to Vite 4.1
* Update to coding standard v2.1.0
* [#798](https://github.com/shlinkio/shlink-web-client/issues/798) Remove stryker and mutation testing.
* [#800](https://github.com/shlinkio/shlink-web-client/issues/800) Use `/tags/stats` endpoint to load tags stats, when the server supports it.
* Update to Vite 4.2
* Update to TypeScript 5
* Update to coding standard v2.1.0
* Decouple tests from RTK internals.

### Deprecated
* *Nothing*
Expand Down
6 changes: 6 additions & 0 deletions src/api/services/ShlinkApiClient.ts
Expand Up @@ -16,6 +16,7 @@ import type {
ShlinkShortUrlsResponse,
ShlinkTags,
ShlinkTagsResponse,
ShlinkTagsStatsResponse,
ShlinkVisits,
ShlinkVisitsOverview,
ShlinkVisitsParams,
Expand Down Expand Up @@ -90,6 +91,11 @@ export class ShlinkApiClient {
.then(({ tags }) => tags)
.then(({ data, stats }) => ({ tags: data, stats }));

public readonly tagsStats = async (): Promise<ShlinkTags> =>
this.performRequest<{ tags: ShlinkTagsStatsResponse }>('/tags/stats', 'GET')
.then(({ tags }) => tags)
.then(({ data }) => ({ tags: data.map(({ tag }) => tag), stats: data }));

public readonly deleteTags = async (tags: string[]): Promise<{ tags: string[] }> =>
this.performEmptyRequest('/tags', 'DELETE', { tags }).then(() => ({ tags }));

Expand Down
5 changes: 5 additions & 0 deletions src/api/types/index.ts
Expand Up @@ -31,9 +31,14 @@ export interface ShlinkTags {

export interface ShlinkTagsResponse {
data: string[];
/** @deprecated Present only when withStats=true is provided, which is deprecated */
stats: ShlinkTagsStats[];
}

export interface ShlinkTagsStatsResponse {
data: ShlinkTagsStats[];
}

export interface ShlinkPaginator {
currentPage: number;
pagesCount: number;
Expand Down
9 changes: 6 additions & 3 deletions src/tags/reducers/tagsList.ts
Expand Up @@ -5,6 +5,7 @@ import type { ShlinkTags } from '../../api/types';
import type { ProblemDetailsError } from '../../api/types/errors';
import { parseApiError } from '../../api/utils';
import type { createShortUrl } from '../../short-urls/reducers/shortUrlCreation';
import { supportedFeatures } from '../../utils/helpers/features';
import { createAsyncThunk } from '../../utils/helpers/redux';
import { createNewVisits } from '../../visits/reducers/visitCreation';
import type { CreateVisit, Stats } from '../../visits/types';
Expand Down Expand Up @@ -70,14 +71,16 @@ const calculateVisitsPerTag = (createdVisits: CreateVisit[]): TagIncrease[] => O
export const listTags = (buildShlinkApiClient: ShlinkApiClientBuilder, force = true) => createAsyncThunk(
`${REDUCER_PREFIX}/listTags`,
async (_: void, { getState }): Promise<ListTags> => {
const { tagsList } = getState();
const { tagsList, selectedServer } = getState();

if (!force && !isEmpty(tagsList.tags)) {
return tagsList;
}

const { listTags: shlinkListTags } = buildShlinkApiClient(getState);
const { tags, stats = [] }: ShlinkTags = await shlinkListTags();
const { listTags: shlinkListTags, tagsStats } = buildShlinkApiClient(getState);
const { tags, stats = [] }: ShlinkTags = await (
supportedFeatures.tagsStats(selectedServer) ? tagsStats() : shlinkListTags()
);
const processedStats = stats.reduce<TagsStatsMap>((acc, { tag, shortUrlsCount, visitsCount }) => {
acc[tag] = { shortUrlsCount, visitsCount };

Expand Down
3 changes: 2 additions & 1 deletion src/utils/helpers/features.ts
Expand Up @@ -14,11 +14,12 @@ export const supportedFeatures = {
defaultDomainRedirectsEdition: matchesMinVersion('2.10.0'),
nonOrphanVisits: matchesMinVersion('3.0.0'),
allTagsFiltering: matchesMinVersion('3.0.0'),
tagsStats: matchesMinVersion('3.0.0'),
domainVisits: matchesMinVersion('3.1.0'),
excludeBotsOnShortUrls: matchesMinVersion('3.4.0'),
filterDisabledUrls: matchesMinVersion('3.4.0'),
deviceLongUrls: matchesMinVersion('3.5.0'),
} as const;
} as const satisfies Record<string, ReturnType<typeof matchesMinVersion>>;

Object.freeze(supportedFeatures);

Expand Down
22 changes: 22 additions & 0 deletions test/api/services/ShlinkApiClient.test.ts
Expand Up @@ -212,6 +212,28 @@ describe('ShlinkApiClient', () => {
});
});

describe('tagsStats', () => {
it('can use /tags/stats endpoint', async () => {
const expectedTags = ['foo', 'bar'];
const expectedStats = expectedTags.map((tag) => ({ tag, shortUrlsCount: 10, visitsCount: 10 }));

fetchJson.mockResolvedValue({
tags: {
data: expectedStats,
},
});
const { tagsStats } = buildApiClient();

const result = await tagsStats();

expect({ tags: expectedTags, stats: expectedStats }).toEqual(result);
expect(fetchJson).toHaveBeenCalledWith(
expect.stringContaining('/tags/stats'),
expect.objectContaining({ method: 'GET' }),
);
});
});

describe('deleteTags', () => {
it('properly deletes provided tags', async () => {
const tags = ['foo', 'bar'];
Expand Down

0 comments on commit 8fa61a6

Please sign in to comment.