feat(ui) - audit - list and search
This commit is contained in:
parent
f12931491a
commit
7feaa376e6
19 changed files with 255 additions and 21 deletions
|
|
@ -24,7 +24,8 @@ const siteIdRequiredPaths = [
|
|||
'/heatmaps',
|
||||
'/custom_metrics',
|
||||
'/dashboards',
|
||||
'/metrics'
|
||||
'/metrics',
|
||||
'/trails',
|
||||
// '/custom_metrics/sessions',
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -2,8 +2,11 @@ import React from 'react';
|
|||
|
||||
function AuditDetailModal(props) {
|
||||
return (
|
||||
<div>
|
||||
|
||||
<div style={{ width: '400px' }} className="bg-white h-screen">
|
||||
<h1 className="text-2xl p-4">Audit Details</h1>
|
||||
<div className="p-4">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,60 @@
|
|||
import React from 'react';
|
||||
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) {
|
||||
return (
|
||||
<div>
|
||||
List
|
||||
</div>
|
||||
);
|
||||
const { auditStore } = useStore();
|
||||
const loading = useObserver(() => auditStore.isLoading);
|
||||
const list = useObserver(() => auditStore.list);
|
||||
const searchQuery = useObserver(() => auditStore.searchQuery);
|
||||
const page = useObserver(() => auditStore.page);
|
||||
const { showModal } = useModal();
|
||||
|
||||
useEffect(() => {
|
||||
auditStore.fetchAudits({
|
||||
page: auditStore.page,
|
||||
limit: auditStore.pageSize,
|
||||
query: auditStore.searchQuery,
|
||||
});
|
||||
}, [page, searchQuery]);
|
||||
|
||||
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 />, { 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,12 +1,17 @@
|
|||
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>
|
||||
|
||||
<div className="grid grid-cols-12 gap-4 items-center py-3">
|
||||
<div className="col-span-5">asd</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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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: '300px'}}>
|
||||
<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';
|
||||
|
|
@ -1,11 +1,24 @@
|
|||
import React from 'react';
|
||||
import { PageTitle } from 'UI';
|
||||
import AuditList from '../AuditList';
|
||||
import AuditSearchField from '../AuditSearchField';
|
||||
import { useStore } from 'App/mstore';
|
||||
import { useObserver } from 'mobx-react-lite';
|
||||
|
||||
function AuditView(props) {
|
||||
return (
|
||||
const { auditStore } = useStore();
|
||||
return useObserver(() => (
|
||||
<div>
|
||||
View
|
||||
<div className="flex items-center mb-4">
|
||||
<PageTitle title="Audit" />
|
||||
<div className="flex items-center ml-auto">
|
||||
<AuditSearchField onChange={(value) => auditStore.updateKey('searchQuery', value) }/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<AuditList />
|
||||
</div>
|
||||
);
|
||||
));
|
||||
}
|
||||
|
||||
export default AuditView;
|
||||
|
|
@ -8,6 +8,7 @@ import ProfileSettings from './ProfileSettings';
|
|||
import Integrations from './Integrations';
|
||||
import ManageUsers from './ManageUsers';
|
||||
import UserView from './Users/UsersView';
|
||||
import AuditView from './Audit/AuditView';
|
||||
import Sites from './Sites';
|
||||
import CustomFields from './CustomFields';
|
||||
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.NOTIFICATIONS) } component={ Notifications } />
|
||||
<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) } />
|
||||
</Switch>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -236,7 +236,7 @@ class ManageUsers extends React.PureComponent {
|
|||
title="No users are available."
|
||||
size="small"
|
||||
show={ members.size === 0 }
|
||||
icon
|
||||
animatedIcon="empty-state"
|
||||
>
|
||||
<div className={ styles.list }>
|
||||
{
|
||||
|
|
|
|||
|
|
@ -78,6 +78,17 @@ function PreferencesMenu({ activeTab, appearance, history, isEnterprise }) {
|
|||
/>
|
||||
</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">
|
||||
<SideMenuitem
|
||||
|
|
@ -95,7 +106,7 @@ function PreferencesMenu({ activeTab, appearance, history, isEnterprise }) {
|
|||
iconName="bell"
|
||||
onClick={() => setTab(CLIENT_TABS.NOTIFICATIONS) }
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
43
frontend/app/mstore/auditStore.ts
Normal file
43
frontend/app/mstore/auditStore.ts
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
import { makeAutoObservable, runInAction, observable, action, reaction } from "mobx"
|
||||
import { auditService } from "App/services"
|
||||
import Audit from './types/audit'
|
||||
|
||||
export default class AuditStore {
|
||||
list: any[] = [];
|
||||
total: number = 0;
|
||||
page: number = 1;
|
||||
pageSize: number = 20;
|
||||
searchQuery: string = '';
|
||||
isLoading: boolean = false;
|
||||
|
||||
constructor() {
|
||||
makeAutoObservable(this, {
|
||||
searchQuery: observable,
|
||||
updateKey: action,
|
||||
fetchAudits: action,
|
||||
})
|
||||
}
|
||||
|
||||
updateKey(key: string, value: any) {
|
||||
console.log(key, value)
|
||||
this[key] = value;
|
||||
}
|
||||
|
||||
fetchAudits = (data: any): Promise<void> => {
|
||||
this.isLoading = true;
|
||||
return new Promise((resolve, reject) => {
|
||||
auditService.all(data).then(response => {
|
||||
console.log('response', response);
|
||||
runInAction(() => {
|
||||
this.list = response.sessions.map(item => Audit.fromJson(item))
|
||||
this.total = response.count
|
||||
})
|
||||
resolve()
|
||||
}).catch(error => {
|
||||
reject(error)
|
||||
}).finally(() => {
|
||||
this.isLoading = false;
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -4,8 +4,9 @@ import MetricStore, { IMetricStore } from './metricStore';
|
|||
import UserStore from './userStore';
|
||||
import RoleStore from './roleStore';
|
||||
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 AuditStore from './auditStore';
|
||||
|
||||
export class RootStore {
|
||||
dashboardStore: IDashboardSotre;
|
||||
|
|
@ -13,6 +14,7 @@ export class RootStore {
|
|||
settingsStore: SettingsStore;
|
||||
userStore: UserStore;
|
||||
roleStore: RoleStore;
|
||||
auditStore: AuditStore;
|
||||
|
||||
constructor() {
|
||||
this.dashboardStore = new DashboardStore();
|
||||
|
|
@ -20,6 +22,7 @@ export class RootStore {
|
|||
this.settingsStore = new SettingsStore();
|
||||
this.userStore = new UserStore();
|
||||
this.roleStore = new RoleStore();
|
||||
this.auditStore = new AuditStore();
|
||||
}
|
||||
|
||||
initClient() {
|
||||
|
|
|
|||
36
frontend/app/mstore/types/audit.ts
Normal file
36
frontend/app/mstore/types/audit.ts
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import { DateTime } from 'luxon';
|
||||
import { unserscoreToSpaceAndCapitalize } from 'App/utils';
|
||||
|
||||
export default class Audit {
|
||||
id: string = '';
|
||||
userName: string = '';
|
||||
action: string = '';
|
||||
createdAt: any = null;
|
||||
endPoint: string = '';
|
||||
parameters: any = {};
|
||||
method: string = '';
|
||||
status: string = '';
|
||||
|
||||
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;
|
||||
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.name = json.name;
|
||||
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.isSuperAdmin = json.superAdmin
|
||||
this.isJoined = json.joined
|
||||
|
|
|
|||
|
|
@ -61,10 +61,11 @@ export const CLIENT_TABS = {
|
|||
PROFILE: 'account',
|
||||
MANAGE_USERS: 'team',
|
||||
MANAGE_ROLES: 'roles',
|
||||
SITES: 'projects',
|
||||
SITES: 'projects',
|
||||
CUSTOM_FIELDS: 'metadata',
|
||||
WEBHOOKS: 'webhooks',
|
||||
NOTIFICATIONS: 'notifications',
|
||||
AUDIT: 'audit',
|
||||
};
|
||||
export const CLIENT_DEFAULT_TAB = CLIENT_TABS.PROFILE;
|
||||
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[0] || []);
|
||||
}
|
||||
|
||||
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 SessionSerivce from "./SessionService";
|
||||
import UserService from "./UserService";
|
||||
import AuditService from './AuditService';
|
||||
|
||||
export const dashboardService: IDashboardService = new DashboardService();
|
||||
export const metricService: IMetricService = new MetricService();
|
||||
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/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 |
|
|
@ -264,4 +264,10 @@ export const convertElementToImage = async (el) => {
|
|||
},
|
||||
});
|
||||
return image;
|
||||
}
|
||||
|
||||
export const unserscoreToSpaceAndCapitalize = (str) => {
|
||||
return str.replace(/_/g, ' ').replace(/\w\S*/g, (txt) => {
|
||||
return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();
|
||||
});
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue