mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2024-12-23 09:30:31 +03:00
Merge pull request #353 from acelaya-forks/feature/more-ui-improvements
Feature/more UI improvements
This commit is contained in:
commit
623deec973
13 changed files with 52 additions and 42 deletions
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
|
@ -39,6 +39,8 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
|
fetch-depth: 0 # needed so that the main branch is also fetched
|
||||||
- name: Use node.js 14.15
|
- name: Use node.js 14.15
|
||||||
uses: actions/setup-node@v1
|
uses: actions/setup-node@v1
|
||||||
with:
|
with:
|
||||||
|
|
|
@ -17,6 +17,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
* Charts are now grouped in tabs, so that only one part of the components is rendered at a time.
|
* Charts are now grouped in tabs, so that only one part of the components is rendered at a time.
|
||||||
* Amount of highlighted visits is now displayed.
|
* Amount of highlighted visits is now displayed.
|
||||||
* Date filtering can be now selected through relative times (last 7 days, last 30 days, etc) or absolute dates using date pickers.
|
* Date filtering can be now selected through relative times (last 7 days, last 30 days, etc) or absolute dates using date pickers.
|
||||||
|
* Only the visits for last 30 days are loaded by default. You can change that at any moment if required.
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
* [#267](https://github.com/shlinkio/shlink-web-client/issues/267) Added some subtle but important improvements on UI/UX.
|
* [#267](https://github.com/shlinkio/shlink-web-client/issues/267) Added some subtle but important improvements on UI/UX.
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
@import '../utils/base';
|
@import '../utils/base';
|
||||||
@import '../utils/mixins/vertical-align';
|
@import '../utils/mixins/vertical-align';
|
||||||
|
|
||||||
$asideMenuMobileWidth: 280px;
|
|
||||||
|
|
||||||
.aside-menu {
|
.aside-menu {
|
||||||
|
width: $asideMenuWidth;
|
||||||
background-color: white;
|
background-color: white;
|
||||||
box-shadow: rgba(0, 0, 0, .05) 0 8px 15px;
|
box-shadow: rgba(0, 0, 0, .05) 0 8px 15px;
|
||||||
position: fixed !important;
|
position: fixed !important;
|
||||||
|
@ -23,7 +22,6 @@ $asideMenuMobileWidth: 280px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: $smMax) {
|
@media (max-width: $smMax) {
|
||||||
width: $asideMenuMobileWidth !important;
|
|
||||||
transition: left 300ms;
|
transition: left 300ms;
|
||||||
top: $headerHeight - 3px;
|
top: $headerHeight - 3px;
|
||||||
box-shadow: -10px 0 50px 11px rgba(0, 0, 0, .55);
|
box-shadow: -10px 0 50px 11px rgba(0, 0, 0, .55);
|
||||||
|
@ -32,7 +30,7 @@ $asideMenuMobileWidth: 280px;
|
||||||
|
|
||||||
.aside-menu--hidden {
|
.aside-menu--hidden {
|
||||||
@media (max-width: $smMax) {
|
@media (max-width: $smMax) {
|
||||||
left: -($asideMenuMobileWidth + 35px);
|
left: -($asideMenuWidth + 35px);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -45,6 +43,10 @@ $asideMenuMobileWidth: 280px;
|
||||||
margin: 0 -15px;
|
margin: 0 -15px;
|
||||||
text-decoration: none !important;
|
text-decoration: none !important;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
|
@media (max-width: $smMax) {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.aside-menu__item:hover {
|
.aside-menu__item:hover {
|
||||||
|
|
|
@ -22,7 +22,6 @@ export interface AsideMenuProps {
|
||||||
|
|
||||||
interface AsideMenuItemProps extends NavLinkProps {
|
interface AsideMenuItemProps extends NavLinkProps {
|
||||||
to: string;
|
to: string;
|
||||||
className?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const AsideMenuItem: FC<AsideMenuItemProps> = ({ children, to, className, ...rest }) => (
|
const AsideMenuItem: FC<AsideMenuItemProps> = ({ children, to, className, ...rest }) => (
|
||||||
|
@ -37,10 +36,10 @@ const AsideMenuItem: FC<AsideMenuItemProps> = ({ children, to, className, ...res
|
||||||
);
|
);
|
||||||
|
|
||||||
const AsideMenu = (DeleteServerButton: FC<DeleteServerButtonProps>) => (
|
const AsideMenu = (DeleteServerButton: FC<DeleteServerButtonProps>) => (
|
||||||
{ selectedServer, className, showOnMobile = false }: AsideMenuProps,
|
{ selectedServer, showOnMobile = false }: AsideMenuProps,
|
||||||
) => {
|
) => {
|
||||||
const serverId = selectedServer ? selectedServer.id : '';
|
const serverId = selectedServer ? selectedServer.id : '';
|
||||||
const asideClass = classNames('aside-menu', className, {
|
const asideClass = classNames('aside-menu', {
|
||||||
'aside-menu--hidden': !showOnMobile,
|
'aside-menu--hidden': !showOnMobile,
|
||||||
});
|
});
|
||||||
const shortUrlsIsActive = (_: null, location: Location) => location.pathname.match('/list-short-urls') !== null;
|
const shortUrlsIsActive = (_: null, location: Location) => location.pathname.match('/list-short-urls') !== null;
|
||||||
|
|
|
@ -33,11 +33,11 @@
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu-layout__container {
|
.menu-layout__container.menu-layout__container {
|
||||||
padding: 20px 0 0;
|
padding: 20px 0 0;
|
||||||
min-height: 100%;
|
min-height: 100%;
|
||||||
|
|
||||||
@media (min-width: $mdMin) {
|
@media (min-width: $mdMin) {
|
||||||
padding: 30px 15px 0;
|
padding: 30px 0 0 $asideMenuWidth;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -56,10 +56,10 @@ const MenuLayout = (
|
||||||
<FontAwesomeIcon icon={burgerIcon} className={burgerClasses} onClick={toggleSidebar} />
|
<FontAwesomeIcon icon={burgerIcon} className={burgerClasses} onClick={toggleSidebar} />
|
||||||
|
|
||||||
<div {...swipeableProps} className="menu-layout__swipeable">
|
<div {...swipeableProps} className="menu-layout__swipeable">
|
||||||
<div className="row menu-layout__swipeable-inner">
|
<div className="menu-layout__swipeable-inner">
|
||||||
<AsideMenu className="col-lg-2 col-md-3" selectedServer={selectedServer} showOnMobile={sidebarVisible} />
|
<AsideMenu selectedServer={selectedServer} showOnMobile={sidebarVisible} />
|
||||||
<div className="col-lg-10 offset-lg-2 col-md-9 offset-md-3" onClick={() => hideSidebar()}>
|
<div className="menu-layout__container" onClick={() => hideSidebar()}>
|
||||||
<div className="menu-layout__container container-xl">
|
<div className="container-xl">
|
||||||
<Switch>
|
<Switch>
|
||||||
<Redirect exact from="/server/:serverId" to="/server/:serverId/overview" />
|
<Redirect exact from="/server/:serverId" to="/server/:serverId/overview" />
|
||||||
<Route exact path="/server/:serverId/overview" component={Overview} />
|
<Route exact path="/server/:serverId/overview" component={Overview} />
|
||||||
|
|
9
src/common/ShlinkVersionsContainer.scss
Normal file
9
src/common/ShlinkVersionsContainer.scss
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
@import '../utils/base';
|
||||||
|
|
||||||
|
.shlink-versions-container--with-server {
|
||||||
|
margin-left: 0;
|
||||||
|
|
||||||
|
@media (min-width: $mdMin) {
|
||||||
|
margin-left: $asideMenuWidth;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,23 +1,20 @@
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { isReachableServer, SelectedServer } from '../servers/data';
|
import { isReachableServer, SelectedServer } from '../servers/data';
|
||||||
import ShlinkVersions from './ShlinkVersions';
|
import ShlinkVersions from './ShlinkVersions';
|
||||||
|
import './ShlinkVersionsContainer.scss';
|
||||||
|
|
||||||
export interface ShlinkVersionsContainerProps {
|
export interface ShlinkVersionsContainerProps {
|
||||||
selectedServer: SelectedServer;
|
selectedServer: SelectedServer;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ShlinkVersionsContainer = ({ selectedServer }: ShlinkVersionsContainerProps) => {
|
const ShlinkVersionsContainer = ({ selectedServer }: ShlinkVersionsContainerProps) => {
|
||||||
const serverIsReachable = isReachableServer(selectedServer);
|
const classes = classNames('text-center', {
|
||||||
const colClasses = classNames('text-center', {
|
'shlink-versions-container--with-server': isReachableServer(selectedServer),
|
||||||
'col-12': !serverIsReachable,
|
|
||||||
'col-lg-10 offset-lg-2 col-md-9 offset-md-3': serverIsReachable,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="row">
|
<div className={classes}>
|
||||||
<div className={colClasses}>
|
<ShlinkVersions selectedServer={selectedServer} />
|
||||||
<ShlinkVersions selectedServer={selectedServer} />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -50,7 +50,7 @@ export const Overview = (
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="row mb-3">
|
<div className="row mb-3">
|
||||||
<div className="col-sm-4">
|
<div className="col-md-6 col-lg-4">
|
||||||
<Card className="overview__card mb-2" body>
|
<Card className="overview__card mb-2" body>
|
||||||
<CardTitle tag="h5" className="overview__card-title">Visits</CardTitle>
|
<CardTitle tag="h5" className="overview__card-title">Visits</CardTitle>
|
||||||
<CardText tag="h2">
|
<CardText tag="h2">
|
||||||
|
@ -63,7 +63,7 @@ export const Overview = (
|
||||||
</CardText>
|
</CardText>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-sm-4">
|
<div className="col-md-6 col-lg-4">
|
||||||
<Card className="overview__card mb-2" body>
|
<Card className="overview__card mb-2" body>
|
||||||
<CardTitle tag="h5" className="overview__card-title">Short URLs</CardTitle>
|
<CardTitle tag="h5" className="overview__card-title">Short URLs</CardTitle>
|
||||||
<CardText tag="h2">
|
<CardText tag="h2">
|
||||||
|
@ -71,7 +71,7 @@ export const Overview = (
|
||||||
</CardText>
|
</CardText>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-sm-4">
|
<div className="col-md-12 col-lg-4">
|
||||||
<Card className="overview__card mb-2" body>
|
<Card className="overview__card mb-2" body>
|
||||||
<CardTitle tag="h5" className="overview__card-title">Tags</CardTitle>
|
<CardTitle tag="h5" className="overview__card-title">Tags</CardTitle>
|
||||||
<CardText tag="h2">{loadingTags ? 'Loading...' : prettify(tagsList.tags.length)}</CardText>
|
<CardText tag="h2">{loadingTags ? 'Loading...' : prettify(tagsList.tags.length)}</CardText>
|
||||||
|
|
|
@ -17,6 +17,7 @@ $mediumGrey: #dee2e6;
|
||||||
|
|
||||||
// Misc
|
// Misc
|
||||||
$headerHeight: 57px;
|
$headerHeight: 57px;
|
||||||
|
$asideMenuWidth: 260px;
|
||||||
$footer-height: 2.3rem;
|
$footer-height: 2.3rem;
|
||||||
$footer-margin: .8rem;
|
$footer-margin: .8rem;
|
||||||
|
|
||||||
|
|
|
@ -4,11 +4,11 @@ import { Button, Card, Nav, NavLink, Progress } from 'reactstrap';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import { faCalendarAlt, faMapMarkedAlt, faList, faChartPie } from '@fortawesome/free-solid-svg-icons';
|
import { faCalendarAlt, faMapMarkedAlt, faList, faChartPie } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { IconDefinition } from '@fortawesome/fontawesome-common-types';
|
import { IconDefinition } from '@fortawesome/fontawesome-common-types';
|
||||||
import moment from 'moment';
|
|
||||||
import { DateRangeSelector } from '../utils/dates/DateRangeSelector';
|
import { DateRangeSelector } from '../utils/dates/DateRangeSelector';
|
||||||
import Message from '../utils/Message';
|
import Message from '../utils/Message';
|
||||||
import { formatDate } from '../utils/helpers/date';
|
import { formatIsoDate } from '../utils/helpers/date';
|
||||||
import { ShlinkVisitsParams } from '../utils/services/types';
|
import { ShlinkVisitsParams } from '../utils/services/types';
|
||||||
|
import { DateInterval, DateRange, intervalToDateRange } from '../utils/dates/types';
|
||||||
import SortableBarGraph from './helpers/SortableBarGraph';
|
import SortableBarGraph from './helpers/SortableBarGraph';
|
||||||
import GraphCard from './helpers/GraphCard';
|
import GraphCard from './helpers/GraphCard';
|
||||||
import LineChartCard from './helpers/LineChartCard';
|
import LineChartCard from './helpers/LineChartCard';
|
||||||
|
@ -46,12 +46,11 @@ const highlightedVisitsToStats = (
|
||||||
|
|
||||||
return acc;
|
return acc;
|
||||||
}, {});
|
}, {});
|
||||||
const format = formatDate();
|
|
||||||
let selectedBar: string | undefined;
|
let selectedBar: string | undefined;
|
||||||
|
const initialInterval: DateInterval = 'last30Days';
|
||||||
|
|
||||||
const VisitsStats: FC<VisitsStatsProps> = ({ children, visitsInfo, getVisits, cancelGetVisits }) => {
|
const VisitsStats: FC<VisitsStatsProps> = ({ children, visitsInfo, getVisits, cancelGetVisits }) => {
|
||||||
const [ startDate, setStartDate ] = useState<moment.Moment | null>(null);
|
const [ dateRange, setDateRange ] = useState<DateRange>(intervalToDateRange(initialInterval));
|
||||||
const [ endDate, setEndDate ] = useState<moment.Moment | null>(null);
|
|
||||||
const [ highlightedVisits, setHighlightedVisits ] = useState<NormalizedVisit[]>([]);
|
const [ highlightedVisits, setHighlightedVisits ] = useState<NormalizedVisit[]>([]);
|
||||||
const [ highlightedLabel, setHighlightedLabel ] = useState<string | undefined>();
|
const [ highlightedLabel, setHighlightedLabel ] = useState<string | undefined>();
|
||||||
const [ activeSection, setActiveSection ] = useState<Section>('byTime');
|
const [ activeSection, setActiveSection ] = useState<Section>('byTime');
|
||||||
|
@ -83,10 +82,12 @@ const VisitsStats: FC<VisitsStatsProps> = ({ children, visitsInfo, getVisits, ca
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => () => cancelGetVisits(), []);
|
useEffect(() => cancelGetVisits, []);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getVisits({ startDate: format(startDate) ?? undefined, endDate: format(endDate) ?? undefined });
|
const { startDate, endDate } = dateRange;
|
||||||
}, [ startDate, endDate ]);
|
|
||||||
|
getVisits({ startDate: formatIsoDate(startDate) ?? undefined, endDate: formatIsoDate(endDate) ?? undefined });
|
||||||
|
}, [ dateRange ]);
|
||||||
|
|
||||||
const renderVisitsContent = () => {
|
const renderVisitsContent = () => {
|
||||||
if (loadingLarge) {
|
if (loadingLarge) {
|
||||||
|
@ -227,11 +228,9 @@ const VisitsStats: FC<VisitsStatsProps> = ({ children, visitsInfo, getVisits, ca
|
||||||
<div className="col-lg-7 col-xl-6">
|
<div className="col-lg-7 col-xl-6">
|
||||||
<DateRangeSelector
|
<DateRangeSelector
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
|
initialDateRange={initialInterval}
|
||||||
defaultText="All visits"
|
defaultText="All visits"
|
||||||
onDatesChange={({ startDate: newStartDate, endDate: newEndDate }) => {
|
onDatesChange={setDateRange}
|
||||||
setStartDate(newStartDate ?? null);
|
|
||||||
setEndDate(newEndDate ?? null);
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{visits.length > 0 && (
|
{visits.length > 0 && (
|
||||||
|
|
|
@ -15,13 +15,13 @@ describe('<ShlinkVersionsContainer />', () => {
|
||||||
afterEach(() => wrapper?.unmount());
|
afterEach(() => wrapper?.unmount());
|
||||||
|
|
||||||
test.each([
|
test.each([
|
||||||
[ null, 'col-12' ],
|
[ null, 'text-center' ],
|
||||||
[ Mock.of<NotFoundServer>({ serverNotFound: true }), 'col-12' ],
|
[ Mock.of<NotFoundServer>({ serverNotFound: true }), 'text-center' ],
|
||||||
[ Mock.of<NonReachableServer>({ serverNotReachable: true }), 'col-12' ],
|
[ Mock.of<NonReachableServer>({ serverNotReachable: true }), 'text-center' ],
|
||||||
[ Mock.of<ReachableServer>({ printableVersion: 'v1.0.0' }), 'col-lg-10 offset-lg-2 col-md-9 offset-md-3' ],
|
[ Mock.of<ReachableServer>({ printableVersion: 'v1.0.0' }), 'text-center shlink-versions-container--with-server' ],
|
||||||
])('renders proper col classes based on type of selected server', (selectedServer, expectedClasses) => {
|
])('renders proper col classes based on type of selected server', (selectedServer, expectedClasses) => {
|
||||||
const wrapper = createWrapper(selectedServer);
|
const wrapper = createWrapper(selectedServer);
|
||||||
|
|
||||||
expect(wrapper.find('div').at(1).prop('className')).toEqual(`text-center ${expectedClasses}`);
|
expect(wrapper.find('div').prop('className')).toEqual(`${expectedClasses}`);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import { shallow, ShallowWrapper } from 'enzyme';
|
import { shallow, ShallowWrapper } from 'enzyme';
|
||||||
import { DropdownItem } from 'reactstrap';
|
import { DropdownItem } from 'reactstrap';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
|
import { Mock } from 'ts-mockery';
|
||||||
import { DateRangeSelector, DateRangeSelectorProps } from '../../../src/utils/dates/DateRangeSelector';
|
import { DateRangeSelector, DateRangeSelectorProps } from '../../../src/utils/dates/DateRangeSelector';
|
||||||
import { DateInterval } from '../../../src/utils/dates/types';
|
import { DateInterval } from '../../../src/utils/dates/types';
|
||||||
import { Mock } from 'ts-mockery';
|
|
||||||
|
|
||||||
describe('<DateRangeSelector />', () => {
|
describe('<DateRangeSelector />', () => {
|
||||||
let wrapper: ShallowWrapper;
|
let wrapper: ShallowWrapper;
|
||||||
|
|
Loading…
Reference in a new issue