remote dev pull and resolved conflcits

This commit is contained in:
Shekar Siri 2023-07-27 12:34:33 +02:00
commit 4826ddcbca
26 changed files with 776 additions and 524 deletions

View file

@ -156,7 +156,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

@ -1441,7 +1441,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

@ -3,31 +3,39 @@ import { tokenRE } from 'Types/integrations/bugsnagConfig';
import IntegrationForm from '../IntegrationForm';
import ProjectListDropdown from './ProjectListDropdown';
import DocLink from 'Shared/DocLink/DocLink';
import IntegrationModalCard from 'Components/Client/Integrations/IntegrationModalCard';
const BugsnagForm = (props) => (
<div className="bg-white h-screen overflow-y-auto" style={{ width: '350px' }}>
<h3 className="p-5 text-2xl">Bugsnag</h3>
<div className="p-5 border-b mb-4">
<div>How to integrate Bugsnag with OpenReplay and see backend errors alongside session recordings.</div>
<DocLink className="mt-4" label="Integrate Bugsnag" url="https://docs.openreplay.com/integrations/bugsnag" />
</div>
<IntegrationForm
{...props}
name="bugsnag"
formFields={[
{
key: 'authorizationToken',
label: 'Authorisation Token',
},
{
key: 'bugsnagProjectId',
label: 'Project',
checkIfDisplayed: (config) => tokenRE.test(config.authorizationToken),
component: ProjectListDropdown,
},
]}
/>
<div className='bg-white h-screen overflow-y-auto' style={{ width: '350px' }}>
<IntegrationModalCard title='Bugsnag' icon='integrations/bugsnag'
description='Integrate Bugsnag to access the OpenReplay session linked to the JS exception within its interface.' />
<div className='p-5 border-b mb-4'>
<div className='font-medium mb-1'>How it works?</div>
<ol className="list-decimal list-inside">
<li>Generate Bugsnag Auth Token</li>
<li>Enter the token below</li>
<li>Propagate openReplaySessionToken</li>
</ol>
<DocLink className='mt-4' label='Integrate Bugsnag' url='https://docs.openreplay.com/integrations/bugsnag' />
</div>
<IntegrationForm
{...props}
name='bugsnag'
formFields={[
{
key: 'authorizationToken',
label: 'Authorisation Token'
},
{
key: 'bugsnagProjectId',
label: 'Project',
checkIfDisplayed: (config) => tokenRE.test(config.authorizationToken),
component: ProjectListDropdown
}
]}
/>
</div>
);
BugsnagForm.displayName = 'BugsnagForm';

View file

@ -4,43 +4,51 @@ import IntegrationForm from '../IntegrationForm';
import LogGroupDropdown from './LogGroupDropdown';
import RegionDropdown from './RegionDropdown';
import DocLink from 'Shared/DocLink/DocLink';
import IntegrationModalCard from 'Components/Client/Integrations/IntegrationModalCard';
const CloudwatchForm = (props) => (
<div className="bg-white h-screen overflow-y-auto" style={{ width: '350px' }}>
<h3 className="p-5 text-2xl">Cloud Watch</h3>
<div className="p-5 border-b mb-4">
<div>How to integrate CloudWatch with OpenReplay and see backend errors alongside session replays.</div>
<DocLink className="mt-4" label="Integrate CloudWatch" url="https://docs.openreplay.com/integrations/cloudwatch" />
</div>
<IntegrationForm
{...props}
name="cloudwatch"
formFields={[
{
key: 'awsAccessKeyId',
label: 'AWS Access Key ID',
},
{
key: 'awsSecretAccessKey',
label: 'AWS Secret Access Key',
},
{
key: 'region',
label: 'Region',
component: RegionDropdown,
},
{
key: 'logGroupName',
label: 'Log Group Name',
component: LogGroupDropdown,
checkIfDisplayed: (config) =>
config.awsSecretAccessKey.length === SECRET_ACCESS_KEY_LENGTH &&
config.region !== '' &&
config.awsAccessKeyId.length === ACCESS_KEY_ID_LENGTH,
},
]}
/>
<div className='bg-white h-screen overflow-y-auto' style={{ width: '350px' }}>
<IntegrationModalCard title='Cloud Watch' icon='integrations/aws'
description='Integrate CloudWatch to see backend logs and errors alongside session replay.' />
<div className='p-5 border-b mb-4'>
<div className='font-medium mb-1'>How it works?</div>
<ol className="list-decimal list-inside">
<li>Create a Service Account</li>
<li>Enter the details below</li>
<li>Propagate openReplaySessionToken</li>
</ol>
<DocLink className='mt-4' label='Integrate CloudWatch'
url='https://docs.openreplay.com/integrations/cloudwatch' />
</div>
<IntegrationForm
{...props}
name='cloudwatch'
formFields={[
{
key: 'awsAccessKeyId',
label: 'AWS Access Key ID'
},
{
key: 'awsSecretAccessKey',
label: 'AWS Secret Access Key'
},
{
key: 'region',
label: 'Region',
component: RegionDropdown
},
{
key: 'logGroupName',
label: 'Log Group Name',
component: LogGroupDropdown,
checkIfDisplayed: (config) =>
config.awsSecretAccessKey.length === SECRET_ACCESS_KEY_LENGTH &&
config.region !== '' &&
config.awsAccessKeyId.length === ACCESS_KEY_ID_LENGTH
}
]}
/>
</div>
);
CloudwatchForm.displayName = 'CloudwatchForm';

