change(ui): custom fields

This commit is contained in:
Shekar Siri 2024-09-16 18:15:21 +05:30
parent 4f2d61d1cf
commit d4d836ad24
10 changed files with 548 additions and 135 deletions

View file

@ -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);

View file

@ -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<CustomFieldFormProps> = ({ siteId }) => {
console.log('siteId', siteId);
const focusElementRef = useRef<HTMLInputElement>(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 (
<div className="bg-white h-screen overflow-y-auto">
<h3 className="p-5 text-2xl">{exists ? 'Update' : 'Add'} Metadata Field</h3>
<Form className={styles.wrapper}>
<Form.Field>
<label>{'Field Name'}</label>
<Input
ref={focusElementRef}
name="key"
value={field?.key}
onChange={write}
placeholder="Field Name"
maxLength={50}
/>
</Form.Field>
<div className="flex justify-between">
<div className="flex items-center">
<Button
onClick={() => onSave(field)}
disabled={!field?.validate()}
loading={loading}
type="primary"
className="float-left mr-2"
>
{exists ? 'Update' : 'Add'}
</Button>
<Button data-hidden={!exists} onClick={hideModal}>
{'Cancel'}
</Button>
</div>
<Button type="text" icon={<Trash />} data-hidden={!exists} onClick={onDelete}></Button>
</div>
</Form>
</div>
);
};
export default observer(CustomFieldForm);

View file

@ -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(<CustomFieldForm onClose={hideModal} onSave={save} onDelete={() => 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(<CustomFieldForm onClose={hideModal} onSave={save} onDelete={() => 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 (
<div className="bg-white rounded-lg shadow-sm border p-5 ">
<div className={cn(styles.tabHeader)}>
<h3 className={cn(styles.tabTitle, 'text-2xl')}>{'Metadata'}</h3>
<div style={{ marginRight: '15px' }}>
<SiteDropdown value={currentSite && currentSite.id} onChange={onChangeSelect} />
</div>
<div className="ml-auto">
<Tooltip title="You've reached the limit of 10 metadata." disabled={fields.size < 10}>
<Button disabled={fields.size >= 10} variant="primary" onClick={() => init()}>Add Metadata</Button>
</Tooltip>
</div>
</div>
<div className="text-base text-disabled-text flex px-5 items-center my-3">
<Icon name="info-circle-fill" className="mr-2" size={16} />
See additonal user information in sessions.
<a href="https://docs.openreplay.com/installation/metadata" className="link ml-1" target="_blank">Learn more</a>
</div>
<Loader loading={loading}>
<NoContent
title={
<div className="flex flex-col items-center justify-center">
<AnimatedSVG name={ICONS.NO_METADATA} size={60} />
{/* <div className="mt-4" /> */}
<div className="text-center my-4">None added yet</div>
</div>
}
size="small"
show={fields.size === 0}
>
<div className={styles.list}>
{fields
.filter((i) => i.index)
.map((field) => (
<>
<ListItem
disabled={deletingItem && deletingItem === field.index}
key={field._key}
field={field}
onEdit={init}
// onDelete={ () => removeMetadata(field) }
/>
<Divider className="m-0" />
</>
))}
</div>
</NoContent>
</Loader>
const { fields, loading } = props;
return (
<div className="bg-white rounded-lg shadow-sm border p-5 ">
<div className={cn(styles.tabHeader)}>
<h3 className={cn(styles.tabTitle, 'text-2xl')}>{'Metadata'}</h3>
<div style={{ marginRight: '15px' }}>
<SiteDropdown value={currentSite && currentSite.id} onChange={onChangeSelect} />
</div>
);
<div className="ml-auto">
<Tooltip title="You've reached the limit of 10 metadata." disabled={fields.size < 10}>
<Button disabled={fields.size >= 10} variant="primary" onClick={() => init()}>Add Metadata</Button>
</Tooltip>
</div>
</div>
<div className="text-base text-disabled-text flex px-5 items-center my-3">
<Icon name="info-circle-fill" className="mr-2" size={16} />
See additonal user information in sessions.
<a href="https://docs.openreplay.com/installation/metadata" className="link ml-1" target="_blank">Learn more</a>
</div>
<Loader loading={loading}>
<NoContent
title={
<div className="flex flex-col items-center justify-center">
<AnimatedSVG name={ICONS.NO_METADATA} size={60} />
{/* <div className="mt-4" /> */}
<div className="text-center my-4">None added yet</div>
</div>
}
size="small"
show={fields.size === 0}
>
<div className={styles.list}>
{fields
.filter((i) => i.index)
.map((field) => (
<>
<ListItem
disabled={deletingItem && deletingItem === field.index}
key={field._key}
field={field}
onEdit={init}
// onDelete={ () => removeMetadata(field) }
/>
<Divider className="m-0" />
</>
))}
</div>
</NoContent>
</Loader>
</div>
);
}
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));

View file

@ -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<CustomFieldsProps> = (props) => {
const [currentSite, setCurrentSite] = useState(props.sites.get(0));
const [deletingItem, setDeletingItem] = useState<number | null>(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(<CustomFieldForm siteId={currentSite.id} />, {
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 (
<div className="bg-white rounded-lg shadow-sm border p-5">
<div className={cn(styles.tabHeader)}>
<h3 className={cn(styles.tabTitle, 'text-2xl')}>{'Metadata'}</h3>
<div style={{ marginRight: '15px' }}>
<SiteDropdown value={currentSite && currentSite.id} onChange={onChangeSelect} />
</div>
<div className="ml-auto">
<Tooltip title="You've reached the limit of 10 metadata." disabled={fields.length < 10}>
<Button disabled={fields.length >= 10} variant="primary" onClick={() => handleInit()}>
Add Metadata
</Button>
</Tooltip>
</div>
</div>
<div className="text-base text-disabled-text flex px-5 items-center my-3">
<Icon name="info-circle-fill" className="mr-2" size={16} />
See additional user information in sessions.
<a href="https://docs.openreplay.com/installation/metadata" className="link ml-1" target="_blank">
Learn more
</a>
</div>
<Loader loading={loading}>
<NoContent
title={
<div className="flex flex-col items-center justify-center">
<AnimatedSVG name={ICONS.NO_METADATA} size={60} />
<div className="text-center my-4">None added yet</div>
</div>
}
size="small"
show={fields.length === 0}
>
<div className={styles.list}>
{fields
.filter((i: any) => i.index)
.map((field: any) => (
<>
<ListItem
disabled={deletingItem !== null && deletingItem === field.index}
key={field._key}
field={field}
onEdit={handleInit}
/>
<Divider className="m-0" />
</>
))}
</div>
</NoContent>
</Loader>
</div>
);
};
export default connect((state: any) => ({
sites: state.getIn(['site', 'list'])
}))(withPageTitle('Metadata - OpenReplay Preferences')(observer(CustomFields)));

View file

@ -4,23 +4,23 @@ import { Button } from 'UI';
import styles from './listItem.module.css';
const ListItem = ({ field, onEdit, disabled }) => {
return (
<div
className={cn(
'group hover:bg-active-blue flex items-center justify-between py-3 px-5 cursor-pointer',
field.index === 0 ? styles.preDefined : '',
{
[styles.disabled]: disabled,
}
)}
onClick={() => field.index != 0 && onEdit(field)}
>
<span>{field.key}</span>
<div className="invisible group-hover:visible" data-hidden={field.index === 0}>
<Button variant="text-primary" icon="pencil" />
</div>
</div>
);
return (
<div
className={cn(
'group hover:bg-active-blue flex items-center justify-between py-3 px-5 cursor-pointer',
field.index === 0 ? styles.preDefined : '',
{
[styles.disabled]: disabled
}
)}
onClick={() => field.index !== 0 && onEdit(field)}
>
<span>{field.key}</span>
<div className="invisible group-hover:visible" data-hidden={field.index === 0}>
<Button variant="text-primary" icon="pencil" />
</div>
</div>
);
};
export default ListItem;

View file

@ -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<CustomField>) => {
Object.assign(this.instance!, field);
};
async fetchList(siteId?: string): Promise<any> {
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<any> {
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<any> {
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<any> {
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<any> {
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;

View file

@ -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() {

View file

@ -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<CustomRecord> = {}) {
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;

View file

@ -0,0 +1,29 @@
import BaseService from './BaseService';
export default class CustomFieldService extends BaseService {
async fetchList(siteId: string): Promise<any> {
return this.client.get(siteId ? `/${siteId}/metadata` : '/metadata')
.then(r => r.json()).then(j => j.data);
}
async get(siteId?: string): Promise<any> {
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<any> {
return this.client.post(`/${siteId}/metadata`, customField)
.then(r => r.json()).then(j => j.data);
}
async update(siteId: string, instance: any): Promise<any> {
return this.client.put(`/${siteId}/metadata/${instance.index}`, instance)
.then(r => r.json()).then(j => j.data);
}
async delete(siteId: string, index: string): Promise<any> {
return this.client.delete(`/${siteId}/metadata/${index}`)
.then(r => r.json()).then(j => j.data);
}
}

View file

@ -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,
];