feat(ui) - audit - daterange with new component
This commit is contained in:
parent
b97c32ad56
commit
5b627c17ec
11 changed files with 240 additions and 28 deletions
|
|
@ -1,11 +1,50 @@
|
|||
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)
|
||||
|
||||
function AuditDetailModal(props) {
|
||||
return (
|
||||
<div style={{ width: '400px' }} className="bg-white h-screen">
|
||||
<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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -16,16 +16,21 @@ function AuditList(props: Props) {
|
|||
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]);
|
||||
}, [page, searchQuery, order, period]);
|
||||
|
||||
return useObserver(() => (
|
||||
<Loader loading={loading}>
|
||||
|
|
@ -40,7 +45,7 @@ function AuditList(props: Props) {
|
|||
<div className="px-2 border-t hover:bg-active-blue" key={index}>
|
||||
<AuditListItem
|
||||
audit={item}
|
||||
onShowDetails={() => showModal(<AuditDetailModal />, { right: true })}
|
||||
onShowDetails={() => showModal(<AuditDetailModal audit={item} />, { right: true })}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ function AuditSearchField(props: Props) {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="relative" style={{ width: '300px'}}>
|
||||
<div className="relative" style={{ width: '220px'}}>
|
||||
<Icon name="search" className="absolute top-0 bottom-0 ml-3 m-auto" size="16" />
|
||||
<input
|
||||
name="searchQuery"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import React from 'react';
|
||||
import { PageTitle } from 'UI';
|
||||
import { PageTitle, Icon } from 'UI';
|
||||
import AuditList from '../AuditList';
|
||||
import AuditSearchField from '../AuditSearchField';
|
||||
import { useStore } from 'App/mstore';
|
||||
|
|
@ -10,21 +10,33 @@ 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="Audit" />
|
||||
<div className="flex items-center ml-auto">
|
||||
<div className="mx-4">
|
||||
{/* <SelectDateRange
|
||||
startDate={auditStore.startDate}
|
||||
endDate={auditStore.endDate}
|
||||
range={auditStore.range}
|
||||
onChange={auditStore.setDateRange}
|
||||
/> */}
|
||||
<PageTitle title={
|
||||
<div className="flex items-center">
|
||||
<span>Audit Trail</span>
|
||||
<span className="color-gray-medium ml-2">{total}</span>
|
||||
</div>
|
||||
<div className="mx-4">
|
||||
} />
|
||||
<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' },
|
||||
|
|
@ -36,6 +48,12 @@ function AuditView(props) {
|
|||
/>
|
||||
</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>
|
||||
|
||||
|
|
|
|||
|
|
@ -8,9 +8,10 @@ interface Props {
|
|||
isSearchable?: boolean;
|
||||
defaultValue?: string;
|
||||
plain?: boolean;
|
||||
components?: 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 = {
|
||||
option: (provided, state) => ({
|
||||
...provided,
|
||||
|
|
@ -19,6 +20,7 @@ export default function({ plain = false, options, isSearchable = false, defaultV
|
|||
menu: (provided, state) => ({
|
||||
...provided,
|
||||
top: 31,
|
||||
minWidth: 'fit-content',
|
||||
}),
|
||||
control: (provided) => {
|
||||
const obj = {
|
||||
|
|
@ -61,6 +63,7 @@ export default function({ plain = false, options, isSearchable = false, defaultV
|
|||
components={{
|
||||
IndicatorSeparator: () => null,
|
||||
DropdownIndicator,
|
||||
...components,
|
||||
}}
|
||||
styles={customStyles}
|
||||
theme={(theme) => ({
|
||||
|
|
|
|||
|
|
@ -1,22 +1,71 @@
|
|||
import React from 'react';
|
||||
import { DATE_RANGE_OPTIONS } from 'App/dateRange'
|
||||
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 {
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
range: string;
|
||||
onChange: (startDate: string, endDate: string) => void;
|
||||
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>
|
||||
<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;
|
||||
export default SelectDateRange;
|
||||
|
||||
|
||||
|
|
@ -2,6 +2,8 @@ import { makeAutoObservable, runInAction, observable, action, reaction } from "m
|
|||
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';
|
||||
|
||||
export default class AuditStore {
|
||||
list: any[] = [];
|
||||
|
|
@ -11,18 +13,20 @@ export default class AuditStore {
|
|||
searchQuery: string = '';
|
||||
isLoading: boolean = false;
|
||||
order: string = 'desc';
|
||||
period: Period = Period({ rangeName: LAST_7_DAYS })
|
||||
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 = new Period(data);
|
||||
this['period'] = data;
|
||||
}
|
||||
|
||||
updateKey(key: string, value: any) {
|
||||
|
|
@ -45,4 +49,34 @@ export default class AuditStore {
|
|||
})
|
||||
})
|
||||
}
|
||||
|
||||
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: 'Status', key: 'status' },
|
||||
{ label: 'Created At', key: 'createdAt' },
|
||||
]
|
||||
// console.log('data', data)
|
||||
exportCSVFile(headers, data.sessions, `audit-${new Date().toLocaleDateString()}.csv`);
|
||||
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,12 +4,14 @@ 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() {
|
||||
}
|
||||
|
|
@ -20,10 +22,12 @@ export default class Audit {
|
|||
audit.username = json.username;
|
||||
audit.action = unserscoreToSpaceAndCapitalize(json.action);
|
||||
audit.createdAt = json.createdAt && DateTime.fromMillis(json.createdAt || 0);
|
||||
audit.endPoint = json.endPoint;
|
||||
audit.endPoint = json.endpoint;
|
||||
audit.parameters = json.parameters;
|
||||
audit.method = json.method;
|
||||
audit.status = json.status;
|
||||
audit.status = json.status
|
||||
audit.email = json.email
|
||||
audit.payload = typeof json.payload === 'string' ? JSON.parse(json.payload) : json.payload
|
||||
return audit;
|
||||
}
|
||||
|
||||
|
|
|
|||
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 |
|
|
@ -103,6 +103,10 @@ export default Record({
|
|||
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() {
|
||||
return {
|
||||
startTimestamp: this.start / 1000,
|
||||
|
|
|
|||
|
|
@ -270,4 +270,57 @@ 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;
|
||||
}, {});
|
||||
console.log('headersMap', headersMap)
|
||||
|
||||
// csv header line
|
||||
// comma seprated header line from array
|
||||
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) => {
|
||||
// if (headers) {
|
||||
// items.unshift(headers);
|
||||
// }
|
||||
|
||||
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) { // feature detection
|
||||
// Browsers that support HTML5 download attribute
|
||||
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