feat ui: dashboards redesign start
This commit is contained in:
parent
c90b82e06f
commit
e05f143812
6 changed files with 140 additions and 130 deletions
|
|
@ -1,18 +1,75 @@
|
|||
import { LockOutlined, TeamOutlined } from '@ant-design/icons';
|
||||
import { Switch, Table, TableColumnsType, Tag, Tooltip } from 'antd';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import React from 'react';
|
||||
import { NoContent, Pagination } from 'UI';
|
||||
import { useStore } from 'App/mstore';
|
||||
import { sliceListPerPage } from 'App/utils';
|
||||
import DashboardListItem from './DashboardListItem';
|
||||
import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG';
|
||||
import { Tooltip } from 'antd';
|
||||
import { connect } from 'react-redux';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
|
||||
function DashboardList() {
|
||||
import { checkForRecent } from 'App/date';
|
||||
import { useStore } from 'App/mstore';
|
||||
import Dashboard from 'App/mstore/types/dashboard';
|
||||
import { dashboardSelected, withSiteId } from 'App/routes';
|
||||
import { NoContent } from 'UI';
|
||||
|
||||
import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG';
|
||||
|
||||
function DashboardList({ history, siteId }: { history: any; siteId: string }) {
|
||||
const { dashboardStore } = useStore();
|
||||
const list = dashboardStore.filteredList;
|
||||
const dashboardsSearch = dashboardStore.filter.query;
|
||||
const lenth = list.length;
|
||||
|
||||
|
||||
const tableConfig: TableColumnsType<Dashboard> = [
|
||||
{
|
||||
title: 'Title',
|
||||
dataIndex: 'name',
|
||||
width: '25%',
|
||||
render: (t) => <div className="link capitalize-first">{t}</div>,
|
||||
},
|
||||
{
|
||||
title: 'Description',
|
||||
ellipsis: {
|
||||
showTitle: false,
|
||||
},
|
||||
width: '25%',
|
||||
dataIndex: 'description',
|
||||
},
|
||||
{
|
||||
title: 'Last Modified',
|
||||
dataIndex: 'updatedAt',
|
||||
width: '16.67%',
|
||||
sorter: (a, b) => a.updatedAt.toMillis() - b.updatedAt.toMillis(),
|
||||
sortDirections: ['ascend', 'descend'],
|
||||
render: (date) => checkForRecent(date, 'LLL dd, yyyy, hh:mm a'),
|
||||
},
|
||||
{
|
||||
title: 'Modified By',
|
||||
dataIndex: 'updatedBy',
|
||||
width: '16.67%',
|
||||
sorter: (a, b) => a.updatedBy.localeCompare(b.updatedBy),
|
||||
sortDirections: ['ascend', 'descend'],
|
||||
},
|
||||
{
|
||||
title: (
|
||||
<div className={'flex items-center justify-between'}>
|
||||
<div>Visibility</div>
|
||||
<Switch checked={!dashboardStore.filter.showMine} onChange={() =>
|
||||
dashboardStore.updateKey('filter', {
|
||||
...dashboardStore.filter,
|
||||
showMine: !dashboardStore.filter.showMine,
|
||||
})} checkedChildren={'Public'} unCheckedChildren={'Private'} />
|
||||
</div>
|
||||
),
|
||||
width: '16.67%',
|
||||
dataIndex: 'isPublic',
|
||||
render: (isPublic: boolean) => (
|
||||
<Tag icon={isPublic ? <TeamOutlined /> : <LockOutlined />}>
|
||||
{isPublic ? 'Team' : 'Private'}
|
||||
</Tag>
|
||||
),
|
||||
},
|
||||
];
|
||||
return (
|
||||
<NoContent
|
||||
show={lenth === 0}
|
||||
|
|
@ -20,48 +77,53 @@ function DashboardList() {
|
|||
<div className="flex flex-col items-center justify-center">
|
||||
<AnimatedSVG name={ICONS.NO_DASHBOARDS} size={180} />
|
||||
<div className="text-center mt-4">
|
||||
{dashboardsSearch !== '' ? 'No matching results' : "You haven't created any dashboards yet"}
|
||||
{dashboardsSearch !== ''
|
||||
? 'No matching results'
|
||||
: "You haven't created any dashboards yet"}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
subtext={
|
||||
<div>
|
||||
A Dashboard is a collection of <Tooltip title={<div className="text-center">Utilize cards to visualize key user interactions or product performance metrics.</div>} className="text-center"><span className="underline decoration-dotted">Cards</span></Tooltip> that can be shared across teams.
|
||||
A Dashboard is a collection of{' '}
|
||||
<Tooltip
|
||||
title={
|
||||
<div className="text-center">
|
||||
Utilize cards to visualize key user interactions or product
|
||||
performance metrics.
|
||||
</div>
|
||||
}
|
||||
className="text-center"
|
||||
>
|
||||
<span className="underline decoration-dotted">Cards</span>
|
||||
</Tooltip>{' '}
|
||||
that can be shared across teams.
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="mt-3 border-b">
|
||||
<div className="grid grid-cols-12 py-2 font-medium px-6">
|
||||
<div className="col-span-8">Title</div>
|
||||
<div className="col-span-2">Visibility</div>
|
||||
<div className="col-span-2 text-right">Last Modified</div>
|
||||
</div>
|
||||
|
||||
{sliceListPerPage(list, dashboardStore.page - 1, dashboardStore.pageSize).map(
|
||||
(dashboard: any) => (
|
||||
<React.Fragment key={dashboard.dashboardId}>
|
||||
<DashboardListItem dashboard={dashboard} />
|
||||
</React.Fragment>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="w-full flex items-center justify-between pt-4 px-6">
|
||||
<div className="text-disabled-text">
|
||||
Showing{' '}
|
||||
<span className="font-semibold">{Math.min(list.length, dashboardStore.pageSize)}</span>{' '}
|
||||
out of <span className="font-semibold">{list.length}</span> Dashboards
|
||||
</div>
|
||||
<Pagination
|
||||
page={dashboardStore.page}
|
||||
total={lenth}
|
||||
onPageChange={(page) => dashboardStore.updateKey('page', page)}
|
||||
limit={dashboardStore.pageSize}
|
||||
debounceRequest={100}
|
||||
/>
|
||||
</div>
|
||||
<Table
|
||||
dataSource={list}
|
||||
columns={tableConfig}
|
||||
pagination={{
|
||||
showTotal: (total, range) =>
|
||||
`Showing ${range[0]}-${range[1]} of ${total} items`,
|
||||
size: 'small',
|
||||
}}
|
||||
onRow={(record) => ({
|
||||
onClick: () => {
|
||||
dashboardStore.selectDashboardById(record.dashboardId);
|
||||
const path = withSiteId(
|
||||
dashboardSelected(record.dashboardId),
|
||||
siteId
|
||||
);
|
||||
history.push(path);
|
||||
},
|
||||
})}
|
||||
/>
|
||||
</NoContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default observer(DashboardList);
|
||||
export default connect((state: any) => ({
|
||||
siteId: state.getIn(['site', 'siteId']),
|
||||
}))(withRouter(observer(DashboardList)));
|
||||
|
|
|
|||
|
|
@ -1,48 +0,0 @@
|
|||
import React from 'react';
|
||||
import { Icon } from 'UI';
|
||||
import { connect } from 'react-redux';
|
||||
import { IDashboard } from 'App/mstore/types/dashboard';
|
||||
import { checkForRecent } from 'App/date';
|
||||
import { withSiteId, dashboardSelected } from 'App/routes';
|
||||
import { useStore } from 'App/mstore';
|
||||
import { withRouter, RouteComponentProps } from 'react-router-dom';
|
||||
|
||||
interface Props extends RouteComponentProps {
|
||||
dashboard: IDashboard;
|
||||
siteId: string;
|
||||
}
|
||||
|
||||
function DashboardListItem(props: Props) {
|
||||
const { dashboard, siteId, history } = props;
|
||||
const { dashboardStore } = useStore();
|
||||
|
||||
const onItemClick = () => {
|
||||
dashboardStore.selectDashboardById(dashboard.dashboardId);
|
||||
const path = withSiteId(dashboardSelected(dashboard.dashboardId), siteId);
|
||||
history.push(path);
|
||||
};
|
||||
return (
|
||||
<div className="hover:bg-active-blue cursor-pointer border-t px-6" onClick={onItemClick}>
|
||||
<div className="grid grid-cols-12 py-4 select-none items-center">
|
||||
<div className="col-span-8 flex items-start">
|
||||
<div className="flex items-center capitalize-first">
|
||||
<div className="w-9 h-9 rounded-full bg-tealx-lightest flex items-center justify-center mr-2">
|
||||
<Icon name="columns-gap" size="16" color="tealx" />
|
||||
</div>
|
||||
<div className="link capitalize-first">{dashboard.name}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<div className="flex items-center">
|
||||
<Icon name={dashboard.isPublic ? 'user-friends' : 'person-fill'} className="mr-2" />
|
||||
<span>{dashboard.isPublic ? 'Team' : 'Private'}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-2 text-right">{checkForRecent(dashboard.createdAt, 'LLL dd, yyyy, hh:mm a')}</div>
|
||||
</div>
|
||||
{dashboard.description ? <div className="color-gray-medium px-2 pb-2">{dashboard.description}</div> : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
// @ts-ignore
|
||||
export default connect((state) => ({ siteId: state.getIn(['site', 'siteId']) }))(withRouter(DashboardListItem));
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { useStore } from 'App/mstore';
|
||||
import { Icon } from 'UI';
|
||||
import { debounce } from 'App/utils';
|
||||
import { Input } from 'antd';
|
||||
|
||||
let debounceUpdate: any = () => {};
|
||||
|
||||
|
|
@ -24,16 +24,15 @@ function DashboardSearch() {
|
|||
};
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<Icon name="search" className="absolute top-0 bottom-0 ml-2 m-auto" size="16" />
|
||||
<input
|
||||
value={query}
|
||||
name="dashboardsSearch"
|
||||
className="bg-white p-2 border border-borderColor-gray-light-shade rounded w-full pl-10"
|
||||
placeholder="Filter by title or description"
|
||||
onChange={write}
|
||||
/>
|
||||
</div>
|
||||
<Input.Search
|
||||
value={query}
|
||||
allowClear
|
||||
name="dashboardsSearch"
|
||||
className="w-full"
|
||||
placeholder="Filter by title or description"
|
||||
onChange={write}
|
||||
onSearch={(value) => dashboardStore.updateKey('filter', { ...dashboardStore.filter, query: value })}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
import React from 'react';
|
||||
import { Button, PageTitle, Toggler, Icon } from 'UI';
|
||||
import Select from 'Shared/Select';
|
||||
import { PageTitle } from 'UI';
|
||||
import DashboardSearch from './DashboardSearch';
|
||||
import { useStore } from 'App/mstore';
|
||||
import { observer, useObserver } from 'mobx-react-lite';
|
||||
import { withSiteId } from 'App/routes';
|
||||
import { Button } from 'antd'
|
||||
import { PlusOutlined } from "@ant-design/icons";
|
||||
|
||||
function Header({ history, siteId }: { history: any; siteId: string }) {
|
||||
const { dashboardStore } = useStore();
|
||||
|
|
@ -19,46 +20,32 @@ function Header({ history, siteId }: { history: any; siteId: string }) {
|
|||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex items-center justify-between px-6">
|
||||
<div className="flex items-center justify-between px-4 pb-2">
|
||||
<div className="flex items-baseline mr-3">
|
||||
<PageTitle title="Dashboards" />
|
||||
</div>
|
||||
<div className="ml-auto flex items-center">
|
||||
<Button variant="primary" onClick={onAddDashboardClick}>
|
||||
New Dashboard
|
||||
<Button
|
||||
icon={<PlusOutlined />}
|
||||
type="primary" onClick={onAddDashboardClick}>
|
||||
Create Dashboard
|
||||
</Button>
|
||||
<div className="mx-2"></div>
|
||||
<div className="w-1/4" style={{ minWidth: 300 }}>
|
||||
<DashboardSearch />
|
||||
</div>
|
||||
</div>
|
||||
{/*<Select*/}
|
||||
{/* options={[*/}
|
||||
{/* { label: 'Newest', value: 'desc' },*/}
|
||||
{/* { label: 'Oldest', value: 'asc' },*/}
|
||||
{/* ]}*/}
|
||||
{/* defaultValue={sort.by}*/}
|
||||
{/* plain*/}
|
||||
{/* onChange={({ value }) => dashboardStore.updateKey('sort', { by: value.value })}*/}
|
||||
{/*/>*/}
|
||||
</div>
|
||||
<div className="border-y px-3 py-1 mt-2 flex items-center w-full justify-end gap-4">
|
||||
<Toggler
|
||||
label="Private Dashboards"
|
||||
checked={dashboardStore.filter.showMine}
|
||||
name="test"
|
||||
className="font-medium mr-2"
|
||||
onChange={() =>
|
||||
dashboardStore.updateKey('filter', {
|
||||
...dashboardStore.filter,
|
||||
showMine: !dashboardStore.filter.showMine,
|
||||
})
|
||||
}
|
||||
/>
|
||||
|
||||
<Select
|
||||
options={[
|
||||
{ label: 'Newest', value: 'desc' },
|
||||
{ label: 'Oldest', value: 'asc' },
|
||||
]}
|
||||
defaultValue={sort.by}
|
||||
plain
|
||||
onChange={({ value }) => dashboardStore.updateKey('sort', { by: value.value })}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { DateTime } from 'luxon';
|
|||
export default class Dashboard {
|
||||
public static get ID_KEY():string { return "dashboardId" }
|
||||
dashboardId?: string = undefined
|
||||
key: string = ""
|
||||
name: string = "Untitled Dashboard"
|
||||
description: string = ""
|
||||
isPublic: boolean = true
|
||||
|
|
@ -23,6 +24,14 @@ export default class Dashboard {
|
|||
this.validate();
|
||||
}
|
||||
|
||||
get updatedAt() {
|
||||
return this.createdAt as unknown as DateTime
|
||||
}
|
||||
|
||||
get updatedBy() {
|
||||
return "no api"
|
||||
}
|
||||
|
||||
update(data: any) {
|
||||
runInAction(() => {
|
||||
Object.assign(this, data)
|
||||
|
|
@ -54,6 +63,7 @@ export default class Dashboard {
|
|||
this.name = json.name
|
||||
this.description = json.description
|
||||
this.isPublic = json.isPublic
|
||||
this.key = json.dashboardId
|
||||
this.createdAt = DateTime.fromMillis(new Date(json.createdAt).getTime())
|
||||
if (json.widgets) {
|
||||
const smallWidgets: any[] = json.widgets.filter(wi => wi.config.col === 1)
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ button {
|
|||
}
|
||||
|
||||
textarea:focus,
|
||||
input:not(.ant-input-number-input, .nofocus):focus {
|
||||
input:not(.ant-input-number-input, .ant-input, .nofocus):focus {
|
||||
border: solid thin $teal !important;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue