change(ui): project settings updates (#1381)
* change(ui): prefs project settings * change(api): projects settings redesign * change(api): projects settings redesign * change(ui): projects pagination
This commit is contained in:
parent
86d47b595d
commit
d3a411f852
11 changed files with 429 additions and 257 deletions
|
|
@ -10,6 +10,7 @@ import Header from 'Components/Header/Header';
|
|||
import { fetchList as fetchSiteList } from 'Duck/site';
|
||||
import { withStore } from 'App/mstore';
|
||||
|
||||
|
||||
import APIClient from './api_client';
|
||||
import * as routes from './routes';
|
||||
import { OB_DEFAULT_TAB, isRoute } from 'App/routes';
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@ const siteIdRequiredPaths = [
|
|||
'/assignments',
|
||||
'/integration/sources',
|
||||
'/issue_types',
|
||||
'/sample_rate',
|
||||
'/saved_search',
|
||||
'/rehydrations',
|
||||
'/sourcemaps',
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import React, { useState, useEffect, ChangeEvent, FormEvent } from 'react';
|
||||
import { connect, ConnectedProps } from 'react-redux';
|
||||
import { Form, Input, Button, Icon } from 'UI';
|
||||
import { save, edit, update, fetchList, remove } from 'Duck/site';
|
||||
import { pushNewSite } from 'Duck/user';
|
||||
import { setSiteId } from 'Duck/site';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import { withRouter, RouteComponentProps } from 'react-router-dom';
|
||||
import styles from './siteForm.module.css';
|
||||
import { confirm } from 'UI';
|
||||
import { clearSearch } from 'Duck/search';
|
||||
|
|
@ -12,6 +12,16 @@ import { clearSearch as clearSearchLive } from 'Duck/liveSearch';
|
|||
import { withStore } from 'App/mstore';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
type OwnProps = {
|
||||
onClose: (arg: any) => void;
|
||||
mstore: any;
|
||||
canDelete: boolean;
|
||||
};
|
||||
|
||||
type PropsFromRedux = ConnectedProps<typeof connector>;
|
||||
|
||||
type Props = PropsFromRedux & RouteComponentProps & OwnProps;
|
||||
|
||||
const NewSiteForm = ({
|
||||
site,
|
||||
loading,
|
||||
|
|
@ -29,7 +39,7 @@ const NewSiteForm = ({
|
|||
mstore,
|
||||
activeSiteId,
|
||||
canDelete,
|
||||
}) => {
|
||||
}: Props) => {
|
||||
const [existsError, setExistsError] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -38,11 +48,11 @@ const NewSiteForm = ({
|
|||
}
|
||||
}, []);
|
||||
|
||||
const onSubmit = (e) => {
|
||||
const onSubmit = (e: FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (site.exists()) {
|
||||
update(site, site.id).then((response) => {
|
||||
update(site, site.id).then((response: any) => {
|
||||
if (!response || !response.errors || response.errors.size === 0) {
|
||||
onClose(null);
|
||||
if (!pathname.includes('onboarding')) {
|
||||
|
|
@ -54,7 +64,7 @@ const NewSiteForm = ({
|
|||
}
|
||||
});
|
||||
} else {
|
||||
save(site).then((response) => {
|
||||
save(site).then((response: any) => {
|
||||
if (!response || !response.errors || response.errors.size === 0) {
|
||||
onClose(null);
|
||||
clearSearch();
|
||||
|
|
@ -78,13 +88,13 @@ const NewSiteForm = ({
|
|||
remove(site.id).then(() => {
|
||||
onClose(null);
|
||||
if (site.id === activeSiteId) {
|
||||
setSiteId(null)
|
||||
setSiteId(null);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleEdit = ({ target: { name, value } }) => {
|
||||
const handleEdit = ({ target: { name, value } }: ChangeEvent<HTMLInputElement>) => {
|
||||
setExistsError(false);
|
||||
edit({ [name]: value });
|
||||
};
|
||||
|
|
@ -128,7 +138,7 @@ const NewSiteForm = ({
|
|||
);
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
const mapStateToProps = (state: any) => ({
|
||||
activeSiteId: state.getIn(['site', 'active', 'id']),
|
||||
site: state.getIn(['site', 'instance']),
|
||||
siteList: state.getIn(['site', 'list']),
|
||||
|
|
@ -136,7 +146,7 @@ const mapStateToProps = (state) => ({
|
|||
canDelete: state.getIn(['site', 'list']).size > 1,
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, {
|
||||
const connector = connect(mapStateToProps, {
|
||||
save,
|
||||
remove,
|
||||
edit,
|
||||
|
|
@ -146,4 +156,6 @@ export default connect(mapStateToProps, {
|
|||
setSiteId,
|
||||
clearSearch,
|
||||
clearSearchLive,
|
||||
})(withRouter(withStore(NewSiteForm)));
|
||||
});
|
||||
|
||||
export default connector(withRouter(withStore(NewSiteForm)));
|
||||
|
|
@ -1,140 +0,0 @@
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import withPageTitle from 'HOCs/withPageTitle';
|
||||
import { Loader, Button, TextLink, NoContent } from 'UI';
|
||||
import { init, remove, fetchGDPR, setSiteId } from 'Duck/site';
|
||||
import stl from './sites.module.css';
|
||||
import NewSiteForm from './NewSiteForm';
|
||||
import { confirm, PageTitle } from 'UI';
|
||||
import SiteSearch from './SiteSearch';
|
||||
import AddProjectButton from './AddProjectButton';
|
||||
import InstallButton from './InstallButton';
|
||||
import ProjectKey from './ProjectKey';
|
||||
import { useModal } from 'App/components/Modal';
|
||||
import { getInitials } from 'App/utils';
|
||||
import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG';
|
||||
import cn from 'classnames'
|
||||
|
||||
const NEW_SITE_FORM = 'NEW_SITE_FORM';
|
||||
|
||||
@connect(
|
||||
(state) => ({
|
||||
site: state.getIn(['site', 'instance']),
|
||||
sites: state.getIn(['site', 'list']),
|
||||
loading: state.getIn(['site', 'loading']),
|
||||
user: state.getIn(['user', 'account']),
|
||||
account: state.getIn(['user', 'account']),
|
||||
}),
|
||||
{
|
||||
init,
|
||||
remove,
|
||||
fetchGDPR,
|
||||
setSiteId,
|
||||
}
|
||||
)
|
||||
@withPageTitle('Projects - OpenReplay Preferences')
|
||||
class Sites extends React.PureComponent {
|
||||
state = {
|
||||
searchQuery: '',
|
||||
};
|
||||
|
||||
edit = (site) => {
|
||||
this.props.init(site);
|
||||
this.setState({ modalContent: NEW_SITE_FORM });
|
||||
};
|
||||
|
||||
remove = async (site) => {
|
||||
if (
|
||||
await confirm({
|
||||
header: 'Projects',
|
||||
confirmation: `Are you sure you want to delete this Project? We won't be able to record anymore sessions.`,
|
||||
})
|
||||
) {
|
||||
this.props.remove(site.id)
|
||||
this.props.setSiteId(null);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { loading, sites, user } = this.props;
|
||||
const isAdmin = user.admin || user.superAdmin;
|
||||
const filteredSites = sites.filter((site) => site.name.toLowerCase().includes(this.state.searchQuery.toLowerCase()));
|
||||
|
||||
return (
|
||||
<Loader loading={loading}>
|
||||
<div className={stl.wrapper}>
|
||||
<div className={cn(stl.tabHeader, 'px-5 pt-5')}>
|
||||
<PageTitle title={<div className="mr-4">Projects</div>} actionButton={<TextLink icon="book" href="https://docs.openreplay.com/installation" label="Installation Docs" />} />
|
||||
|
||||
<div className="flex ml-auto items-center">
|
||||
<AddProjectButton isAdmin={isAdmin} />
|
||||
<div className="mx-2" />
|
||||
<SiteSearch onChange={(value) => this.setState({ searchQuery: value })} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={stl.list}>
|
||||
<NoContent
|
||||
title={
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<AnimatedSVG name={ICONS.NO_PROJECTS} size={170} />
|
||||
<div className="text-center my-4">No matching results.</div>
|
||||
</div>
|
||||
}
|
||||
size="small"
|
||||
show={!loading && filteredSites.size === 0}
|
||||
>
|
||||
<div className="grid grid-cols-12 gap-2 w-full items-center px-5 py-3 font-medium">
|
||||
<div className="col-span-4">Project Name</div>
|
||||
<div className="col-span-4">Key</div>
|
||||
<div className="col-span-4"></div>
|
||||
</div>
|
||||
{filteredSites.map((_site) => (
|
||||
<div
|
||||
key={_site.key}
|
||||
className="grid grid-cols-12 gap-2 w-full group hover:bg-active-blue items-center border-t px-5 py-3"
|
||||
>
|
||||
<div className="col-span-4">
|
||||
<div className="flex items-center">
|
||||
<div className="relative flex items-center justify-center w-10 h-10">
|
||||
<div
|
||||
className="absolute left-0 right-0 top-0 bottom-0 mx-auto w-10 h-10 rounded-full opacity-30 bg-tealx"
|
||||
/>
|
||||
<div className="text-lg uppercase color-tealx">
|
||||
{getInitials(_site.name)}
|
||||
</div>
|
||||
</div>
|
||||
<span className="ml-2">{_site.host}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-4">
|
||||
<ProjectKey value={_site.projectKey} tooltip="Project key copied to clipboard" />
|
||||
</div>
|
||||
<div className="col-span-4 justify-self-end flex items-center">
|
||||
<div className="mr-4">
|
||||
<InstallButton site={_site} />
|
||||
</div>
|
||||
<div className="invisible group-hover:visible">
|
||||
<EditButton isAdmin={isAdmin} onClick={() => this.props.init(_site)} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</NoContent>
|
||||
</div>
|
||||
</div>
|
||||
</Loader>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Sites;
|
||||
|
||||
function EditButton({ isAdmin, onClick }) {
|
||||
const { showModal, hideModal } = useModal();
|
||||
const _onClick = () => {
|
||||
onClick();
|
||||
showModal(<NewSiteForm onClose={hideModal} />);
|
||||
};
|
||||
return <Button icon="edit" variant="text-primary" disabled={!isAdmin} onClick={_onClick} />;
|
||||
}
|
||||
220
frontend/app/components/Client/Sites/Sites.tsx
Normal file
220
frontend/app/components/Client/Sites/Sites.tsx
Normal file
|
|
@ -0,0 +1,220 @@
|
|||
import React, { useState } from 'react';
|
||||
import { connect, ConnectedProps } from 'react-redux';
|
||||
import { Drawer } from 'antd';
|
||||
import cn from 'classnames';
|
||||
import {
|
||||
Loader,
|
||||
Button,
|
||||
TextLink,
|
||||
NoContent,
|
||||
Pagination,
|
||||
PageTitle
|
||||
} from 'UI';
|
||||
import {
|
||||
init,
|
||||
remove,
|
||||
fetchGDPR,
|
||||
setSiteId
|
||||
} from 'Duck/site';
|
||||
import withPageTitle from 'HOCs/withPageTitle';
|
||||
import stl from './sites.module.css';
|
||||
import NewSiteForm from './NewSiteForm';
|
||||
import SiteSearch from './SiteSearch';
|
||||
import AddProjectButton from './AddProjectButton';
|
||||
import InstallButton from './InstallButton';
|
||||
import ProjectKey from './ProjectKey';
|
||||
import { getInitials, sliceListPerPage } from 'App/utils';
|
||||
import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG';
|
||||
import { useModal } from 'App/components/Modal';
|
||||
import CaptureRate from 'Shared/SessionSettings/components/CaptureRate';
|
||||
|
||||
type Project = {
|
||||
id: number;
|
||||
name: string;
|
||||
host: string;
|
||||
projectKey: string;
|
||||
sampleRate: number;
|
||||
};
|
||||
|
||||
type PropsFromRedux = ConnectedProps<typeof connector>;
|
||||
|
||||
const Sites = ({
|
||||
loading,
|
||||
sites,
|
||||
user,
|
||||
init
|
||||
}: PropsFromRedux) => {
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [showCaptureRate, setShowCaptureRate] = useState(true);
|
||||
const [activeProject, setActiveProject] = useState<Project | null>(null);
|
||||
const [page, setPage] = useState(1);
|
||||
const pageSize = 5;
|
||||
|
||||
const isAdmin = user.admin || user.superAdmin;
|
||||
const filteredSites = sites.filter((site: { name: string }) =>
|
||||
site.name.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
);
|
||||
|
||||
const { showModal, hideModal } = useModal();
|
||||
|
||||
const EditButton = ({
|
||||
isAdmin,
|
||||
onClick
|
||||
}: {
|
||||
isAdmin: boolean;
|
||||
onClick: () => void;
|
||||
}) => {
|
||||
const _onClick = () => {
|
||||
onClick();
|
||||
showModal(<NewSiteForm onClose={hideModal} />, { right: true });
|
||||
};
|
||||
|
||||
return (
|
||||
<Button
|
||||
icon='edit'
|
||||
variant='text-primary'
|
||||
disabled={!isAdmin}
|
||||
onClick={_onClick}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const captureRateClickHandler = (project: Project) => {
|
||||
setActiveProject(project);
|
||||
setShowCaptureRate(true);
|
||||
};
|
||||
|
||||
const updatePage = (page: number) => {
|
||||
setPage(page);
|
||||
};
|
||||
|
||||
const ProjectItem = ({ project }: { project: Project }) => (
|
||||
<div
|
||||
key={project.id}
|
||||
className='grid grid-cols-12 gap-2 w-full group hover:bg-active-blue items-center border-t px-5 py-3'
|
||||
>
|
||||
<div className='col-span-4'>
|
||||
<div className='flex items-center'>
|
||||
<div className='relative flex items-center justify-center w-10 h-10'>
|
||||
<div
|
||||
className='absolute left-0 right-0 top-0 bottom-0 mx-auto w-10 h-10 rounded-full opacity-30 bg-tealx' />
|
||||
<div className='text-lg uppercase color-tealx'>
|
||||
{getInitials(project.name)}
|
||||
</div>
|
||||
</div>
|
||||
<span className='ml-2'>{project.host}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className='col-span-3'>
|
||||
<ProjectKey
|
||||
value={project.projectKey}
|
||||
tooltip='Project key copied to clipboard'
|
||||
/>
|
||||
</div>
|
||||
<div className='col-span-2'>
|
||||
<span
|
||||
className='link'
|
||||
onClick={() => captureRateClickHandler(project)}
|
||||
>
|
||||
{project.sampleRate}%
|
||||
</span>
|
||||
</div>
|
||||
<div className='col-span-3 justify-self-end flex items-center'>
|
||||
<div className='mr-4'>
|
||||
<InstallButton site={project} />
|
||||
</div>
|
||||
<div className='invisible group-hover:visible'>
|
||||
<EditButton isAdmin={isAdmin} onClick={() => init(project)} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<Loader loading={loading}>
|
||||
<div className={stl.wrapper}>
|
||||
<div className={cn(stl.tabHeader, 'px-5 pt-5')}>
|
||||
<PageTitle
|
||||
title={<div className='mr-4'>Projects</div>}
|
||||
actionButton={
|
||||
<TextLink
|
||||
icon='book'
|
||||
href='https://docs.openreplay.com/installation'
|
||||
label='Installation Docs'
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<div className='flex ml-auto items-center'>
|
||||
<AddProjectButton isAdmin={isAdmin} />
|
||||
<div className='mx-2' />
|
||||
<SiteSearch onChange={(value) => setSearchQuery(value)} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={stl.list}>
|
||||
<NoContent
|
||||
title={
|
||||
<div className='flex flex-col items-center justify-center'>
|
||||
<AnimatedSVG name={ICONS.NO_PROJECTS} size={170} />
|
||||
<div className='text-center text-gray-600 my-4'>
|
||||
No matching results.
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
size='small'
|
||||
show={!loading && filteredSites.size === 0}
|
||||
>
|
||||
<div className='grid grid-cols-12 gap-2 w-full items-center px-5 py-3 font-medium'>
|
||||
<div className='col-span-4'>Project Name</div>
|
||||
<div className='col-span-3'>Key</div>
|
||||
<div className='col-span-2'>Capture Rate</div>
|
||||
<div className='col-span-3'></div>
|
||||
</div>
|
||||
{sliceListPerPage(filteredSites, page - 1, pageSize).map(
|
||||
(project: Project) => (
|
||||
<ProjectItem project={project} />
|
||||
)
|
||||
)}
|
||||
|
||||
<div className='w-full flex items-center justify-center py-10'>
|
||||
<Pagination
|
||||
page={page}
|
||||
totalPages={Math.ceil(filteredSites.size / pageSize)}
|
||||
onPageChange={(page) => updatePage(page)}
|
||||
limit={pageSize}
|
||||
/>
|
||||
</div>
|
||||
</NoContent>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Drawer
|
||||
open={showCaptureRate && !!activeProject}
|
||||
onClose={() => setShowCaptureRate(!showCaptureRate)}
|
||||
title='Capture Rate'
|
||||
closable={false}
|
||||
destroyOnClose
|
||||
>
|
||||
{activeProject && <CaptureRate projectId={activeProject.id} />}
|
||||
</Drawer>
|
||||
</Loader>
|
||||
);
|
||||
};
|
||||
|
||||
const mapStateToProps = (state: any) => ({
|
||||
site: state.getIn(['site', 'instance']),
|
||||
sites: state.getIn(['site', 'list']),
|
||||
loading: state.getIn(['site', 'loading']),
|
||||
user: state.getIn(['user', 'account']),
|
||||
account: state.getIn(['user', 'account'])
|
||||
});
|
||||
|
||||
const connector = connect(mapStateToProps, {
|
||||
init,
|
||||
remove,
|
||||
fetchGDPR,
|
||||
setSiteId
|
||||
});
|
||||
|
||||
export default connector(withPageTitle('Projects - OpenReplay Preferences')(Sites));
|
||||
68
frontend/app/components/hocs/userDrawer.tsx
Normal file
68
frontend/app/components/hocs/userDrawer.tsx
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
import React, { useState } from 'react';
|
||||
import { Drawer, DrawerProps } from 'antd';
|
||||
|
||||
|
||||
interface ExtendedDrawerProps extends DrawerProps {
|
||||
visible: boolean;
|
||||
onClose: any;
|
||||
}
|
||||
|
||||
const DrawerComponent: React.FC<ExtendedDrawerProps> = ({
|
||||
visible,
|
||||
onClose,
|
||||
title,
|
||||
placement,
|
||||
children
|
||||
}) => {
|
||||
return (
|
||||
<Drawer
|
||||
visible={visible}
|
||||
onClose={onClose}
|
||||
destroyOnClose
|
||||
width={400}
|
||||
maskClosable={false}
|
||||
title={title}
|
||||
placement={placement}
|
||||
>
|
||||
{children}
|
||||
</Drawer>
|
||||
);
|
||||
};
|
||||
|
||||
const useDrawer = () => {
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [content, setContent] = useState<React.ReactNode>(null);
|
||||
const [drawerProps, setDrawerProps] = useState<DrawerProps>({
|
||||
title: '',
|
||||
children: null,
|
||||
placement: 'right'
|
||||
});
|
||||
|
||||
const showDrawer = (component: React.ReactNode, props: DrawerProps) => {
|
||||
console.log('here');
|
||||
setContent(component);
|
||||
setDrawerProps(props);
|
||||
setVisible(true);
|
||||
};
|
||||
|
||||
|
||||
const DrawerWrapper: React.FC = () => {
|
||||
return (
|
||||
<DrawerComponent
|
||||
visible={visible}
|
||||
onClose={() => setVisible(false)}
|
||||
{...drawerProps}
|
||||
>
|
||||
{content}
|
||||
</DrawerComponent>
|
||||
);
|
||||
};
|
||||
|
||||
return {
|
||||
showDrawer,
|
||||
// hideDrawer,
|
||||
DrawerWrapper
|
||||
};
|
||||
};
|
||||
|
||||
export default useDrawer;
|
||||
|
|
@ -3,31 +3,35 @@ import ListingVisibility from './components/ListingVisibility';
|
|||
import DefaultPlaying from './components/DefaultPlaying';
|
||||
import DefaultTimezone from './components/DefaultTimezone';
|
||||
import CaptureRate from './components/CaptureRate';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
function SessionSettings() {
|
||||
return (
|
||||
<div className="bg-white box-shadow h-screen overflow-y-auto">
|
||||
<div className="px-6 pt-6">
|
||||
<h1 className="text-2xl">Sessions Settings</h1>
|
||||
</div>
|
||||
function SessionSettings({ projectId }: { projectId: number }) {
|
||||
return (
|
||||
<div className='bg-white box-shadow h-screen overflow-y-auto'>
|
||||
<div className='px-6 pt-6'>
|
||||
<h1 className='text-2xl'>Sessions Settings</h1>
|
||||
</div>
|
||||
|
||||
<div className="p-6 border-b py-8">
|
||||
<ListingVisibility />
|
||||
</div>
|
||||
<div className='p-6 border-b py-8'>
|
||||
<ListingVisibility />
|
||||
</div>
|
||||
|
||||
<div className="p-6 border-b py-8">
|
||||
<DefaultPlaying />
|
||||
</div>
|
||||
<div className='p-6 border-b py-8'>
|
||||
<DefaultPlaying />
|
||||
</div>
|
||||
|
||||
<div className="p-6 border-b py-8">
|
||||
<DefaultTimezone />
|
||||
</div>
|
||||
<div className='p-6 border-b py-8'>
|
||||
<DefaultTimezone />
|
||||
</div>
|
||||
|
||||
<div className="p-6 py-8">
|
||||
<CaptureRate />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
<div className='p-6 py-8'>
|
||||
<h3 className='text-lg'>Capture Rate</h3>
|
||||
<CaptureRate projectId={projectId} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default SessionSettings
|
||||
export default connect((state: any) => ({
|
||||
projectId: state.getIn(['site', 'siteId'])
|
||||
}))(SessionSettings);
|
||||
|
|
|
|||
|
|
@ -5,89 +5,96 @@ import { observer } from 'mobx-react-lite';
|
|||
import { connect } from 'react-redux';
|
||||
import cn from 'classnames';
|
||||
|
||||
function CaptureRate({ isAdmin = false }) {
|
||||
const { settingsStore } = useStore();
|
||||
const [changed, setChanged] = useState(false);
|
||||
const [sessionSettings] = useState(settingsStore.sessionSettings);
|
||||
const [loading] = useState(settingsStore.loadingCaptureRate);
|
||||
type Props = {
|
||||
isAdmin: boolean;
|
||||
projectId: number;
|
||||
}
|
||||
|
||||
const captureRate = sessionSettings.captureRate;
|
||||
const setCaptureRate = sessionSettings.changeCaptureRate;
|
||||
const captureAll = sessionSettings.captureAll;
|
||||
const setCaptureAll = sessionSettings.changeCaptureAll;
|
||||
function CaptureRate(props: Props) {
|
||||
const { isAdmin, projectId } = props;
|
||||
const { settingsStore } = useStore();
|
||||
const [changed, setChanged] = useState(false);
|
||||
const [sessionSettings] = useState(settingsStore.sessionSettings);
|
||||
const loading = settingsStore.loadingCaptureRate;
|
||||
|
||||
useEffect(() => {
|
||||
settingsStore.fetchCaptureRate();
|
||||
}, []);
|
||||
const captureRate = sessionSettings.captureRate;
|
||||
const setCaptureRate = sessionSettings.changeCaptureRate;
|
||||
const captureAll = sessionSettings.captureAll;
|
||||
const setCaptureAll = sessionSettings.changeCaptureAll;
|
||||
|
||||
const changeCaptureRate = (input: string) => {
|
||||
setChanged(true);
|
||||
setCaptureRate(input);
|
||||
};
|
||||
useEffect(() => {
|
||||
settingsStore.fetchCaptureRate(projectId);
|
||||
}, [projectId]);
|
||||
|
||||
const toggleRate = () => {
|
||||
const newValue = !captureAll;
|
||||
setChanged(true);
|
||||
if (newValue === true) {
|
||||
const updateObj = {
|
||||
rate: '100',
|
||||
captureAll: true,
|
||||
};
|
||||
settingsStore.saveCaptureRate(updateObj);
|
||||
} else {
|
||||
setCaptureAll(newValue);
|
||||
}
|
||||
};
|
||||
const changeCaptureRate = (input: string) => {
|
||||
setChanged(true);
|
||||
setCaptureRate(input);
|
||||
};
|
||||
|
||||
return (
|
||||
<Loader loading={loading}>
|
||||
<h3 className="text-lg">Capture Rate</h3>
|
||||
<div className="my-1">The percentage of session you want to capture</div>
|
||||
<Tooltip title="You don't have permission to change." disabled={isAdmin} delay={0}>
|
||||
<div className={cn('mt-2 mb-4 mr-1 flex items-center', { disabled: !isAdmin })}>
|
||||
<Toggler checked={captureAll} name="test" onChange={toggleRate} />
|
||||
<span className="ml-2" style={{ color: captureAll ? '#000000' : '#999' }}>
|
||||
const toggleRate = () => {
|
||||
const newValue = !captureAll;
|
||||
setChanged(true);
|
||||
if (newValue) {
|
||||
const updateObj = {
|
||||
rate: '100',
|
||||
captureAll: true
|
||||
};
|
||||
settingsStore.saveCaptureRate(projectId, updateObj);
|
||||
} else {
|
||||
setCaptureAll(newValue);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Loader loading={loading}>
|
||||
{/*<h3 className='text-lg'>Capture Rate</h3>*/}
|
||||
<div className='my-1'>The percentage of session you want to capture</div>
|
||||
<Tooltip title="You don't have permission to change." disabled={isAdmin} delay={0}>
|
||||
<div className={cn('mt-2 mb-4 mr-1 flex items-center', { disabled: !isAdmin })}>
|
||||
<Toggler checked={captureAll} name='test' onChange={toggleRate} />
|
||||
<span className='ml-2' style={{ color: captureAll ? '#000000' : '#999' }}>
|
||||
100%
|
||||
</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
{!captureAll && (
|
||||
<div className="flex items-center">
|
||||
<Tooltip title="You don't have permission to change." disabled={isAdmin} delay={0}>
|
||||
<div className={cn("relative", { 'disabled' : !isAdmin })}>
|
||||
<Input
|
||||
type="number"
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => changeCaptureRate(e.target.value)}
|
||||
value={captureRate.toString()}
|
||||
style={{ height: '38px', width: '100px' }}
|
||||
disabled={captureAll}
|
||||
min={0}
|
||||
max={100}
|
||||
/>
|
||||
<Icon className="absolute right-0 mr-6 top-0 bottom-0 m-auto" name="percent" color="gray-medium" size="18" />
|
||||
</div>
|
||||
</Tooltip>
|
||||
<span className="mx-3">of the sessions</span>
|
||||
<Button
|
||||
disabled={!changed}
|
||||
variant="outline"
|
||||
onClick={() =>
|
||||
settingsStore
|
||||
.saveCaptureRate({
|
||||
rate: captureRate,
|
||||
captureAll,
|
||||
})
|
||||
.finally(() => setChanged(false))
|
||||
}
|
||||
>
|
||||
Update
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</Loader>
|
||||
);
|
||||
</div>
|
||||
</Tooltip>
|
||||
{!captureAll && (
|
||||
<div className='flex items-center'>
|
||||
<Tooltip title="You don't have permission to change." disabled={isAdmin} delay={0}>
|
||||
<div className={cn('relative', { 'disabled': !isAdmin })}>
|
||||
<Input
|
||||
type='number'
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => changeCaptureRate(e.target.value)}
|
||||
value={captureRate.toString()}
|
||||
style={{ height: '38px', width: '100px' }}
|
||||
disabled={captureAll}
|
||||
min={0}
|
||||
max={100}
|
||||
/>
|
||||
<Icon className='absolute right-0 mr-6 top-0 bottom-0 m-auto' name='percent' color='gray-medium'
|
||||
size='18' />
|
||||
</div>
|
||||
</Tooltip>
|
||||
<span className='mx-3'>of the sessions</span>
|
||||
<Button
|
||||
disabled={!changed}
|
||||
variant='outline'
|
||||
onClick={() =>
|
||||
settingsStore
|
||||
.saveCaptureRate(projectId, {
|
||||
rate: captureRate,
|
||||
captureAll
|
||||
})
|
||||
.finally(() => setChanged(false))
|
||||
}
|
||||
>
|
||||
Update
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</Loader>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect((state: any) => ({
|
||||
isAdmin: state.getIn(['user', 'account', 'admin']) || state.getIn(['user', 'account', 'superAdmin']),
|
||||
isAdmin: state.getIn(['user', 'account', 'admin']) || state.getIn(['user', 'account', 'superAdmin'])
|
||||
}))(observer(CaptureRate));
|
||||
|
|
|
|||
|
|
@ -22,9 +22,9 @@ export default class SettingsStore {
|
|||
});
|
||||
}
|
||||
|
||||
saveCaptureRate(data: any) {
|
||||
saveCaptureRate(projectId: number, data: any) {
|
||||
return sessionService
|
||||
.saveCaptureRate(data)
|
||||
.saveCaptureRate(projectId, data)
|
||||
.then((data) => data.json())
|
||||
.then(({ data }) => {
|
||||
this.sessionSettings.merge({
|
||||
|
|
@ -38,10 +38,10 @@ export default class SettingsStore {
|
|||
});
|
||||
}
|
||||
|
||||
fetchCaptureRate(): Promise<any> {
|
||||
fetchCaptureRate(projectId: number): Promise<any> {
|
||||
this.loadingCaptureRate = true;
|
||||
return sessionService
|
||||
.fetchCaptureRate()
|
||||
.fetchCaptureRate(projectId)
|
||||
.then((data) => {
|
||||
this.sessionSettings.merge({
|
||||
captureRate: data.rate,
|
||||
|
|
|
|||
|
|
@ -14,13 +14,13 @@ export default class SettingsService {
|
|||
this.client = client || new APIClient();
|
||||
}
|
||||
|
||||
saveCaptureRate(data: any) {
|
||||
return this.client.post('/sample_rate', data);
|
||||
saveCaptureRate(projectId: number, data: any) {
|
||||
return this.client.post(`/${projectId}/sample_rate`, data);
|
||||
}
|
||||
|
||||
fetchCaptureRate() {
|
||||
fetchCaptureRate(projectId: number) {
|
||||
return this.client
|
||||
.get('/sample_rate')
|
||||
.get(`/${projectId}/sample_rate`)
|
||||
.then((response) => response.json())
|
||||
.then((response) => response.data || 0);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ export default Record({
|
|||
projectKey: undefined,
|
||||
trackerVersion: undefined,
|
||||
saveRequestPayloads: false,
|
||||
sampleRate: 0,
|
||||
}, {
|
||||
idKey: 'id',
|
||||
methods: {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue