mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2026-03-11 18:13:47 +00:00
Created base structure to manage domains
This commit is contained in:
@@ -65,9 +65,16 @@ export interface ShlinkShortUrlData extends ShortUrlMeta {
|
|||||||
tags?: string[];
|
tags?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ShlinkDomainRedirects {
|
||||||
|
baseUrlRedirect: string,
|
||||||
|
regular404Redirect: string,
|
||||||
|
invalidShortUrlRedirect: string
|
||||||
|
}
|
||||||
|
|
||||||
export interface ShlinkDomain {
|
export interface ShlinkDomain {
|
||||||
domain: string;
|
domain: string;
|
||||||
isDefault: boolean;
|
isDefault: boolean;
|
||||||
|
redirects?: ShlinkDomainRedirects; // Optional only for Shlink older than 2.8
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ShlinkDomainsResponse {
|
export interface ShlinkDomainsResponse {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
faTags as tagsIcon,
|
faTags as tagsIcon,
|
||||||
faPen as editIcon,
|
faPen as editIcon,
|
||||||
faHome as overviewIcon,
|
faHome as overviewIcon,
|
||||||
|
faGlobe as domainsIcon,
|
||||||
} from '@fortawesome/free-solid-svg-icons';
|
} from '@fortawesome/free-solid-svg-icons';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import { FC } from 'react';
|
import { FC } from 'react';
|
||||||
@@ -11,11 +12,12 @@ import { NavLink, NavLinkProps } from 'react-router-dom';
|
|||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { Location } from 'history';
|
import { Location } from 'history';
|
||||||
import { DeleteServerButtonProps } from '../servers/DeleteServerButton';
|
import { DeleteServerButtonProps } from '../servers/DeleteServerButton';
|
||||||
import { ServerWithId } from '../servers/data';
|
import { isServerWithId, SelectedServer } from '../servers/data';
|
||||||
|
import { supportsDomainRedirects } from '../utils/helpers/features';
|
||||||
import './AsideMenu.scss';
|
import './AsideMenu.scss';
|
||||||
|
|
||||||
export interface AsideMenuProps {
|
export interface AsideMenuProps {
|
||||||
selectedServer: ServerWithId;
|
selectedServer: SelectedServer;
|
||||||
className?: string;
|
className?: string;
|
||||||
showOnMobile?: boolean;
|
showOnMobile?: boolean;
|
||||||
}
|
}
|
||||||
@@ -38,7 +40,8 @@ const AsideMenuItem: FC<AsideMenuItemProps> = ({ children, to, className, ...res
|
|||||||
const AsideMenu = (DeleteServerButton: FC<DeleteServerButtonProps>) => (
|
const AsideMenu = (DeleteServerButton: FC<DeleteServerButtonProps>) => (
|
||||||
{ selectedServer, showOnMobile = false }: AsideMenuProps,
|
{ selectedServer, showOnMobile = false }: AsideMenuProps,
|
||||||
) => {
|
) => {
|
||||||
const serverId = selectedServer ? selectedServer.id : '';
|
const serverId = isServerWithId(selectedServer) ? selectedServer.id : '';
|
||||||
|
const addManageDomainsLink = supportsDomainRedirects(selectedServer);
|
||||||
const asideClass = classNames('aside-menu', {
|
const asideClass = classNames('aside-menu', {
|
||||||
'aside-menu--hidden': !showOnMobile,
|
'aside-menu--hidden': !showOnMobile,
|
||||||
});
|
});
|
||||||
@@ -64,15 +67,23 @@ const AsideMenu = (DeleteServerButton: FC<DeleteServerButtonProps>) => (
|
|||||||
<FontAwesomeIcon icon={tagsIcon} />
|
<FontAwesomeIcon icon={tagsIcon} />
|
||||||
<span className="aside-menu__item-text">Manage tags</span>
|
<span className="aside-menu__item-text">Manage tags</span>
|
||||||
</AsideMenuItem>
|
</AsideMenuItem>
|
||||||
|
{addManageDomainsLink && (
|
||||||
|
<AsideMenuItem to={buildPath('/manage-domains')}>
|
||||||
|
<FontAwesomeIcon icon={domainsIcon} />
|
||||||
|
<span className="aside-menu__item-text">Manage domains</span>
|
||||||
|
</AsideMenuItem>
|
||||||
|
)}
|
||||||
<AsideMenuItem to={buildPath('/edit')} className="aside-menu__item--push">
|
<AsideMenuItem to={buildPath('/edit')} className="aside-menu__item--push">
|
||||||
<FontAwesomeIcon icon={editIcon} />
|
<FontAwesomeIcon icon={editIcon} />
|
||||||
<span className="aside-menu__item-text">Edit this server</span>
|
<span className="aside-menu__item-text">Edit this server</span>
|
||||||
</AsideMenuItem>
|
</AsideMenuItem>
|
||||||
<DeleteServerButton
|
{isServerWithId(selectedServer) && (
|
||||||
className="aside-menu__item aside-menu__item--danger"
|
<DeleteServerButton
|
||||||
textClassName="aside-menu__item-text"
|
className="aside-menu__item aside-menu__item--danger"
|
||||||
server={selectedServer}
|
textClassName="aside-menu__item-text"
|
||||||
/>
|
server={selectedServer}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</nav>
|
</nav>
|
||||||
</aside>
|
</aside>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
|||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { withSelectedServer } from '../servers/helpers/withSelectedServer';
|
import { withSelectedServer } from '../servers/helpers/withSelectedServer';
|
||||||
import { useSwipeable, useToggle } from '../utils/helpers/hooks';
|
import { useSwipeable, useToggle } from '../utils/helpers/hooks';
|
||||||
import { supportsOrphanVisits, supportsTagVisits } from '../utils/helpers/features';
|
import { supportsDomainRedirects, supportsOrphanVisits, supportsTagVisits } from '../utils/helpers/features';
|
||||||
import { isReachableServer } from '../servers/data';
|
import { isReachableServer } from '../servers/data';
|
||||||
import NotFound from './NotFound';
|
import NotFound from './NotFound';
|
||||||
import { AsideMenuProps } from './AsideMenu';
|
import { AsideMenuProps } from './AsideMenu';
|
||||||
@@ -22,6 +22,7 @@ const MenuLayout = (
|
|||||||
ServerError: FC,
|
ServerError: FC,
|
||||||
Overview: FC,
|
Overview: FC,
|
||||||
EditShortUrl: FC,
|
EditShortUrl: FC,
|
||||||
|
ManageDomains: FC,
|
||||||
) => withSelectedServer(({ location, selectedServer }) => {
|
) => withSelectedServer(({ location, selectedServer }) => {
|
||||||
const [ sidebarVisible, toggleSidebar, showSidebar, hideSidebar ] = useToggle();
|
const [ sidebarVisible, toggleSidebar, showSidebar, hideSidebar ] = useToggle();
|
||||||
|
|
||||||
@@ -33,6 +34,7 @@ const MenuLayout = (
|
|||||||
|
|
||||||
const addTagsVisitsRoute = supportsTagVisits(selectedServer);
|
const addTagsVisitsRoute = supportsTagVisits(selectedServer);
|
||||||
const addOrphanVisitsRoute = supportsOrphanVisits(selectedServer);
|
const addOrphanVisitsRoute = supportsOrphanVisits(selectedServer);
|
||||||
|
const addManageDomainsRoute = supportsDomainRedirects(selectedServer);
|
||||||
const burgerClasses = classNames('menu-layout__burger-icon', { 'menu-layout__burger-icon--active': sidebarVisible });
|
const burgerClasses = classNames('menu-layout__burger-icon', { 'menu-layout__burger-icon--active': sidebarVisible });
|
||||||
const swipeableProps = useSwipeable(showSidebar, hideSidebar);
|
const swipeableProps = useSwipeable(showSidebar, hideSidebar);
|
||||||
|
|
||||||
@@ -55,6 +57,7 @@ const MenuLayout = (
|
|||||||
{addTagsVisitsRoute && <Route path="/server/:serverId/tag/:tag/visits" component={TagVisits} />}
|
{addTagsVisitsRoute && <Route path="/server/:serverId/tag/:tag/visits" component={TagVisits} />}
|
||||||
{addOrphanVisitsRoute && <Route path="/server/:serverId/orphan-visits" component={OrphanVisits} />}
|
{addOrphanVisitsRoute && <Route path="/server/:serverId/orphan-visits" component={OrphanVisits} />}
|
||||||
<Route exact path="/server/:serverId/manage-tags" component={TagsList} />
|
<Route exact path="/server/:serverId/manage-tags" component={TagsList} />
|
||||||
|
{addManageDomainsRoute && <Route exact path="/server/:serverId/manage-domains" component={ManageDomains} />}
|
||||||
<Route
|
<Route
|
||||||
render={() => <NotFound to={`/server/${selectedServer.id}/list-short-urls/1`}>List short URLs</NotFound>}
|
render={() => <NotFound to={`/server/${selectedServer.id}/list-short-urls/1`}>List short URLs</NotFound>}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator, withRouter:
|
|||||||
'ServerError',
|
'ServerError',
|
||||||
'Overview',
|
'Overview',
|
||||||
'EditShortUrl',
|
'EditShortUrl',
|
||||||
|
'ManageDomains',
|
||||||
);
|
);
|
||||||
bottle.decorator('MenuLayout', connect([ 'selectedServer', 'shortUrlsListParams' ], [ 'selectServer' ]));
|
bottle.decorator('MenuLayout', connect([ 'selectedServer', 'shortUrlsListParams' ], [ 'selectServer' ]));
|
||||||
bottle.decorator('MenuLayout', withRouter);
|
bottle.decorator('MenuLayout', withRouter);
|
||||||
|
|||||||
92
src/domains/ManageDomains.tsx
Normal file
92
src/domains/ManageDomains.tsx
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
import { FC, useEffect } from 'react';
|
||||||
|
import { faCheck as defaultDomainIcon, faEdit as editIcon, faBan as forbiddenIcon } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
|
import { Button, UncontrolledTooltip } from 'reactstrap';
|
||||||
|
import Message from '../utils/Message';
|
||||||
|
import { Result } from '../utils/Result';
|
||||||
|
import { ShlinkApiError } from '../api/ShlinkApiError';
|
||||||
|
import { SimpleCard } from '../utils/SimpleCard';
|
||||||
|
import { DomainsList } from './reducers/domainsList';
|
||||||
|
import SearchField from '../utils/SearchField';
|
||||||
|
|
||||||
|
interface ManageDomainsProps {
|
||||||
|
listDomains: Function;
|
||||||
|
domainsList: DomainsList;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Na: FC = () => <i><small>N/A</small></i>;
|
||||||
|
const DefaultDomain: FC = () => (
|
||||||
|
<>
|
||||||
|
<FontAwesomeIcon icon={defaultDomainIcon} className="text-primary" id="defaultDomainIcon" />
|
||||||
|
<UncontrolledTooltip target="defaultDomainIcon" placement="right">Default domain</UncontrolledTooltip>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const ManageDomains: FC<ManageDomainsProps> = ({ listDomains, domainsList }) => {
|
||||||
|
const { domains, loading, error } = domainsList;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
listDomains();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const renderContent = () => {
|
||||||
|
if (loading) {
|
||||||
|
return <Message loading />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<Result type="error">
|
||||||
|
<ShlinkApiError fallbackMessage="Error loading domains :(" />
|
||||||
|
</Result>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SimpleCard>
|
||||||
|
<table className="table table-hover mb-0">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th />
|
||||||
|
<th>Domain</th>
|
||||||
|
<th>Base path redirect</th>
|
||||||
|
<th>Regular 404 redirect</th>
|
||||||
|
<th>Invalid short URL redirect</th>
|
||||||
|
<th />
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{domains.map((domain) => (
|
||||||
|
<tr key={domain.domain}>
|
||||||
|
<td>{domain.isDefault ? <DefaultDomain /> : ''}</td>
|
||||||
|
<th>{domain.domain}</th>
|
||||||
|
<td>{domain.redirects?.baseUrlRedirect ?? <Na />}</td>
|
||||||
|
<td>{domain.redirects?.regular404Redirect ?? <Na />}</td>
|
||||||
|
<td>{domain.redirects?.invalidShortUrlRedirect ?? <Na />}</td>
|
||||||
|
<td>
|
||||||
|
<span id={`domainEdit${domain.domain.replace('.', '')}`}>
|
||||||
|
<Button outline size="sm" disabled={domain.isDefault}>
|
||||||
|
<FontAwesomeIcon icon={domain.isDefault ? forbiddenIcon : editIcon} />
|
||||||
|
</Button>
|
||||||
|
</span>
|
||||||
|
{domain.isDefault && (
|
||||||
|
<UncontrolledTooltip target={`domainEdit${domain.domain.replace('.', '')}`} placement="left">
|
||||||
|
Redirects for default domain cannot be edited here.
|
||||||
|
</UncontrolledTooltip>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</SimpleCard>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<SearchField className="mb-3" onChange={() => {}} />
|
||||||
|
{renderContent()}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -2,12 +2,16 @@ import Bottle from 'bottlejs';
|
|||||||
import { ConnectDecorator } from '../../container/types';
|
import { ConnectDecorator } from '../../container/types';
|
||||||
import { listDomains } from '../reducers/domainsList';
|
import { listDomains } from '../reducers/domainsList';
|
||||||
import { DomainSelector } from '../DomainSelector';
|
import { DomainSelector } from '../DomainSelector';
|
||||||
|
import { ManageDomains } from '../ManageDomains';
|
||||||
|
|
||||||
const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
|
const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
|
||||||
// Components
|
// Components
|
||||||
bottle.serviceFactory('DomainSelector', () => DomainSelector);
|
bottle.serviceFactory('DomainSelector', () => DomainSelector);
|
||||||
bottle.decorator('DomainSelector', connect([ 'domainsList' ], [ 'listDomains' ]));
|
bottle.decorator('DomainSelector', connect([ 'domainsList' ], [ 'listDomains' ]));
|
||||||
|
|
||||||
|
bottle.serviceFactory('ManageDomains', () => ManageDomains);
|
||||||
|
bottle.decorator('ManageDomains', connect([ 'domainsList' ], [ 'listDomains' ]));
|
||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
bottle.serviceFactory('listDomains', listDomains, 'buildShlinkApiClient');
|
bottle.serviceFactory('listDomains', listDomains, 'buildShlinkApiClient');
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
export class Topics {
|
export class Topics {
|
||||||
public static visits = () => 'https://shlink.io/new-visit';
|
public static readonly visits = 'https://shlink.io/new-visit';
|
||||||
|
|
||||||
public static shortUrlVisits = (shortCode: string) => `https://shlink.io/new-visit/${shortCode}`;
|
public static readonly orphanVisits = 'https://shlink.io/new-orphan-visit';
|
||||||
|
|
||||||
public static orphanVisits = () => 'https://shlink.io/new-orphan-visit';
|
public static readonly shortUrlVisits = (shortCode: string) => `https://shlink.io/new-visit/${shortCode}`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -120,4 +120,4 @@ export const Overview = (
|
|||||||
</Card>
|
</Card>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}, () => [ Topics.visits(), Topics.orphanVisits() ]);
|
}, () => [ Topics.visits, Topics.orphanVisits ]);
|
||||||
|
|||||||
@@ -99,6 +99,6 @@ const ShortUrlsList = (ShortUrlsTable: FC<ShortUrlsTableProps>) => boundToMercur
|
|||||||
</Card>
|
</Card>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}, () => [ Topics.visits() ]);
|
}, () => [ Topics.visits ]);
|
||||||
|
|
||||||
export default ShortUrlsList;
|
export default ShortUrlsList;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { FC, useEffect, useState } from 'react';
|
import { FC, useEffect, useState } from 'react';
|
||||||
import { splitEvery } from 'ramda';
|
import { splitEvery } from 'ramda';
|
||||||
|
import { Row } from 'reactstrap';
|
||||||
import Message from '../utils/Message';
|
import Message from '../utils/Message';
|
||||||
import SearchField from '../utils/SearchField';
|
import SearchField from '../utils/SearchField';
|
||||||
import { SelectedServer } from '../servers/data';
|
import { SelectedServer } from '../servers/data';
|
||||||
@@ -51,7 +52,7 @@ const TagsList = (TagCard: FC<TagCardProps>) => boundToMercureHub((
|
|||||||
const tagsGroups = splitEvery(ceil(tagsCount / TAGS_GROUPS_AMOUNT), tagsList.filteredTags);
|
const tagsGroups = splitEvery(ceil(tagsCount / TAGS_GROUPS_AMOUNT), tagsList.filteredTags);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="row">
|
<Row>
|
||||||
{tagsGroups.map((group, index) => (
|
{tagsGroups.map((group, index) => (
|
||||||
<div key={index} className="col-md-6 col-xl-3">
|
<div key={index} className="col-md-6 col-xl-3">
|
||||||
{group.map((tag) => (
|
{group.map((tag) => (
|
||||||
@@ -66,7 +67,7 @@ const TagsList = (TagCard: FC<TagCardProps>) => boundToMercureHub((
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</Row>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -76,6 +77,6 @@ const TagsList = (TagCard: FC<TagCardProps>) => boundToMercureHub((
|
|||||||
{renderContent()}
|
{renderContent()}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}, () => [ Topics.visits() ]);
|
}, () => [ Topics.visits ]);
|
||||||
|
|
||||||
export default TagsList;
|
export default TagsList;
|
||||||
|
|||||||
@@ -29,3 +29,5 @@ export const supportsBotVisits = serverMatchesVersions({ minVersion: '2.7.0' });
|
|||||||
export const supportsCrawlableVisits = supportsBotVisits;
|
export const supportsCrawlableVisits = supportsBotVisits;
|
||||||
|
|
||||||
export const supportsQrErrorCorrection = serverMatchesVersions({ minVersion: '2.8.0' });
|
export const supportsQrErrorCorrection = serverMatchesVersions({ minVersion: '2.8.0' });
|
||||||
|
|
||||||
|
export const supportsDomainRedirects = supportsQrErrorCorrection;
|
||||||
|
|||||||
@@ -41,4 +41,4 @@ export const OrphanVisits = ({ exportVisits }: VisitsExporter) => boundToMercure
|
|||||||
<OrphanVisitsHeader orphanVisits={orphanVisits} goBack={goBack} />
|
<OrphanVisitsHeader orphanVisits={orphanVisits} goBack={goBack} />
|
||||||
</VisitsStats>
|
</VisitsStats>
|
||||||
);
|
);
|
||||||
}, () => [ Topics.orphanVisits() ]);
|
}, () => [ Topics.orphanVisits ]);
|
||||||
|
|||||||
@@ -43,6 +43,6 @@ const TagVisits = (colorGenerator: ColorGenerator, { exportVisits }: VisitsExpor
|
|||||||
<TagVisitsHeader tagVisits={tagVisits} goBack={goBack} colorGenerator={colorGenerator} />
|
<TagVisitsHeader tagVisits={tagVisits} goBack={goBack} colorGenerator={colorGenerator} />
|
||||||
</VisitsStats>
|
</VisitsStats>
|
||||||
);
|
);
|
||||||
}, () => [ Topics.visits() ]);
|
}, () => [ Topics.visits ]);
|
||||||
|
|
||||||
export default TagVisits;
|
export default TagVisits;
|
||||||
|
|||||||
Reference in New Issue
Block a user