From f12931491a6941abf2cb7bc87b16f7563de4ae1e Mon Sep 17 00:00:00 2001 From: Shekar Siri Date: Fri, 6 May 2022 12:06:55 +0200 Subject: [PATCH 1/5] feat(ui) - audit - base views --- .../Audit/AuditDetailModal/AuditDetailModal.tsx | 11 +++++++++++ .../Client/Audit/AuditDetailModal/index.ts | 1 + .../Client/Audit/AuditList/AuditList.tsx | 14 ++++++++++++++ .../app/components/Client/Audit/AuditList/index.ts | 1 + .../Client/Audit/AuditListItem/AuditListItem.tsx | 14 ++++++++++++++ .../components/Client/Audit/AuditListItem/index.ts | 1 + .../Client/Audit/AuditView/AuditView.tsx | 11 +++++++++++ .../app/components/Client/Audit/AuditView/index.ts | 1 + 8 files changed, 54 insertions(+) create mode 100644 frontend/app/components/Client/Audit/AuditDetailModal/AuditDetailModal.tsx create mode 100644 frontend/app/components/Client/Audit/AuditDetailModal/index.ts create mode 100644 frontend/app/components/Client/Audit/AuditList/AuditList.tsx create mode 100644 frontend/app/components/Client/Audit/AuditList/index.ts create mode 100644 frontend/app/components/Client/Audit/AuditListItem/AuditListItem.tsx create mode 100644 frontend/app/components/Client/Audit/AuditListItem/index.ts create mode 100644 frontend/app/components/Client/Audit/AuditView/AuditView.tsx create mode 100644 frontend/app/components/Client/Audit/AuditView/index.ts diff --git a/frontend/app/components/Client/Audit/AuditDetailModal/AuditDetailModal.tsx b/frontend/app/components/Client/Audit/AuditDetailModal/AuditDetailModal.tsx new file mode 100644 index 000000000..03a5eee0d --- /dev/null +++ b/frontend/app/components/Client/Audit/AuditDetailModal/AuditDetailModal.tsx @@ -0,0 +1,11 @@ +import React from 'react'; + +function AuditDetailModal(props) { + return ( +
+ +
+ ); +} + +export default AuditDetailModal; \ No newline at end of file diff --git a/frontend/app/components/Client/Audit/AuditDetailModal/index.ts b/frontend/app/components/Client/Audit/AuditDetailModal/index.ts new file mode 100644 index 000000000..fcf6bb2b4 --- /dev/null +++ b/frontend/app/components/Client/Audit/AuditDetailModal/index.ts @@ -0,0 +1 @@ +export { default } from './AuditDetailModal'; \ No newline at end of file diff --git a/frontend/app/components/Client/Audit/AuditList/AuditList.tsx b/frontend/app/components/Client/Audit/AuditList/AuditList.tsx new file mode 100644 index 000000000..fc135f102 --- /dev/null +++ b/frontend/app/components/Client/Audit/AuditList/AuditList.tsx @@ -0,0 +1,14 @@ +import React from 'react'; + +interface Props { + +} +function AuditList(props: Props) { + return ( +
+ List +
+ ); +} + +export default AuditList; \ No newline at end of file diff --git a/frontend/app/components/Client/Audit/AuditList/index.ts b/frontend/app/components/Client/Audit/AuditList/index.ts new file mode 100644 index 000000000..2e6bc3739 --- /dev/null +++ b/frontend/app/components/Client/Audit/AuditList/index.ts @@ -0,0 +1 @@ +export { default } from './AuditList' \ No newline at end of file diff --git a/frontend/app/components/Client/Audit/AuditListItem/AuditListItem.tsx b/frontend/app/components/Client/Audit/AuditListItem/AuditListItem.tsx new file mode 100644 index 000000000..b587f08e7 --- /dev/null +++ b/frontend/app/components/Client/Audit/AuditListItem/AuditListItem.tsx @@ -0,0 +1,14 @@ +import React from 'react'; + +interface Props { + +} +function AuditListItem(props: Props) { + return ( +
+ +
+ ); +} + +export default AuditListItem; \ No newline at end of file diff --git a/frontend/app/components/Client/Audit/AuditListItem/index.ts b/frontend/app/components/Client/Audit/AuditListItem/index.ts new file mode 100644 index 000000000..821ee9639 --- /dev/null +++ b/frontend/app/components/Client/Audit/AuditListItem/index.ts @@ -0,0 +1 @@ +export { default } from './AuditListItem'; \ No newline at end of file diff --git a/frontend/app/components/Client/Audit/AuditView/AuditView.tsx b/frontend/app/components/Client/Audit/AuditView/AuditView.tsx new file mode 100644 index 000000000..02554fe1d --- /dev/null +++ b/frontend/app/components/Client/Audit/AuditView/AuditView.tsx @@ -0,0 +1,11 @@ +import React from 'react'; + +function AuditView(props) { + return ( +
+ View +
+ ); +} + +export default AuditView; \ No newline at end of file diff --git a/frontend/app/components/Client/Audit/AuditView/index.ts b/frontend/app/components/Client/Audit/AuditView/index.ts new file mode 100644 index 000000000..ba32b1be0 --- /dev/null +++ b/frontend/app/components/Client/Audit/AuditView/index.ts @@ -0,0 +1 @@ +export { default } from './AuditView' \ No newline at end of file From 7feaa376e6d83139e7d16b92d23aca3e66df7516 Mon Sep 17 00:00:00 2001 From: Shekar Siri Date: Fri, 6 May 2022 17:31:35 +0200 Subject: [PATCH 2/5] feat(ui) - audit - list and search --- frontend/app/api_client.js | 3 +- .../AuditDetailModal/AuditDetailModal.tsx | 7 ++- .../Client/Audit/AuditList/AuditList.tsx | 58 +++++++++++++++++-- .../Audit/AuditListItem/AuditListItem.tsx | 11 +++- .../AuditSearchField/AuditSearchField.tsx | 33 +++++++++++ .../Client/Audit/AuditSearchField/index.ts | 1 + .../Client/Audit/AuditView/AuditView.tsx | 19 +++++- frontend/app/components/Client/Client.js | 2 + .../Client/ManageUsers/ManageUsers.js | 2 +- .../Client/PreferencesMenu/PreferencesMenu.js | 13 ++++- frontend/app/mstore/auditStore.ts | 43 ++++++++++++++ frontend/app/mstore/index.tsx | 5 +- frontend/app/mstore/types/audit.ts | 36 ++++++++++++ frontend/app/mstore/types/user.ts | 2 +- frontend/app/routes.js | 3 +- frontend/app/services/AuditService.ts | 25 ++++++++ frontend/app/services/index.ts | 4 +- frontend/app/svg/icons/list-ul.svg | 3 + frontend/app/utils.js | 6 ++ 19 files changed, 255 insertions(+), 21 deletions(-) create mode 100644 frontend/app/components/Client/Audit/AuditSearchField/AuditSearchField.tsx create mode 100644 frontend/app/components/Client/Audit/AuditSearchField/index.ts create mode 100644 frontend/app/mstore/auditStore.ts create mode 100644 frontend/app/mstore/types/audit.ts create mode 100644 frontend/app/services/AuditService.ts create mode 100644 frontend/app/svg/icons/list-ul.svg diff --git a/frontend/app/api_client.js b/frontend/app/api_client.js index 98a1f4dfd..a5fae45e7 100644 --- a/frontend/app/api_client.js +++ b/frontend/app/api_client.js @@ -24,7 +24,8 @@ const siteIdRequiredPaths = [ '/heatmaps', '/custom_metrics', '/dashboards', - '/metrics' + '/metrics', + '/trails', // '/custom_metrics/sessions', ]; diff --git a/frontend/app/components/Client/Audit/AuditDetailModal/AuditDetailModal.tsx b/frontend/app/components/Client/Audit/AuditDetailModal/AuditDetailModal.tsx index 03a5eee0d..b7c257e3e 100644 --- a/frontend/app/components/Client/Audit/AuditDetailModal/AuditDetailModal.tsx +++ b/frontend/app/components/Client/Audit/AuditDetailModal/AuditDetailModal.tsx @@ -2,8 +2,11 @@ import React from 'react'; function AuditDetailModal(props) { return ( -
- +
+

Audit Details

+
+ +
); } diff --git a/frontend/app/components/Client/Audit/AuditList/AuditList.tsx b/frontend/app/components/Client/Audit/AuditList/AuditList.tsx index fc135f102..2b4249155 100644 --- a/frontend/app/components/Client/Audit/AuditList/AuditList.tsx +++ b/frontend/app/components/Client/Audit/AuditList/AuditList.tsx @@ -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 ( -
- List -
- ); + 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(() => ( + + +
+
Name
+
Status
+
Time
+
+ + {list.map((item, index) => ( +
+ showModal(, { right: true })} + /> +
+ ))} + +
+ auditStore.updateKey('page', page)} + limit={auditStore.pageSize} + debounceRequest={200} + /> +
+
+
+ )); } export default AuditList; \ No newline at end of file diff --git a/frontend/app/components/Client/Audit/AuditListItem/AuditListItem.tsx b/frontend/app/components/Client/Audit/AuditListItem/AuditListItem.tsx index b587f08e7..492d5c140 100644 --- a/frontend/app/components/Client/Audit/AuditListItem/AuditListItem.tsx +++ b/frontend/app/components/Client/Audit/AuditListItem/AuditListItem.tsx @@ -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 ( -
- +
+
asd
+
{audit.action}
+
{audit.createdAt && checkForRecent(audit.createdAt, 'LLL dd, yyyy, hh:mm a')}
); } diff --git a/frontend/app/components/Client/Audit/AuditSearchField/AuditSearchField.tsx b/frontend/app/components/Client/Audit/AuditSearchField/AuditSearchField.tsx new file mode 100644 index 000000000..8c0d32d5b --- /dev/null +++ b/frontend/app/components/Client/Audit/AuditSearchField/AuditSearchField.tsx @@ -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 ( +
+ + +
+ ); +} + +export default AuditSearchField; \ No newline at end of file diff --git a/frontend/app/components/Client/Audit/AuditSearchField/index.ts b/frontend/app/components/Client/Audit/AuditSearchField/index.ts new file mode 100644 index 000000000..646947095 --- /dev/null +++ b/frontend/app/components/Client/Audit/AuditSearchField/index.ts @@ -0,0 +1 @@ +export { default } from './AuditSearchField'; \ No newline at end of file diff --git a/frontend/app/components/Client/Audit/AuditView/AuditView.tsx b/frontend/app/components/Client/Audit/AuditView/AuditView.tsx index 02554fe1d..ec9904567 100644 --- a/frontend/app/components/Client/Audit/AuditView/AuditView.tsx +++ b/frontend/app/components/Client/Audit/AuditView/AuditView.tsx @@ -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(() => (
- View +
+ +
+ auditStore.updateKey('searchQuery', value) }/> +
+
+ +
- ); + )); } export default AuditView; \ No newline at end of file diff --git a/frontend/app/components/Client/Client.js b/frontend/app/components/Client/Client.js index 518148a5c..c58056b5f 100644 --- a/frontend/app/components/Client/Client.js +++ b/frontend/app/components/Client/Client.js @@ -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 { + ) diff --git a/frontend/app/components/Client/ManageUsers/ManageUsers.js b/frontend/app/components/Client/ManageUsers/ManageUsers.js index 9071ee46b..1504a0631 100644 --- a/frontend/app/components/Client/ManageUsers/ManageUsers.js +++ b/frontend/app/components/Client/ManageUsers/ManageUsers.js @@ -236,7 +236,7 @@ class ManageUsers extends React.PureComponent { title="No users are available." size="small" show={ members.size === 0 } - icon + animatedIcon="empty-state" >
{ diff --git a/frontend/app/components/Client/PreferencesMenu/PreferencesMenu.js b/frontend/app/components/Client/PreferencesMenu/PreferencesMenu.js index 0c57c1ab7..682d1519a 100644 --- a/frontend/app/components/Client/PreferencesMenu/PreferencesMenu.js +++ b/frontend/app/components/Client/PreferencesMenu/PreferencesMenu.js @@ -78,6 +78,17 @@ function PreferencesMenu({ activeTab, appearance, history, isEnterprise }) { />
)} + + { isEnterprise && ( +
+ setTab(CLIENT_TABS.AUDIT) } + /> +
+ )}
setTab(CLIENT_TABS.NOTIFICATIONS) } /> -
+
) } diff --git a/frontend/app/mstore/auditStore.ts b/frontend/app/mstore/auditStore.ts new file mode 100644 index 000000000..11b0ba9f4 --- /dev/null +++ b/frontend/app/mstore/auditStore.ts @@ -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 => { + 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; + }) + }) + } +} \ No newline at end of file diff --git a/frontend/app/mstore/index.tsx b/frontend/app/mstore/index.tsx index 251b70f9e..2a1897b42 100644 --- a/frontend/app/mstore/index.tsx +++ b/frontend/app/mstore/index.tsx @@ -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() { diff --git a/frontend/app/mstore/types/audit.ts b/frontend/app/mstore/types/audit.ts new file mode 100644 index 000000000..b9c30343f --- /dev/null +++ b/frontend/app/mstore/types/audit.ts @@ -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 + }; + } +} \ No newline at end of file diff --git a/frontend/app/mstore/types/user.ts b/frontend/app/mstore/types/user.ts index c69b31067..53ae5dbf0 100644 --- a/frontend/app/mstore/types/user.ts +++ b/frontend/app/mstore/types/user.ts @@ -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 diff --git a/frontend/app/routes.js b/frontend/app/routes.js index 7a6c5379e..4492a4b63 100644 --- a/frontend/app/routes.js +++ b/frontend/app/routes.js @@ -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('|') })`; diff --git a/frontend/app/services/AuditService.ts b/frontend/app/services/AuditService.ts new file mode 100644 index 000000000..0a93d6bb6 --- /dev/null +++ b/frontend/app/services/AuditService.ts @@ -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 { + return this.client.post('/trails', data) + .then(response => response.json()) + .then(response => response.data[0] || []); + } + + one(id: string): Promise { + return this.client.get('/trails/' + id) + .then(response => response.json()) + .then(response => response.data || {}); + } +} \ No newline at end of file diff --git a/frontend/app/services/index.ts b/frontend/app/services/index.ts index 3cb64a7ad..5b4d59651 100644 --- a/frontend/app/services/index.ts +++ b/frontend/app/services/index.ts @@ -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(); \ No newline at end of file +export const userService: UserService = new UserService(); +export const auditService: AuditService = new AuditService(); \ No newline at end of file diff --git a/frontend/app/svg/icons/list-ul.svg b/frontend/app/svg/icons/list-ul.svg new file mode 100644 index 000000000..a07021b20 --- /dev/null +++ b/frontend/app/svg/icons/list-ul.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/frontend/app/utils.js b/frontend/app/utils.js index bdece4058..571c18561 100644 --- a/frontend/app/utils.js +++ b/frontend/app/utils.js @@ -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(); + }); } \ No newline at end of file From b97c32ad5604e44274777316696d1aa979d3e1eb Mon Sep 17 00:00:00 2001 From: Shekar Siri Date: Fri, 6 May 2022 18:54:25 +0200 Subject: [PATCH 3/5] feat(ui) - audit - filters --- .../Client/Audit/AuditList/AuditList.tsx | 4 ++- .../Audit/AuditListItem/AuditListItem.tsx | 2 +- .../Client/Audit/AuditView/AuditView.tsx | 23 ++++++++++++++ .../app/components/shared/Select/Select.tsx | 31 ++++++++++++++++--- .../SelectDateRange/SelectDateRange.tsx | 22 +++++++++++++ .../shared/SelectDateRange/index.ts | 1 + frontend/app/dateRange.js | 7 +++++ frontend/app/mstore/auditStore.ts | 9 ++++-- frontend/app/mstore/types/audit.ts | 6 ++-- frontend/app/services/AuditService.ts | 2 +- frontend/app/svg/icons/text-paragraph.svg | 2 +- 11 files changed, 96 insertions(+), 13 deletions(-) create mode 100644 frontend/app/components/shared/SelectDateRange/SelectDateRange.tsx create mode 100644 frontend/app/components/shared/SelectDateRange/index.ts diff --git a/frontend/app/components/Client/Audit/AuditList/AuditList.tsx b/frontend/app/components/Client/Audit/AuditList/AuditList.tsx index 2b4249155..549b25b6d 100644 --- a/frontend/app/components/Client/Audit/AuditList/AuditList.tsx +++ b/frontend/app/components/Client/Audit/AuditList/AuditList.tsx @@ -15,6 +15,7 @@ function AuditList(props: Props) { const list = useObserver(() => auditStore.list); const searchQuery = useObserver(() => auditStore.searchQuery); const page = useObserver(() => auditStore.page); + const order = useObserver(() => auditStore.order); const { showModal } = useModal(); useEffect(() => { @@ -22,8 +23,9 @@ function AuditList(props: Props) { page: auditStore.page, limit: auditStore.pageSize, query: auditStore.searchQuery, + order: auditStore.order, }); - }, [page, searchQuery]); + }, [page, searchQuery, order]); return useObserver(() => ( diff --git a/frontend/app/components/Client/Audit/AuditListItem/AuditListItem.tsx b/frontend/app/components/Client/Audit/AuditListItem/AuditListItem.tsx index 492d5c140..7d584bb06 100644 --- a/frontend/app/components/Client/Audit/AuditListItem/AuditListItem.tsx +++ b/frontend/app/components/Client/Audit/AuditListItem/AuditListItem.tsx @@ -9,7 +9,7 @@ function AuditListItem(props: Props) { const { audit, onShowDetails } = props; return (
-
asd
+
{audit.username}
{audit.action}
{audit.createdAt && checkForRecent(audit.createdAt, 'LLL dd, yyyy, hh:mm a')}
diff --git a/frontend/app/components/Client/Audit/AuditView/AuditView.tsx b/frontend/app/components/Client/Audit/AuditView/AuditView.tsx index ec9904567..1e53f24b5 100644 --- a/frontend/app/components/Client/Audit/AuditView/AuditView.tsx +++ b/frontend/app/components/Client/Audit/AuditView/AuditView.tsx @@ -4,14 +4,37 @@ 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); + return useObserver(() => (
+
+ {/* */} +
+
+ +
+ ); +} + +export default SelectDateRange; \ No newline at end of file diff --git a/frontend/app/components/shared/SelectDateRange/index.ts b/frontend/app/components/shared/SelectDateRange/index.ts new file mode 100644 index 000000000..9b59aa99b --- /dev/null +++ b/frontend/app/components/shared/SelectDateRange/index.ts @@ -0,0 +1 @@ +export { default } from './SelectDateRange'; \ No newline at end of file diff --git a/frontend/app/dateRange.js b/frontend/app/dateRange.js index b50ec2fb9..4a2ea923d 100644 --- a/frontend/app/dateRange.js +++ b/frontend/app/dateRange.js @@ -23,6 +23,13 @@ Object.keys(DATE_RANGE_LABELS).forEach((key) => { DATE_RANGE_VALUES[ key ] = key export { 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) { return moment.range( moment(start), diff --git a/frontend/app/mstore/auditStore.ts b/frontend/app/mstore/auditStore.ts index 11b0ba9f4..b3df0aa6d 100644 --- a/frontend/app/mstore/auditStore.ts +++ b/frontend/app/mstore/auditStore.ts @@ -1,6 +1,7 @@ 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'; export default class AuditStore { list: any[] = []; @@ -9,6 +10,8 @@ export default class AuditStore { pageSize: number = 20; searchQuery: string = ''; isLoading: boolean = false; + order: string = 'desc'; + period: Period = Period({ rangeName: LAST_7_DAYS }) constructor() { makeAutoObservable(this, { @@ -18,8 +21,11 @@ export default class AuditStore { }) } + setDateRange(data: any) { + this.period = new Period(data); + } + updateKey(key: string, value: any) { - console.log(key, value) this[key] = value; } @@ -27,7 +33,6 @@ export default class AuditStore { 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 diff --git a/frontend/app/mstore/types/audit.ts b/frontend/app/mstore/types/audit.ts index b9c30343f..104ff447e 100644 --- a/frontend/app/mstore/types/audit.ts +++ b/frontend/app/mstore/types/audit.ts @@ -3,7 +3,7 @@ import { unserscoreToSpaceAndCapitalize } from 'App/utils'; export default class Audit { id: string = ''; - userName: string = ''; + username: string = ''; action: string = ''; createdAt: any = null; endPoint: string = ''; @@ -17,7 +17,7 @@ export default class Audit { static fromJson(json: any): Audit { const audit = new Audit(); audit.id = json.rn; - audit.userName = json.userName; + audit.username = json.username; audit.action = unserscoreToSpaceAndCapitalize(json.action); audit.createdAt = json.createdAt && DateTime.fromMillis(json.createdAt || 0); audit.endPoint = json.endPoint; @@ -30,7 +30,7 @@ export default class Audit { toJson(): any { return { id: this.id, - userName: this.userName + username: this.username }; } } \ No newline at end of file diff --git a/frontend/app/services/AuditService.ts b/frontend/app/services/AuditService.ts index 0a93d6bb6..39b7521ed 100644 --- a/frontend/app/services/AuditService.ts +++ b/frontend/app/services/AuditService.ts @@ -14,7 +14,7 @@ export default class AuditService { all(data: any): Promise { return this.client.post('/trails', data) .then(response => response.json()) - .then(response => response.data[0] || []); + .then(response => response.data || []); } one(id: string): Promise { diff --git a/frontend/app/svg/icons/text-paragraph.svg b/frontend/app/svg/icons/text-paragraph.svg index 9779beabf..f06f83174 100644 --- a/frontend/app/svg/icons/text-paragraph.svg +++ b/frontend/app/svg/icons/text-paragraph.svg @@ -1,3 +1,3 @@ - + \ No newline at end of file From 5b627c17ecfde97e831927a860d2a70f5cf89314 Mon Sep 17 00:00:00 2001 From: Shekar Siri Date: Mon, 9 May 2022 19:02:07 +0200 Subject: [PATCH 4/5] feat(ui) - audit - daterange with new component --- .../AuditDetailModal/AuditDetailModal.tsx | 43 ++++++++++++- .../Client/Audit/AuditList/AuditList.tsx | 9 ++- .../AuditSearchField/AuditSearchField.tsx | 2 +- .../Client/Audit/AuditView/AuditView.tsx | 40 ++++++++---- .../app/components/shared/Select/Select.tsx | 5 +- .../SelectDateRange/SelectDateRange.tsx | 63 ++++++++++++++++--- frontend/app/mstore/auditStore.ts | 38 ++++++++++- frontend/app/mstore/types/audit.ts | 8 ++- frontend/app/svg/icons/grid-3x3.svg | 3 + frontend/app/types/app/period.js | 4 ++ frontend/app/utils.js | 53 ++++++++++++++++ 11 files changed, 240 insertions(+), 28 deletions(-) create mode 100644 frontend/app/svg/icons/grid-3x3.svg diff --git a/frontend/app/components/Client/Audit/AuditDetailModal/AuditDetailModal.tsx b/frontend/app/components/Client/Audit/AuditDetailModal/AuditDetailModal.tsx index b7c257e3e..934604dbb 100644 --- a/frontend/app/components/Client/Audit/AuditDetailModal/AuditDetailModal.tsx +++ b/frontend/app/components/Client/Audit/AuditDetailModal/AuditDetailModal.tsx @@ -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 ( -
+

Audit Details

+
{ 'URL'}
+
{ audit.endPoint }
+
+
+
Username
+
{audit.username}
+
+
+
Created At
+
{audit.createdAt && checkForRecent(audit.createdAt, 'LLL dd, yyyy, hh:mm a')}
+
+
+ +
+
+
Action
+
{audit.action}
+
+
+
Method
+
{audit.method}
+
+
+ + { audit.payload && ( +
+
Payload
+ +
+ )}
); diff --git a/frontend/app/components/Client/Audit/AuditList/AuditList.tsx b/frontend/app/components/Client/Audit/AuditList/AuditList.tsx index 549b25b6d..5ab4c0d3a 100644 --- a/frontend/app/components/Client/Audit/AuditList/AuditList.tsx +++ b/frontend/app/components/Client/Audit/AuditList/AuditList.tsx @@ -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(() => ( @@ -40,7 +45,7 @@ function AuditList(props: Props) {
showModal(, { right: true })} + onShowDetails={() => showModal(, { right: true })} />
))} diff --git a/frontend/app/components/Client/Audit/AuditSearchField/AuditSearchField.tsx b/frontend/app/components/Client/Audit/AuditSearchField/AuditSearchField.tsx index 8c0d32d5b..5574da847 100644 --- a/frontend/app/components/Client/Audit/AuditSearchField/AuditSearchField.tsx +++ b/frontend/app/components/Client/Audit/AuditSearchField/AuditSearchField.tsx @@ -18,7 +18,7 @@ function AuditSearchField(props: Props) { } return ( -
+
auditStore.order); + const total = useObserver(() => auditStore.total); + + const exportToCsv = () => { + auditStore.exportToCsv(); + } + + const onChange = (data) => { + auditStore.setDateRange(data); + } return useObserver(() => (
- -
-
- {/* */} + + Audit Trail + {total}
-
+ } /> +
+
+ +
+
onChange(value)} + components={{ SingleValue: ({ children, ...props} : any) => { + return ( + + {period.rangeName === CUSTOM_RANGE ? period.rangeFormatted() : children} + + ) + } }} + period={period} /> + { + isCustom && + setIsCustom(false)} + > +
+ setIsCustom(false) } + selectedDateRange={ period.range } + /> +
+
+ }
); } -export default SelectDateRange; \ No newline at end of file +export default SelectDateRange; + + \ No newline at end of file diff --git a/frontend/app/mstore/auditStore.ts b/frontend/app/mstore/auditStore.ts index b3df0aa6d..de7bd4433 100644 --- a/frontend/app/mstore/auditStore.ts +++ b/frontend/app/mstore/auditStore.ts @@ -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 => { + 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 => { + const promise = this.fetchAllAudits({ limit: this.total }) + toast.promise(promise, { + pending: 'Exporting...', + success: 'Export successful', + }) + } } \ No newline at end of file diff --git a/frontend/app/mstore/types/audit.ts b/frontend/app/mstore/types/audit.ts index 104ff447e..dd8f7c095 100644 --- a/frontend/app/mstore/types/audit.ts +++ b/frontend/app/mstore/types/audit.ts @@ -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; } diff --git a/frontend/app/svg/icons/grid-3x3.svg b/frontend/app/svg/icons/grid-3x3.svg new file mode 100644 index 000000000..c40d98c96 --- /dev/null +++ b/frontend/app/svg/icons/grid-3x3.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/frontend/app/types/app/period.js b/frontend/app/types/app/period.js index deebf9cad..b4f8798c5 100644 --- a/frontend/app/types/app/period.js +++ b/frontend/app/types/app/period.js @@ -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, diff --git a/frontend/app/utils.js b/frontend/app/utils.js index 571c18561..be25b585e 100644 --- a/frontend/app/utils.js +++ b/frontend/app/utils.js @@ -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); + } + } } \ No newline at end of file From eae31eac373940562ca4c5c954ba02087440d094 Mon Sep 17 00:00:00 2001 From: Shekar Siri Date: Mon, 9 May 2022 19:34:59 +0200 Subject: [PATCH 5/5] feat(ui) - audit - date --- frontend/app/mstore/auditStore.ts | 10 +++++++--- frontend/app/utils.js | 10 +--------- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/frontend/app/mstore/auditStore.ts b/frontend/app/mstore/auditStore.ts index de7bd4433..efc34d65d 100644 --- a/frontend/app/mstore/auditStore.ts +++ b/frontend/app/mstore/auditStore.ts @@ -4,6 +4,8 @@ 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[] = []; @@ -60,11 +62,13 @@ export default class AuditStore { { 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`); + 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) diff --git a/frontend/app/utils.js b/frontend/app/utils.js index be25b585e..029e24599 100644 --- a/frontend/app/utils.js +++ b/frontend/app/utils.js @@ -279,10 +279,7 @@ export const convertToCSV = (headers, objArray) => { 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++) { @@ -299,10 +296,6 @@ export const convertToCSV = (headers, objArray) => { } 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'; @@ -312,8 +305,7 @@ export const exportCSVFile = (headers, items, fileTitle) => { navigator.msSaveBlob(blob, exportedFilenmae); } else { var link = document.createElement("a"); - if (link.download !== undefined) { // feature detection - // Browsers that support HTML5 download attribute + if (link.download !== undefined) { var url = URL.createObjectURL(blob); link.setAttribute("href", url); link.setAttribute("download", exportedFilenmae);