change(ui): integration changes

This commit is contained in:
Shekar Siri 2023-07-04 09:14:56 +02:00
parent ee1a775379
commit 3f1473e653
9 changed files with 310 additions and 168 deletions

View file

@ -157,7 +157,7 @@ def create_feature_flag(project_id: int, user_id: int, feature_flag_data: schema
"""
if variants_len > 0:
variants_query = f""",
variants_query = f"""{conditions_len > 0 and "," or ""}
inserted_variants AS (
INSERT INTO feature_flags_variants(feature_flag_id, value, description, rollout_percentage, payload)
VALUES {",".join([f"((SELECT feature_flag_id FROM inserted_flag),"

View file

@ -1433,7 +1433,7 @@ class FeatureFlagSchema(BaseModel):
flag_type: FeatureFlagType = Field(default=FeatureFlagType.single_variant)
is_persist: Optional[bool] = Field(default=False)
is_active: Optional[bool] = Field(default=True)
conditions: List[FeatureFlagCondition] = Field(default=[], min_items=1)
conditions: List[FeatureFlagCondition] = Field(default=[])
variants: List[FeatureFlagVariant] = Field(default=[])
class Config:

View file

@ -1,5 +1,5 @@
import React from 'react';
import { withRouter } from 'react-router-dom';
import { withRouter, RouteComponentProps } from 'react-router-dom';
import { Switch, Route, Redirect } from 'react-router';
import { CLIENT_TABS, client as clientRoute } from 'App/routes';
@ -17,17 +17,15 @@ import Notifications from './Notifications';
import Roles from './Roles';
import SessionsListingSettings from 'Components/Client/SessionsListingSettings';
@withRouter
export default class Client extends React.PureComponent {
constructor(props) {
super(props);
}
interface MatchParams {
activeTab?: string;
}
setTab = (tab) => {
this.props.history.push(clientRoute(tab));
};
const Client: React.FC<RouteComponentProps<MatchParams>> = ({ match }) => {
const { activeTab } = match.params;
const isIntegrations = activeTab === CLIENT_TABS.INTEGRATIONS;
renderActiveTab = () => (
const renderActiveTab = () => (
<Switch>
<Route exact strict path={clientRoute(CLIENT_TABS.PROFILE)} component={ProfileSettings} />
<Route exact strict path={clientRoute(CLIENT_TABS.SESSIONS_LISTING)} component={SessionsListingSettings} />
@ -43,23 +41,18 @@ export default class Client extends React.PureComponent {
</Switch>
);
render() {
const {
match: {
params: { activeTab },
},
} = this.props;
return (
<div className={cn('page-margin container-90 flex relative')}>
<div className={styles.tabMenu}>
<PreferencesMenu activeTab={activeTab} />
</div>
<div className={cn('side-menu-margined w-full')}>
<div className="bg-white w-full rounded-lg mx-auto mb-8 border" style={{ maxWidth: '1300px'}}>
{activeTab && this.renderActiveTab()}
</div>
</div>
return (
<div className={cn('page-margin container-90 flex relative')}>
<div className={styles.tabMenu}>
<PreferencesMenu activeTab={activeTab!} />
</div>
);
}
}
<div className={cn('side-menu-margined w-full', { 'bg-white rounded-lg border' : !isIntegrations })}>
<div className='w-full mx-auto mb-8' style={{ maxWidth: '1300px' }}>
{activeTab && renderActiveTab()}
</div>
</div>
</div>
);
};
export default withRouter(Client);

View file

@ -0,0 +1,53 @@
import React from 'react';
import { Icon } from 'UI';
import cn from 'classnames';
// const FILTERS = [
// { key: 'all', name: 'All', icon: 'play' },
// { key: 'issue-reporting', name: 'Issue Reporting', icon: 'play' },
// { key: 'backend-logging', name: 'Backend Logging', icon: 'play' },
// { key: 'stack-tracing', name: 'Stack Tracing', icon: 'play' },
// { key: 'state-management', name: 'State Management', icon: 'play' },
// { key: 'collaboration', name: 'Collaboration', icon: 'play' },
// { key: 'plugins', name: 'Plugins', icon: 'play' }
// ];
interface Props {
onChange: any;
activeItem: string;
filters: any;
}
const allItem = { key: 'all', title: 'All', icon: 'play' };
function FilterButton(props: { activeItem: string, item: any, onClick: () => any }) {
return <div
className={cn('cursor-pointer transition group rounded px-2 py-1 flex items-center uppercase text-sm hover:bg-active-blue hover:text-teal', {
'bg-active-blue text-teal': props.activeItem === props.item.key
})}
style={{ height: '36px' }}
onClick={props.onClick}
>
{props.item.icon && <Icon name={props.item.icon} className='mr-2' />}
<span>{props.item.title}</span>
</div>;
}
function IntegrationFilters(props: Props) {
return (
<div className='flex items-center gap-4'>
<FilterButton
activeItem={props.activeItem}
item={allItem}
onClick={() => props.onChange(allItem.key)}
/>
{props.filters.map((item: any) => (
<FilterButton activeItem={props.activeItem} item={item} onClick={() => props.onChange(item.key)} />
))}
</div>
);
}
export default IntegrationFilters;

View file

@ -1,35 +1,44 @@
import React from 'react';
import cn from 'classnames';
import { Icon, Tooltip } from 'UI';
import { Icon } from 'UI';
import stl from './integrationItem.module.css';
import { Tooltip } from 'antd';
interface Props {
integration: any;
onClick?: (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => void;
integrated?: boolean;
hide?: boolean;
integration: any;
onClick?: (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => void;
integrated?: boolean;
hide?: boolean;
}
const IntegrationItem = (props: Props) => {
const { integration, integrated, hide = false } = props;
return hide ? <></> : (
<div className={cn(stl.wrapper, { [stl.integrated]: integrated })} onClick={(e) => props.onClick(e)}>
{integrated && (
<div className="m-2 absolute right-0 top-0 h-4 w-4 rounded-full bg-teal flex items-center justify-center">
<Tooltip title="Integrated" delay={0}>
<Icon name="check" size="14" color="white" />
</Tooltip>
</div>
)}
{integration.icon.length ? <img className="h-12 w-12" src={'/assets/' + integration.icon + '.svg'} alt="integration" /> : (
<span style={{ fontSize: '3rem', lineHeight: '3rem' }}>{integration.header}</span>
)}
<div className="text-center mt-2">
<h4 className="">{integration.title}</h4>
{/* <p className="text-sm color-gray-medium m-0 p-0 h-3">{integration.subtitle && integration.subtitle}</p> */}
</div>
const { integration, integrated, hide = false } = props;
return hide ? <></> : (
<div
className={cn('flex flex-col border p-3 bg-white relative justify-between cursor-pointer hover:bg-active-blue')}
onClick={(e) => props.onClick(e)}
style={{ height: '126px' }}
>
<div className='flex gap-3'>
{integration.icon.length ?
<img className='h-10 w-10' src={'/assets/' + integration.icon + '.svg'} alt='integration' /> :
(<span style={{ fontSize: '3rem', lineHeight: '3rem' }}>{integration.header}</span>)}
<div className='flex flex-col'>
<h4 className='text-lg'>{integration.title}</h4>
<p className='text-sm color-gray-medium m-0 p-0 h-3'>{integration.subtitle && integration.subtitle}</p>
</div>
);
</div>
{integrated && (
<Tooltip title='Integrated' delay={0}>
<div className='ml-12 p-1 flex items-center justify-center color-tealx border rounded w-fit'>
<Icon name='check-circle-fill' size='14' color='tealx' className="mr-2" />
<span>Installed</span>
</div>
</Tooltip>
)}
</div>
);
};
export default IntegrationItem;

View file

@ -1,5 +1,25 @@
import React, { useEffect, useState } from 'react';
import { connect } from 'react-redux';
import { useModal } from 'App/components/Modal';
import React, { useEffect } from 'react';
import cn from 'classnames';
import { fetch, init } from 'Duck/integrations/actions';
import { fetchIntegrationList, setSiteId } from 'Duck/integrations/integrations';
import SiteDropdown from 'Shared/SiteDropdown';
import ReduxDoc from './ReduxDoc';
import VueDoc from './VueDoc';
import GraphQLDoc from './GraphQLDoc';
import NgRxDoc from './NgRxDoc';
import MobxDoc from './MobxDoc';
import ProfilerDoc from './ProfilerDoc';
import AssistDoc from './AssistDoc';
import PiniaDoc from './PiniaDoc';
import ZustandDoc from './ZustandDoc';
import MSTeams from './Teams';
import DocCard from 'Shared/DocCard/DocCard';
import { PageTitle, Tooltip } from 'UI';
import withPageTitle from 'HOCs/withPageTitle';
import BugsnagForm from './BugsnagForm';
import CloudwatchForm from './CloudwatchForm';
import DatadogForm from './DatadogForm';
@ -13,25 +33,7 @@ import SentryForm from './SentryForm';
import SlackForm from './SlackForm';
import StackdriverForm from './StackdriverForm';
import SumoLogicForm from './SumoLogicForm';
import { fetch, init } from 'Duck/integrations/actions';
import { fetchIntegrationList, setSiteId } from 'Duck/integrations/integrations';
import { connect } from 'react-redux';
import SiteDropdown from 'Shared/SiteDropdown';
import ReduxDoc from './ReduxDoc';
import VueDoc from './VueDoc';
import GraphQLDoc from './GraphQLDoc';
import NgRxDoc from './NgRxDoc';
import MobxDoc from './MobxDoc';
import ProfilerDoc from './ProfilerDoc';
import AssistDoc from './AssistDoc';
import { PageTitle, Tooltip } from 'UI';
import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG';
import withPageTitle from 'HOCs/withPageTitle';
import PiniaDoc from './PiniaDoc';
import ZustandDoc from './ZustandDoc';
import MSTeams from './Teams';
import DocCard from 'Shared/DocCard/DocCard';
import cn from 'classnames';
import IntegrationFilters from 'Components/Client/Integrations/IntegrationFilters';
interface Props {
fetch: (name: string, siteId: string) => void;
@ -44,10 +46,12 @@ interface Props {
hideHeader?: boolean;
loading?: boolean;
}
function Integrations(props: Props) {
const { initialSiteId, hideHeader = false, loading = false } = props;
const { showModal } = useModal();
const [integratedList, setIntegratedList] = React.useState<any>([]);
const [integratedList, setIntegratedList] = useState<string[]>([]);
const [activeFilter, setActiveFilter] = useState<string>('all');
useEffect(() => {
const list = props.integratedList
@ -68,7 +72,7 @@ function Integrations(props: Props) {
showModal(
React.cloneElement(integration.component, {
integrated: integratedList.includes(integration.slug),
integrated: integratedList.includes(integration.slug)
}),
{ right: true, width }
);
@ -79,55 +83,55 @@ function Integrations(props: Props) {
props.fetchIntegrationList(value.value);
};
return (
<div className="mb-4 p-5">
{!hideHeader && <PageTitle title={<div>Integrations</div>} />}
{integrations.map((cat: any) => (
<div className="grid grid-cols-6 border-b last:border-none gap-4">
<div
className={cn('col-span-4 mb-2 py-3', cat.docs ? 'col-span-4' : 'col-span-6')}
key={cat.key}
>
<div className="flex items-center">
<h2 className="font-medium text-lg">{cat.title}</h2>
{cat.isProject && (
<div className="flex items-center">
<div className="flex flex-wrap mx-4">
<SiteDropdown value={props.siteId} onChange={onChangeSelect} />
</div>
{loading && cat.isProject && <AnimatedSVG name={ICONS.LOADER} size={20} />}
</div>
)}
</div>
<div className="">{cat.description}</div>
const onChange = (key: string) => {
setActiveFilter(key);
};
<div className="flex flex-wrap mt-4 gap-3">
{cat.integrations.map((integration: any) => (
<React.Fragment key={integration.slug}>
<Tooltip
delay={50}
title="Global configuration, available to all team members."
disabled={!integration.shared}
placement={'bottom'}
>
<IntegrationItem
integrated={integratedList.includes(integration.slug)}
integration={integration}
onClick={() => onClick(integration, cat.title === 'Plugins' ? 500 : 350)}
hide={
(integration.slug === 'github' && integratedList.includes('jira')) ||
(integration.slug === 'jira' && integratedList.includes('github'))
}
/>
</Tooltip>
</React.Fragment>
))}
</div>
</div>
{cat.docs && <div className="col-span-2 py-6">{cat.docs()}</div>}
const filteredIntegrations = integrations.filter((cat: any) => {
if (activeFilter === 'all') {
return true;
}
return cat.key === activeFilter;
});
const filters = integrations.map((cat: any) => ({
key: cat.key,
title: cat.title,
icon: cat.icon,
}));
return (
<>
<div className='mb-4 p-5 bg-white rounded-lg border'>
{!hideHeader && <PageTitle title={<div>Integrations</div>} />}
<IntegrationFilters onChange={onChange} activeItem={activeFilter} filters={filters}/>
</div>
<div className='mb-4' />
{filteredIntegrations.map((cat: any) => (
<div className='grid grid-cols-3 mt-4 gap-3 auto-cols-max'>
{cat.integrations.map((integration: any) => (
<IntegrationItem
integrated={integratedList.includes(integration.slug)}
integration={integration}
onClick={() =>
onClick(integration, cat.title === 'Plugins' ? 500 : 350)
}
hide={
(integration.slug === 'github' &&
integratedList.includes('jira')) ||
(integration.slug === 'jira' &&
integratedList.includes('github'))
}
/>
))}
</div>
))}
</div>
</>
);
}
@ -136,62 +140,68 @@ export default connect(
initialSiteId: state.getIn(['site', 'siteId']),
integratedList: state.getIn(['integrations', 'list']) || [],
loading: state.getIn(['integrations', 'fetchRequest', 'loading']),
siteId: state.getIn(['integrations', 'siteId']),
siteId: state.getIn(['integrations', 'siteId'])
}),
{ fetch, init, fetchIntegrationList, setSiteId }
)(withPageTitle('Integrations - OpenReplay Preferences')(Integrations));
const integrations = [
{
title: 'Issue Reporting and Collaborations',
key: 1,
title: 'Issue Reporting',
key: 'issue-reporting',
description: 'Seamlessly report issues or share issues with your team right from OpenReplay.',
isProject: false,
icon: 'integrations/issue-reporting',
integrations: [
{
title: 'Jira',
subtitle: 'Lorem Ipsum is simply dummy text of the printing and typesetting industry.',
slug: 'jira',
category: 'Errors',
icon: 'integrations/jira',
component: <JiraForm />,
component: <JiraForm />
},
{
title: 'Github',
subtitle: 'Lorem Ipsum is simply dummy text of the printing and typesetting industry.',
slug: 'github',
category: 'Errors',
icon: 'integrations/github',
component: <GithubForm />,
component: <GithubForm />
},
{
title: 'Slack',
subtitle: 'Lorem Ipsum is simply dummy text of the printing and typesetting industry.',
slug: 'slack',
category: 'Errors',
icon: 'integrations/slack',
component: <SlackForm />,
shared: true,
shared: true
},
{
title: 'MS Teams',
subtitle: 'Lorem Ipsum is simply dummy text of the printing and typesetting industry.',
slug: 'msteams',
category: 'Errors',
icon: 'integrations/teams',
component: <MSTeams />,
shared: true,
},
],
shared: true
}
]
},
{
title: 'Backend Logging',
key: 2,
key: 'backend-logging',
isProject: true,
description:
'Sync your backend errors with sessions replays and see what happened front-to-back.',
docs: () => (
<DocCard
title="Why use integrations?"
icon="question-lg"
iconBgColor="bg-red-lightest"
iconColor="red"
title='Why use integrations?'
icon='question-lg'
iconBgColor='bg-red-lightest'
iconColor='red'
>
Sync your backend errors with sessions replays and see what happened front-to-back.
</DocCard>
@ -200,81 +210,148 @@ const integrations = [
{ title: 'Sentry', slug: 'sentry', icon: 'integrations/sentry', component: <SentryForm /> },
{
title: 'Bugsnag',
subtitle: 'Lorem Ipsum is simply dummy text of the printing and typesetting industry.',
slug: 'bugsnag',
icon: 'integrations/bugsnag',
component: <BugsnagForm />,
component: <BugsnagForm />
},
{
title: 'Rollbar',
subtitle: 'Lorem Ipsum is simply dummy text of the printing and typesetting industry.',
slug: 'rollbar',
icon: 'integrations/rollbar',
component: <RollbarForm />,
component: <RollbarForm />
},
{
title: 'Elasticsearch',
subtitle: 'Lorem Ipsum is simply dummy text of the printing and typesetting industry.',
slug: 'elasticsearch',
icon: 'integrations/elasticsearch',
component: <ElasticsearchForm />,
component: <ElasticsearchForm />
},
{
title: 'Datadog',
subtitle: 'Lorem Ipsum is simply dummy text of the printing and typesetting industry.',
slug: 'datadog',
icon: 'integrations/datadog',
component: <DatadogForm />,
component: <DatadogForm />
},
{
title: 'Sumo Logic',
subtitle: 'Lorem Ipsum is simply dummy text of the printing and typesetting industry.',
slug: 'sumologic',
icon: 'integrations/sumologic',
component: <SumoLogicForm />,
component: <SumoLogicForm />
},
{
title: 'Stackdriver',
subtitle: 'Lorem Ipsum is simply dummy text of the printing and typesetting industry.',
slug: 'stackdriver',
icon: 'integrations/google-cloud',
component: <StackdriverForm />,
component: <StackdriverForm />
},
{
title: 'CloudWatch',
subtitle: 'Lorem Ipsum is simply dummy text of the printing and typesetting industry.',
slug: 'cloudwatch',
icon: 'integrations/aws',
component: <CloudwatchForm />,
component: <CloudwatchForm />
},
{
title: 'Newrelic',
subtitle: 'Lorem Ipsum is simply dummy text of the printing and typesetting industry.',
slug: 'newrelic',
icon: 'integrations/newrelic',
component: <NewrelicForm />,
},
],
component: <NewrelicForm />
}
]
},
{
title: 'Collaboration',
key: 'collaboration',
isProject: false,
description: 'Share your sessions with your team and collaborate on issues.',
integrations: []
},
{
title: 'State Management',
key: 'state-management',
isProject: true,
description: 'Sync your Redux or VueX store with sessions replays and see what happened front-to-back.',
integrations: []
},
{
title: 'Plugins',
key: 3,
key: 'plugins',
isProject: true,
docs: () => (
<DocCard
title="What are plugins?"
icon="question-lg"
iconBgColor="bg-red-lightest"
iconColor="red"
title='What are plugins?'
icon='question-lg'
iconBgColor='bg-red-lightest'
iconColor='red'
>
Plugins capture your applications store, monitor queries, track performance issues and even
assist your end user through live sessions.
</DocCard>
),
description:
"Reproduce issues as if they happened in your own browser. Plugins help capture your application's store, HTTP requeets, GraphQL queries, and more.",
'Reproduce issues as if they happened in your own browser. Plugins help capture your application\'s store, HTTP requeets, GraphQL queries, and more.',
integrations: [
{ title: 'Redux', icon: 'integrations/redux', component: <ReduxDoc /> },
{ title: 'VueX', icon: 'integrations/vuejs', component: <VueDoc /> },
{ title: 'Pinia', icon: 'integrations/pinia', component: <PiniaDoc /> },
{ title: 'GraphQL', icon: 'integrations/graphql', component: <GraphQLDoc /> },
{ title: 'NgRx', icon: 'integrations/ngrx', component: <NgRxDoc /> },
{ title: 'MobX', icon: 'integrations/mobx', component: <MobxDoc /> },
{ title: 'Profiler', icon: 'integrations/openreplay', component: <ProfilerDoc /> },
{ title: 'Assist', icon: 'integrations/openreplay', component: <AssistDoc /> },
{ title: 'Zustand', icon: '', header: '🐻', component: <ZustandDoc /> },
],
},
{
title: 'Redux',
subtitle: 'Lorem Ipsum is simply dummy text of the printing and typesetting industry.',
icon: 'integrations/redux', component: <ReduxDoc />
},
{
title: 'VueX',
subtitle: 'Lorem Ipsum is simply dummy text of the printing and typesetting industry.',
icon: 'integrations/vuejs',
component: <VueDoc />
},
{
title: 'Pinia',
subtitle: 'Lorem Ipsum is simply dummy text of the printing and typesetting industry.',
icon: 'integrations/pinia',
component: <PiniaDoc />
},
{
title: 'GraphQL',
subtitle: 'Lorem Ipsum is simply dummy text of the printing and typesetting industry.',
icon: 'integrations/graphql',
component: <GraphQLDoc />
},
{
title: 'NgRx',
subtitle: 'Lorem Ipsum is simply dummy text of the printing and typesetting industry.',
icon: 'integrations/ngrx',
component: <NgRxDoc />
},
{
title: 'MobX',
subtitle: 'Lorem Ipsum is simply dummy text of the printing and typesetting industry.',
icon: 'integrations/mobx',
component: <MobxDoc />
},
{
title: 'Profiler',
subtitle: 'Lorem Ipsum is simply dummy text of the printing and typesetting industry.',
icon: 'integrations/openreplay',
component: <ProfilerDoc />
},
{
title: 'Assist',
subtitle: 'Lorem Ipsum is simply dummy text of the printing and typesetting industry.',
icon: 'integrations/openreplay',
component: <AssistDoc />
},
{
title: 'Zustand',
subtitle: 'Lorem Ipsum is simply dummy text of the printing and typesetting industry.',
icon: '',
header: '🐻',
component: <ZustandDoc />
}
]
}
];

View file

@ -35,7 +35,7 @@ export default (props: Props) => {
let iconColor = variant === 'text' || variant === 'default' ? 'gray-dark' : 'teal';
const variantClasses = {
default: 'bg-white hover:bg-gray-light border border-gray-light',
default: 'bg-white hover:!bg-gray-light border border-gray-light',
primary: 'bg-teal color-white hover:bg-teal-dark',
green: 'bg-green color-white hover:bg-green-dark',
text: 'bg-transparent text-black hover:bg-active-blue hover:!text-teal hover-fill-teal',
@ -49,7 +49,8 @@ export default (props: Props) => {
variantClasses[variant],
{ 'opacity-40 pointer-events-none': disabled },
{ '!rounded-full h-10 w-10 justify-center': rounded },
className
className,
'btn'
);
if (variant === 'primary') {

View file

@ -23,6 +23,12 @@ input.no-focus:focus {
}
}
.border-b {
border-bottom: 1px solid #eee;
button,
[type='button'],
[type='reset'],
[type='submit'] {
-webkit-appearance: button; /* 1 */
background-color: none; /* 2 */
background-image: none; /* 2 */
}

View file

@ -35,6 +35,9 @@ module.exports = {
'border-main': `0 0 0 1px ${colors['main']}`,
'border-gray': '0 0 0 1px #999'
},
button: {
'background-color': 'red'
}
}
},
variants: {
@ -42,6 +45,6 @@ module.exports = {
},
plugins: [],
corePlugins: {
preflight: false
// preflight: false
}
};