Merge pull request #353 from acelaya-forks/feature/more-ui-improvements

Feature/more UI improvements
This commit is contained in:
Alejandro Celaya 2020-12-15 19:04:04 +01:00 committed by GitHub
commit 623deec973
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 52 additions and 42 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,9 @@
@import '../utils/base';
.shlink-versions-container--with-server {
margin-left: 0;
@media (min-width: $mdMin) {
margin-left: $asideMenuWidth;
}
}

View file

@ -1,24 +1,21 @@
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>
); );
}; };

View file

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

View file

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

View file

@ -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 && (

View file

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

View file

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