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
new file mode 100644
index 000000000..934604dbb
--- /dev/null
+++ b/frontend/app/components/Client/Audit/AuditDetailModal/AuditDetailModal.tsx
@@ -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 (
+
+
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 && (
+
+ )}
+
+
+ );
+}
+
+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..5ab4c0d3a
--- /dev/null
+++ b/frontend/app/components/Client/Audit/AuditList/AuditList.tsx
@@ -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(() => (
+
+
+
+
+ {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/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..7d584bb06
--- /dev/null
+++ b/frontend/app/components/Client/Audit/AuditListItem/AuditListItem.tsx
@@ -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 (
+
+
{audit.username}
+
{audit.action}
+
{audit.createdAt && checkForRecent(audit.createdAt, 'LLL dd, yyyy, hh:mm a')}
+
+ );
+}
+
+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/AuditSearchField/AuditSearchField.tsx b/frontend/app/components/Client/Audit/AuditSearchField/AuditSearchField.tsx
new file mode 100644
index 000000000..5574da847
--- /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
new file mode 100644
index 000000000..41d506b11
--- /dev/null
+++ b/frontend/app/components/Client/Audit/AuditView/AuditView.tsx
@@ -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(() => (
+
+
+
+ Audit Trail
+ {total}
+
+ } />
+
+
+
+
+
+
+
auditStore.updateKey('searchQuery', value) }/>
+
+
+
+
+
+
+
+
+ ));
+}
+
+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
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/components/shared/Select/Select.tsx b/frontend/app/components/shared/Select/Select.tsx
index 307fbb0ea..8d963c16b 100644
--- a/frontend/app/components/shared/Select/Select.tsx
+++ b/frontend/app/components/shared/Select/Select.tsx
@@ -1,14 +1,17 @@
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 {
options: any[];
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,
@@ -17,14 +20,26 @@ export default function({ plain = false, options, isSearchable = false, defaultV
menu: (provided, state) => ({
...provided,
top: 31,
+ minWidth: 'fit-content',
}),
control: (provided) => {
const obj = {
...provided,
- border: 'solid thin #ddd'
+ border: 'solid thin #ddd',
+ cursor: 'pointer',
}
if (plain) {
obj['border'] = '1px solid transparent'
+ obj['&:hover'] = {
+ borderColor: 'transparent',
+ backgroundColor: colors['gray-light']
+ }
+ obj['&:focus'] = {
+ borderColor: 'transparent'
+ }
+ obj['&:active'] = {
+ borderColor: 'transparent'
+ }
}
return obj;
},
@@ -46,7 +61,9 @@ export default function({ plain = false, options, isSearchable = false, defaultV
isSearchable={isSearchable}
defaultValue={defaultSelected}
components={{
- IndicatorSeparator: () => null
+ IndicatorSeparator: () => null,
+ DropdownIndicator,
+ ...components,
}}
styles={customStyles}
theme={(theme) => ({
@@ -56,9 +73,18 @@ export default function({ plain = false, options, isSearchable = false, defaultV
primary: '#394EFF',
}
})}
+ blurInputOnSelect={true}
{...rest}
/>
);
}
-// export default Select;
\ No newline at end of file
+const DropdownIndicator = (
+ props: DropdownIndicatorProps
+ ) => {
+ return (
+
+
+
+ );
+ };
\ No newline at end of file
diff --git a/frontend/app/components/shared/SelectDateRange/SelectDateRange.tsx b/frontend/app/components/shared/SelectDateRange/SelectDateRange.tsx
new file mode 100644
index 000000000..0713b2dc4
--- /dev/null
+++ b/frontend/app/components/shared/SelectDateRange/SelectDateRange.tsx
@@ -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 (
+
+
+ );
+}
+
+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
new file mode 100644
index 000000000..efc34d65d
--- /dev/null
+++ b/frontend/app/mstore/auditStore.ts
@@ -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 => {
+ 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 => {
+ 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 => {
+ 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/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..dd8f7c095
--- /dev/null
+++ b/frontend/app/mstore/types/audit.ts
@@ -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
+ };
+ }
+}
\ 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..39b7521ed
--- /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 || []);
+ }
+
+ 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/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/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/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 @@
-