diff --git a/api/chalicelib/core/feature_flags.py b/api/chalicelib/core/feature_flags.py index 520fb3cf5..6276bd91d 100644 --- a/api/chalicelib/core/feature_flags.py +++ b/api/chalicelib/core/feature_flags.py @@ -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)," diff --git a/api/schemas.py b/api/schemas.py index 2304f1b32..926202ab0 100644 --- a/api/schemas.py +++ b/api/schemas.py @@ -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: diff --git a/frontend/app/components/Client/Client.js b/frontend/app/components/Client/Client.tsx similarity index 68% rename from frontend/app/components/Client/Client.js rename to frontend/app/components/Client/Client.tsx index ebf9db834..44722153f 100644 --- a/frontend/app/components/Client/Client.js +++ b/frontend/app/components/Client/Client.tsx @@ -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> = ({ match }) => { + const { activeTab } = match.params; + const isIntegrations = activeTab === CLIENT_TABS.INTEGRATIONS; - renderActiveTab = () => ( + const renderActiveTab = () => ( @@ -43,23 +41,18 @@ export default class Client extends React.PureComponent { ); - render() { - const { - match: { - params: { activeTab }, - }, - } = this.props; - return ( -
-
- -
-
-
- {activeTab && this.renderActiveTab()} -
-
+ return ( +
+
+
- ); - } -} +
+
+ {activeTab && renderActiveTab()} +
+
+
+ ); +}; + +export default withRouter(Client); diff --git a/frontend/app/components/Client/Integrations/IntegrationFilters.tsx b/frontend/app/components/Client/Integrations/IntegrationFilters.tsx new file mode 100644 index 000000000..51edb90bf --- /dev/null +++ b/frontend/app/components/Client/Integrations/IntegrationFilters.tsx @@ -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
+ {props.item.icon && } + {props.item.title} +
; +} + +function IntegrationFilters(props: Props) { + + return ( +
+ props.onChange(allItem.key)} + /> + {props.filters.map((item: any) => ( + props.onChange(item.key)} /> + ))} +
+ ); +} + +export default IntegrationFilters; \ No newline at end of file diff --git a/frontend/app/components/Client/Integrations/IntegrationItem.tsx b/frontend/app/components/Client/Integrations/IntegrationItem.tsx index efcdefd8a..cd8b5f005 100644 --- a/frontend/app/components/Client/Integrations/IntegrationItem.tsx +++ b/frontend/app/components/Client/Integrations/IntegrationItem.tsx @@ -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) => void; - integrated?: boolean; - hide?: boolean; + integration: any; + onClick?: (e: React.MouseEvent) => void; + integrated?: boolean; + hide?: boolean; } const IntegrationItem = (props: Props) => { - const { integration, integrated, hide = false } = props; - return hide ? <> : ( -
props.onClick(e)}> - {integrated && ( -
- - - -
- )} - {integration.icon.length ? integration : ( - {integration.header} - )} -
-

{integration.title}

- {/*

{integration.subtitle && integration.subtitle}

*/} -
+ const { integration, integrated, hide = false } = props; + return hide ? <> : ( +
props.onClick(e)} + style={{ height: '126px' }} + > +
+ {integration.icon.length ? + integration : + ({integration.header})} +
+

{integration.title}

+

{integration.subtitle && integration.subtitle}

- ); +
+ + {integrated && ( + +
+ + Installed +
+
+ )} +
+ ); }; export default IntegrationItem; diff --git a/frontend/app/components/Client/Integrations/Integrations.tsx b/frontend/app/components/Client/Integrations/Integrations.tsx index 98cbf4f0a..411bbe2af 100644 --- a/frontend/app/components/Client/Integrations/Integrations.tsx +++ b/frontend/app/components/Client/Integrations/Integrations.tsx @@ -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([]); + const [integratedList, setIntegratedList] = useState([]); + const [activeFilter, setActiveFilter] = useState('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 ( -
- {!hideHeader && Integrations
} />} - {integrations.map((cat: any) => ( -
-
-
-

{cat.title}

- {cat.isProject && ( -
-
- -
- {loading && cat.isProject && } -
- )} -
-
{cat.description}
+ const onChange = (key: string) => { + setActiveFilter(key); + }; -
- {cat.integrations.map((integration: any) => ( - - - onClick(integration, cat.title === 'Plugins' ? 500 : 350)} - hide={ - (integration.slug === 'github' && integratedList.includes('jira')) || - (integration.slug === 'jira' && integratedList.includes('github')) - } - /> - - - ))} -
-
- {cat.docs &&
{cat.docs()}
} + 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 ( + <> +
+ {!hideHeader && Integrations
} />} + + +
+ +
+ + {filteredIntegrations.map((cat: any) => ( +
+ {cat.integrations.map((integration: any) => ( + + onClick(integration, cat.title === 'Plugins' ? 500 : 350) + } + hide={ + (integration.slug === 'github' && + integratedList.includes('jira')) || + (integration.slug === 'jira' && + integratedList.includes('github')) + } + /> + ))}
))} -
+ ); } @@ -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: , + component: }, { title: 'Github', + subtitle: 'Lorem Ipsum is simply dummy text of the printing and typesetting industry.', slug: 'github', category: 'Errors', icon: 'integrations/github', - component: , + component: }, { title: 'Slack', + subtitle: 'Lorem Ipsum is simply dummy text of the printing and typesetting industry.', slug: 'slack', category: 'Errors', icon: 'integrations/slack', component: , - 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: , - 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: () => ( Sync your backend errors with sessions replays and see what happened front-to-back. @@ -200,81 +210,148 @@ const integrations = [ { title: 'Sentry', slug: 'sentry', icon: 'integrations/sentry', component: }, { title: 'Bugsnag', + subtitle: 'Lorem Ipsum is simply dummy text of the printing and typesetting industry.', slug: 'bugsnag', icon: 'integrations/bugsnag', - component: , + component: }, { title: 'Rollbar', + subtitle: 'Lorem Ipsum is simply dummy text of the printing and typesetting industry.', slug: 'rollbar', icon: 'integrations/rollbar', - component: , + component: }, { title: 'Elasticsearch', + subtitle: 'Lorem Ipsum is simply dummy text of the printing and typesetting industry.', slug: 'elasticsearch', icon: 'integrations/elasticsearch', - component: , + component: }, { title: 'Datadog', + subtitle: 'Lorem Ipsum is simply dummy text of the printing and typesetting industry.', slug: 'datadog', icon: 'integrations/datadog', - component: , + component: }, { title: 'Sumo Logic', + subtitle: 'Lorem Ipsum is simply dummy text of the printing and typesetting industry.', slug: 'sumologic', icon: 'integrations/sumologic', - component: , + component: }, { title: 'Stackdriver', + subtitle: 'Lorem Ipsum is simply dummy text of the printing and typesetting industry.', slug: 'stackdriver', icon: 'integrations/google-cloud', - component: , + component: }, { title: 'CloudWatch', + subtitle: 'Lorem Ipsum is simply dummy text of the printing and typesetting industry.', slug: 'cloudwatch', icon: 'integrations/aws', - component: , + component: }, { title: 'Newrelic', + subtitle: 'Lorem Ipsum is simply dummy text of the printing and typesetting industry.', slug: 'newrelic', icon: 'integrations/newrelic', - component: , - }, - ], + component: + } + ] + }, + { + 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: () => ( Plugins capture your application’s store, monitor queries, track performance issues and even assist your end user through live sessions. ), 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: }, - { title: 'VueX', icon: 'integrations/vuejs', component: }, - { title: 'Pinia', icon: 'integrations/pinia', component: }, - { title: 'GraphQL', icon: 'integrations/graphql', component: }, - { title: 'NgRx', icon: 'integrations/ngrx', component: }, - { title: 'MobX', icon: 'integrations/mobx', component: }, - { title: 'Profiler', icon: 'integrations/openreplay', component: }, - { title: 'Assist', icon: 'integrations/openreplay', component: }, - { title: 'Zustand', icon: '', header: '🐻', component: }, - ], - }, + { + title: 'Redux', + subtitle: 'Lorem Ipsum is simply dummy text of the printing and typesetting industry.', + icon: 'integrations/redux', component: + }, + { + title: 'VueX', + subtitle: 'Lorem Ipsum is simply dummy text of the printing and typesetting industry.', + icon: 'integrations/vuejs', + component: + }, + { + title: 'Pinia', + subtitle: 'Lorem Ipsum is simply dummy text of the printing and typesetting industry.', + icon: 'integrations/pinia', + component: + }, + { + title: 'GraphQL', + subtitle: 'Lorem Ipsum is simply dummy text of the printing and typesetting industry.', + icon: 'integrations/graphql', + component: + }, + { + title: 'NgRx', + subtitle: 'Lorem Ipsum is simply dummy text of the printing and typesetting industry.', + icon: 'integrations/ngrx', + component: + }, + { + title: 'MobX', + subtitle: 'Lorem Ipsum is simply dummy text of the printing and typesetting industry.', + icon: 'integrations/mobx', + component: + }, + { + title: 'Profiler', + subtitle: 'Lorem Ipsum is simply dummy text of the printing and typesetting industry.', + icon: 'integrations/openreplay', + component: + }, + { + title: 'Assist', + subtitle: 'Lorem Ipsum is simply dummy text of the printing and typesetting industry.', + icon: 'integrations/openreplay', + component: + }, + { + title: 'Zustand', + subtitle: 'Lorem Ipsum is simply dummy text of the printing and typesetting industry.', + icon: '', + header: '🐻', + component: + } + ] + } ]; diff --git a/frontend/app/components/ui/Button/Button.tsx b/frontend/app/components/ui/Button/Button.tsx index b4c6c6b61..2b0514c87 100644 --- a/frontend/app/components/ui/Button/Button.tsx +++ b/frontend/app/components/ui/Button/Button.tsx @@ -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') { diff --git a/frontend/app/styles/global.scss b/frontend/app/styles/global.scss index b8df873a9..42d7a063e 100644 --- a/frontend/app/styles/global.scss +++ b/frontend/app/styles/global.scss @@ -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 */ } diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js index 6839bc5ee..871f7d36b 100644 --- a/frontend/tailwind.config.js +++ b/frontend/tailwind.config.js @@ -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 } };