mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2024-12-23 01:20:24 +03:00
Decouple shlink-web-component from the concept of servers
This commit is contained in:
parent
5f6dc186e3
commit
21525ef945
22 changed files with 55 additions and 82 deletions
|
@ -3,9 +3,8 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import type { FC } from 'react';
|
import type { FC } from 'react';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { UncontrolledTooltip } from 'reactstrap';
|
import { UncontrolledTooltip } from 'reactstrap';
|
||||||
import type { ShlinkDomainRedirects } from '../../api/types';
|
|
||||||
import type { SelectedServer } from '../../servers/data';
|
|
||||||
import type { OptionalString } from '../../utils/utils';
|
import type { OptionalString } from '../../utils/utils';
|
||||||
|
import type { ShlinkDomainRedirects } from '../api-contract';
|
||||||
import type { Domain } from './data';
|
import type { Domain } from './data';
|
||||||
import { DomainDropdown } from './helpers/DomainDropdown';
|
import { DomainDropdown } from './helpers/DomainDropdown';
|
||||||
import { DomainStatusIcon } from './helpers/DomainStatusIcon';
|
import { DomainStatusIcon } from './helpers/DomainStatusIcon';
|
||||||
|
@ -16,7 +15,6 @@ interface DomainRowProps {
|
||||||
defaultRedirects?: ShlinkDomainRedirects;
|
defaultRedirects?: ShlinkDomainRedirects;
|
||||||
editDomainRedirects: (redirects: EditDomainRedirects) => Promise<void>;
|
editDomainRedirects: (redirects: EditDomainRedirects) => Promise<void>;
|
||||||
checkDomainHealth: (domain: string) => void;
|
checkDomainHealth: (domain: string) => void;
|
||||||
selectedServer: SelectedServer;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const Nr: FC<{ fallback: OptionalString }> = ({ fallback }) => (
|
const Nr: FC<{ fallback: OptionalString }> = ({ fallback }) => (
|
||||||
|
@ -33,7 +31,7 @@ const DefaultDomain: FC = () => (
|
||||||
);
|
);
|
||||||
|
|
||||||
export const DomainRow: FC<DomainRowProps> = (
|
export const DomainRow: FC<DomainRowProps> = (
|
||||||
{ domain, editDomainRedirects, checkDomainHealth, defaultRedirects, selectedServer },
|
{ domain, editDomainRedirects, checkDomainHealth, defaultRedirects },
|
||||||
) => {
|
) => {
|
||||||
const { domain: authority, isDefault, redirects, status } = domain;
|
const { domain: authority, isDefault, redirects, status } = domain;
|
||||||
|
|
||||||
|
@ -58,7 +56,7 @@ export const DomainRow: FC<DomainRowProps> = (
|
||||||
<DomainStatusIcon status={status} />
|
<DomainStatusIcon status={status} />
|
||||||
</td>
|
</td>
|
||||||
<td className="responsive-table__cell text-end">
|
<td className="responsive-table__cell text-end">
|
||||||
<DomainDropdown domain={domain} editDomainRedirects={editDomainRedirects} selectedServer={selectedServer} />
|
<DomainDropdown domain={domain} editDomainRedirects={editDomainRedirects} />
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import type { FC } from 'react';
|
import type { FC } from 'react';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { ShlinkApiError } from '../../api/ShlinkApiError';
|
import { ShlinkApiError } from '../../api/ShlinkApiError';
|
||||||
import type { SelectedServer } from '../../servers/data';
|
|
||||||
import { Message } from '../../utils/Message';
|
import { Message } from '../../utils/Message';
|
||||||
import { Result } from '../../utils/Result';
|
import { Result } from '../../utils/Result';
|
||||||
import { SearchField } from '../../utils/SearchField';
|
import { SearchField } from '../../utils/SearchField';
|
||||||
|
@ -16,13 +15,12 @@ interface ManageDomainsProps {
|
||||||
editDomainRedirects: (redirects: EditDomainRedirects) => Promise<void>;
|
editDomainRedirects: (redirects: EditDomainRedirects) => Promise<void>;
|
||||||
checkDomainHealth: (domain: string) => void;
|
checkDomainHealth: (domain: string) => void;
|
||||||
domainsList: DomainsList;
|
domainsList: DomainsList;
|
||||||
selectedServer: SelectedServer;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const headers = ['', 'Domain', 'Base path redirect', 'Regular 404 redirect', 'Invalid short URL redirect', '', ''];
|
const headers = ['', 'Domain', 'Base path redirect', 'Regular 404 redirect', 'Invalid short URL redirect', '', ''];
|
||||||
|
|
||||||
export const ManageDomains: FC<ManageDomainsProps> = (
|
export const ManageDomains: FC<ManageDomainsProps> = (
|
||||||
{ listDomains, domainsList, filterDomains, editDomainRedirects, checkDomainHealth, selectedServer },
|
{ listDomains, domainsList, filterDomains, editDomainRedirects, checkDomainHealth },
|
||||||
) => {
|
) => {
|
||||||
const { filteredDomains: domains, defaultRedirects, loading, error, errorData } = domainsList;
|
const { filteredDomains: domains, defaultRedirects, loading, error, errorData } = domainsList;
|
||||||
const resolvedDefaultRedirects = defaultRedirects ?? domains.find(({ isDefault }) => isDefault)?.redirects;
|
const resolvedDefaultRedirects = defaultRedirects ?? domains.find(({ isDefault }) => isDefault)?.redirects;
|
||||||
|
@ -59,7 +57,6 @@ export const ManageDomains: FC<ManageDomainsProps> = (
|
||||||
editDomainRedirects={editDomainRedirects}
|
editDomainRedirects={editDomainRedirects}
|
||||||
checkDomainHealth={checkDomainHealth}
|
checkDomainHealth={checkDomainHealth}
|
||||||
defaultRedirects={resolvedDefaultRedirects}
|
defaultRedirects={resolvedDefaultRedirects}
|
||||||
selectedServer={selectedServer}
|
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import type { ShlinkDomain } from '../../../api/types';
|
import type { ShlinkDomain } from '../../api-contract';
|
||||||
|
|
||||||
export type DomainStatus = 'validating' | 'valid' | 'invalid';
|
export type DomainStatus = 'validating' | 'valid' | 'invalid';
|
||||||
|
|
||||||
|
|
|
@ -3,11 +3,10 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import type { FC } from 'react';
|
import type { FC } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { DropdownItem } from 'reactstrap';
|
import { DropdownItem } from 'reactstrap';
|
||||||
import type { SelectedServer } from '../../../servers/data';
|
|
||||||
import { getServerId } from '../../../servers/data';
|
|
||||||
import { useToggle } from '../../../utils/helpers/hooks';
|
import { useToggle } from '../../../utils/helpers/hooks';
|
||||||
import { RowDropdownBtn } from '../../../utils/RowDropdownBtn';
|
import { RowDropdownBtn } from '../../../utils/RowDropdownBtn';
|
||||||
import { useFeature } from '../../utils/features';
|
import { useFeature } from '../../utils/features';
|
||||||
|
import { useRoutesPrefix } from '../../utils/routesPrefix';
|
||||||
import { DEFAULT_DOMAIN } from '../../visits/reducers/domainVisits';
|
import { DEFAULT_DOMAIN } from '../../visits/reducers/domainVisits';
|
||||||
import type { Domain } from '../data';
|
import type { Domain } from '../data';
|
||||||
import type { EditDomainRedirects } from '../reducers/domainRedirects';
|
import type { EditDomainRedirects } from '../reducers/domainRedirects';
|
||||||
|
@ -16,22 +15,21 @@ import { EditDomainRedirectsModal } from './EditDomainRedirectsModal';
|
||||||
interface DomainDropdownProps {
|
interface DomainDropdownProps {
|
||||||
domain: Domain;
|
domain: Domain;
|
||||||
editDomainRedirects: (redirects: EditDomainRedirects) => Promise<void>;
|
editDomainRedirects: (redirects: EditDomainRedirects) => Promise<void>;
|
||||||
selectedServer: SelectedServer;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DomainDropdown: FC<DomainDropdownProps> = ({ domain, editDomainRedirects, selectedServer }) => {
|
export const DomainDropdown: FC<DomainDropdownProps> = ({ domain, editDomainRedirects }) => {
|
||||||
const [isModalOpen, toggleModal] = useToggle();
|
const [isModalOpen, toggleModal] = useToggle();
|
||||||
const { isDefault } = domain;
|
const { isDefault } = domain;
|
||||||
const canBeEdited = !isDefault || useFeature('defaultDomainRedirectsEdition');
|
const canBeEdited = !isDefault || useFeature('defaultDomainRedirectsEdition');
|
||||||
const withVisits = useFeature('domainVisits');
|
const withVisits = useFeature('domainVisits');
|
||||||
const serverId = getServerId(selectedServer);
|
const routesPrefix = useRoutesPrefix();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RowDropdownBtn>
|
<RowDropdownBtn>
|
||||||
{withVisits && (
|
{withVisits && (
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
tag={Link}
|
tag={Link}
|
||||||
to={`/server/${serverId}/domain/${domain.domain}${domain.isDefault ? `_${DEFAULT_DOMAIN}` : ''}/visits`}
|
to={`${routesPrefix}/domain/${domain.domain}${domain.isDefault ? `_${DEFAULT_DOMAIN}` : ''}/visits`}
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={pieChartIcon} fixedWidth /> Visit stats
|
<FontAwesomeIcon icon={pieChartIcon} fixedWidth /> Visit stats
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
|
|
|
@ -13,7 +13,7 @@ export const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
|
||||||
|
|
||||||
bottle.serviceFactory('ManageDomains', () => ManageDomains);
|
bottle.serviceFactory('ManageDomains', () => ManageDomains);
|
||||||
bottle.decorator('ManageDomains', connect(
|
bottle.decorator('ManageDomains', connect(
|
||||||
['domainsList', 'selectedServer'],
|
['domainsList'],
|
||||||
['listDomains', 'filterDomains', 'editDomainRedirects', 'checkDomainHealth'],
|
['listDomains', 'filterDomains', 'editDomainRedirects', 'checkDomainHealth'],
|
||||||
));
|
));
|
||||||
|
|
||||||
|
|
|
@ -2,12 +2,8 @@ import type { FC } from 'react';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { Link, useNavigate } from 'react-router-dom';
|
import { Link, useNavigate } from 'react-router-dom';
|
||||||
import { Card, CardBody, CardHeader, Row } from 'reactstrap';
|
import { Card, CardBody, CardHeader, Row } from 'reactstrap';
|
||||||
import type { ShlinkShortUrlsListParams } from '../../api/types';
|
|
||||||
import type { SelectedServer } from '../../servers/data';
|
|
||||||
import { getServerId } from '../../servers/data';
|
|
||||||
import { HighlightCard } from '../../servers/helpers/HighlightCard';
|
|
||||||
import { VisitsHighlightCard } from '../../servers/helpers/VisitsHighlightCard';
|
|
||||||
import { prettify } from '../../utils/helpers/numbers';
|
import { prettify } from '../../utils/helpers/numbers';
|
||||||
|
import type { ShlinkShortUrlsListParams } from '../api-contract';
|
||||||
import { boundToMercureHub } from '../mercure/helpers/boundToMercureHub';
|
import { boundToMercureHub } from '../mercure/helpers/boundToMercureHub';
|
||||||
import { Topics } from '../mercure/helpers/Topics';
|
import { Topics } from '../mercure/helpers/Topics';
|
||||||
import type { CreateShortUrlProps } from '../short-urls/CreateShortUrl';
|
import type { CreateShortUrlProps } from '../short-urls/CreateShortUrl';
|
||||||
|
@ -16,15 +12,17 @@ import { ITEMS_IN_OVERVIEW_PAGE } from '../short-urls/reducers/shortUrlsList';
|
||||||
import type { ShortUrlsTableType } from '../short-urls/ShortUrlsTable';
|
import type { ShortUrlsTableType } from '../short-urls/ShortUrlsTable';
|
||||||
import type { TagsList } from '../tags/reducers/tagsList';
|
import type { TagsList } from '../tags/reducers/tagsList';
|
||||||
import { useFeature } from '../utils/features';
|
import { useFeature } from '../utils/features';
|
||||||
|
import { useRoutesPrefix } from '../utils/routesPrefix';
|
||||||
import { useSetting } from '../utils/settings';
|
import { useSetting } from '../utils/settings';
|
||||||
import type { VisitsOverview } from '../visits/reducers/visitsOverview';
|
import type { VisitsOverview } from '../visits/reducers/visitsOverview';
|
||||||
|
import { HighlightCard } from './helpers/HighlightCard';
|
||||||
|
import { VisitsHighlightCard } from './helpers/VisitsHighlightCard';
|
||||||
|
|
||||||
interface OverviewConnectProps {
|
interface OverviewConnectProps {
|
||||||
shortUrlsList: ShortUrlsListState;
|
shortUrlsList: ShortUrlsListState;
|
||||||
listShortUrls: (params: ShlinkShortUrlsListParams) => void;
|
listShortUrls: (params: ShlinkShortUrlsListParams) => void;
|
||||||
listTags: Function;
|
listTags: Function;
|
||||||
tagsList: TagsList;
|
tagsList: TagsList;
|
||||||
selectedServer: SelectedServer;
|
|
||||||
visitsOverview: VisitsOverview;
|
visitsOverview: VisitsOverview;
|
||||||
loadVisitsOverview: Function;
|
loadVisitsOverview: Function;
|
||||||
}
|
}
|
||||||
|
@ -37,14 +35,13 @@ export const Overview = (
|
||||||
listShortUrls,
|
listShortUrls,
|
||||||
listTags,
|
listTags,
|
||||||
tagsList,
|
tagsList,
|
||||||
selectedServer,
|
|
||||||
loadVisitsOverview,
|
loadVisitsOverview,
|
||||||
visitsOverview,
|
visitsOverview,
|
||||||
}: OverviewConnectProps) => {
|
}: OverviewConnectProps) => {
|
||||||
const { loading, shortUrls } = shortUrlsList;
|
const { loading, shortUrls } = shortUrlsList;
|
||||||
const { loading: loadingTags } = tagsList;
|
const { loading: loadingTags } = tagsList;
|
||||||
const { loading: loadingVisits, nonOrphanVisits, orphanVisits } = visitsOverview;
|
const { loading: loadingVisits, nonOrphanVisits, orphanVisits } = visitsOverview;
|
||||||
const serverId = getServerId(selectedServer);
|
const routesPrefix = useRoutesPrefix();
|
||||||
const linkToNonOrphanVisits = useFeature('nonOrphanVisits');
|
const linkToNonOrphanVisits = useFeature('nonOrphanVisits');
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const visits = useSetting('visits');
|
const visits = useSetting('visits');
|
||||||
|
@ -61,7 +58,7 @@ export const Overview = (
|
||||||
<div className="col-lg-6 col-xl-3 mb-3">
|
<div className="col-lg-6 col-xl-3 mb-3">
|
||||||
<VisitsHighlightCard
|
<VisitsHighlightCard
|
||||||
title="Visits"
|
title="Visits"
|
||||||
link={linkToNonOrphanVisits ? `/server/${serverId}/non-orphan-visits` : undefined}
|
link={linkToNonOrphanVisits ? `${routesPrefix}/non-orphan-visits` : undefined}
|
||||||
excludeBots={visits?.excludeBots ?? false}
|
excludeBots={visits?.excludeBots ?? false}
|
||||||
loading={loadingVisits}
|
loading={loadingVisits}
|
||||||
visitsSummary={nonOrphanVisits}
|
visitsSummary={nonOrphanVisits}
|
||||||
|
@ -70,19 +67,19 @@ export const Overview = (
|
||||||
<div className="col-lg-6 col-xl-3 mb-3">
|
<div className="col-lg-6 col-xl-3 mb-3">
|
||||||
<VisitsHighlightCard
|
<VisitsHighlightCard
|
||||||
title="Orphan visits"
|
title="Orphan visits"
|
||||||
link={`/server/${serverId}/orphan-visits`}
|
link={`${routesPrefix}/orphan-visits`}
|
||||||
excludeBots={visits?.excludeBots ?? false}
|
excludeBots={visits?.excludeBots ?? false}
|
||||||
loading={loadingVisits}
|
loading={loadingVisits}
|
||||||
visitsSummary={orphanVisits}
|
visitsSummary={orphanVisits}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-lg-6 col-xl-3 mb-3">
|
<div className="col-lg-6 col-xl-3 mb-3">
|
||||||
<HighlightCard title="Short URLs" link={`/server/${serverId}/list-short-urls/1`}>
|
<HighlightCard title="Short URLs" link={`${routesPrefix}/list-short-urls/1`}>
|
||||||
{loading ? 'Loading...' : prettify(shortUrls?.pagination.totalItems ?? 0)}
|
{loading ? 'Loading...' : prettify(shortUrls?.pagination.totalItems ?? 0)}
|
||||||
</HighlightCard>
|
</HighlightCard>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-lg-6 col-xl-3 mb-3">
|
<div className="col-lg-6 col-xl-3 mb-3">
|
||||||
<HighlightCard title="Tags" link={`/server/${serverId}/manage-tags`}>
|
<HighlightCard title="Tags" link={`${routesPrefix}/manage-tags`}>
|
||||||
{loadingTags ? 'Loading...' : prettify(tagsList.tags.length)}
|
{loadingTags ? 'Loading...' : prettify(tagsList.tags.length)}
|
||||||
</HighlightCard>
|
</HighlightCard>
|
||||||
</div>
|
</div>
|
||||||
|
@ -92,7 +89,7 @@ export const Overview = (
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<span className="d-sm-none">Create a short URL</span>
|
<span className="d-sm-none">Create a short URL</span>
|
||||||
<h5 className="d-none d-sm-inline">Create a short URL</h5>
|
<h5 className="d-none d-sm-inline">Create a short URL</h5>
|
||||||
<Link className="float-end" to={`/server/${serverId}/create-short-url`}>Advanced options »</Link>
|
<Link className="float-end" to={`${routesPrefix}/create-short-url`}>Advanced options »</Link>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardBody>
|
<CardBody>
|
||||||
<CreateShortUrl basicMode />
|
<CreateShortUrl basicMode />
|
||||||
|
@ -102,13 +99,13 @@ export const Overview = (
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<span className="d-sm-none">Recently created URLs</span>
|
<span className="d-sm-none">Recently created URLs</span>
|
||||||
<h5 className="d-none d-sm-inline">Recently created URLs</h5>
|
<h5 className="d-none d-sm-inline">Recently created URLs</h5>
|
||||||
<Link className="float-end" to={`/server/${serverId}/list-short-urls/1`}>See all »</Link>
|
<Link className="float-end" to={`${routesPrefix}/list-short-urls/1`}>See all »</Link>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardBody>
|
<CardBody>
|
||||||
<ShortUrlsTable
|
<ShortUrlsTable
|
||||||
shortUrlsList={shortUrlsList}
|
shortUrlsList={shortUrlsList}
|
||||||
className="mb-0"
|
className="mb-0"
|
||||||
onTagClick={(tag) => navigate(`/server/${serverId}/list-short-urls/1?tags=${encodeURIComponent(tag)}`)}
|
onTagClick={(tag) => navigate(`${routesPrefix}/list-short-urls/1?tags=${encodeURIComponent(tag)}`)}
|
||||||
/>
|
/>
|
||||||
</CardBody>
|
</CardBody>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
@import '../../utils/base';
|
@import '../../../utils/base';
|
||||||
|
|
||||||
.highlight-card.highlight-card {
|
.highlight-card.highlight-card {
|
||||||
text-align: center;
|
text-align: center;
|
|
@ -3,7 +3,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import type { FC, PropsWithChildren, ReactNode } from 'react';
|
import type { FC, PropsWithChildren, ReactNode } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { Card, CardText, CardTitle, UncontrolledTooltip } from 'reactstrap';
|
import { Card, CardText, CardTitle, UncontrolledTooltip } from 'reactstrap';
|
||||||
import { useElementRef } from '../../utils/helpers/hooks';
|
import { useElementRef } from '../../../utils/helpers/hooks';
|
||||||
import './HighlightCard.scss';
|
import './HighlightCard.scss';
|
||||||
|
|
||||||
export type HighlightCardProps = PropsWithChildren<{
|
export type HighlightCardProps = PropsWithChildren<{
|
|
@ -1,6 +1,6 @@
|
||||||
import type { FC } from 'react';
|
import type { FC } from 'react';
|
||||||
import type { PartialVisitsSummary } from '../../shlink-web-component/visits/reducers/visitsOverview';
|
import { prettify } from '../../../utils/helpers/numbers';
|
||||||
import { prettify } from '../../utils/helpers/numbers';
|
import type { PartialVisitsSummary } from '../../visits/reducers/visitsOverview';
|
||||||
import type { HighlightCardProps } from './HighlightCard';
|
import type { HighlightCardProps } from './HighlightCard';
|
||||||
import { HighlightCard } from './HighlightCard';
|
import { HighlightCard } from './HighlightCard';
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { Overview } from '../Overview';
|
||||||
export function provideServices(bottle: Bottle, connect: ConnectDecorator) {
|
export function provideServices(bottle: Bottle, connect: ConnectDecorator) {
|
||||||
bottle.serviceFactory('Overview', Overview, 'ShortUrlsTable', 'CreateShortUrl');
|
bottle.serviceFactory('Overview', Overview, 'ShortUrlsTable', 'CreateShortUrl');
|
||||||
bottle.decorator('Overview', connect(
|
bottle.decorator('Overview', connect(
|
||||||
['shortUrlsList', 'tagsList', 'selectedServer', 'mercureInfo', 'visitsOverview'],
|
['shortUrlsList', 'tagsList', 'mercureInfo', 'visitsOverview'],
|
||||||
['listShortUrls', 'listTags', 'createNewVisits', 'loadMercureInfo', 'loadVisitsOverview'],
|
['listShortUrls', 'listTags', 'createNewVisits', 'loadMercureInfo', 'loadVisitsOverview'],
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
import type { FC } from 'react';
|
import type { FC } from 'react';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import type { ReportExporter } from '../../../common/services/ReportExporter';
|
import type { ReportExporter } from '../../../common/services/ReportExporter';
|
||||||
import type { SelectedServer } from '../../../servers/data';
|
|
||||||
import { isServerWithId } from '../../../servers/data';
|
|
||||||
import { ExportBtn } from '../../../utils/ExportBtn';
|
import { ExportBtn } from '../../../utils/ExportBtn';
|
||||||
import { useToggle } from '../../../utils/helpers/hooks';
|
import { useToggle } from '../../../utils/helpers/hooks';
|
||||||
import type { ShlinkApiClient } from '../../api-contract';
|
import type { ShlinkApiClient } from '../../api-contract';
|
||||||
|
@ -13,23 +11,15 @@ export interface ExportShortUrlsBtnProps {
|
||||||
amount?: number;
|
amount?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ExportShortUrlsBtnConnectProps extends ExportShortUrlsBtnProps {
|
|
||||||
selectedServer: SelectedServer;
|
|
||||||
}
|
|
||||||
|
|
||||||
const itemsPerPage = 20;
|
const itemsPerPage = 20;
|
||||||
|
|
||||||
export const ExportShortUrlsBtn = (
|
export const ExportShortUrlsBtn = (
|
||||||
apiClient: ShlinkApiClient,
|
apiClient: ShlinkApiClient,
|
||||||
{ exportShortUrls }: ReportExporter,
|
{ exportShortUrls }: ReportExporter,
|
||||||
): FC<ExportShortUrlsBtnConnectProps> => ({ amount = 0, selectedServer }) => {
|
): FC<ExportShortUrlsBtnProps> => ({ amount = 0 }) => {
|
||||||
const [{ tags, search, startDate, endDate, orderBy, tagsMode }] = useShortUrlsQuery();
|
const [{ tags, search, startDate, endDate, orderBy, tagsMode }] = useShortUrlsQuery();
|
||||||
const [loading,, startLoading, stopLoading] = useToggle();
|
const [loading,, startLoading, stopLoading] = useToggle();
|
||||||
const exportAllUrls = useCallback(async () => {
|
const exportAllUrls = useCallback(async () => {
|
||||||
if (!isServerWithId(selectedServer)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const totalPages = amount / itemsPerPage;
|
const totalPages = amount / itemsPerPage;
|
||||||
const loadAllUrls = async (page = 1): Promise<ShortUrl[]> => {
|
const loadAllUrls = async (page = 1): Promise<ShortUrl[]> => {
|
||||||
const { data } = await apiClient.listShortUrls(
|
const { data } = await apiClient.listShortUrls(
|
||||||
|
@ -63,7 +53,7 @@ export const ExportShortUrlsBtn = (
|
||||||
};
|
};
|
||||||
}));
|
}));
|
||||||
stopLoading();
|
stopLoading();
|
||||||
}, [selectedServer]);
|
}, []);
|
||||||
|
|
||||||
return <ExportBtn loading={loading} className="btn-md-block" amount={amount} onClick={exportAllUrls} />;
|
return <ExportBtn loading={loading} className="btn-md-block" amount={amount} onClick={exportAllUrls} />;
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
import { isEmpty, pipe } from 'ramda';
|
import { isEmpty, pipe } from 'ramda';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { useLocation, useNavigate, useParams } from 'react-router-dom';
|
import { useLocation, useNavigate } from 'react-router-dom';
|
||||||
import type { TagsFilteringMode } from '../../../api/types';
|
|
||||||
import { orderToString, stringToOrder } from '../../../utils/helpers/ordering';
|
import { orderToString, stringToOrder } from '../../../utils/helpers/ordering';
|
||||||
import { parseQuery, stringifyQuery } from '../../../utils/helpers/query';
|
import { parseQuery, stringifyQuery } from '../../../utils/helpers/query';
|
||||||
import type { BooleanString } from '../../../utils/utils';
|
import type { BooleanString } from '../../../utils/utils';
|
||||||
import { parseOptionalBooleanToString } from '../../../utils/utils';
|
import { parseOptionalBooleanToString } from '../../../utils/utils';
|
||||||
|
import type { TagsFilteringMode } from '../../api-contract';
|
||||||
|
import { useRoutesPrefix } from '../../utils/routesPrefix';
|
||||||
import type { ShortUrlsOrder, ShortUrlsOrderableFields } from '../data';
|
import type { ShortUrlsOrder, ShortUrlsOrderableFields } from '../data';
|
||||||
|
|
||||||
interface ShortUrlsQueryCommon {
|
interface ShortUrlsQueryCommon {
|
||||||
|
@ -36,7 +37,7 @@ type ToFirstPage = (extra: Partial<ShortUrlsFiltering>) => void;
|
||||||
export const useShortUrlsQuery = (): [ShortUrlsFiltering, ToFirstPage] => {
|
export const useShortUrlsQuery = (): [ShortUrlsFiltering, ToFirstPage] => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { search } = useLocation();
|
const { search } = useLocation();
|
||||||
const { serverId = '' } = useParams<{ serverId: string }>();
|
const routesPrefix = useRoutesPrefix();
|
||||||
|
|
||||||
const filtering = useMemo(
|
const filtering = useMemo(
|
||||||
pipe(
|
pipe(
|
||||||
|
@ -70,7 +71,7 @@ export const useShortUrlsQuery = (): [ShortUrlsFiltering, ToFirstPage] => {
|
||||||
const stringifiedQuery = stringifyQuery(query);
|
const stringifiedQuery = stringifyQuery(query);
|
||||||
const queryString = isEmpty(stringifiedQuery) ? '' : `?${stringifiedQuery}`;
|
const queryString = isEmpty(stringifiedQuery) ? '' : `?${stringifiedQuery}`;
|
||||||
|
|
||||||
navigate(`/server/${serverId}/list-short-urls/1${queryString}`);
|
navigate(`${routesPrefix}/list-short-urls/1${queryString}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
return [filtering, toFirstPageWithExtra];
|
return [filtering, toFirstPageWithExtra];
|
||||||
|
|
|
@ -55,7 +55,6 @@ export const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
|
||||||
bottle.serviceFactory('ShortUrlsFilteringBar', ShortUrlsFilteringBar, 'ExportShortUrlsBtn', 'TagsSelector');
|
bottle.serviceFactory('ShortUrlsFilteringBar', ShortUrlsFilteringBar, 'ExportShortUrlsBtn', 'TagsSelector');
|
||||||
|
|
||||||
bottle.serviceFactory('ExportShortUrlsBtn', ExportShortUrlsBtn, 'apiClient', 'ReportExporter');
|
bottle.serviceFactory('ExportShortUrlsBtn', ExportShortUrlsBtn, 'apiClient', 'ReportExporter');
|
||||||
bottle.decorator('ExportShortUrlsBtn', connect(['selectedServer']));
|
|
||||||
|
|
||||||
// Reducers
|
// Reducers
|
||||||
bottle.serviceFactory(
|
bottle.serviceFactory(
|
||||||
|
|
|
@ -3,7 +3,6 @@ import type { FC } from 'react';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { Row } from 'reactstrap';
|
import { Row } from 'reactstrap';
|
||||||
import { ShlinkApiError } from '../../api/ShlinkApiError';
|
import { ShlinkApiError } from '../../api/ShlinkApiError';
|
||||||
import type { SelectedServer } from '../../servers/data';
|
|
||||||
import { determineOrderDir, sortList } from '../../utils/helpers/ordering';
|
import { determineOrderDir, sortList } from '../../utils/helpers/ordering';
|
||||||
import { Message } from '../../utils/Message';
|
import { Message } from '../../utils/Message';
|
||||||
import { OrderingDropdown } from '../../utils/OrderingDropdown';
|
import { OrderingDropdown } from '../../utils/OrderingDropdown';
|
||||||
|
@ -22,11 +21,10 @@ export interface TagsListProps {
|
||||||
filterTags: (searchTerm: string) => void;
|
filterTags: (searchTerm: string) => void;
|
||||||
forceListTags: Function;
|
forceListTags: Function;
|
||||||
tagsList: TagsListState;
|
tagsList: TagsListState;
|
||||||
selectedServer: SelectedServer;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TagsList = (TagsTable: FC<TagsTableProps>) => boundToMercureHub((
|
export const TagsList = (TagsTable: FC<TagsTableProps>) => boundToMercureHub((
|
||||||
{ filterTags, forceListTags, tagsList, selectedServer }: TagsListProps,
|
{ filterTags, forceListTags, tagsList }: TagsListProps,
|
||||||
) => {
|
) => {
|
||||||
const settings = useSettings();
|
const settings = useSettings();
|
||||||
const [order, setOrder] = useState<TagsOrder>(settings.tags?.defaultOrdering ?? {});
|
const [order, setOrder] = useState<TagsOrder>(settings.tags?.defaultOrdering ?? {});
|
||||||
|
@ -78,7 +76,6 @@ export const TagsList = (TagsTable: FC<TagsTableProps>) => boundToMercureHub((
|
||||||
return (
|
return (
|
||||||
<TagsTable
|
<TagsTable
|
||||||
sortedTags={sortedTags}
|
sortedTags={sortedTags}
|
||||||
selectedServer={selectedServer}
|
|
||||||
currentOrder={order}
|
currentOrder={order}
|
||||||
orderByColumn={orderByColumn}
|
orderByColumn={orderByColumn}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -19,7 +19,7 @@ export interface TagsTableProps extends TagsListChildrenProps {
|
||||||
const TAGS_PER_PAGE = 20; // TODO Allow customizing this value in settings
|
const TAGS_PER_PAGE = 20; // TODO Allow customizing this value in settings
|
||||||
|
|
||||||
export const TagsTable = (TagsTableRow: FC<TagsTableRowProps>) => (
|
export const TagsTable = (TagsTableRow: FC<TagsTableRowProps>) => (
|
||||||
{ sortedTags, selectedServer, orderByColumn, currentOrder }: TagsTableProps,
|
{ sortedTags, orderByColumn, currentOrder }: TagsTableProps,
|
||||||
) => {
|
) => {
|
||||||
const isFirstLoad = useRef(true);
|
const isFirstLoad = useRef(true);
|
||||||
const { search } = useLocation();
|
const { search } = useLocation();
|
||||||
|
@ -57,7 +57,7 @@ export const TagsTable = (TagsTableRow: FC<TagsTableRowProps>) => (
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{currentPage.length === 0 && <tr><td colSpan={4} className="text-center">No results found</td></tr>}
|
{currentPage.length === 0 && <tr><td colSpan={4} className="text-center">No results found</td></tr>}
|
||||||
{currentPage.map((tag) => <TagsTableRow key={tag.tag} tag={tag} selectedServer={selectedServer} />)}
|
{currentPage.map((tag) => <TagsTableRow key={tag.tag} tag={tag} />)}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
|
|
@ -3,28 +3,26 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import type { FC } from 'react';
|
import type { FC } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { DropdownItem } from 'reactstrap';
|
import { DropdownItem } from 'reactstrap';
|
||||||
import type { SelectedServer } from '../../servers/data';
|
|
||||||
import { getServerId } from '../../servers/data';
|
|
||||||
import { useToggle } from '../../utils/helpers/hooks';
|
import { useToggle } from '../../utils/helpers/hooks';
|
||||||
import { prettify } from '../../utils/helpers/numbers';
|
import { prettify } from '../../utils/helpers/numbers';
|
||||||
import { RowDropdownBtn } from '../../utils/RowDropdownBtn';
|
import { RowDropdownBtn } from '../../utils/RowDropdownBtn';
|
||||||
import type { ColorGenerator } from '../../utils/services/ColorGenerator';
|
import type { ColorGenerator } from '../../utils/services/ColorGenerator';
|
||||||
|
import { useRoutesPrefix } from '../utils/routesPrefix';
|
||||||
import type { SimplifiedTag, TagModalProps } from './data';
|
import type { SimplifiedTag, TagModalProps } from './data';
|
||||||
import { TagBullet } from './helpers/TagBullet';
|
import { TagBullet } from './helpers/TagBullet';
|
||||||
|
|
||||||
export interface TagsTableRowProps {
|
export interface TagsTableRowProps {
|
||||||
tag: SimplifiedTag;
|
tag: SimplifiedTag;
|
||||||
selectedServer: SelectedServer;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TagsTableRow = (
|
export const TagsTableRow = (
|
||||||
DeleteTagConfirmModal: FC<TagModalProps>,
|
DeleteTagConfirmModal: FC<TagModalProps>,
|
||||||
EditTagModal: FC<TagModalProps>,
|
EditTagModal: FC<TagModalProps>,
|
||||||
colorGenerator: ColorGenerator,
|
colorGenerator: ColorGenerator,
|
||||||
) => ({ tag, selectedServer }: TagsTableRowProps) => {
|
) => ({ tag }: TagsTableRowProps) => {
|
||||||
const [isDeleteModalOpen, toggleDelete] = useToggle();
|
const [isDeleteModalOpen, toggleDelete] = useToggle();
|
||||||
const [isEditModalOpen, toggleEdit] = useToggle();
|
const [isEditModalOpen, toggleEdit] = useToggle();
|
||||||
const serverId = getServerId(selectedServer);
|
const routesPrefix = useRoutesPrefix();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<tr className="responsive-table__row">
|
<tr className="responsive-table__row">
|
||||||
|
@ -32,12 +30,12 @@ export const TagsTableRow = (
|
||||||
<TagBullet tag={tag.tag} colorGenerator={colorGenerator} /> {tag.tag}
|
<TagBullet tag={tag.tag} colorGenerator={colorGenerator} /> {tag.tag}
|
||||||
</th>
|
</th>
|
||||||
<td className="responsive-table__cell text-lg-end" data-th="Short URLs">
|
<td className="responsive-table__cell text-lg-end" data-th="Short URLs">
|
||||||
<Link to={`/server/${serverId}/list-short-urls/1?tags=${encodeURIComponent(tag.tag)}`}>
|
<Link to={`${routesPrefix}/list-short-urls/1?tags=${encodeURIComponent(tag.tag)}`}>
|
||||||
{prettify(tag.shortUrls)}
|
{prettify(tag.shortUrls)}
|
||||||
</Link>
|
</Link>
|
||||||
</td>
|
</td>
|
||||||
<td className="responsive-table__cell text-lg-end" data-th="Visits">
|
<td className="responsive-table__cell text-lg-end" data-th="Visits">
|
||||||
<Link to={`/server/${serverId}/tag/${tag.tag}/visits`}>
|
<Link to={`${routesPrefix}/tag/${tag.tag}/visits`}>
|
||||||
{prettify(tag.visits)}
|
{prettify(tag.visits)}
|
||||||
</Link>
|
</Link>
|
||||||
</td>
|
</td>
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import type { SelectedServer } from '../../../servers/data';
|
|
||||||
import type { Order } from '../../../utils/helpers/ordering';
|
import type { Order } from '../../../utils/helpers/ordering';
|
||||||
import type { SimplifiedTag } from './index';
|
import type { SimplifiedTag } from './index';
|
||||||
|
|
||||||
|
@ -14,5 +13,4 @@ export type TagsOrder = Order<TagsOrderableFields>;
|
||||||
|
|
||||||
export interface TagsListChildrenProps {
|
export interface TagsListChildrenProps {
|
||||||
sortedTags: SimplifiedTag[];
|
sortedTags: SimplifiedTag[];
|
||||||
selectedServer: SelectedServer;
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,9 @@
|
||||||
import { createAction, createSlice } from '@reduxjs/toolkit';
|
import { createAction, createSlice } from '@reduxjs/toolkit';
|
||||||
import { isEmpty, reject } from 'ramda';
|
import { isEmpty, reject } from 'ramda';
|
||||||
import { isReachableServer } from '../../../servers/data';
|
|
||||||
import { createAsyncThunk } from '../../../utils/helpers/redux';
|
import { createAsyncThunk } from '../../../utils/helpers/redux';
|
||||||
import type { ProblemDetailsError, ShlinkApiClient, ShlinkTags } from '../../api-contract';
|
import type { ProblemDetailsError, ShlinkApiClient, ShlinkTags } from '../../api-contract';
|
||||||
import { parseApiError } from '../../api-contract/utils';
|
import { parseApiError } from '../../api-contract/utils';
|
||||||
import type { createShortUrl } from '../../short-urls/reducers/shortUrlCreation';
|
import type { createShortUrl } from '../../short-urls/reducers/shortUrlCreation';
|
||||||
import { isFeatureEnabledForVersion } from '../../utils/features';
|
|
||||||
import { createNewVisits } from '../../visits/reducers/visitCreation';
|
import { createNewVisits } from '../../visits/reducers/visitCreation';
|
||||||
import type { CreateVisit } from '../../visits/types';
|
import type { CreateVisit } from '../../visits/types';
|
||||||
import type { TagStats } from '../data';
|
import type { TagStats } from '../data';
|
||||||
|
@ -85,17 +83,13 @@ const calculateVisitsPerTag = (createdVisits: CreateVisit[]): TagIncrease[] => O
|
||||||
export const listTags = (apiClient: ShlinkApiClient, force = true) => createAsyncThunk(
|
export const listTags = (apiClient: ShlinkApiClient, force = true) => createAsyncThunk(
|
||||||
`${REDUCER_PREFIX}/listTags`,
|
`${REDUCER_PREFIX}/listTags`,
|
||||||
async (_: void, { getState }): Promise<ListTags> => {
|
async (_: void, { getState }): Promise<ListTags> => {
|
||||||
const { tagsList, selectedServer } = getState();
|
const { tagsList } = getState();
|
||||||
|
|
||||||
if (!force && !isEmpty(tagsList.tags)) {
|
if (!force && !isEmpty(tagsList.tags)) {
|
||||||
return tagsList;
|
return tagsList;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { tags, stats }: ShlinkTags = await (
|
const { tags, stats }: ShlinkTags = await apiClient.tagsStats();
|
||||||
isReachableServer(selectedServer) && isFeatureEnabledForVersion('tagsStats', selectedServer.version)
|
|
||||||
? apiClient.tagsStats()
|
|
||||||
: apiClient.listTags()
|
|
||||||
);
|
|
||||||
const processedStats = stats.reduce<TagsStatsMap>((acc, { tag, ...rest }) => {
|
const processedStats = stats.reduce<TagsStatsMap>((acc, { tag, ...rest }) => {
|
||||||
acc[tag] = rest;
|
acc[tag] = rest;
|
||||||
return acc;
|
return acc;
|
||||||
|
|
|
@ -28,7 +28,7 @@ export const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
|
||||||
|
|
||||||
bottle.serviceFactory('TagsList', TagsList, 'TagsTable');
|
bottle.serviceFactory('TagsList', TagsList, 'TagsTable');
|
||||||
bottle.decorator('TagsList', connect(
|
bottle.decorator('TagsList', connect(
|
||||||
['tagsList', 'selectedServer', 'mercureInfo'],
|
['tagsList', 'mercureInfo'],
|
||||||
['forceListTags', 'filterTags', 'createNewVisits', 'loadMercureInfo'],
|
['forceListTags', 'filterTags', 'createNewVisits', 'loadMercureInfo'],
|
||||||
));
|
));
|
||||||
|
|
||||||
|
|
|
@ -3,12 +3,15 @@ import type { SemVer } from '../../utils/helpers/version';
|
||||||
import { versionMatch } from '../../utils/helpers/version';
|
import { versionMatch } from '../../utils/helpers/version';
|
||||||
|
|
||||||
const supportedFeatures = {
|
const supportedFeatures = {
|
||||||
|
// Deprecated
|
||||||
forwardQuery: '2.9.0',
|
forwardQuery: '2.9.0',
|
||||||
nonRestCors: '2.9.0',
|
nonRestCors: '2.9.0',
|
||||||
defaultDomainRedirectsEdition: '2.10.0',
|
defaultDomainRedirectsEdition: '2.10.0',
|
||||||
nonOrphanVisits: '3.0.0',
|
nonOrphanVisits: '3.0.0',
|
||||||
allTagsFiltering: '3.0.0',
|
allTagsFiltering: '3.0.0',
|
||||||
tagsStats: '3.0.0',
|
tagsStats: '3.0.0',
|
||||||
|
// End deprecated
|
||||||
|
|
||||||
domainVisits: '3.1.0',
|
domainVisits: '3.1.0',
|
||||||
excludeBotsOnShortUrls: '3.4.0',
|
excludeBotsOnShortUrls: '3.4.0',
|
||||||
filterDisabledUrls: '3.4.0',
|
filterDisabledUrls: '3.4.0',
|
||||||
|
@ -23,12 +26,15 @@ export const isFeatureEnabledForVersion = (feature: Feature, serverVersion: SemV
|
||||||
versionMatch(serverVersion, { minVersion: supportedFeatures[feature] });
|
versionMatch(serverVersion, { minVersion: supportedFeatures[feature] });
|
||||||
|
|
||||||
const getFeaturesForVersion = (serverVersion: SemVer): Record<Feature, boolean> => ({
|
const getFeaturesForVersion = (serverVersion: SemVer): Record<Feature, boolean> => ({
|
||||||
|
// Deprecated
|
||||||
forwardQuery: isFeatureEnabledForVersion('forwardQuery', serverVersion),
|
forwardQuery: isFeatureEnabledForVersion('forwardQuery', serverVersion),
|
||||||
nonRestCors: isFeatureEnabledForVersion('nonRestCors', serverVersion),
|
nonRestCors: isFeatureEnabledForVersion('nonRestCors', serverVersion),
|
||||||
defaultDomainRedirectsEdition: isFeatureEnabledForVersion('defaultDomainRedirectsEdition', serverVersion),
|
defaultDomainRedirectsEdition: isFeatureEnabledForVersion('defaultDomainRedirectsEdition', serverVersion),
|
||||||
nonOrphanVisits: isFeatureEnabledForVersion('nonOrphanVisits', serverVersion),
|
nonOrphanVisits: isFeatureEnabledForVersion('nonOrphanVisits', serverVersion),
|
||||||
allTagsFiltering: isFeatureEnabledForVersion('allTagsFiltering', serverVersion),
|
allTagsFiltering: isFeatureEnabledForVersion('allTagsFiltering', serverVersion),
|
||||||
tagsStats: isFeatureEnabledForVersion('tagsStats', serverVersion),
|
tagsStats: isFeatureEnabledForVersion('tagsStats', serverVersion),
|
||||||
|
// End
|
||||||
|
|
||||||
domainVisits: isFeatureEnabledForVersion('domainVisits', serverVersion),
|
domainVisits: isFeatureEnabledForVersion('domainVisits', serverVersion),
|
||||||
excludeBotsOnShortUrls: isFeatureEnabledForVersion('excludeBotsOnShortUrls', serverVersion),
|
excludeBotsOnShortUrls: isFeatureEnabledForVersion('excludeBotsOnShortUrls', serverVersion),
|
||||||
filterDisabledUrls: isFeatureEnabledForVersion('filterDisabledUrls', serverVersion),
|
filterDisabledUrls: isFeatureEnabledForVersion('filterDisabledUrls', serverVersion),
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { screen, waitFor } from '@testing-library/react';
|
import { screen, waitFor } from '@testing-library/react';
|
||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
import { MemoryRouter } from 'react-router-dom';
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
import type { HighlightCardProps } from '../../../src/servers/helpers/HighlightCard';
|
import type { HighlightCardProps } from '../../../src/shlink-web-component/overview/helpers/HighlightCard';
|
||||||
import { HighlightCard } from '../../../src/servers/helpers/HighlightCard';
|
import { HighlightCard } from '../../../src/shlink-web-component/overview/helpers/HighlightCard';
|
||||||
import { renderWithEvents } from '../../__helpers__/setUpTest';
|
import { renderWithEvents } from '../../__helpers__/setUpTest';
|
||||||
|
|
||||||
describe('<HighlightCard />', () => {
|
describe('<HighlightCard />', () => {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { screen, waitFor } from '@testing-library/react';
|
import { screen, waitFor } from '@testing-library/react';
|
||||||
import type { VisitsHighlightCardProps } from '../../../src/servers/helpers/VisitsHighlightCard';
|
import type { VisitsHighlightCardProps } from '../../../src/shlink-web-component/overview/helpers/VisitsHighlightCard';
|
||||||
import { VisitsHighlightCard } from '../../../src/servers/helpers/VisitsHighlightCard';
|
import { VisitsHighlightCard } from '../../../src/shlink-web-component/overview/helpers/VisitsHighlightCard';
|
||||||
import { renderWithEvents } from '../../__helpers__/setUpTest';
|
import { renderWithEvents } from '../../__helpers__/setUpTest';
|
||||||
|
|
||||||
describe('<VisitsHighlightCard />', () => {
|
describe('<VisitsHighlightCard />', () => {
|
||||||
|
|
Loading…
Reference in a new issue