View file

@ -1,30 +1,37 @@
import React from 'react';
import IntegrationForm from './IntegrationForm';
import DocLink from 'Shared/DocLink/DocLink';
import IntegrationModalCard from 'Components/Client/Integrations/IntegrationModalCard';
const DatadogForm = (props) => (
<div className="bg-white h-screen overflow-y-auto" style={{ width: '350px' }}>
<h3 className="p-5 text-2xl">Datadog</h3>
<div className="p-5 border-b mb-4">
<div>How to integrate Datadog with OpenReplay and see backend errors alongside session recordings.</div>
<DocLink className="mt-4" label="Integrate Datadog" url="https://docs.openreplay.com/integrations/datadog" />
</div>
<IntegrationForm
{...props}
name="datadog"
formFields={[
{
key: 'apiKey',
label: 'API Key',
autoFocus: true,
},
{
key: 'applicationKey',
label: 'Application Key',
},
]}
/>
<div className='bg-white h-screen overflow-y-auto' style={{ width: '350px' }}>
<IntegrationModalCard title='Datadog' icon='integrations/datadog'
description='Incorporate DataDog to visualize backend errors alongside session replay, for easy troubleshooting.' />
<div className='p-5 border-b mb-4'>
<div className='font-medium mb-1'>How it works?</div>
<ol className="list-decimal list-inside">
<li>Generate Datadog API Key & Application Key</li>
<li>Enter the API key below</li>
<li>Propagate openReplaySessionToken</li>
</ol>
<DocLink className='mt-4' label='Integrate Datadog' url='https://docs.openreplay.com/integrations/datadog' />
</div>
<IntegrationForm
{...props}
name='datadog'
formFields={[
{
key: 'apiKey',
label: 'API Key',
autoFocus: true
},
{
key: 'applicationKey',
label: 'Application Key'
}
]}
/>
</div>
);
DatadogForm.displayName = 'DatadogForm';

View file

@ -4,85 +4,94 @@ import IntegrationForm from './IntegrationForm';
import { withRequest } from 'HOCs';
import { edit } from 'Duck/integrations/actions';
import DocLink from 'Shared/DocLink/DocLink';
import IntegrationModalCard from 'Components/Client/Integrations/IntegrationModalCard';
@connect(
(state) => ({
config: state.getIn(['elasticsearch', 'instance']),
}),
{ edit }
(state) => ({
config: state.getIn(['elasticsearch', 'instance'])
}),
{ edit }
)
@withRequest({
dataName: 'isValid',
initialData: false,
dataWrapper: (data) => data.state,
requestName: 'validateConfig',
endpoint: '/integrations/elasticsearch/test',
method: 'POST',
dataName: 'isValid',
initialData: false,
dataWrapper: (data) => data.state,
requestName: 'validateConfig',
endpoint: '/integrations/elasticsearch/test',
method: 'POST'
})
export default class ElasticsearchForm extends React.PureComponent {
componentWillReceiveProps(newProps) {
const {
config: { host, port, apiKeyId, apiKey },
} = this.props;
const { loading, config } = newProps;
const valuesChanged = host !== config.host || port !== config.port || apiKeyId !== config.apiKeyId || apiKey !== config.apiKey;
if (!loading && valuesChanged && newProps.config.validateKeys() && newProps) {
this.validateConfig(newProps);
}
componentWillReceiveProps(newProps) {
const {
config: { host, port, apiKeyId, apiKey }
} = this.props;
const { loading, config } = newProps;
const valuesChanged = host !== config.host || port !== config.port || apiKeyId !== config.apiKeyId || apiKey !== config.apiKey;
if (!loading && valuesChanged && newProps.config.validateKeys() && newProps) {
this.validateConfig(newProps);
}
}
validateConfig = (newProps) => {
const { config } = newProps;
this.props
.validateConfig({
host: config.host,
port: config.port,
apiKeyId: config.apiKeyId,
apiKey: config.apiKey,
})
.then((res) => {
const { isValid } = this.props;
this.props.edit('elasticsearch', { isValid: isValid });
});
};
validateConfig = (newProps) => {
const { config } = newProps;
this.props
.validateConfig({
host: config.host,
port: config.port,
apiKeyId: config.apiKeyId,
apiKey: config.apiKey
})
.then((res) => {
const { isValid } = this.props;
this.props.edit('elasticsearch', { isValid: isValid });
});
};
render() {
const props = this.props;
return (
<div className="bg-white h-screen overflow-y-auto" style={{ width: '350px' }}>
<h3 className="p-5 text-2xl">Elasticsearch</h3>
<div className="p-5 border-b mb-4">
<div>How to integrate Elasticsearch with OpenReplay and see backend errors alongside session recordings.</div>
<DocLink className="mt-4" label="Integrate Elasticsearch" url="https://docs.openreplay.com/integrations/elastic" />
</div>
<IntegrationForm
{...props}
name="elasticsearch"
formFields={[
{
key: 'host',
label: 'Host',
},
{
key: 'apiKeyId',
label: 'API Key ID',
},
{
key: 'apiKey',
label: 'API Key',
},
{
key: 'indexes',
label: 'Indexes',
},
{
key: 'port',
label: 'Port',
type: 'number',
},
]}
/>
</div>
);
}
render() {
const props = this.props;
return (
<div className='bg-white h-screen overflow-y-auto' style={{ width: '350px' }}>
<IntegrationModalCard title='Elasticsearch' icon='integrations/elasticsearch'
description='Integrate Elasticsearch with session replays to seamlessly observe backend errors.' />
<div className='p-5 border-b mb-4'>
<div className='font-medium mb-1'>How it works?</div>
<ol className="list-decimal list-inside">
<li>Create a new Elastic API key</li>
<li>Enter the API key below</li>
<li>Propagate openReplaySessionToken</li>
</ol>
<DocLink className='mt-4' label='Integrate Elasticsearch'
url='https://docs.openreplay.com/integrations/elastic' />
</div>
<IntegrationForm
{...props}
name='elasticsearch'
formFields={[
{
key: 'host',
label: 'Host'
},
{
key: 'apiKeyId',
label: 'API Key ID'
},
{
key: 'apiKey',
label: 'API Key'
},
{
key: 'indexes',
label: 'Indexes'
},
{
key: 'port',
label: 'Port',
type: 'number'
}
]}
/>
</div>
);
}
}

View file

@ -1,29 +1,31 @@
import React from 'react';
import IntegrationForm from './IntegrationForm';
import DocLink from 'Shared/DocLink/DocLink';
import IntegrationModalCard from 'Components/Client/Integrations/IntegrationModalCard';
const GithubForm = (props) => (
<div className="bg-white h-screen overflow-y-auto" style={{ width: '350px' }}>
<h3 className="p-5 text-2xl">Github</h3>
<div className="p-5 border-b mb-4">
<div>Integrate GitHub with OpenReplay and create issues directly from the recording page.</div>
<div className="mt-8">
<DocLink className="mt-4" label="Integrate Github" url="https://docs.openreplay.com/integrations/github" />
</div>
</div>
<IntegrationForm
{...props}
ignoreProject
name="github"
customPath="github"
formFields={[
{
key: 'token',
label: 'Token',
},
]}
/>
<div className='bg-white h-screen overflow-y-auto' style={{ width: '350px' }}>
<IntegrationModalCard title='Github' icon='integrations/github'
description='Integrate GitHub with OpenReplay to enable the direct creation of a new issue from a session.' />
<div className='p-5 border-b mb-4'>
<div>Integrate GitHub with OpenReplay and create issues directly from the recording page.</div>
<div className='mt-8'>
<DocLink className='mt-4' label='Integrate Github' url='https://docs.openreplay.com/integrations/github' />
</div>
</div>
<IntegrationForm
{...props}
ignoreProject
name='github'
customPath='github'
formFields={[
{
key: 'token',
label: 'Token'
}
]}
/>
</div>
);
GithubForm.displayName = 'GithubForm';

View file

@ -0,0 +1,42 @@
import React from 'react';
import { Icon } from 'UI';
import cn from 'classnames';
interface Props {
onChange: any;
activeItem: string;
filters: any;
}
const allItem = { key: 'all', title: 'All' };
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,47 @@
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 rounded-lg 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="shrink-0">
<img className='h-10 w-10' src={'/assets/' + integration.icon + '.svg'} alt='integration' />
</div>
);
<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

