commit
5f64bc90dc
30 changed files with 592 additions and 13 deletions
|
|
@ -24,7 +24,8 @@ const siteIdRequiredPaths = [
|
||||||
'/heatmaps',
|
'/heatmaps',
|
||||||
'/custom_metrics',
|
'/custom_metrics',
|
||||||
'/dashboards',
|
'/dashboards',
|
||||||
'/metrics'
|
'/metrics',
|
||||||
|
'/trails',
|
||||||
// '/custom_metrics/sessions',
|
// '/custom_metrics/sessions',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { JSONTree } from 'UI';
|
||||||
|
import { checkForRecent } from 'App/date';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
audit: any;
|
||||||
|
}
|
||||||
|
function AuditDetailModal(props: Props) {
|
||||||
|
const { audit } = props;
|
||||||
|
// const jsonResponse = typeof audit.payload === 'string' ? JSON.parse(audit.payload) : audit.payload;
|
||||||
|
// console.log('jsonResponse', jsonResponse)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ width: '500px' }} className="bg-white h-screen overflow-y-auto">
|
||||||
|
<h1 className="text-2xl p-4">Audit Details</h1>
|
||||||
|
<div className="p-4">
|
||||||
|
<h5 className="mb-2">{ 'URL'}</h5>
|
||||||
|
<div className="color-gray-darkest p-2 bg-gray-lightest rounded">{ audit.endPoint }</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 my-6">
|
||||||
|
<div className="">
|
||||||
|
<div className="font-medium mb-2">Username</div>
|
||||||
|
<div>{audit.username}</div>
|
||||||
|
</div>
|
||||||
|
<div className="">
|
||||||
|
<div className="font-medium mb-2">Created At</div>
|
||||||
|
<div>{audit.createdAt && checkForRecent(audit.createdAt, 'LLL dd, yyyy, hh:mm a')}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 my-6">
|
||||||
|
<div className="">
|
||||||
|
<div className="font-medium mb-2">Action</div>
|
||||||
|
<div>{audit.action}</div>
|
||||||
|
</div>
|
||||||
|
<div className="">
|
||||||
|
<div className="font-medium mb-2">Method</div>
|
||||||
|
<div>{audit.method}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{ audit.payload && (
|
||||||
|
<div className="my-6">
|
||||||
|
<div className="font-medium mb-3">Payload</div>
|
||||||
|
<JSONTree src={ audit.payload } collapsed={ false } enableClipboard />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AuditDetailModal;
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
export { default } from './AuditDetailModal';
|
||||||
67
frontend/app/components/Client/Audit/AuditList/AuditList.tsx
Normal file
67
frontend/app/components/Client/Audit/AuditList/AuditList.tsx
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
import { useModal } from 'App/components/Modal';
|
||||||
|
import { useStore } from 'App/mstore';
|
||||||
|
import { useObserver } from 'mobx-react-lite';
|
||||||
|
import React, { useEffect } from 'react';
|
||||||
|
import { Loader, Pagination, NoContent } from 'UI';
|
||||||
|
import AuditDetailModal from '../AuditDetailModal';
|
||||||
|
import AuditListItem from '../AuditListItem';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
|
||||||
|
}
|
||||||
|
function AuditList(props: Props) {
|
||||||
|
const { auditStore } = useStore();
|
||||||
|
const loading = useObserver(() => auditStore.isLoading);
|
||||||
|
const list = useObserver(() => auditStore.list);
|
||||||
|
const searchQuery = useObserver(() => auditStore.searchQuery);
|
||||||
|
const page = useObserver(() => auditStore.page);
|
||||||
|
const order = useObserver(() => auditStore.order);
|
||||||
|
const period = useObserver(() => auditStore.period);
|
||||||
|
const { showModal } = useModal();
|
||||||
|
console.log('AuditList', period.toTimestamps());
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const { startTimestamp, endTimestamp } = period.toTimestamps();
|
||||||
|
auditStore.fetchAudits({
|
||||||
|
page: auditStore.page,
|
||||||
|
limit: auditStore.pageSize,
|
||||||
|
query: auditStore.searchQuery,
|
||||||
|
order: auditStore.order,
|
||||||
|
startDate: startTimestamp,
|
||||||
|
endDate: endTimestamp,
|
||||||
|
});
|
||||||
|
}, [page, searchQuery, order, period]);
|
||||||
|
|
||||||
|
return useObserver(() => (
|
||||||
|
<Loader loading={loading}>
|
||||||
|
<NoContent show={list.length === 0} animatedIcon="empty-state">
|
||||||
|
<div className="px-2 grid grid-cols-12 gap-4 items-center py-3 font-medium">
|
||||||
|
<div className="col-span-5">Name</div>
|
||||||
|
<div className="col-span-4">Status</div>
|
||||||
|
<div className="col-span-3">Time</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{list.map((item, index) => (
|
||||||
|
<div className="px-2 border-t hover:bg-active-blue" key={index}>
|
||||||
|
<AuditListItem
|
||||||
|
audit={item}
|
||||||
|
onShowDetails={() => showModal(<AuditDetailModal audit={item} />, { right: true })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<div className="w-full flex items-center justify-center py-10">
|
||||||
|
<Pagination
|
||||||
|
page={auditStore.page}
|
||||||
|
totalPages={Math.ceil(auditStore.total / auditStore.pageSize)}
|
||||||
|
onPageChange={(page) => auditStore.updateKey('page', page)}
|
||||||
|
limit={auditStore.pageSize}
|
||||||
|
debounceRequest={200}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</NoContent>
|
||||||
|
</Loader>
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AuditList;
|
||||||
1
frontend/app/components/Client/Audit/AuditList/index.ts
Normal file
1
frontend/app/components/Client/Audit/AuditList/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
export { default } from './AuditList'
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { checkForRecent } from 'App/date';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
audit: any;
|
||||||
|
onShowDetails: () => void;
|
||||||
|
}
|
||||||
|
function AuditListItem(props: Props) {
|
||||||
|
const { audit, onShowDetails } = props;
|
||||||
|
return (
|
||||||
|
<div className="grid grid-cols-12 gap-4 items-center py-3">
|
||||||
|
<div className="col-span-5">{audit.username}</div>
|
||||||
|
<div className="col-span-4 link cursor-pointer select-none" onClick={onShowDetails}>{audit.action}</div>
|
||||||
|
<div className="col-span-3">{audit.createdAt && checkForRecent(audit.createdAt, 'LLL dd, yyyy, hh:mm a')}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AuditListItem;
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
export { default } from './AuditListItem';
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
import React, { useEffect } from 'react';
|
||||||
|
import { Icon } from 'UI';
|
||||||
|
import { debounce } from 'App/utils';
|
||||||
|
|
||||||
|
let debounceUpdate: any = () => {}
|
||||||
|
interface Props {
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
}
|
||||||
|
function AuditSearchField(props: Props) {
|
||||||
|
const { onChange } = props;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
debounceUpdate = debounce((value) => onChange(value), 500);
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const write = ({ target: { name, value } }) => {
|
||||||
|
debounceUpdate(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative" style={{ width: '220px'}}>
|
||||||
|
<Icon name="search" className="absolute top-0 bottom-0 ml-3 m-auto" size="16" />
|
||||||
|
<input
|
||||||
|
name="searchQuery"
|
||||||
|
className="bg-white p-2 border border-gray-light rounded w-full pl-10"
|
||||||
|
placeholder="Filter by Name"
|
||||||
|
onChange={write}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AuditSearchField;
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
export { default } from './AuditSearchField';
|
||||||
65
frontend/app/components/Client/Audit/AuditView/AuditView.tsx
Normal file
65
frontend/app/components/Client/Audit/AuditView/AuditView.tsx
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { PageTitle, Icon } from 'UI';
|
||||||
|
import AuditList from '../AuditList';
|
||||||
|
import AuditSearchField from '../AuditSearchField';
|
||||||
|
import { useStore } from 'App/mstore';
|
||||||
|
import { useObserver } from 'mobx-react-lite';
|
||||||
|
import Select from 'Shared/Select';
|
||||||
|
import SelectDateRange from 'Shared/SelectDateRange';
|
||||||
|
|
||||||
|
function AuditView(props) {
|
||||||
|
const { auditStore } = useStore();
|
||||||
|
const order = useObserver(() => auditStore.order);
|
||||||
|
const total = useObserver(() => auditStore.total);
|
||||||
|
|
||||||
|
const exportToCsv = () => {
|
||||||
|
auditStore.exportToCsv();
|
||||||
|
}
|
||||||
|
|
||||||
|
const onChange = (data) => {
|
||||||
|
auditStore.setDateRange(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
return useObserver(() => (
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center mb-4">
|
||||||
|
<PageTitle title={
|
||||||
|
<div className="flex items-center">
|
||||||
|
<span>Audit Trail</span>
|
||||||
|
<span className="color-gray-medium ml-2">{total}</span>
|
||||||
|
</div>
|
||||||
|
} />
|
||||||
|
<div className="flex items-center ml-auto">
|
||||||
|
<div className="mx-2">
|
||||||
|
<SelectDateRange
|
||||||
|
period={auditStore.period}
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="mx-2">
|
||||||
|
<Select
|
||||||
|
options={[
|
||||||
|
{ label: 'Newest First', value: 'desc' },
|
||||||
|
{ label: 'Oldest First', value: 'asc' },
|
||||||
|
]}
|
||||||
|
defaultValue={order}
|
||||||
|
plain
|
||||||
|
onChange={({ value }) => auditStore.updateKey('order', value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<AuditSearchField onChange={(value) => auditStore.updateKey('searchQuery', value) }/>
|
||||||
|
<div>
|
||||||
|
<button className="color-teal flex items-center ml-3" onClick={exportToCsv}>
|
||||||
|
<Icon name="grid-3x3" />
|
||||||
|
<span className="ml-2">Export to CSV</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<AuditList />
|
||||||
|
</div>
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AuditView;
|
||||||
1
frontend/app/components/Client/Audit/AuditView/index.ts
Normal file
1
frontend/app/components/Client/Audit/AuditView/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
export { default } from './AuditView'
|
||||||
|
|
@ -8,6 +8,7 @@ import ProfileSettings from './ProfileSettings';
|
||||||
import Integrations from './Integrations';
|
import Integrations from './Integrations';
|
||||||
import ManageUsers from './ManageUsers';
|
import ManageUsers from './ManageUsers';
|
||||||
import UserView from './Users/UsersView';
|
import UserView from './Users/UsersView';
|
||||||
|
import AuditView from './Audit/AuditView';
|
||||||
import Sites from './Sites';
|
import Sites from './Sites';
|
||||||
import CustomFields from './CustomFields';
|
import CustomFields from './CustomFields';
|
||||||
import Webhooks from './Webhooks';
|
import Webhooks from './Webhooks';
|
||||||
|
|
@ -43,6 +44,7 @@ export default class Client extends React.PureComponent {
|
||||||
<Route exact strict path={ clientRoute(CLIENT_TABS.WEBHOOKS) } component={ Webhooks } />
|
<Route exact strict path={ clientRoute(CLIENT_TABS.WEBHOOKS) } component={ Webhooks } />
|
||||||
<Route exact strict path={ clientRoute(CLIENT_TABS.NOTIFICATIONS) } component={ Notifications } />
|
<Route exact strict path={ clientRoute(CLIENT_TABS.NOTIFICATIONS) } component={ Notifications } />
|
||||||
<Route exact strict path={ clientRoute(CLIENT_TABS.MANAGE_ROLES) } component={ Roles } />
|
<Route exact strict path={ clientRoute(CLIENT_TABS.MANAGE_ROLES) } component={ Roles } />
|
||||||
|
<Route exact strict path={ clientRoute(CLIENT_TABS.AUDIT) } component={ AuditView } />
|
||||||
<Redirect to={ clientRoute(CLIENT_TABS.PROFILE) } />
|
<Redirect to={ clientRoute(CLIENT_TABS.PROFILE) } />
|
||||||
</Switch>
|
</Switch>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -236,7 +236,7 @@ class ManageUsers extends React.PureComponent {
|
||||||
title="No users are available."
|
title="No users are available."
|
||||||
size="small"
|
size="small"
|
||||||
show={ members.size === 0 }
|
show={ members.size === 0 }
|
||||||
icon
|
animatedIcon="empty-state"
|
||||||
>
|
>
|
||||||
<div className={ styles.list }>
|
<div className={ styles.list }>
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -78,6 +78,17 @@ function PreferencesMenu({ activeTab, appearance, history, isEnterprise }) {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{ isEnterprise && (
|
||||||
|
<div className="mb-4">
|
||||||
|
<SideMenuitem
|
||||||
|
active={ activeTab === CLIENT_TABS.AUDIT }
|
||||||
|
title="Audit"
|
||||||
|
iconName="list-ul"
|
||||||
|
onClick={() => setTab(CLIENT_TABS.AUDIT) }
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<SideMenuitem
|
<SideMenuitem
|
||||||
|
|
@ -95,7 +106,7 @@ function PreferencesMenu({ activeTab, appearance, history, isEnterprise }) {
|
||||||
iconName="bell"
|
iconName="bell"
|
||||||
onClick={() => setTab(CLIENT_TABS.NOTIFICATIONS) }
|
onClick={() => setTab(CLIENT_TABS.NOTIFICATIONS) }
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,17 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Select from 'react-select';
|
import Select, { components, DropdownIndicatorProps } from 'react-select';
|
||||||
|
import { Icon } from 'UI';
|
||||||
|
import colors from 'App/theme/colors';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
options: any[];
|
options: any[];
|
||||||
isSearchable?: boolean;
|
isSearchable?: boolean;
|
||||||
defaultValue?: string;
|
defaultValue?: string;
|
||||||
plain?: boolean;
|
plain?: boolean;
|
||||||
|
components?: any;
|
||||||
[x:string]: any;
|
[x:string]: any;
|
||||||
}
|
}
|
||||||
export default function({ plain = false, options, isSearchable = false, defaultValue = '', ...rest }: Props) {
|
export default function({ plain = false, options, isSearchable = false, components = {}, defaultValue = '', ...rest }: Props) {
|
||||||
const customStyles = {
|
const customStyles = {
|
||||||
option: (provided, state) => ({
|
option: (provided, state) => ({
|
||||||
...provided,
|
...provided,
|
||||||
|
|
@ -17,14 +20,26 @@ export default function({ plain = false, options, isSearchable = false, defaultV
|
||||||
menu: (provided, state) => ({
|
menu: (provided, state) => ({
|
||||||
...provided,
|
...provided,
|
||||||
top: 31,
|
top: 31,
|
||||||
|
minWidth: 'fit-content',
|
||||||
}),
|
}),
|
||||||
control: (provided) => {
|
control: (provided) => {
|
||||||
const obj = {
|
const obj = {
|
||||||
...provided,
|
...provided,
|
||||||
border: 'solid thin #ddd'
|
border: 'solid thin #ddd',
|
||||||
|
cursor: 'pointer',
|
||||||
}
|
}
|
||||||
if (plain) {
|
if (plain) {
|
||||||
obj['border'] = '1px solid transparent'
|
obj['border'] = '1px solid transparent'
|
||||||
|
obj['&:hover'] = {
|
||||||
|
borderColor: 'transparent',
|
||||||
|
backgroundColor: colors['gray-light']
|
||||||
|
}
|
||||||
|
obj['&:focus'] = {
|
||||||
|
borderColor: 'transparent'
|
||||||
|
}
|
||||||
|
obj['&:active'] = {
|
||||||
|
borderColor: 'transparent'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return obj;
|
return obj;
|
||||||
},
|
},
|
||||||
|
|
@ -46,7 +61,9 @@ export default function({ plain = false, options, isSearchable = false, defaultV
|
||||||
isSearchable={isSearchable}
|
isSearchable={isSearchable}
|
||||||
defaultValue={defaultSelected}
|
defaultValue={defaultSelected}
|
||||||
components={{
|
components={{
|
||||||
IndicatorSeparator: () => null
|
IndicatorSeparator: () => null,
|
||||||
|
DropdownIndicator,
|
||||||
|
...components,
|
||||||
}}
|
}}
|
||||||
styles={customStyles}
|
styles={customStyles}
|
||||||
theme={(theme) => ({
|
theme={(theme) => ({
|
||||||
|
|
@ -56,9 +73,18 @@ export default function({ plain = false, options, isSearchable = false, defaultV
|
||||||
primary: '#394EFF',
|
primary: '#394EFF',
|
||||||
}
|
}
|
||||||
})}
|
})}
|
||||||
|
blurInputOnSelect={true}
|
||||||
{...rest}
|
{...rest}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// export default Select;
|
const DropdownIndicator = (
|
||||||
|
props: DropdownIndicatorProps<true>
|
||||||
|
) => {
|
||||||
|
return (
|
||||||
|
<components.DropdownIndicator {...props}>
|
||||||
|
<Icon name="chevron-down" size="18" />
|
||||||
|
</components.DropdownIndicator>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,71 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { DATE_RANGE_OPTIONS, CUSTOM_RANGE } from 'App/dateRange'
|
||||||
|
import Select from 'Shared/Select';
|
||||||
|
import Period, { LAST_7_DAYS } from 'Types/app/period';
|
||||||
|
import { components } from 'react-select';
|
||||||
|
import DateRangePopup from 'Shared/DateRangeDropdown/DateRangePopup';
|
||||||
|
import OutsideClickDetectingDiv from 'Shared/OutsideClickDetectingDiv';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
period: any,
|
||||||
|
onChange: (data: any) => void;
|
||||||
|
}
|
||||||
|
function SelectDateRange(props: Props) {
|
||||||
|
const [isCustom, setIsCustom] = React.useState(false);
|
||||||
|
const { period } = props;
|
||||||
|
const selectedValue = DATE_RANGE_OPTIONS.find(obj => obj.value === period.rangeName)
|
||||||
|
|
||||||
|
const onChange = (value: any) => {
|
||||||
|
if (value === CUSTOM_RANGE) {
|
||||||
|
setIsCustom(true);
|
||||||
|
} else {
|
||||||
|
props.onChange(new Period({ rangeName: value }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onApplyDateRange = (value: any) => {
|
||||||
|
props.onChange(new Period({ rangeName: CUSTOM_RANGE, start: value.start, end: value.end }));
|
||||||
|
setIsCustom(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative">
|
||||||
|
<Select
|
||||||
|
plain
|
||||||
|
value={selectedValue}
|
||||||
|
options={DATE_RANGE_OPTIONS}
|
||||||
|
onChange={({ value }) => onChange(value)}
|
||||||
|
components={{ SingleValue: ({ children, ...props} : any) => {
|
||||||
|
return (
|
||||||
|
<components.SingleValue {...props}>
|
||||||
|
{period.rangeName === CUSTOM_RANGE ? period.rangeFormatted() : children}
|
||||||
|
</components.SingleValue>
|
||||||
|
)
|
||||||
|
} }}
|
||||||
|
period={period}
|
||||||
|
/>
|
||||||
|
{
|
||||||
|
isCustom &&
|
||||||
|
<OutsideClickDetectingDiv
|
||||||
|
onClickOutside={() => setIsCustom(false)}
|
||||||
|
>
|
||||||
|
<div className="absolute top-0 mx-auto mt-10 z-40" style={{
|
||||||
|
width: '770px',
|
||||||
|
margin: 'auto 50vh 0',
|
||||||
|
transform: 'translateX(-50%)'
|
||||||
|
}}>
|
||||||
|
<DateRangePopup
|
||||||
|
onApply={ onApplyDateRange }
|
||||||
|
onCancel={ () => setIsCustom(false) }
|
||||||
|
selectedDateRange={ period.range }
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</OutsideClickDetectingDiv>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SelectDateRange;
|
||||||
|
|
||||||
|
|
||||||
1
frontend/app/components/shared/SelectDateRange/index.ts
Normal file
1
frontend/app/components/shared/SelectDateRange/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
export { default } from './SelectDateRange';
|
||||||
|
|
@ -23,6 +23,13 @@ Object.keys(DATE_RANGE_LABELS).forEach((key) => { DATE_RANGE_VALUES[ key ] = key
|
||||||
export { DATE_RANGE_VALUES };
|
export { DATE_RANGE_VALUES };
|
||||||
export const dateRangeValues = Object.keys(DATE_RANGE_VALUES);
|
export const dateRangeValues = Object.keys(DATE_RANGE_VALUES);
|
||||||
|
|
||||||
|
export const DATE_RANGE_OPTIONS = Object.keys(DATE_RANGE_LABELS).map((key) => {
|
||||||
|
return {
|
||||||
|
label: DATE_RANGE_LABELS[ key ],
|
||||||
|
value: key,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
export function getDateRangeFromTs(start, end) {
|
export function getDateRangeFromTs(start, end) {
|
||||||
return moment.range(
|
return moment.range(
|
||||||
moment(start),
|
moment(start),
|
||||||
|
|
|
||||||
86
frontend/app/mstore/auditStore.ts
Normal file
86
frontend/app/mstore/auditStore.ts
Normal file
|
|
@ -0,0 +1,86 @@
|
||||||
|
import { makeAutoObservable, runInAction, observable, action, reaction } from "mobx"
|
||||||
|
import { auditService } from "App/services"
|
||||||
|
import Audit from './types/audit'
|
||||||
|
import Period, { LAST_7_DAYS } from 'Types/app/period';
|
||||||
|
import { toast } from 'react-toastify';
|
||||||
|
import { exportCSVFile } from 'App/utils';
|
||||||
|
import { formatDateTimeDefault } from 'App/date';
|
||||||
|
import { DateTime, Duration } from 'luxon'; // TODO
|
||||||
|
|
||||||
|
export default class AuditStore {
|
||||||
|
list: any[] = [];
|
||||||
|
total: number = 0;
|
||||||
|
page: number = 1;
|
||||||
|
pageSize: number = 20;
|
||||||
|
searchQuery: string = '';
|
||||||
|
isLoading: boolean = false;
|
||||||
|
order: string = 'desc';
|
||||||
|
period: Period|null = Period({ rangeName: LAST_7_DAYS })
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
makeAutoObservable(this, {
|
||||||
|
searchQuery: observable,
|
||||||
|
period: observable,
|
||||||
|
updateKey: action,
|
||||||
|
fetchAudits: action,
|
||||||
|
setDateRange: action,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
setDateRange(data: any) {
|
||||||
|
this['period'] = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateKey(key: string, value: any) {
|
||||||
|
this[key] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchAudits = (data: any): Promise<void> => {
|
||||||
|
this.isLoading = true;
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
auditService.all(data).then(response => {
|
||||||
|
runInAction(() => {
|
||||||
|
this.list = response.sessions.map(item => Audit.fromJson(item))
|
||||||
|
this.total = response.count
|
||||||
|
})
|
||||||
|
resolve()
|
||||||
|
}).catch(error => {
|
||||||
|
reject(error)
|
||||||
|
}).finally(() => {
|
||||||
|
this.isLoading = false;
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchAllAudits = async (data: any): Promise<any> => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
auditService.all(data).then((data) => {
|
||||||
|
const headers = [
|
||||||
|
{ label: 'User', key: 'username' },
|
||||||
|
{ label: 'Email', key: 'email' },
|
||||||
|
{ label: 'UserID', key: 'userId' },
|
||||||
|
{ label: 'Method', key: 'method' },
|
||||||
|
{ label: 'Action', key: 'action' },
|
||||||
|
{ label: 'Endpoint', key: 'endpoint' },
|
||||||
|
{ label: 'Created At', key: 'createdAt' },
|
||||||
|
]
|
||||||
|
data = data.sessions.map(item => ({
|
||||||
|
...item,
|
||||||
|
createdAt: DateTime.fromMillis(item.createdAt).toFormat('LLL dd yyyy hh:mm a')
|
||||||
|
}))
|
||||||
|
exportCSVFile(headers, data, `audit-${new Date().toLocaleDateString()}`);
|
||||||
|
resolve(data)
|
||||||
|
}).catch(error => {
|
||||||
|
reject(error)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
exportToCsv = async (): Promise<void> => {
|
||||||
|
const promise = this.fetchAllAudits({ limit: this.total })
|
||||||
|
toast.promise(promise, {
|
||||||
|
pending: 'Exporting...',
|
||||||
|
success: 'Export successful',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -4,8 +4,9 @@ import MetricStore, { IMetricStore } from './metricStore';
|
||||||
import UserStore from './userStore';
|
import UserStore from './userStore';
|
||||||
import RoleStore from './roleStore';
|
import RoleStore from './roleStore';
|
||||||
import APIClient from 'App/api_client';
|
import APIClient from 'App/api_client';
|
||||||
import { dashboardService, metricService, sessionService, userService } from 'App/services';
|
import { dashboardService, metricService, sessionService, userService, auditService } from 'App/services';
|
||||||
import SettingsStore from './settingsStore';
|
import SettingsStore from './settingsStore';
|
||||||
|
import AuditStore from './auditStore';
|
||||||
|
|
||||||
export class RootStore {
|
export class RootStore {
|
||||||
dashboardStore: IDashboardSotre;
|
dashboardStore: IDashboardSotre;
|
||||||
|
|
@ -13,6 +14,7 @@ export class RootStore {
|
||||||
settingsStore: SettingsStore;
|
settingsStore: SettingsStore;
|
||||||
userStore: UserStore;
|
userStore: UserStore;
|
||||||
roleStore: RoleStore;
|
roleStore: RoleStore;
|
||||||
|
auditStore: AuditStore;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.dashboardStore = new DashboardStore();
|
this.dashboardStore = new DashboardStore();
|
||||||
|
|
@ -20,6 +22,7 @@ export class RootStore {
|
||||||
this.settingsStore = new SettingsStore();
|
this.settingsStore = new SettingsStore();
|
||||||
this.userStore = new UserStore();
|
this.userStore = new UserStore();
|
||||||
this.roleStore = new RoleStore();
|
this.roleStore = new RoleStore();
|
||||||
|
this.auditStore = new AuditStore();
|
||||||
}
|
}
|
||||||
|
|
||||||
initClient() {
|
initClient() {
|
||||||
|
|
|
||||||
40
frontend/app/mstore/types/audit.ts
Normal file
40
frontend/app/mstore/types/audit.ts
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
import { DateTime } from 'luxon';
|
||||||
|
import { unserscoreToSpaceAndCapitalize } from 'App/utils';
|
||||||
|
|
||||||
|
export default class Audit {
|
||||||
|
id: string = '';
|
||||||
|
username: string = '';
|
||||||
|
email: string = '';
|
||||||
|
action: string = '';
|
||||||
|
createdAt: any = null;
|
||||||
|
endPoint: string = '';
|
||||||
|
parameters: any = {};
|
||||||
|
method: string = '';
|
||||||
|
status: string = '';
|
||||||
|
payload: any = {}
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
}
|
||||||
|
|
||||||
|
static fromJson(json: any): Audit {
|
||||||
|
const audit = new Audit();
|
||||||
|
audit.id = json.rn;
|
||||||
|
audit.username = json.username;
|
||||||
|
audit.action = unserscoreToSpaceAndCapitalize(json.action);
|
||||||
|
audit.createdAt = json.createdAt && DateTime.fromMillis(json.createdAt || 0);
|
||||||
|
audit.endPoint = json.endpoint;
|
||||||
|
audit.parameters = json.parameters;
|
||||||
|
audit.method = json.method;
|
||||||
|
audit.status = json.status
|
||||||
|
audit.email = json.email
|
||||||
|
audit.payload = typeof json.payload === 'string' ? JSON.parse(json.payload) : json.payload
|
||||||
|
return audit;
|
||||||
|
}
|
||||||
|
|
||||||
|
toJson(): any {
|
||||||
|
return {
|
||||||
|
id: this.id,
|
||||||
|
username: this.username
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -60,7 +60,7 @@ export default class User implements IUser {
|
||||||
this.userId = json.userId || json.id; // TODO api returning id
|
this.userId = json.userId || json.id; // TODO api returning id
|
||||||
this.name = json.name;
|
this.name = json.name;
|
||||||
this.email = json.email;
|
this.email = json.email;
|
||||||
this.createdAt = json.createdAt && DateTime.fromISO(json.createdAt || 0)
|
this.createdAt = json.createdAt && DateTime.fromMillis(json.createdAt || 0)
|
||||||
this.isAdmin = json.admin
|
this.isAdmin = json.admin
|
||||||
this.isSuperAdmin = json.superAdmin
|
this.isSuperAdmin = json.superAdmin
|
||||||
this.isJoined = json.joined
|
this.isJoined = json.joined
|
||||||
|
|
|
||||||
|
|
@ -61,10 +61,11 @@ export const CLIENT_TABS = {
|
||||||
PROFILE: 'account',
|
PROFILE: 'account',
|
||||||
MANAGE_USERS: 'team',
|
MANAGE_USERS: 'team',
|
||||||
MANAGE_ROLES: 'roles',
|
MANAGE_ROLES: 'roles',
|
||||||
SITES: 'projects',
|
SITES: 'projects',
|
||||||
CUSTOM_FIELDS: 'metadata',
|
CUSTOM_FIELDS: 'metadata',
|
||||||
WEBHOOKS: 'webhooks',
|
WEBHOOKS: 'webhooks',
|
||||||
NOTIFICATIONS: 'notifications',
|
NOTIFICATIONS: 'notifications',
|
||||||
|
AUDIT: 'audit',
|
||||||
};
|
};
|
||||||
export const CLIENT_DEFAULT_TAB = CLIENT_TABS.PROFILE;
|
export const CLIENT_DEFAULT_TAB = CLIENT_TABS.PROFILE;
|
||||||
const routerClientTabString = `:activeTab(${ Object.values(CLIENT_TABS).join('|') })`;
|
const routerClientTabString = `:activeTab(${ Object.values(CLIENT_TABS).join('|') })`;
|
||||||
|
|
|
||||||
25
frontend/app/services/AuditService.ts
Normal file
25
frontend/app/services/AuditService.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
import APIClient from 'App/api_client';
|
||||||
|
|
||||||
|
export default class AuditService {
|
||||||
|
private client: APIClient;
|
||||||
|
|
||||||
|
constructor(client?: APIClient) {
|
||||||
|
this.client = client ? client : new APIClient();
|
||||||
|
}
|
||||||
|
|
||||||
|
initClient(client?: APIClient) {
|
||||||
|
this.client = client || new APIClient();
|
||||||
|
}
|
||||||
|
|
||||||
|
all(data: any): Promise<any> {
|
||||||
|
return this.client.post('/trails', data)
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(response => response.data || []);
|
||||||
|
}
|
||||||
|
|
||||||
|
one(id: string): Promise<any> {
|
||||||
|
return this.client.get('/trails/' + id)
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(response => response.data || {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,8 +2,10 @@ import DashboardService, { IDashboardService } from "./DashboardService";
|
||||||
import MetricService, { IMetricService } from "./MetricService";
|
import MetricService, { IMetricService } from "./MetricService";
|
||||||
import SessionSerivce from "./SessionService";
|
import SessionSerivce from "./SessionService";
|
||||||
import UserService from "./UserService";
|
import UserService from "./UserService";
|
||||||
|
import AuditService from './AuditService';
|
||||||
|
|
||||||
export const dashboardService: IDashboardService = new DashboardService();
|
export const dashboardService: IDashboardService = new DashboardService();
|
||||||
export const metricService: IMetricService = new MetricService();
|
export const metricService: IMetricService = new MetricService();
|
||||||
export const sessionService: SessionSerivce = new SessionSerivce();
|
export const sessionService: SessionSerivce = new SessionSerivce();
|
||||||
export const userService: UserService = new UserService();
|
export const userService: UserService = new UserService();
|
||||||
|
export const auditService: AuditService = new AuditService();
|
||||||
3
frontend/app/svg/icons/grid-3x3.svg
Normal file
3
frontend/app/svg/icons/grid-3x3.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-grid-3x3" viewBox="0 0 16 16">
|
||||||
|
<path d="M0 1.5A1.5 1.5 0 0 1 1.5 0h13A1.5 1.5 0 0 1 16 1.5v13a1.5 1.5 0 0 1-1.5 1.5h-13A1.5 1.5 0 0 1 0 14.5v-13zM1.5 1a.5.5 0 0 0-.5.5V5h4V1H1.5zM5 6H1v4h4V6zm1 4h4V6H6v4zm-1 1H1v3.5a.5.5 0 0 0 .5.5H5v-4zm1 0v4h4v-4H6zm5 0v4h3.5a.5.5 0 0 0 .5-.5V11h-4zm0-1h4V6h-4v4zm0-5h4V1.5a.5.5 0 0 0-.5-.5H11v4zm-1 0V1H6v4h4z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 454 B |
3
frontend/app/svg/icons/list-ul.svg
Normal file
3
frontend/app/svg/icons/list-ul.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="bi bi-list-ul" viewBox="0 0 16 16">
|
||||||
|
<path fill-rule="evenodd" d="M5 11.5a.5.5 0 0 1 .5-.5h9a.5.5 0 0 1 0 1h-9a.5.5 0 0 1-.5-.5zm0-4a.5.5 0 0 1 .5-.5h9a.5.5 0 0 1 0 1h-9a.5.5 0 0 1-.5-.5zm0-4a.5.5 0 0 1 .5-.5h9a.5.5 0 0 1 0 1h-9a.5.5 0 0 1-.5-.5zm-3 1a1 1 0 1 0 0-2 1 1 0 0 0 0 2zm0 4a1 1 0 1 0 0-2 1 1 0 0 0 0 2zm0 4a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 404 B |
|
|
@ -1,3 +1,3 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-text-paragraph" viewBox="0 0 16 16">
|
<svg xmlns="http://www.w3.org/2000/svg" class="bi bi-text-paragraph" viewBox="0 0 16 16">
|
||||||
<path fill-rule="evenodd" d="M2 12.5a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7a.5.5 0 0 1-.5-.5zm0-3a.5.5 0 0 1 .5-.5h11a.5.5 0 0 1 0 1h-11a.5.5 0 0 1-.5-.5zm0-3a.5.5 0 0 1 .5-.5h11a.5.5 0 0 1 0 1h-11a.5.5 0 0 1-.5-.5zm4-3a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7a.5.5 0 0 1-.5-.5z"/>
|
<path fill-rule="evenodd" d="M2 12.5a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7a.5.5 0 0 1-.5-.5zm0-3a.5.5 0 0 1 .5-.5h11a.5.5 0 0 1 0 1h-11a.5.5 0 0 1-.5-.5zm0-3a.5.5 0 0 1 .5-.5h11a.5.5 0 0 1 0 1h-11a.5.5 0 0 1-.5-.5zm4-3a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7a.5.5 0 0 1-.5-.5z"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
Before Width: | Height: | Size: 417 B After Width: | Height: | Size: 374 B |
|
|
@ -103,6 +103,10 @@ export default Record({
|
||||||
endTimestamp: this.end,
|
endTimestamp: this.end,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
rangeFormatted(format = 'MMM Do YY, hh:mm A') {
|
||||||
|
console.log('period', this)
|
||||||
|
return this.range.start.format(format) + ' - ' + this.range.end.format(format);
|
||||||
|
},
|
||||||
toTimestampstwo() {
|
toTimestampstwo() {
|
||||||
return {
|
return {
|
||||||
startTimestamp: this.start / 1000,
|
startTimestamp: this.start / 1000,
|
||||||
|
|
|
||||||
|
|
@ -264,4 +264,55 @@ export const convertElementToImage = async (el) => {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
return image;
|
return image;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const unserscoreToSpaceAndCapitalize = (str) => {
|
||||||
|
return str.replace(/_/g, ' ').replace(/\w\S*/g, (txt) => {
|
||||||
|
return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export const convertToCSV = (headers, objArray) => {
|
||||||
|
var array = typeof objArray != 'object' ? JSON.parse(objArray) : objArray;
|
||||||
|
var str = '';
|
||||||
|
const headersMap = headers.reduce((acc, curr) => {
|
||||||
|
acc[curr.key] = curr;
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
str += headers.map(h => h.label).join(',') + '\r\n';
|
||||||
|
|
||||||
|
for (var i = 0; i < array.length; i++) {
|
||||||
|
var line = '';
|
||||||
|
for (var index in headersMap) {
|
||||||
|
if (line !== '') line += ',';
|
||||||
|
line += array[i][index];
|
||||||
|
}
|
||||||
|
str += line + '\r\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const exportCSVFile = (headers, items, fileTitle) => {
|
||||||
|
var jsonObject = JSON.stringify(items);
|
||||||
|
var csv = convertToCSV(headers, jsonObject);
|
||||||
|
var exportedFilenmae = fileTitle + '.csv' || 'export.csv';
|
||||||
|
|
||||||
|
var blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
|
||||||
|
if (navigator.msSaveBlob) { // IE 10+
|
||||||
|
navigator.msSaveBlob(blob, exportedFilenmae);
|
||||||
|
} else {
|
||||||
|
var link = document.createElement("a");
|
||||||
|
if (link.download !== undefined) {
|
||||||
|
var url = URL.createObjectURL(blob);
|
||||||
|
link.setAttribute("href", url);
|
||||||
|
link.setAttribute("download", exportedFilenmae);
|
||||||
|
link.style.visibility = 'hidden';
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Loading…
Add table
Reference in a new issue