Created base structure to manage domains

This commit is contained in:
Alejandro Celaya
2021-08-20 17:30:07 +02:00
parent 5eee86003d
commit a28a4846bc
13 changed files with 140 additions and 19 deletions

View File

@@ -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 {

View File

@@ -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>
); );

View File

@@ -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>}
/> />

View File

@@ -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);

View 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()}
</>
);
};

View File

@@ -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');
}; };

View File

@@ -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}`;
} }

View File

@@ -120,4 +120,4 @@ export const Overview = (
</Card> </Card>
</> </>
); );
}, () => [ Topics.visits(), Topics.orphanVisits() ]); }, () => [ Topics.visits, Topics.orphanVisits ]);

View File

@@ -99,6 +99,6 @@ const ShortUrlsList = (ShortUrlsTable: FC<ShortUrlsTableProps>) => boundToMercur
</Card> </Card>
</> </>
); );
}, () => [ Topics.visits() ]); }, () => [ Topics.visits ]);
export default ShortUrlsList; export default ShortUrlsList;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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 ]);

View File

@@ -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;