mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2025-01-09 01:37:24 +03:00
Normalized Message component, making it autocontained
This commit is contained in:
parent
852e791c80
commit
5cf0c86a14
6 changed files with 55 additions and 62 deletions
|
@ -17,17 +17,15 @@ export const ServerError = (DeleteServerButton: FC<DeleteServerButtonProps>): FC
|
||||||
) => (
|
) => (
|
||||||
<NoMenuLayout>
|
<NoMenuLayout>
|
||||||
<div className="server-error__container flex-column">
|
<div className="server-error__container flex-column">
|
||||||
<div className="row w-100 mb-3 mb-md-5">
|
<Message className="w-100 mb-3 mb-md-5" type="error" fullWidth>
|
||||||
<Message type="error" fullWidth noMargin>
|
{!isServerWithId(selectedServer) && 'Could not find this Shlink server.'}
|
||||||
{!isServerWithId(selectedServer) && 'Could not find this Shlink server.'}
|
{isServerWithId(selectedServer) && (
|
||||||
{isServerWithId(selectedServer) && (
|
<>
|
||||||
<>
|
<p>Oops! Could not connect to this Shlink server.</p>
|
||||||
<p>Oops! Could not connect to this Shlink server.</p>
|
Make sure you have internet connection, and the server is properly configured and on-line.
|
||||||
Make sure you have internet connection, and the server is properly configured and on-line.
|
</>
|
||||||
</>
|
)}
|
||||||
)}
|
</Message>
|
||||||
</Message>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ServersListGroup servers={Object.values(servers)}>
|
<ServersListGroup servers={Object.values(servers)}>
|
||||||
These are the Shlink servers currently configured. Choose one of
|
These are the Shlink servers currently configured. Choose one of
|
||||||
|
|
|
@ -20,7 +20,7 @@ export function withSelectedServer<T = {}>(WrappedComponent: FC<WithSelectedServ
|
||||||
if (!selectedServer) {
|
if (!selectedServer) {
|
||||||
return (
|
return (
|
||||||
<NoMenuLayout>
|
<NoMenuLayout>
|
||||||
<Message loading noMargin />
|
<Message loading />
|
||||||
</NoMenuLayout>
|
</NoMenuLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,15 +28,11 @@ const TagsList = (TagCard: FC<TagCardProps>) => boundToMercureHub((
|
||||||
|
|
||||||
const renderContent = () => {
|
const renderContent = () => {
|
||||||
if (tagsList.loading) {
|
if (tagsList.loading) {
|
||||||
return <Message noMargin loading />;
|
return <Message loading />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tagsList.error) {
|
if (tagsList.error) {
|
||||||
return (
|
return <div className="bg-danger p-2 text-white text-center">Error loading tags :(</div>;
|
||||||
<div className="col-12">
|
|
||||||
<div className="bg-danger p-2 text-white text-center">Error loading tags :(</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const tagsCount = tagsList.filteredTags.length;
|
const tagsCount = tagsList.filteredTags.length;
|
||||||
|
@ -48,7 +44,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">
|
||||||
{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) => (
|
||||||
|
@ -63,16 +59,14 @@ const TagsList = (TagCard: FC<TagCardProps>) => boundToMercureHub((
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{!tagsList.loading && <SearchField className="mb-3" placeholder="Search tags..." onChange={filterTags} />}
|
{!tagsList.loading && <SearchField className="mb-3" placeholder="Search tags..." onChange={filterTags} />}
|
||||||
<div className="row">
|
{renderContent()}
|
||||||
{renderContent()}
|
|
||||||
</div>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}, () => 'https://shlink.io/new-visit');
|
}, () => 'https://shlink.io/new-visit');
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { FC } from 'react';
|
import { FC } from 'react';
|
||||||
import { Card } from 'reactstrap';
|
import { Card, Row } from 'reactstrap';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { faCircleNotch as preloader } from '@fortawesome/free-solid-svg-icons';
|
import { faCircleNotch as preloader } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
|
@ -24,31 +24,30 @@ const getTextClassForType = (type: MessageType) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
interface MessageProps {
|
interface MessageProps {
|
||||||
noMargin?: boolean;
|
className?: string;
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
fullWidth?: boolean;
|
fullWidth?: boolean;
|
||||||
type?: MessageType;
|
type?: MessageType;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Message: FC<MessageProps> = (
|
const Message: FC<MessageProps> = ({ className, children, loading = false, type = 'default', fullWidth = false }) => {
|
||||||
{ children, loading = false, noMargin = false, type = 'default', fullWidth = false },
|
|
||||||
) => {
|
|
||||||
const cardClasses = classNames(getClassForType(type), { 'mt-4': !noMargin });
|
|
||||||
const classes = classNames({
|
const classes = classNames({
|
||||||
'col-md-12': fullWidth,
|
'col-md-12': fullWidth,
|
||||||
'col-md-10 offset-md-1': !fullWidth,
|
'col-md-10 offset-md-1': !fullWidth,
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classes}>
|
<Row noGutters className={className}>
|
||||||
<Card className={cardClasses} body>
|
<div className={classes}>
|
||||||
<h3 className={classNames('text-center mb-0', getTextClassForType(type))}>
|
<Card className={getClassForType(type)} body>
|
||||||
{loading && <FontAwesomeIcon icon={preloader} spin />}
|
<h3 className={classNames('text-center mb-0', getTextClassForType(type))}>
|
||||||
{loading && <span className="ml-2">{children ?? 'Loading...'}</span>}
|
{loading && <FontAwesomeIcon icon={preloader} spin />}
|
||||||
{!loading && children}
|
{loading && <span className="ml-2">{children ?? 'Loading...'}</span>}
|
||||||
</h3>
|
{!loading && children}
|
||||||
</Card>
|
</h3>
|
||||||
</div>
|
</Card>
|
||||||
|
</div>
|
||||||
|
</Row>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -28,10 +28,16 @@ export interface VisitsStatsProps {
|
||||||
domain?: string;
|
domain?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface VisitsNavLinkProps {
|
||||||
|
title: string;
|
||||||
|
subPath: string;
|
||||||
|
icon: IconDefinition;
|
||||||
|
}
|
||||||
|
|
||||||
type HighlightableProps = 'referer' | 'country' | 'city';
|
type HighlightableProps = 'referer' | 'country' | 'city';
|
||||||
type Section = 'byTime' | 'byContext' | 'byLocation' | 'list';
|
type Section = 'byTime' | 'byContext' | 'byLocation' | 'list';
|
||||||
|
|
||||||
const sections: Record<Section, { title: string; subPath: string; icon: IconDefinition }> = {
|
const sections: Record<Section, VisitsNavLinkProps> = {
|
||||||
byTime: { title: 'By time', subPath: '', icon: faCalendarAlt },
|
byTime: { title: 'By time', subPath: '', icon: faCalendarAlt },
|
||||||
byContext: { title: 'By context', subPath: '/by-context', icon: faChartPie },
|
byContext: { title: 'By context', subPath: '/by-context', icon: faChartPie },
|
||||||
byLocation: { title: 'By location', subPath: '/by-location', icon: faMapMarkedAlt },
|
byLocation: { title: 'By location', subPath: '/by-location', icon: faMapMarkedAlt },
|
||||||
|
@ -53,6 +59,19 @@ const highlightedVisitsToStats = (
|
||||||
let selectedBar: string | undefined;
|
let selectedBar: string | undefined;
|
||||||
const initialInterval: DateInterval = 'last30Days';
|
const initialInterval: DateInterval = 'last30Days';
|
||||||
|
|
||||||
|
const VisitsNavLink: FC<VisitsNavLinkProps> = ({ subPath, title, icon, children }) => (
|
||||||
|
<NavLink
|
||||||
|
tag={RouterNavLink}
|
||||||
|
className="visits-stats__nav-link"
|
||||||
|
to={children}
|
||||||
|
isActive={(_: null, { pathname }: Location) => pathname.endsWith(`/visits${subPath}`)}
|
||||||
|
replace
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={icon} />
|
||||||
|
<span className="ml-2 d-none d-sm-inline">{title}</span>
|
||||||
|
</NavLink>
|
||||||
|
);
|
||||||
|
|
||||||
const VisitsStats: FC<VisitsStatsProps> = ({ children, visitsInfo, getVisits, cancelGetVisits, baseUrl, domain }) => {
|
const VisitsStats: FC<VisitsStatsProps> = ({ children, visitsInfo, getVisits, cancelGetVisits, baseUrl, domain }) => {
|
||||||
const [ dateRange, setDateRange ] = useState<DateRange>(intervalToDateRange(initialInterval));
|
const [ dateRange, setDateRange ] = useState<DateRange>(intervalToDateRange(initialInterval));
|
||||||
const [ highlightedVisits, setHighlightedVisits ] = useState<NormalizedVisit[]>([]);
|
const [ highlightedVisits, setHighlightedVisits ] = useState<NormalizedVisit[]>([]);
|
||||||
|
@ -112,7 +131,7 @@ const VisitsStats: FC<VisitsStatsProps> = ({ children, visitsInfo, getVisits, ca
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
<Card className="mt-4" body inverse color="danger">
|
<Card body inverse color="danger">
|
||||||
An error occurred while loading visits :(
|
An error occurred while loading visits :(
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
@ -124,23 +143,10 @@ const VisitsStats: FC<VisitsStatsProps> = ({ children, visitsInfo, getVisits, ca
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Card className="visits-stats__nav p-0 mt-4 overflow-hidden" body>
|
<Card className="visits-stats__nav p-0 overflow-hidden" body>
|
||||||
<Nav pills justified>
|
<Nav pills justified>
|
||||||
{Object.entries(sections).map(
|
{Object.entries(sections).map(([ section, props ]) =>
|
||||||
([ section, { title, icon, subPath }]) => (
|
<VisitsNavLink key={section} {...props}>{buildSectionUrl(props.subPath)}</VisitsNavLink>)}
|
||||||
<NavLink
|
|
||||||
key={section}
|
|
||||||
tag={RouterNavLink}
|
|
||||||
className="visits-stats__nav-link"
|
|
||||||
to={buildSectionUrl(subPath)}
|
|
||||||
isActive={(_: null, { pathname }: Location) => pathname.endsWith(`/visits${subPath}`)}
|
|
||||||
replace
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={icon} />
|
|
||||||
<span className="ml-2 d-none d-sm-inline">{title}</span>
|
|
||||||
</NavLink>
|
|
||||||
),
|
|
||||||
)}
|
|
||||||
</Nav>
|
</Nav>
|
||||||
</Card>
|
</Card>
|
||||||
<div className="row">
|
<div className="row">
|
||||||
|
@ -259,7 +265,7 @@ const VisitsStats: FC<VisitsStatsProps> = ({ children, visitsInfo, getVisits, ca
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section>
|
<section className="mt-4">
|
||||||
{renderVisitsContent()}
|
{renderVisitsContent()}
|
||||||
</section>
|
</section>
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { shallow, ShallowWrapper } from 'enzyme';
|
import { shallow, ShallowWrapper } from 'enzyme';
|
||||||
import { Card, NavLink, Progress } from 'reactstrap';
|
import { Card, Progress } from 'reactstrap';
|
||||||
import { Mock } from 'ts-mockery';
|
import { Mock } from 'ts-mockery';
|
||||||
import VisitStats from '../../src/visits/VisitsStats';
|
import VisitStats from '../../src/visits/VisitsStats';
|
||||||
import Message from '../../src/utils/Message';
|
import Message from '../../src/utils/Message';
|
||||||
|
@ -80,10 +80,6 @@ describe('<VisitStats />', () => {
|
||||||
|
|
||||||
it('holds the map button content generator on cities graph extraHeaderContent', () => {
|
it('holds the map button content generator on cities graph extraHeaderContent', () => {
|
||||||
const wrapper = createComponent({ loading: false, error: false, visits });
|
const wrapper = createComponent({ loading: false, error: false, visits });
|
||||||
const locationNav = wrapper.find(NavLink).at(2);
|
|
||||||
|
|
||||||
locationNav.simulate('click');
|
|
||||||
|
|
||||||
const citiesGraph = wrapper.find(SortableBarGraph).find('[title="Cities"]');
|
const citiesGraph = wrapper.find(SortableBarGraph).find('[title="Cities"]');
|
||||||
const extraHeaderContent = citiesGraph.prop('extraHeaderContent');
|
const extraHeaderContent = citiesGraph.prop('extraHeaderContent');
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue