From d4d836ad243bd7e9b5a4d524b1206c70f755f7d2 Mon Sep 17 00:00:00 2001 From: Shekar Siri Date: Mon, 16 Sep 2024 18:15:21 +0530 Subject: [PATCH] change(ui): custom fields --- .../Client/CustomFields/CustomFieldForm.js | 2 +- .../Client/CustomFields/CustomFieldForm.tsx | 92 ++++++++ .../Client/CustomFields/CustomFields.js | 222 +++++++++--------- .../Client/CustomFields/CustomFields.tsx | 114 +++++++++ .../Client/CustomFields/ListItem.js | 34 +-- frontend/app/mstore/customFieldStore.ts | 117 +++++++++ frontend/app/mstore/index.tsx | 15 +- frontend/app/mstore/types/customField.ts | 55 +++++ frontend/app/services/CustomFieldService.ts | 29 +++ frontend/app/services/index.ts | 3 + 10 files changed, 548 insertions(+), 135 deletions(-) create mode 100644 frontend/app/components/Client/CustomFields/CustomFieldForm.tsx create mode 100644 frontend/app/components/Client/CustomFields/CustomFields.tsx create mode 100644 frontend/app/mstore/customFieldStore.ts create mode 100644 frontend/app/mstore/types/customField.ts create mode 100644 frontend/app/services/CustomFieldService.ts diff --git a/frontend/app/components/Client/CustomFields/CustomFieldForm.js b/frontend/app/components/Client/CustomFields/CustomFieldForm.js index 4f2d1e278..b988c89e2 100644 --- a/frontend/app/components/Client/CustomFields/CustomFieldForm.js +++ b/frontend/app/components/Client/CustomFields/CustomFieldForm.js @@ -55,7 +55,7 @@ const CustomFieldForm = ({ field, saving, errors, edit, save, onSave, onClose, o const mapStateToProps = (state) => ({ field: state.getIn(['customFields', 'instance']), saving: state.getIn(['customFields', 'saveRequest', 'loading']), - errors: state.getIn(['customFields', 'saveRequest', 'errors']), + errors: state.getIn(['customFields', 'saveRequest', 'errors']) }); export default connect(mapStateToProps, { edit, save })(CustomFieldForm); diff --git a/frontend/app/components/Client/CustomFields/CustomFieldForm.tsx b/frontend/app/components/Client/CustomFields/CustomFieldForm.tsx new file mode 100644 index 000000000..577153da2 --- /dev/null +++ b/frontend/app/components/Client/CustomFields/CustomFieldForm.tsx @@ -0,0 +1,92 @@ +import React, { useRef, useState } from 'react'; +import { Form, Input, confirm } from 'UI'; +import styles from './customFieldForm.module.css'; +import { useStore } from 'App/mstore'; +import { useModal } from 'Components/Modal'; +import { toast } from 'react-toastify'; +import { Button } from 'antd'; +import { Trash } from 'UI/Icons'; +import { observer } from 'mobx-react-lite'; + +interface CustomFieldFormProps { + siteId: string; +} + +const CustomFieldForm: React.FC = ({ siteId }) => { + console.log('siteId', siteId); + const focusElementRef = useRef(null); + const { customFieldStore: store } = useStore(); + const field = store.instance; + const { hideModal } = useModal(); + const [loading, setLoading] = useState(false); + + const write = ({ target: { value, name } }: any) => store.edit({ [name]: value }); + const exists = field?.exists(); + + const onDelete = async () => { + if ( + await confirm({ + header: 'Metadata', + confirmation: `Are you sure you want to remove?` + }) + ) { + store.remove(siteId, field?.index!).then(() => { + hideModal(); + }); + } + }; + + const onSave = (field: any) => { + setLoading(true); + store.save(siteId, field).then((response) => { + if (!response || !response.errors || response.errors.size === 0) { + hideModal(); + toast.success('Metadata added successfully!'); + } else { + toast.error(response.errors[0]); + } + }).finally(() => { + setLoading(false); + }); + }; + + return ( +
+

{exists ? 'Update' : 'Add'} Metadata Field

+
+ + + + + +
+
+ + +
+ + +
+
+
+ ); +}; + +export default observer(CustomFieldForm); diff --git a/frontend/app/components/Client/CustomFields/CustomFields.js b/frontend/app/components/Client/CustomFields/CustomFields.js index a525094f3..1b800095a 100644 --- a/frontend/app/components/Client/CustomFields/CustomFields.js +++ b/frontend/app/components/Client/CustomFields/CustomFields.js @@ -14,124 +14,124 @@ import { useModal } from 'App/components/Modal'; import { toast } from 'react-toastify'; function CustomFields(props) { - const [currentSite, setCurrentSite] = React.useState(props.sites.get(0)); - const [deletingItem, setDeletingItem] = React.useState(null); - const { showModal, hideModal } = useModal(); + const [currentSite, setCurrentSite] = React.useState(props.sites.get(0)); + const [deletingItem, setDeletingItem] = React.useState(null); + const { showModal, hideModal } = useModal(); - useEffect(() => { - const activeSite = props.sites.get(0); - if (!activeSite) return; + useEffect(() => { + const activeSite = props.sites.get(0); + if (!activeSite) return; - props.fetchList(activeSite.id); - }, []); + props.fetchList(activeSite.id); + }, []); - const save = (field) => { - props.save(currentSite.id, field).then((response) => { - if (!response || !response.errors || response.errors.size === 0) { - hideModal(); - toast.success('Metadata added successfully!'); - } else { - toast.error(response.errors[0]); - } + const save = (field) => { + props.save(currentSite.id, field).then((response) => { + if (!response || !response.errors || response.errors.size === 0) { + hideModal(); + toast.success('Metadata added successfully!'); + } else { + toast.error(response.errors[0]); + } + }); + }; + + const init = (field) => { + props.init(field); + showModal( removeMetadata(field)} />); + }; + + const onChangeSelect = ({ value }) => { + const site = props.sites.find((s) => s.id === value.value); + setCurrentSite(site); + props.fetchList(site.id); + }; + + const removeMetadata = async (field) => { + if ( + await confirm({ + header: 'Metadata', + confirmation: `Are you sure you want to remove?` + }) + ) { + setDeletingItem(field.index); + props + .remove(currentSite.id, field.index) + .then(() => { + hideModal(); + }) + .finally(() => { + setDeletingItem(null); }); - }; + } + }; - const init = (field) => { - props.init(field); - showModal( removeMetadata(field)} />); - }; - - const onChangeSelect = ({ value }) => { - const site = props.sites.find((s) => s.id === value.value); - setCurrentSite(site); - props.fetchList(site.id); - }; - - const removeMetadata = async (field) => { - if ( - await confirm({ - header: 'Metadata', - confirmation: `Are you sure you want to remove?`, - }) - ) { - setDeletingItem(field.index); - props - .remove(currentSite.id, field.index) - .then(() => { - hideModal(); - }) - .finally(() => { - setDeletingItem(null); - }); - } - }; - - const { fields, loading } = props; - return ( -
-
-

{'Metadata'}

-
- -
-
- - - -
-
-
- - See additonal user information in sessions. - Learn more -
- - - - - {/*
*/} -
None added yet
-
- } - size="small" - show={fields.size === 0} - > -
- {fields - .filter((i) => i.index) - .map((field) => ( - <> - removeMetadata(field) } - /> - - - ))} -
-
-
+ const { fields, loading } = props; + return ( +
+
+

{'Metadata'}

+
+
- ); +
+ + + +
+
+
+ + See additonal user information in sessions. + Learn more +
+ + + + + {/*
*/} +
None added yet
+
+ } + size="small" + show={fields.size === 0} + > +
+ {fields + .filter((i) => i.index) + .map((field) => ( + <> + removeMetadata(field) } + /> + + + ))} +
+
+
+
+ ); } export default connect( - (state) => ({ - fields: state.getIn(['customFields', 'list']).sortBy((i) => i.index), - field: state.getIn(['customFields', 'instance']), - loading: state.getIn(['customFields', 'fetchRequest', 'loading']), - sites: state.getIn(['site', 'list']), - errors: state.getIn(['customFields', 'saveRequest', 'errors']), - }), - { - init, - fetchList, - save, - remove, - } + (state) => ({ + fields: state.getIn(['customFields', 'list']).sortBy((i) => i.index), + field: state.getIn(['customFields', 'instance']), + loading: state.getIn(['customFields', 'fetchRequest', 'loading']), + sites: state.getIn(['site', 'list']), + errors: state.getIn(['customFields', 'saveRequest', 'errors']) + }), + { + init, + fetchList, + save, + remove + } )(withPageTitle('Metadata - OpenReplay Preferences')(CustomFields)); diff --git a/frontend/app/components/Client/CustomFields/CustomFields.tsx b/frontend/app/components/Client/CustomFields/CustomFields.tsx new file mode 100644 index 000000000..dd42911e4 --- /dev/null +++ b/frontend/app/components/Client/CustomFields/CustomFields.tsx @@ -0,0 +1,114 @@ +import React, { useEffect, useState } from 'react'; +import cn from 'classnames'; +import { connect } from 'react-redux'; +import withPageTitle from 'HOCs/withPageTitle'; +import { Button, Loader, NoContent, Icon, Tooltip, Divider } from 'UI'; +import SiteDropdown from 'Shared/SiteDropdown'; +import styles from './customFields.module.css'; +import CustomFieldForm from './CustomFieldForm'; +import ListItem from './ListItem'; +import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG'; +import { useModal } from 'App/components/Modal'; +import { useStore } from 'App/mstore'; +import { observer } from 'mobx-react-lite'; + +interface CustomFieldsProps { + sites: any; +} + +const CustomFields: React.FC = (props) => { + const [currentSite, setCurrentSite] = useState(props.sites.get(0)); + const [deletingItem, setDeletingItem] = useState(null); + const { showModal, hideModal } = useModal(); + const { customFieldStore: store } = useStore(); + const fields = store.list; + const [loading, setLoading] = useState(false); + + useEffect(() => { + const activeSite = props.sites.get(0); + if (!activeSite) return; + + setCurrentSite(activeSite); + + setLoading(true); + store.fetchList(activeSite.id).finally(() => { + setLoading(false); + }); + }, [props.sites]); + + const handleInit = (field?: any) => { + console.log('field', field); + store.init(field); + showModal(, { + title: field ? 'Edit Metadata' : 'Add Metadata', right: true + }); + }; + + const onChangeSelect = ({ value }: { value: { value: number } }) => { + const site = props.sites.find((s: any) => s.id === value.value); + setCurrentSite(site); + + setLoading(true); + store.fetchList(site.id).finally(() => { + setLoading(false); + }); + }; + + return ( +
+
+