@ -0,0 +1,26 @@
import React from 'react';
import { Icon } from 'UI';
import DocLink from 'Shared/DocLink';
interface Props {
title: string;
icon: string;
description: string;
}
function IntegrationModalCard(props: Props) {
const { title, icon, description } = props;
return (
<div className='flex items-start p-5 gap-4'>
<div className='border rounded-lg p-2 shrink-0'>
<img className='h-20 w-20' src={'/assets/' + icon + '.svg'} alt='integration' />
</div>
<div>
<h3 className='text-2xl'>{title}</h3>
<div>{description}</div>
</div>
</div>
);
}
export default IntegrationModalCard;

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,145 +140,228 @@ 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: 'exclamation-triangle',
integrations: [
{
title: 'Jira',
subtitle: 'Integrate Jira with OpenReplay to enable the creation of a new ticket directly from a session.',
slug: 'jira',
category: 'Errors',
icon: 'integrations/jira',
component: <JiraForm />,
component: <JiraForm />
},
{
title: 'Github',
subtitle: 'Integrate GitHub with OpenReplay to enable the direct creation of a new issue from a session.',
slug: 'github',
category: 'Errors',
icon: 'integrations/github',
component: <GithubForm />,
component: <GithubForm />
},
{
title: 'Slack',
subtitle: 'Integrate Slack to empower every user in your org with the ability to send sessions to any Slack channel.',
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,
icon: 'terminal',
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>
),
integrations: [
{ title: 'Sentry', slug: 'sentry', icon: 'integrations/sentry', component: <SentryForm /> },
{
title: 'Sentry',
subtitle: 'Integrate Sentry with session replays to seamlessly observe backend errors.',
slug: 'sentry',
icon: 'integrations/sentry',
component: <SentryForm />
},
{
title: 'Bugsnag',
subtitle: 'Integrate Bugsnag to access the OpenReplay session linked to the JS exception within its interface.',
slug: 'bugsnag',
icon: 'integrations/bugsnag',
component: <BugsnagForm />,
component: <BugsnagForm />
},
{
title: 'Rollbar',
subtitle: 'Integrate Rollbar with session replays to seamlessly observe backend errors.',
slug: 'rollbar',
icon: 'integrations/rollbar',
component: <RollbarForm />,
component: <RollbarForm />
},
{
title: 'Elasticsearch',
subtitle: 'Integrate Elasticsearch with session replays to seamlessly observe backend errors.',
slug: 'elasticsearch',
icon: 'integrations/elasticsearch',
component: <ElasticsearchForm />,
component: <ElasticsearchForm />
},
{
title: 'Datadog',
subtitle: 'Incorporate DataDog to visualize backend errors alongside session replay, for easy troubleshooting.',
slug: 'datadog',
icon: 'integrations/datadog',
component: <DatadogForm />,
component: <DatadogForm />
},
{
title: 'Sumo Logic',
subtitle: 'Integrate Sumo Logic with session replays to seamlessly observe backend errors.',
slug: 'sumologic',
icon: 'integrations/sumologic',
component: <SumoLogicForm />,
component: <SumoLogicForm />
},
{
title: 'Stackdriver',
title: 'Google Cloud',
subtitle: 'Integrate Google Cloud to view backend logs and errors in conjunction with session replay',
slug: 'stackdriver',
icon: 'integrations/google-cloud',
component: <StackdriverForm />,
component: <StackdriverForm />
},
{
title: 'CloudWatch',
subtitle: 'Integrate CloudWatch to see backend logs and errors alongside session replay.',
slug: 'cloudwatch',
icon: 'integrations/aws',
component: <CloudwatchForm />,
component: <CloudwatchForm />
},
{
title: 'Newrelic',
subtitle: 'Integrate NewRelic with session replays to seamlessly observe backend errors.',
slug: 'newrelic',
icon: 'integrations/newrelic',
component: <NewrelicForm />,
},
],
component: <NewrelicForm />
}
]
},
{
title: 'Collaboration',
key: 'collaboration',
isProject: false,
icon: 'file-code',
description: 'Share your sessions with your team and collaborate on issues.',
integrations: []
},
{
title: 'State Management',
key: 'state-management',
isProject: true,
icon: 'layers-half',
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,
icon: 'chat-left-text',
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

@ -2,43 +2,55 @@ import React from 'react';
import IntegrationForm from '../IntegrationForm';
import DocLink from 'Shared/DocLink/DocLink';
import { useModal } from 'App/components/Modal';
import { Icon } from 'UI';
import IntegrationModalCard from 'Components/Client/Integrations/IntegrationModalCard';
const JiraForm = (props) => {
const { hideModal } = useModal();
return (
<div className="bg-white h-screen overflow-y-auto" style={{ width: '350px' }}>
<h3 className="p-5 text-2xl">Jira</h3>
<div className="p-5 border-b mb-4">
<div>How to integrate Jira Cloud with OpenReplay.</div>
<div className="mt-8">
<DocLink className="mt-4" label="Integrate Jira Cloud" url="https://docs.openreplay.com/integrations/jira" />
</div>
</div>
<IntegrationForm
{...props}
ignoreProject={true}
name="jira"
customPath="jira"
onClose={hideModal}
formFields={[
{
key: 'username',
label: 'Username',
autoFocus: true,
},
{
key: 'token',
label: 'API Token',
},
{
key: 'url',
label: 'JIRA URL',
placeholder: 'E.x. https://myjira.atlassian.net',
},
]}
/>
const { hideModal } = useModal();
return (
<div className='bg-white h-screen overflow-y-auto' style={{ width: '350px' }}>
<IntegrationModalCard title='Jira' icon='integrations/jira'
description='Integrate Jira with OpenReplay to enable the creation of a new ticket directly from a session.' />
<div className='border-b my-4 p-5'>
<div className='font-medium mb-1'>How it works?</div>
<ol className='list-decimal list-inside'>
<li>Create a new API token</li>
<li>Enter the token below</li>
</ol>
<div className='mt-8'>
<DocLink className='mt-4' label='Integrate Jira Cloud'
url='https://docs.openreplay.com/integrations/jira' />
</div>
)
</div>
<IntegrationForm
{...props}
ignoreProject={true}
name='jira'
customPath='jira'
onClose={hideModal}
formFields={[
{
key: 'username',
label: 'Username',
autoFocus: true
},
{
key: 'token',
label: 'API Token'
},
{
key: 'url',
label: 'JIRA URL',
placeholder: 'E.x. https://myjira.atlassian.net'
}
]}
/>
</div>
);
};
JiraForm.displayName = 'JiraForm';

View file

@ -1,34 +1,41 @@
import React from 'react';
import IntegrationForm from '../IntegrationForm';
import DocLink from 'Shared/DocLink/DocLink';
import IntegrationModalCard from 'Components/Client/Integrations/IntegrationModalCard';
const NewrelicForm = (props) => (
<div className="bg-white h-screen overflow-y-auto" style={{ width: '350px' }}>
<h3 className="p-5 text-2xl">New Relic</h3>
<div className="p-5 border-b mb-4">
<div>How to integrate NewRelic with OpenReplay and see backend errors alongside session recordings.</div>
<DocLink className="mt-4" label="Integrate NewRelic" url="https://docs.openreplay.com/integrations/newrelic" />
</div>
<IntegrationForm
{...props}
name="newrelic"
formFields={[
{
key: 'applicationId',
label: 'Application Id',
},
{
key: 'xQueryKey',
label: 'X-Query-Key',
},
{
key: 'region',
label: 'EU Region',
type: 'checkbox',
},
]}
/>
<div className='bg-white h-screen overflow-y-auto' style={{ width: '350px' }}>
<IntegrationModalCard title='New Relic' icon='integrations/newrelic'
description='Integrate NewRelic with session replays to seamlessly observe backend errors.' />
<div className='p-5 border-b mb-4'>
<div className='font-medium mb-1'>How it works?</div>
<ol className="list-decimal list-inside">
<li>Create Query Key</li>
<li>Enter the details below</li>
<li>Propagate openReplaySessionToken</li>
</ol>
<DocLink className='mt-4' label='Integrate NewRelic' url='https://docs.openreplay.com/integrations/newrelic' />
</div>
<IntegrationForm
{...props}
name='newrelic'
formFields={[
{
key: 'applicationId',
label: 'Application Id'
},
{
key: 'xQueryKey',
label: 'X-Query-Key'
},
{
key: 'region',
label: 'EU Region',
type: 'checkbox'
}
]}
/>
</div>
);
NewrelicForm.displayName = 'NewrelicForm';

View file

@ -1,25 +1,32 @@
import React from 'react';
import IntegrationForm from './IntegrationForm';
import DocLink from 'Shared/DocLink/DocLink';
import IntegrationModalCard from 'Components/Client/Integrations/IntegrationModalCard';
const RollbarForm = (props) => (
<div className="bg-white h-screen overflow-y-auto" style={{ width: '350px' }}>
<h3 className="p-5 text-2xl">Rollbar</h3>
<div className="p-5 border-b mb-4">
<div>How to integrate Rollbar with OpenReplay and see backend errors alongside session replays.</div>
<DocLink className="mt-4" label="Integrate Rollbar" url="https://docs.openreplay.com/integrations/rollbar" />
</div>
<IntegrationForm
{...props}
name="rollbar"
formFields={[
{
key: 'accessToken',
label: 'Access Token',
},
]}
/>
<div className='bg-white h-screen overflow-y-auto' style={{ width: '350px' }}>
<IntegrationModalCard title='Rollbar' icon='integrations/rollbar'
description='Integrate Rollbar with session replays to seamlessly observe backend errors.' />
<div className='p-5 border-b mb-4'>
<div className='font-medium mb-1'>How it works?</div>
<ol className="list-decimal list-inside">
<li>Create Rollbar Access Tokens</li>
<li>Enter the token below</li>
<li>Propagate openReplaySessionToken</li>
</ol>
<DocLink className='mt-4' label='Integrate Rollbar' url='https://docs.openreplay.com/integrations/rollbar' />
</div>
<IntegrationForm
{...props}
name='rollbar'
formFields={[
{
key: 'accessToken',
label: 'Access Token'
}
]}
/>
</div>
);
RollbarForm.displayName = 'RollbarForm';

View file

@ -1,33 +1,40 @@
import React from 'react';
import IntegrationForm from './IntegrationForm';
import DocLink from 'Shared/DocLink/DocLink';
import IntegrationModalCard from 'Components/Client/Integrations/IntegrationModalCard';
const SentryForm = (props) => (
<div className="bg-white h-screen overflow-y-auto" style={{ width: '350px' }}>
<h3 className="p-5 text-2xl">Sentry</h3>
<div className="p-5 border-b mb-4">
<div>How to integrate Sentry with OpenReplay and see backend errors alongside session recordings.</div>
<DocLink className="mt-4" label="Integrate Sentry" url="https://docs.openreplay.com/integrations/sentry" />
</div>
<IntegrationForm
{...props}
name="sentry"
formFields={[
{
key: 'organizationSlug',
label: 'Organization Slug',
},
{
key: 'projectSlug',
label: 'Project Slug',
},
{
key: 'token',
label: 'Token',
},
]}
/>
<div className='bg-white h-screen overflow-y-auto' style={{ width: '350px' }}>
<IntegrationModalCard title='Sentry' icon='integrations/sentry'
description='Integrate Sentry with session replays to seamlessly observe backend errors.' />
<div className='p-5 border-b mb-4'>
<div className='font-medium mb-1'>How it works?</div>
<ol className="list-decimal list-inside">
<li>Generate Sentry Auth Token</li>
<li>Enter the token below</li>
<li>Propagate openReplaySessionToken</li>
</ol>
<DocLink className='mt-4' label='See detailed steps' url='https://docs.openreplay.com/integrations/sentry' />
</div>
<IntegrationForm
{...props}
name='sentry'
formFields={[
{
key: 'organizationSlug',
label: 'Organization Slug'
},
{
key: 'projectSlug',
label: 'Project Slug'
},
{
key: 'token',
label: 'Token'
}
]}
/>
</div>
);
SentryForm.displayName = 'SentryForm';

View file

@ -1,30 +1,38 @@
import React from 'react';
import IntegrationForm from './IntegrationForm';
import DocLink from 'Shared/DocLink/DocLink';
import IntegrationModalCard from 'Components/Client/Integrations/IntegrationModalCard';
const StackdriverForm = (props) => (
<div className="bg-white h-screen overflow-y-auto" style={{ width: '350px' }}>
<h3 className="p-5 text-2xl">Stackdriver</h3>
<div className="p-5 border-b mb-4">
<div>How to integrate Stackdriver with OpenReplay and see backend errors alongside session recordings.</div>
<DocLink className="mt-4" label="Integrate Stackdriver" url="https://docs.openreplay.com/integrations/stackdriver" />
</div>
<IntegrationForm
{...props}
name="stackdriver"
formFields={[
{
key: 'logName',
label: 'Log Name',
},
{
key: 'serviceAccountCredentials',
label: 'Service Account Credentials (JSON)',
component: 'textarea',
},
]}
/>
<div className='bg-white h-screen overflow-y-auto' style={{ width: '350px' }}>
<IntegrationModalCard title='Google Cloud' icon='integrations/google-cloud'
description='Integrate Google Cloud Watch to see backend logs and errors alongside session replay.' />
<div className='p-5 border-b mb-4'>
<div className='font-medium mb-1'>How it works?</div>
<ol className="list-decimal list-inside">
<li>Create Google Cloud Service Account</li>
<li>Enter the details below</li>
<li>Propagate openReplaySessionToken</li>
</ol>
<DocLink className='mt-4' label='Integrate Stackdriver'
url='https://docs.openreplay.com/integrations/stackdriver' />
</div>
<IntegrationForm
{...props}
name='stackdriver'
formFields={[
{
key: 'logName',
label: 'Log Name'
},
{
key: 'serviceAccountCredentials',
label: 'Service Account Credentials (JSON)',
component: 'textarea'
}
]}
/>
</div>
);
StackdriverForm.displayName = 'StackdriverForm';

View file

@ -2,34 +2,41 @@ import React from 'react';
import IntegrationForm from '../IntegrationForm';
import RegionDropdown from './RegionDropdown';
import DocLink from 'Shared/DocLink/DocLink';
import IntegrationModalCard from 'Components/Client/Integrations/IntegrationModalCard';
const SumoLogicForm = (props) => (
<div className="bg-white h-screen overflow-y-auto" style={{ width: '350px' }}>
<h3 className="p-5 text-2xl">Sumologic</h3>
<div className="p-5 border-b mb-4">
<div>How to integrate SumoLogic with OpenReplay and see backend errors alongside session recordings.</div>
<DocLink className="mt-4" label="Integrate SumoLogic" url="https://docs.openreplay.com/integrations/sumo" />
</div>
<IntegrationForm
{...props}
name="sumologic"
formFields={[
{
key: 'accessId',
label: 'Access ID',
},
{
key: 'accessKey',
label: 'Access Key',
},
{
key: 'region',
label: 'Region',
component: RegionDropdown,
},
]}
/>
<div className='bg-white h-screen overflow-y-auto' style={{ width: '350px' }}>
<IntegrationModalCard title='Sumologic' icon='integrations/sumologic'
description='Integrate Sumo Logic with session replays to seamlessly observe backend errors.' />
<div className='p-5 border-b mb-4'>
<div className='font-medium mb-1'>How it works?</div>
<ol className="list-decimal list-inside">
<li>Create a new Access ID and Access Key</li>
<li>Enter the details below</li>
<li>Propagate openReplaySessionToken</li>
</ol>
<DocLink className='mt-4' label='Integrate SumoLogic' url='https://docs.openreplay.com/integrations/sumo' />
</div>
<IntegrationForm
{...props}
name='sumologic'
formFields={[
{
key: 'accessId',
label: 'Access ID'
},
{
key: 'accessKey',
label: 'Access Key'
},
{
key: 'region',
label: 'Region',
component: RegionDropdown
}
]}
/>
</div>
);
SumoLogicForm.displayName = 'SumoLogicForm';

View file

@ -8,7 +8,7 @@ export default function DocLink({ className = '', url, label }) {
return (
<div className={className}>
<Button variant="outline" onClick={openLink}>
<Button variant="text-primary" onClick={openLink}>
<span className="mr-2">{ label }</span>
<Icon name="external-link-alt" color="teal" />
</Button>

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') {

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" class="bi bi-chat-left-text" viewBox="0 0 16 16">
<path d="M14 1a1 1 0 0 1 1 1v8a1 1 0 0 1-1 1H4.414A2 2 0 0 0 3 11.586l-2 2V2a1 1 0 0 1 1-1h12zM2 0a2 2 0 0 0-2 2v12.793a.5.5 0 0 0 .854.353l2.853-2.853A1 1 0 0 1 4.414 12H14a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2z"/>
<path d="M3 3.5a.5.5 0 0 1 .5-.5h9a.5.5 0 0 1 0 1h-9a.5.5 0 0 1-.5-.5zM3 6a.5.5 0 0 1 .5-.5h9a.5.5 0 0 1 0 1h-9A.5.5 0 0 1 3 6zm0 2.5a.5.5 0 0 1 .5-.5h5a.5.5 0 0 1 0 1h-5a.5.5 0 0 1-.5-.5z"/>
</svg>

After

Width:  |  Height:  |  Size: 503 B

View file

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" class="bi bi-exclamation-triangle" viewBox="0 0 16 16">
<path d="M7.938 2.016A.13.13 0 0 1 8.002 2a.13.13 0 0 1 .063.016.146.146 0 0 1 .054.057l6.857 11.667c.036.06.035.124.002.183a.163.163 0 0 1-.054.06.116.116 0 0 1-.066.017H1.146a.115.115 0 0 1-.066-.017.163.163 0 0 1-.054-.06.176.176 0 0 1 .002-.183L7.884 2.073a.147.147 0 0 1 .054-.057zm1.044-.45a1.13 1.13 0 0 0-1.96 0L.165 13.233c-.457.778.091 1.767.98 1.767h13.713c.889 0 1.438-.99.98-1.767L8.982 1.566z"/>
<path d="M7.002 12a1 1 0 1 1 2 0 1 1 0 0 1-2 0zM7.1 5.995a.905.905 0 1 1 1.8 0l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 5.995z"/>
</svg>

After

Width:  |  Height:  |  Size: 641 B

View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" class="bi bi-layers-half" viewBox="0 0 16 16">
<path d="M8.235 1.559a.5.5 0 0 0-.47 0l-7.5 4a.5.5 0 0 0 0 .882L3.188 8 .264 9.559a.5.5 0 0 0 0 .882l7.5 4a.5.5 0 0 0 .47 0l7.5-4a.5.5 0 0 0 0-.882L12.813 8l2.922-1.559a.5.5 0 0 0 0-.882l-7.5-4zM8 9.433 1.562 6 8 2.567 14.438 6 8 9.433z"/>
</svg>

After

Width:  |  Height:  |  Size: 335 B

View file

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" class="bi bi-terminal" viewBox="0 0 16 16">
<path d="M6 9a.5.5 0 0 1 .5-.5h3a.5.5 0 0 1 0 1h-3A.5.5 0 0 1 6 9zM3.854 4.146a.5.5 0 1 0-.708.708L4.793 6.5 3.146 8.146a.5.5 0 1 0 .708.708l2-2a.5.5 0 0 0 0-.708l-2-2z"/>
<path d="M2 1a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V3a2 2 0 0 0-2-2H2zm12 1a1 1 0 0 1 1 1v10a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1V3a1 1 0 0 1 1-1h12z"/>
</svg>

After

Width:  |  Height:  |  Size: 422 B

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
}
};