{'Metadata'}

+
+ +
+
+ + + +
+
+
+ + See additional user information in sessions. + + Learn more + +
+ + + + +
None added yet
+
+ } + size="small" + show={fields.length === 0} + > +
+ {fields + .filter((i: any) => i.index) + .map((field: any) => ( + <> + + + + ))} +
+ + +
+ ); +}; + +export default connect((state: any) => ({ + sites: state.getIn(['site', 'list']) +}))(withPageTitle('Metadata - OpenReplay Preferences')(observer(CustomFields))); diff --git a/frontend/app/components/Client/CustomFields/ListItem.js b/frontend/app/components/Client/CustomFields/ListItem.js index 326faa1b5..9c38e12e9 100644 --- a/frontend/app/components/Client/CustomFields/ListItem.js +++ b/frontend/app/components/Client/CustomFields/ListItem.js @@ -4,23 +4,23 @@ import { Button } from 'UI'; import styles from './listItem.module.css'; const ListItem = ({ field, onEdit, disabled }) => { - return ( -
field.index != 0 && onEdit(field)} - > - {field.key} -
-
-
- ); + return ( +
field.index !== 0 && onEdit(field)} + > + {field.key} +
+
+
+ ); }; export default ListItem; diff --git a/frontend/app/mstore/customFieldStore.ts b/frontend/app/mstore/customFieldStore.ts new file mode 100644 index 000000000..b8bd2ebb7 --- /dev/null +++ b/frontend/app/mstore/customFieldStore.ts @@ -0,0 +1,117 @@ +import { makeAutoObservable } from 'mobx'; +import { customFieldService } from 'App/services'; + +import { + addElementToConditionalFiltersMap, + addElementToMobileConditionalFiltersMap, + addElementToFiltersMap, + addElementToFlagConditionsMap, + addElementToLiveFiltersMap, + clearMetaFilters +} from 'Types/filter/newFilter'; +import { FilterCategory } from 'Types/filter/filterType'; +import CustomField from 'App/mstore/types/customField'; +import customFields from 'Components/Client/CustomFields'; + +class CustomFieldStore { + isLoading: boolean = false; + isSaving: boolean = false; + list: CustomField[] = []; + instance: CustomField = new CustomField(); + sources: CustomField[] = []; + fetchedMetadata: boolean = false; + search: string = ''; + + constructor() { + makeAutoObservable(this); + } + + edit = (field: Partial) => { + Object.assign(this.instance!, field); + }; + + async fetchList(siteId?: string): Promise { + this.isLoading = true; + try { + const response = await customFieldService.get(siteId); + this.list = response.map((item: any) => new CustomField(item)); + } finally { + this.isLoading = false; + } + } + + async fetchListActive(siteId?: string): Promise { + this.isLoading = true; + try { + const response = await customFieldService.get(siteId); + clearMetaFilters(); + response.forEach((item: any) => { + addElementToFiltersMap(FilterCategory.METADATA, '_' + item.key); + addElementToLiveFiltersMap(FilterCategory.METADATA, '_' + item.key); + addElementToFlagConditionsMap(FilterCategory.METADATA, '_' + item.key); + addElementToConditionalFiltersMap(FilterCategory.METADATA, '_' + item.key); + addElementToMobileConditionalFiltersMap(FilterCategory.METADATA, '_' + item.key); + }); + this.list = response.map((item_1: any) => new CustomField(item_1)); + this.fetchedMetadata = true; + } finally { + this.isLoading = false; + } + } + + async fetchSources(): Promise { + this.isLoading = true; + try { + const response = await customFieldService.get('/integration/sources'); + this.sources = response.map(({ value, ...item }: any) => new CustomField({ + label: value, + key: value, + ...item + })); + } finally { + this.isLoading = false; + } + } + + async save(siteId: string, instance: CustomField): Promise { + this.isSaving = true; + try { + const wasCreating = !instance.exists(); + const response = instance.exists() ? await customFieldService.create(siteId, instance.toData()) : + await customFieldService.update(siteId, instance.toData()); + const updatedInstance = new CustomField(response); + + if (wasCreating) { + this.list.push(updatedInstance); + } else { + const index = this.list.findIndex(item => item.index === instance.index); + if (index >= 0) + this.list[index] = updatedInstance; + } + } finally { + this.isSaving = false; + } + } + + async remove(siteId: string, index: string): Promise { + this.isSaving = true; + try { + await customFieldService.delete(siteId, index); + this.list = this.list.filter(item => item.index !== index); + } finally { + this.isSaving = false; + } + } + + init(instance?: any) { + // this.instance = new CustomField(instance); + if (instance) { + this.instance = new CustomField().fromJson(instance); + } else { + this.instance = new CustomField(); + } + } +} + + +export default CustomFieldStore; diff --git a/frontend/app/mstore/index.tsx b/frontend/app/mstore/index.tsx index 2d536aefd..723655adb 100644 --- a/frontend/app/mstore/index.tsx +++ b/frontend/app/mstore/index.tsx @@ -18,14 +18,15 @@ import WeeklyReportStore from './weeklyReportConfigStore'; import AlertStore from './alertsStore'; import FeatureFlagsStore from './featureFlagsStore'; import UxtestingStore from './uxtestingStore'; -import TagWatchStore from './tagWatchStore'; -import AiSummaryStore from "./aiSummaryStore"; -import AiFiltersStore from "./aiFiltersStore"; -import SpotStore from "./spotStore"; -import LoginStore from "./loginStore"; -import FilterStore from "./filterStore"; +import TagWatchStore from './tagWatchStore'; +import AiSummaryStore from './aiSummaryStore'; +import AiFiltersStore from './aiFiltersStore'; +import SpotStore from './spotStore'; +import LoginStore from './loginStore'; +import FilterStore from './filterStore'; import UiPlayerStore from './uiPlayerStore'; import IssueReportingStore from './issueReportingStore'; +import CustomFieldStore from './customFieldStore'; export class RootStore { dashboardStore: DashboardStore; @@ -53,6 +54,7 @@ export class RootStore { filterStore: FilterStore; uiPlayerStore: UiPlayerStore; issueReportingStore: IssueReportingStore; + customFieldStore: CustomFieldStore; constructor() { this.dashboardStore = new DashboardStore(); @@ -80,6 +82,7 @@ export class RootStore { this.filterStore = new FilterStore(); this.uiPlayerStore = new UiPlayerStore(); this.issueReportingStore = new IssueReportingStore(); + this.customFieldStore = new CustomFieldStore(); } initClient() { diff --git a/frontend/app/mstore/types/customField.ts b/frontend/app/mstore/types/customField.ts new file mode 100644 index 000000000..41ddac22e --- /dev/null +++ b/frontend/app/mstore/types/customField.ts @@ -0,0 +1,55 @@ +import { makeAutoObservable, runInAction } from 'mobx'; + +const varRegExp = new RegExp('^[A-Za-z_-][A-Za-z0-9_-]*$'); + +export const BOOLEAN = 'boolean'; +export const STRING = 'string'; +export const NUMBER = 'number'; +export const MAX_COUNT = 20; + +interface CustomRecord { + index?: string; + key: string; + label: string; + type: string; + validate: () => boolean; + toData: () => any; +} + +class CustomField implements CustomRecord { + index: string = ''; + key: string = ''; + label: string = ''; + type: string = STRING; + + constructor(props: Partial = {}) { + Object.assign(this, props); + makeAutoObservable(this); + } + + fromJson(json: any) { + runInAction(() => { + Object.assign(this, json); + }); + return this; + } + + exists(): boolean { + return Boolean(this.index); + } + + validate(): boolean { + return varRegExp.test(this.key) && this.type !== ''; + } + + toData(): any { + return { + index: this.index, + key: this.key, + label: this.label, + type: this.type + }; + } +} + +export default CustomField; diff --git a/frontend/app/services/CustomFieldService.ts b/frontend/app/services/CustomFieldService.ts new file mode 100644 index 000000000..d1dbb0e40 --- /dev/null +++ b/frontend/app/services/CustomFieldService.ts @@ -0,0 +1,29 @@ +import BaseService from './BaseService'; + +export default class CustomFieldService extends BaseService { + async fetchList(siteId: string): Promise { + return this.client.get(siteId ? `/${siteId}/metadata` : '/metadata') + .then(r => r.json()).then(j => j.data); + } + + async get(siteId?: string): Promise { + const url = siteId ? `/${siteId}/metadata` : '/metadata'; + return this.client.get(url) + .then(r => r.json()).then(j => j.data); + } + + async create(siteId: string, customField: any): Promise { + return this.client.post(`/${siteId}/metadata`, customField) + .then(r => r.json()).then(j => j.data); + } + + async update(siteId: string, instance: any): Promise { + return this.client.put(`/${siteId}/metadata/${instance.index}`, instance) + .then(r => r.json()).then(j => j.data); + } + + async delete(siteId: string, index: string): Promise { + return this.client.delete(`/${siteId}/metadata/${index}`) + .then(r => r.json()).then(j => j.data); + } +} diff --git a/frontend/app/services/index.ts b/frontend/app/services/index.ts index a6bf3062b..6afacb791 100644 --- a/frontend/app/services/index.ts +++ b/frontend/app/services/index.ts @@ -21,6 +21,7 @@ import SpotService from './spotService'; import LoginService from "./loginService"; import FilterService from "./FilterService"; import IssueReportsService from "./IssueReportsService"; +import CustomFieldService from './CustomFieldService'; import IntegrationsService from './IntegrationsService'; export const dashboardService = new DashboardService(); @@ -45,6 +46,7 @@ export const spotService = new SpotService(); export const loginService = new LoginService(); export const filterService = new FilterService(); export const issueReportsService = new IssueReportsService(); +export const customFieldService = new CustomFieldService(); export const integrationsService = new IntegrationsService(); export const services = [ @@ -70,5 +72,6 @@ export const services = [ loginService, filterService, issueReportsService, + customFieldService, integrationsService, ];