diff --git a/api/Dockerfile b/api/Dockerfile index a6077fd13..20dfe9b86 100644 --- a/api/Dockerfile +++ b/api/Dockerfile @@ -1,9 +1,7 @@ FROM python:3.10-alpine LABEL Maintainer="Rajesh Rajendran" LABEL Maintainer="KRAIEM Taha Yassine" -RUN apk upgrade busybox --no-cache --repository=http://dl-cdn.alpinelinux.org/alpine/edge/main RUN apk add --no-cache build-base nodejs npm tini -RUN apk upgrade busybox --no-cache --repository=http://dl-cdn.alpinelinux.org/alpine/edge/main ARG envarg # Add Tini # Startup daemon diff --git a/api/Dockerfile.alerts b/api/Dockerfile.alerts index c4614b3c1..dbb0c581d 100644 --- a/api/Dockerfile.alerts +++ b/api/Dockerfile.alerts @@ -1,7 +1,6 @@ FROM python:3.10-alpine LABEL Maintainer="Rajesh Rajendran" LABEL Maintainer="KRAIEM Taha Yassine" -RUN apk upgrade busybox --no-cache --repository=http://dl-cdn.alpinelinux.org/alpine/edge/main RUN apk add --no-cache build-base tini ARG envarg ENV APP_NAME=alerts \ diff --git a/api/chalicelib/utils/jira_client.py b/api/chalicelib/utils/jira_client.py index b1734660c..4306cfab2 100644 --- a/api/chalicelib/utils/jira_client.py +++ b/api/chalicelib/utils/jira_client.py @@ -18,7 +18,7 @@ class JiraManager: self._config = {"JIRA_PROJECT_ID": project_id, "JIRA_URL": url, "JIRA_USERNAME": username, "JIRA_PASSWORD": password} try: - self._jira = JIRA(url, basic_auth=(username, password), logging=True, max_retries=1) + self._jira = JIRA(url, basic_auth=(username, password), logging=True, max_retries=0, timeout=3) except Exception as e: print("!!! JIRA AUTH ERROR") print(e) diff --git a/api/requirements-alerts.txt b/api/requirements-alerts.txt index 81198b0f3..788c58767 100644 --- a/api/requirements-alerts.txt +++ b/api/requirements-alerts.txt @@ -4,7 +4,7 @@ boto3==1.24.26 pyjwt==2.4.0 psycopg2-binary==2.9.3 elasticsearch==8.3.1 -jira==3.3.0 +jira==3.3.1 diff --git a/api/requirements.txt b/api/requirements.txt index 81198b0f3..788c58767 100644 --- a/api/requirements.txt +++ b/api/requirements.txt @@ -4,7 +4,7 @@ boto3==1.24.26 pyjwt==2.4.0 psycopg2-binary==2.9.3 elasticsearch==8.3.1 -jira==3.3.0 +jira==3.3.1 diff --git a/api/routers/core.py b/api/routers/core.py index 12503f438..80dc429cb 100644 --- a/api/routers/core.py +++ b/api/routers/core.py @@ -431,29 +431,47 @@ def get_integration_status(context: schemas.CurrentContext = Depends(OR_context) return {"data": integration.get_obfuscated()} +@app.get('/integrations/jira', tags=["integrations"]) +def get_integration_status_jira(context: schemas.CurrentContext = Depends(OR_context)): + error, integration = integrations_manager.get_integration(tenant_id=context.tenant_id, + user_id=context.user_id, + tool=integration_jira_cloud.PROVIDER) + if error is not None and integration is None: + return error + return {"data": integration.get_obfuscated()} + + +@app.get('/integrations/github', tags=["integrations"]) +def get_integration_status_github(context: schemas.CurrentContext = Depends(OR_context)): + error, integration = integrations_manager.get_integration(tenant_id=context.tenant_id, + user_id=context.user_id, + tool=integration_github.PROVIDER) + if error is not None and integration is None: + return error + return {"data": integration.get_obfuscated()} + + @app.post('/integrations/jira', tags=["integrations"]) @app.put('/integrations/jira', tags=["integrations"]) -def add_edit_jira_cloud(data: schemas.JiraGithubSchema = Body(...), +def add_edit_jira_cloud(data: schemas.JiraSchema = Body(...), context: schemas.CurrentContext = Depends(OR_context)): error, integration = integrations_manager.get_integration(tool=integration_jira_cloud.PROVIDER, tenant_id=context.tenant_id, user_id=context.user_id) if error is not None and integration is None: return error - data.provider = integration_jira_cloud.PROVIDER return {"data": integration.add_edit(data=data.dict())} @app.post('/integrations/github', tags=["integrations"]) @app.put('/integrations/github', tags=["integrations"]) -def add_edit_github(data: schemas.JiraGithubSchema = Body(...), +def add_edit_github(data: schemas.GithubSchema = Body(...), context: schemas.CurrentContext = Depends(OR_context)): error, integration = integrations_manager.get_integration(tool=integration_github.PROVIDER, tenant_id=context.tenant_id, user_id=context.user_id) if error is not None: return error - data.provider = integration_github.PROVIDER return {"data": integration.add_edit(data=data.dict())} diff --git a/api/routers/core_dynamic.py b/api/routers/core_dynamic.py index e9f8cf83a..d2f115dd3 100644 --- a/api/routers/core_dynamic.py +++ b/api/routers/core_dynamic.py @@ -87,18 +87,6 @@ def edit_slack_integration(integrationId: int, data: schemas.EditSlackSchema = B changes={"name": data.name, "endpoint": data.url})} -# this endpoint supports both jira & github based on `provider` attribute -@app.post('/integrations/issues', tags=["integrations"]) -def add_edit_jira_cloud_github(data: schemas.JiraGithubSchema, - context: schemas.CurrentContext = Depends(OR_context)): - provider = data.provider.upper() - error, integration = integrations_manager.get_integration(tool=provider, tenant_id=context.tenant_id, - user_id=context.user_id) - if error is not None: - return error - return {"data": integration.add_edit(data=data.dict())} - - @app.post('/client/members', tags=["client"]) @app.put('/client/members', tags=["client"]) def add_member(background_tasks: BackgroundTasks, data: schemas.CreateMemberSchema = Body(...), diff --git a/api/schemas.py b/api/schemas.py index 644df328d..eae89c513 100644 --- a/api/schemas.py +++ b/api/schemas.py @@ -100,10 +100,12 @@ class NotificationsViewSchema(BaseModel): endTimestamp: Optional[int] = Field(default=None) -class JiraGithubSchema(BaseModel): - provider: str = Field(...) - username: str = Field(...) +class GithubSchema(BaseModel): token: str = Field(...) + + +class JiraSchema(GithubSchema): + username: str = Field(...) url: HttpUrl = Field(...) @validator('url') diff --git a/backend/Dockerfile b/backend/Dockerfile index fe8f9e7e4..c2375d800 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -19,7 +19,6 @@ RUN CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -o service -tags musl openrep FROM alpine AS entrypoint -RUN apk upgrade busybox --no-cache --repository=http://dl-cdn.alpinelinux.org/alpine/edge/main RUN apk add --no-cache ca-certificates RUN adduser -u 1001 openreplay -D @@ -46,6 +45,7 @@ ENV TZ=UTC \ AWS_REGION_WEB=eu-central-1 \ AWS_REGION_IOS=eu-west-1 \ AWS_REGION_ASSETS=eu-central-1 \ + AWS_SKIP_SSL_VALIDATION=false \ CACHE_ASSETS=true \ ASSETS_SIZE_LIMIT=6291456 \ ASSETS_HEADERS="{ \"Cookie\": \"ABv=3;\" }" \ diff --git a/backend/pkg/env/aws.go b/backend/pkg/env/aws.go index cb7445797..e25a3a561 100644 --- a/backend/pkg/env/aws.go +++ b/backend/pkg/env/aws.go @@ -1,7 +1,9 @@ package env import ( + "crypto/tls" "log" + "net/http" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/credentials" @@ -20,6 +22,15 @@ func AWSSessionOnRegion(region string) *_session.Session { config.Endpoint = aws.String(AWS_ENDPOINT) config.DisableSSL = aws.Bool(true) config.S3ForcePathStyle = aws.Bool(true) + + AWS_SKIP_SSL_VALIDATION := Bool("AWS_SKIP_SSL_VALIDATION") + if AWS_SKIP_SSL_VALIDATION { + tr := &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + } + client := &http.Client{Transport: tr} + config.HTTPClient = client + } } aws_session, err := _session.NewSession(config) if err != nil { diff --git a/ee/api/Dockerfile b/ee/api/Dockerfile index 2a2aef8f0..2e04fa330 100644 --- a/ee/api/Dockerfile +++ b/ee/api/Dockerfile @@ -1,9 +1,7 @@ FROM python:3.10-alpine LABEL Maintainer="Rajesh Rajendran" LABEL Maintainer="KRAIEM Taha Yassine" -RUN apk upgrade busybox --no-cache --repository=http://dl-cdn.alpinelinux.org/alpine/edge/main RUN apk add --no-cache build-base libressl libffi-dev libressl-dev libxslt-dev libxml2-dev xmlsec-dev xmlsec nodejs npm tini -RUN apk upgrade busybox --no-cache --repository=http://dl-cdn.alpinelinux.org/alpine/edge/main ARG envarg ENV SOURCE_MAP_VERSION=0.7.4 \ APP_NAME=chalice \ diff --git a/ee/api/Dockerfile.alerts b/ee/api/Dockerfile.alerts index 785b0a5f9..351fce661 100644 --- a/ee/api/Dockerfile.alerts +++ b/ee/api/Dockerfile.alerts @@ -1,7 +1,6 @@ FROM python:3.10-alpine LABEL Maintainer="Rajesh Rajendran" LABEL Maintainer="KRAIEM Taha Yassine" -RUN apk upgrade busybox --no-cache --repository=http://dl-cdn.alpinelinux.org/alpine/edge/main RUN apk add --no-cache build-base tini ARG envarg ENV APP_NAME=alerts \ diff --git a/ee/api/Dockerfile.crons b/ee/api/Dockerfile.crons index 0647c6fc6..96b9e6453 100644 --- a/ee/api/Dockerfile.crons +++ b/ee/api/Dockerfile.crons @@ -1,7 +1,6 @@ FROM python:3.10-alpine LABEL Maintainer="Rajesh Rajendran" LABEL Maintainer="KRAIEM Taha Yassine" -RUN apk upgrade busybox --no-cache --repository=http://dl-cdn.alpinelinux.org/alpine/edge/main RUN apk add --no-cache build-base tini ARG envarg ENV APP_NAME=crons \ diff --git a/ee/api/requirements-alerts.txt b/ee/api/requirements-alerts.txt index 66fa84713..906189999 100644 --- a/ee/api/requirements-alerts.txt +++ b/ee/api/requirements-alerts.txt @@ -4,7 +4,7 @@ boto3==1.24.26 pyjwt==2.4.0 psycopg2-binary==2.9.3 elasticsearch==8.3.1 -jira==3.3.0 +jira==3.3.1 diff --git a/ee/api/requirements-crons.txt b/ee/api/requirements-crons.txt index 66fa84713..906189999 100644 --- a/ee/api/requirements-crons.txt +++ b/ee/api/requirements-crons.txt @@ -4,7 +4,7 @@ boto3==1.24.26 pyjwt==2.4.0 psycopg2-binary==2.9.3 elasticsearch==8.3.1 -jira==3.3.0 +jira==3.3.1 diff --git a/ee/api/requirements.txt b/ee/api/requirements.txt index 5ce044904..0a8ca819e 100644 --- a/ee/api/requirements.txt +++ b/ee/api/requirements.txt @@ -4,7 +4,7 @@ boto3==1.24.26 pyjwt==2.4.0 psycopg2-binary==2.9.3 elasticsearch==8.3.1 -jira==3.3.0 +jira==3.3.1 diff --git a/ee/api/routers/core_dynamic.py b/ee/api/routers/core_dynamic.py index 3c5c21905..9d09198a6 100644 --- a/ee/api/routers/core_dynamic.py +++ b/ee/api/routers/core_dynamic.py @@ -90,18 +90,6 @@ def edit_slack_integration(integrationId: int, data: schemas.EditSlackSchema = B changes={"name": data.name, "endpoint": data.url})} -# this endpoint supports both jira & github based on `provider` attribute -@app.post('/integrations/issues', tags=["integrations"]) -def add_edit_jira_cloud_github(data: schemas.JiraGithubSchema, - context: schemas.CurrentContext = Depends(OR_context)): - provider = data.provider.upper() - error, integration = integrations_manager.get_integration(tool=provider, tenant_id=context.tenant_id, - user_id=context.user_id) - if error is not None: - return error - return {"data": integration.add_edit(data=data.dict())} - - @app.post('/client/members', tags=["client"]) @app.put('/client/members', tags=["client"]) def add_member(background_tasks: BackgroundTasks, data: schemas_ee.CreateMemberSchema = Body(...), diff --git a/ee/utilities/Dockerfile b/ee/utilities/Dockerfile index 2de6197a2..b4592048d 100644 --- a/ee/utilities/Dockerfile +++ b/ee/utilities/Dockerfile @@ -1,6 +1,5 @@ FROM node:18-alpine LABEL Maintainer="KRAIEM Taha Yassine" -RUN apk upgrade busybox --no-cache --repository=http://dl-cdn.alpinelinux.org/alpine/edge/main RUN apk add --no-cache tini git libc6-compat && ln -s /lib/libc.musl-x86_64.so.1 /lib/ld-linux-x86-64.so.2 ARG envarg diff --git a/frontend/Dockerfile b/frontend/Dockerfile index bfa86857d..5e6c9b3b0 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -14,7 +14,6 @@ COPY nginx.conf /etc/nginx/conf.d/default.conf # Default step in docker build FROM nginx:alpine LABEL maintainer=Rajesh -RUN apk upgrade busybox --no-cache --repository=http://dl-cdn.alpinelinux.org/alpine/edge/main COPY --from=builder /work/public /var/www/openreplay COPY nginx.conf /etc/nginx/conf.d/default.conf diff --git a/frontend/app/assets/integrations/aws.svg b/frontend/app/assets/integrations/aws.svg new file mode 100644 index 000000000..c18fbdab2 --- /dev/null +++ b/frontend/app/assets/integrations/aws.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/app/assets/integrations/bugsnag.svg b/frontend/app/assets/integrations/bugsnag.svg index 26a3a13b8..cc97e195b 100644 --- a/frontend/app/assets/integrations/bugsnag.svg +++ b/frontend/app/assets/integrations/bugsnag.svg @@ -1 +1,4 @@ - \ No newline at end of file + + + + diff --git a/frontend/app/assets/integrations/google-cloud.svg b/frontend/app/assets/integrations/google-cloud.svg new file mode 100644 index 000000000..93f614043 --- /dev/null +++ b/frontend/app/assets/integrations/google-cloud.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/frontend/app/assets/integrations/newrelic.svg b/frontend/app/assets/integrations/newrelic.svg index cc4aea514..061e7e0a3 100644 --- a/frontend/app/assets/integrations/newrelic.svg +++ b/frontend/app/assets/integrations/newrelic.svg @@ -1 +1,12 @@ -NewRelic-logo-square \ No newline at end of file + + + + + + + + + + + + diff --git a/frontend/app/assets/integrations/rollbar.svg b/frontend/app/assets/integrations/rollbar.svg index 2f6538118..0d183182b 100644 --- a/frontend/app/assets/integrations/rollbar.svg +++ b/frontend/app/assets/integrations/rollbar.svg @@ -1,20 +1,10 @@ - - - - -rollbar-logo-color-vertical - - - - - + + + + + + + + diff --git a/frontend/app/components/Client/Client.js b/frontend/app/components/Client/Client.js index adae9f536..2574f0758 100644 --- a/frontend/app/components/Client/Client.js +++ b/frontend/app/components/Client/Client.js @@ -52,7 +52,7 @@ export default class Client extends React.PureComponent {
-
+
{ activeTab && this.renderActiveTab() }
diff --git a/frontend/app/components/Client/CustomFields/CustomFieldForm.js b/frontend/app/components/Client/CustomFields/CustomFieldForm.js index 76ee849d5..adc9ac884 100644 --- a/frontend/app/components/Client/CustomFields/CustomFieldForm.js +++ b/frontend/app/components/Client/CustomFields/CustomFieldForm.js @@ -4,59 +4,74 @@ import { edit, save } from 'Duck/customField'; import { Form, Input, Button, Message } from 'UI'; import styles from './customFieldForm.module.css'; -@connect(state => ({ - field: state.getIn(['customFields', 'instance']), - saving: state.getIn(['customFields', 'saveRequest', 'loading']), - errors: state.getIn([ 'customFields', 'saveRequest', 'errors' ]), -}), { - edit, - save, -}) +@connect( + (state) => ({ + field: state.getIn(['customFields', 'instance']), + saving: state.getIn(['customFields', 'saveRequest', 'loading']), + errors: state.getIn(['customFields', 'saveRequest', 'errors']), + }), + { + edit, + save, + } +) class CustomFieldForm extends React.PureComponent { - setFocus = () => this.focusElement.focus(); - onChangeSelect = (event, { name, value }) => this.props.edit({ [ name ]: value }); - write = ({ target: { value, name } }) => this.props.edit({ [ name ]: value }); + setFocus = () => this.focusElement.focus(); + onChangeSelect = (event, { name, value }) => this.props.edit({ [name]: value }); + write = ({ target: { value, name } }) => this.props.edit({ [name]: value }); - render() { - const { field, errors} = this.props; - const exists = field.exists(); - return ( -
- - - { this.focusElement = ref; } } - name="key" - value={ field.key } - onChange={ this.write } - placeholder="Field Name" - /> - + render() { + const { field, errors } = this.props; + const exists = field.exists(); + return ( +
+

{exists ? 'Update' : 'Add'} Metadata Field

+ + + + { + this.focusElement = ref; + }} + name="key" + value={field.key} + onChange={this.write} + placeholder="Field Name" + /> + - { errors && -
- { errors.map(error => { error }) } -
- } + {errors && ( +
+ {errors.map((error) => ( + + {error} + + ))} +
+ )} - - - - ); - } +
+
+ + +
+ + +
+ +
+ ); + } } export default CustomFieldForm; diff --git a/frontend/app/components/Client/CustomFields/CustomFields.js b/frontend/app/components/Client/CustomFields/CustomFields.js index 4c3d0bbc8..d5abbb2c6 100644 --- a/frontend/app/components/Client/CustomFields/CustomFields.js +++ b/frontend/app/components/Client/CustomFields/CustomFields.js @@ -1,8 +1,8 @@ -import React from 'react'; +import React, { useEffect } from 'react'; import cn from 'classnames'; import { connect } from 'react-redux'; import withPageTitle from 'HOCs/withPageTitle'; -import { IconButton, SlideModal, Loader, NoContent, Icon, TextLink } from 'UI'; +import { Button, Loader, NoContent, TextLink } from 'UI'; import { init, fetchList, save, remove } from 'Duck/customField'; import SiteDropdown from 'Shared/SiteDropdown'; import styles from './customFields.module.css'; @@ -10,121 +10,118 @@ import CustomFieldForm from './CustomFieldForm'; import ListItem from './ListItem'; import { confirm } from 'UI'; import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG'; +import { useModal } from 'App/components/Modal'; -@connect(state => ({ - fields: state.getIn(['customFields', 'list']).sortBy(i => i.index), - field: state.getIn(['customFields', 'instance']), - loading: state.getIn(['customFields', 'fetchRequest', 'loading']), - sites: state.getIn([ 'site', 'list' ]), - errors: state.getIn([ 'customFields', 'saveRequest', 'errors' ]), -}), { - init, - fetchList, - save, - remove, -}) -@withPageTitle('Metadata - OpenReplay Preferences') -class CustomFields extends React.Component { - state = { showModal: false, currentSite: this.props.sites.get(0), deletingItem: null }; +function CustomFields(props) { + const [currentSite, setCurrentSite] = React.useState(props.sites.get(0)); + const [deletingItem, setDeletingItem] = React.useState(null); + const { showModal, hideModal } = useModal(); - componentWillMount() { - const activeSite = this.props.sites.get(0); - if (!activeSite) return; - - this.props.fetchList(activeSite.id); - } + useEffect(() => { + const activeSite = props.sites.get(0); + if (!activeSite) return; - save = (field) => { - const { currentSite } = this.state; - this.props.save(currentSite.id, field).then(() => { - const { errors } = this.props; - if (!errors || errors.size === 0) { - return this.closeModal(); - } - }); - }; + props.fetchList(activeSite.id); + }, []); - closeModal = () => this.setState({ showModal: false }); - init = (field) => { - this.props.init(field); - this.setState({ showModal: true }); - } - - onChangeSelect = ({ value }) => { - const site = this.props.sites.find(s => s.id === value.value); - this.setState({ currentSite: site }) - this.props.fetchList(site.id); - } - - removeMetadata = async (field) => { - if (await confirm({ - header: 'Metadata', - confirmation: `Are you sure you want to remove?` - })) { - const { currentSite } = this.state; - this.setState({ deletingItem: field.index }); - this.props.remove(currentSite.id, field.index) - .then(() => this.setState({ deletingItem: null })); - } - } - - render() { - const { fields, field, loading } = this.props; - const { showModal, currentSite, deletingItem } = this.state; - return ( -
- } - onClose={ this.closeModal } - /> -
-

{ 'Metadata' }

-
- -
- this.init() } /> - -
- - - - -
No data available.
-
+ const save = (field) => { + props.save(currentSite.id, field).then(() => { + const { errors } = props; + if (!errors || errors.size === 0) { + hideModal(); } - size="small" - show={ fields.size === 0 } - // animatedIcon="empty-state" - > -
- { fields.filter(i => i.index).map(field => ( - this.removeMetadata(field) } + }); + }; + + const init = (field) => { + props.init(field); + showModal( removeMetadata(field)} />); + }; + + const onChangeSelect = ({ value }) => { + const site = props.sites.find((s) => s.id === value.value); + setCurrentSite(site); + props.fetchList(site.id); + }; + + const removeMetadata = async (field) => { + if ( + await confirm({ + header: 'Metadata', + confirmation: `Are you sure you want to remove?`, + }) + ) { + setDeletingItem(field.index); + props + .remove(currentSite.id, field.index) + .then(() => { + hideModal(); + }) + .finally(() => { + setDeletingItem(null); + }); + } + }; + + const { fields, loading } = props; + return ( +
+
+

{'Metadata'}

+
+ +
+
- - -
+ + + + +
No data available.
+
+ } + size="small" + show={fields.size === 0} + > +
+ {fields + .filter((i) => i.index) + .map((field) => ( + removeMetadata(field) } + /> + ))} +
+ + + ); - } } -export default CustomFields; +export default connect( + (state) => ({ + fields: state.getIn(['customFields', 'list']).sortBy((i) => i.index), + field: state.getIn(['customFields', 'instance']), + loading: state.getIn(['customFields', 'fetchRequest', 'loading']), + sites: state.getIn(['site', 'list']), + errors: state.getIn(['customFields', 'saveRequest', 'errors']), + }), + { + init, + fetchList, + save, + remove, + } +)(withPageTitle('Metadata - OpenReplay Preferences')(CustomFields)); diff --git a/frontend/app/components/Client/CustomFields/ListItem.js b/frontend/app/components/Client/CustomFields/ListItem.js index ef806fc93..019327b47 100644 --- a/frontend/app/components/Client/CustomFields/ListItem.js +++ b/frontend/app/components/Client/CustomFields/ListItem.js @@ -1,22 +1,28 @@ import React from 'react'; -import cn from 'classnames' +import cn from 'classnames'; import { Icon } from 'UI'; import styles from './listItem.module.css'; -const ListItem = ({ field, onEdit, onDelete, disabled }) => { - return ( -
field.index != 0 && onEdit(field) } > - { field.key } -
-
{ e.stopPropagation(); onDelete(field) } }> - +const ListItem = ({ field, onEdit, disabled }) => { + return ( +
field.index != 0 && onEdit(field)} + > + {field.key} +
+
+ +
+
-
- -
-
-
- ); + ); }; export default ListItem; diff --git a/frontend/app/components/Client/Integrations/AssistDoc/AssistDoc.js b/frontend/app/components/Client/Integrations/AssistDoc/AssistDoc.js index 4cf2d0e7f..83319959a 100644 --- a/frontend/app/components/Client/Integrations/AssistDoc/AssistDoc.js +++ b/frontend/app/components/Client/Integrations/AssistDoc/AssistDoc.js @@ -1,59 +1,56 @@ import React from 'react'; -import Highlight from 'react-highlight' +import Highlight from 'react-highlight'; import DocLink from 'Shared/DocLink/DocLink'; -import AssistScript from './AssistScript' -import AssistNpm from './AssistNpm' +import AssistScript from './AssistScript'; +import AssistNpm from './AssistNpm'; import { Tabs } from 'UI'; import { useState } from 'react'; -const NPM = 'NPM' -const SCRIPT = 'SCRIPT' +const NPM = 'NPM'; +const SCRIPT = 'SCRIPT'; const TABS = [ - { key: SCRIPT, text: SCRIPT }, - { key: NPM, text: NPM }, -] + { key: SCRIPT, text: SCRIPT }, + { key: NPM, text: NPM }, +]; const AssistDoc = (props) => { - const { projectKey } = props; - const [activeTab, setActiveTab] = useState(SCRIPT) - + const { projectKey } = props; + const [activeTab, setActiveTab] = useState(SCRIPT); - const renderActiveTab = () => { - switch (activeTab) { - case SCRIPT: - return - case NPM: - return - } - return null; - } + const renderActiveTab = () => { + switch (activeTab) { + case SCRIPT: + return ; + case NPM: + return ; + } + return null; + }; + return ( +
+

Assist

+
+
+ OpenReplay Assist allows you to support your users by seeing their live screen and instantly hopping on call (WebRTC) with them + without requiring any 3rd-party screen sharing software. +
- return ( -
-
OpenReplay Assist allows you to support your users by seeing their live screen and instantly hopping on call (WebRTC) with them without requiring any 3rd-party screen sharing software.
+
Installation
+ {`npm i @openreplay/tracker-assist`} +
-
Installation
- - {`npm i @openreplay/tracker-assist`} - -
+
Usage
+ setActiveTab(tab)} /> -
Usage
- setActiveTab(tab) } - /> +
{renderActiveTab()}
-
- { renderActiveTab() } -
- - -
- ) + +
+
+ ); }; -AssistDoc.displayName = "AssistDoc"; +AssistDoc.displayName = 'AssistDoc'; export default AssistDoc; diff --git a/frontend/app/components/Client/Integrations/AxiosDoc/AxiosDoc.js b/frontend/app/components/Client/Integrations/AxiosDoc/AxiosDoc.js index 8fe32cfd0..9b624bbe4 100644 --- a/frontend/app/components/Client/Integrations/AxiosDoc/AxiosDoc.js +++ b/frontend/app/components/Client/Integrations/AxiosDoc/AxiosDoc.js @@ -1,40 +1,46 @@ import React from 'react'; -import Highlight from 'react-highlight' -import ToggleContent from 'Shared/ToggleContent' +import Highlight from 'react-highlight'; +import ToggleContent from 'Shared/ToggleContent'; import DocLink from 'Shared/DocLink/DocLink'; const AxiosDoc = (props) => { - const { projectKey } = props; - return ( -
-
This plugin allows you to capture axios requests and inspect them later on while replaying session recordings. This is very useful for understanding and fixing issues.
- -
Installation
- - {`npm i @openreplay/tracker-axios`} - - -
Usage
-

Initialize the @openreplay/tracker package as usual then load the axios plugin. Note that OpenReplay axios plugin requires axios@^0.21.2 as a peer dependency.

-
+ const { projectKey } = props; + return ( +
+

Axios

+
+
+ This plugin allows you to capture axios requests and inspect them later on while replaying session recordings. This is very useful + for understanding and fixing issues. +
-
Usage
- - {`import tracker from '@openreplay/tracker'; +
Installation
+ {`npm i @openreplay/tracker-axios`} + +
Usage
+

+ Initialize the @openreplay/tracker package as usual then load the axios plugin. Note that OpenReplay axios plugin requires + axios@^0.21.2 as a peer dependency. +

+
+ +
Usage
+ + {`import tracker from '@openreplay/tracker'; import trackerAxios from '@openreplay/tracker-axios'; const tracker = new OpenReplay({ projectKey: '${projectKey}' }); tracker.use(trackerAxios(options)); // check list of available options below tracker.start();`} - - } - second={ - - {`import OpenReplay from '@openreplay/tracker/cjs'; + + } + second={ + + {`import OpenReplay from '@openreplay/tracker/cjs'; import trackerAxios from '@openreplay/tracker-axios/cjs'; const tracker = new OpenReplay({ projectKey: '${projectKey}' @@ -47,15 +53,16 @@ function MyApp() { }, []) //... }`} - - } - /> + + } + /> - -
- ) + +
+
+ ); }; -AxiosDoc.displayName = "AxiosDoc"; +AxiosDoc.displayName = 'AxiosDoc'; export default AxiosDoc; diff --git a/frontend/app/components/Client/Integrations/BugsnagForm/BugsnagForm.js b/frontend/app/components/Client/Integrations/BugsnagForm/BugsnagForm.js index b1aba5a30..15d8ddef1 100644 --- a/frontend/app/components/Client/Integrations/BugsnagForm/BugsnagForm.js +++ b/frontend/app/components/Client/Integrations/BugsnagForm/BugsnagForm.js @@ -1,32 +1,35 @@ import React from 'react'; import { tokenRE } from 'Types/integrations/bugsnagConfig'; -import IntegrationForm from '../IntegrationForm'; +import IntegrationForm from '../IntegrationForm'; import ProjectListDropdown from './ProjectListDropdown'; import DocLink from 'Shared/DocLink/DocLink'; const BugsnagForm = (props) => ( - <> -
-
How to integrate Bugsnag with OpenReplay and see backend errors alongside session recordings.
- +
+

Bugsnag

+
+
How to integrate Bugsnag with OpenReplay and see backend errors alongside session recordings.
+ +
+ tokenRE.test(config.authorizationToken), + component: ProjectListDropdown, + }, + ]} + />
- tokenRE.test(config.authorizationToken), - component: ProjectListDropdown, - } - ]} - /> - ); -BugsnagForm.displayName = "BugsnagForm"; +BugsnagForm.displayName = 'BugsnagForm'; -export default BugsnagForm; \ No newline at end of file +export default BugsnagForm; diff --git a/frontend/app/components/Client/Integrations/CloudwatchForm/CloudwatchForm.js b/frontend/app/components/Client/Integrations/CloudwatchForm/CloudwatchForm.js index 482167c72..bd9604b01 100644 --- a/frontend/app/components/Client/Integrations/CloudwatchForm/CloudwatchForm.js +++ b/frontend/app/components/Client/Integrations/CloudwatchForm/CloudwatchForm.js @@ -1,43 +1,48 @@ import React from 'react'; import { ACCESS_KEY_ID_LENGTH, SECRET_ACCESS_KEY_LENGTH } from 'Types/integrations/cloudwatchConfig'; -import IntegrationForm from '../IntegrationForm'; +import IntegrationForm from '../IntegrationForm'; import LogGroupDropdown from './LogGroupDropdown'; import RegionDropdown from './RegionDropdown'; import DocLink from 'Shared/DocLink/DocLink'; const CloudwatchForm = (props) => ( - <> -
-
How to integrate CloudWatch with OpenReplay and see backend errors alongside session replays.
- +
+

Cloud Watch

+
+
How to integrate CloudWatch with OpenReplay and see backend errors alongside session replays.
+ +
+ + config.awsSecretAccessKey.length === SECRET_ACCESS_KEY_LENGTH && + config.region !== '' && + config.awsAccessKeyId.length === ACCESS_KEY_ID_LENGTH, + }, + ]} + />
- - config.awsSecretAccessKey.length === SECRET_ACCESS_KEY_LENGTH && - config.region !== '' && - config.awsAccessKeyId.length === ACCESS_KEY_ID_LENGTH - } - ]} - /> - ); -CloudwatchForm.displayName = "CloudwatchForm"; +CloudwatchForm.displayName = 'CloudwatchForm'; -export default CloudwatchForm; \ No newline at end of file +export default CloudwatchForm; diff --git a/frontend/app/components/Client/Integrations/DatadogForm.js b/frontend/app/components/Client/Integrations/DatadogForm.js index 76ca0734d..46360259c 100644 --- a/frontend/app/components/Client/Integrations/DatadogForm.js +++ b/frontend/app/components/Client/Integrations/DatadogForm.js @@ -1,29 +1,32 @@ import React from 'react'; -import IntegrationForm from './IntegrationForm'; +import IntegrationForm from './IntegrationForm'; import DocLink from 'Shared/DocLink/DocLink'; const DatadogForm = (props) => ( - <> -
-
How to integrate Datadog with OpenReplay and see backend errors alongside session recordings.
- +
+

Datadog

+
+
How to integrate Datadog with OpenReplay and see backend errors alongside session recordings.
+ +
+
- - ); -DatadogForm.displayName = "DatadogForm"; +DatadogForm.displayName = 'DatadogForm'; export default DatadogForm; diff --git a/frontend/app/components/Client/Integrations/ElasticsearchForm.js b/frontend/app/components/Client/Integrations/ElasticsearchForm.js index 271ccefe1..ad33b6302 100644 --- a/frontend/app/components/Client/Integrations/ElasticsearchForm.js +++ b/frontend/app/components/Client/Integrations/ElasticsearchForm.js @@ -1,75 +1,88 @@ import React from 'react'; import { connect } from 'react-redux'; -import IntegrationForm from './IntegrationForm'; +import IntegrationForm from './IntegrationForm'; import { withRequest } from 'HOCs'; import { edit } from 'Duck/integrations/actions'; import DocLink from 'Shared/DocLink/DocLink'; -@connect(state => ({ - config: state.getIn([ 'elasticsearch', 'instance' ]) -}), { edit }) +@connect( + (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 ( - <> -
-
How to integrate Elasticsearch with OpenReplay and see backend errors alongside session recordings.
- -
- - - ) - } -}; + render() { + const props = this.props; + return ( +
+

Elasticsearch

+
+
How to integrate Elasticsearch with OpenReplay and see backend errors alongside session recordings.
+ +
+ +
+ ); + } +} diff --git a/frontend/app/components/Client/Integrations/FetchDoc/FetchDoc.js b/frontend/app/components/Client/Integrations/FetchDoc/FetchDoc.js index 8d9bbd5b9..b4b8b537d 100644 --- a/frontend/app/components/Client/Integrations/FetchDoc/FetchDoc.js +++ b/frontend/app/components/Client/Integrations/FetchDoc/FetchDoc.js @@ -1,29 +1,32 @@ import React from 'react'; -import Highlight from 'react-highlight' -import ToggleContent from 'Shared/ToggleContent' +import Highlight from 'react-highlight'; +import ToggleContent from 'Shared/ToggleContent'; import DocLink from 'Shared/DocLink/DocLink'; const FetchDoc = (props) => { - const { projectKey } = props; - return ( -
-
This plugin allows you to capture fetch payloads and inspect them later on while replaying session recordings. This is very useful for understanding and fixing issues.
- -
Installation
- - {`npm i @openreplay/tracker-fetch --save`} - - -
Usage
-

Use the provided fetch method from the plugin instead of the one built-in.

-
+ const { projectKey } = props; + return ( +
+

Fetch

+
+
+ This plugin allows you to capture fetch payloads and inspect them later on while replaying session recordings. This is very useful + for understanding and fixing issues. +
-
Usage
- - {`import tracker from '@openreplay/tracker'; +
Installation
+ {`npm i @openreplay/tracker-fetch --save`} + +
Usage
+

Use the provided fetch method from the plugin instead of the one built-in.

+
+ +
Usage
+ + {`import tracker from '@openreplay/tracker'; import trackerFetch from '@openreplay/tracker-fetch'; //... const tracker = new OpenReplay({ @@ -34,11 +37,11 @@ tracker.start(); export const fetch = tracker.use(trackerFetch()); // check list of available options below //... fetch('https://api.openreplay.com/').then(response => console.log(response.json()));`} - - } - second={ - - {`import OpenReplay from '@openreplay/tracker/cjs'; + + } + second={ + + {`import OpenReplay from '@openreplay/tracker/cjs'; import trackerFetch from '@openreplay/tracker-fetch/cjs'; //... const tracker = new OpenReplay({ @@ -54,15 +57,16 @@ export const fetch = tracker.use(trackerFetch()); // check list of avai //... fetch('https://api.openreplay.com/').then(response => console.log(response.json())); }`} - - } - /> + + } + /> - -
- ) + +
+
+ ); }; -FetchDoc.displayName = "FetchDoc"; +FetchDoc.displayName = 'FetchDoc'; export default FetchDoc; diff --git a/frontend/app/components/Client/Integrations/GithubForm.js b/frontend/app/components/Client/Integrations/GithubForm.js index 586ab3093..7d140732b 100644 --- a/frontend/app/components/Client/Integrations/GithubForm.js +++ b/frontend/app/components/Client/Integrations/GithubForm.js @@ -1,30 +1,31 @@ import React from 'react'; -import IntegrationForm from './IntegrationForm'; +import IntegrationForm from './IntegrationForm'; import DocLink from 'Shared/DocLink/DocLink'; const GithubForm = (props) => ( - <> -
-
Integrate GitHub with OpenReplay and create issues directly from the recording page.
-
- -
+
+

Github

+
+
Integrate GitHub with OpenReplay and create issues directly from the recording page.
+
+ +
+
+
- - ); -GithubForm.displayName = "GithubForm"; +GithubForm.displayName = 'GithubForm'; -export default GithubForm; \ No newline at end of file +export default GithubForm; diff --git a/frontend/app/components/Client/Integrations/GraphQLDoc/GraphQLDoc.js b/frontend/app/components/Client/Integrations/GraphQLDoc/GraphQLDoc.js index a9150bc44..36e883f25 100644 --- a/frontend/app/components/Client/Integrations/GraphQLDoc/GraphQLDoc.js +++ b/frontend/app/components/Client/Integrations/GraphQLDoc/GraphQLDoc.js @@ -1,30 +1,36 @@ import React from 'react'; -import Highlight from 'react-highlight' +import Highlight from 'react-highlight'; import DocLink from 'Shared/DocLink/DocLink'; import ToggleContent from 'Shared/ToggleContent'; const GraphQLDoc = (props) => { - const { projectKey } = props; - return ( -
-

This plugin allows you to capture GraphQL requests and inspect them later on while replaying session recordings. This is very useful for understanding and fixing issues.

-

GraphQL plugin is compatible with Apollo and Relay implementations.

- -
Installation
- - {`npm i @openreplay/tracker-graphql --save`} - - -
Usage
-

The plugin call will return the function, which receives four variables operationKind, operationName, variables and result. It returns result without changes.

- -
+ const { projectKey } = props; + return ( +
+

GraphQL

+
+

+ This plugin allows you to capture GraphQL requests and inspect them later on while replaying session recordings. This is very + useful for understanding and fixing issues. +

+

GraphQL plugin is compatible with Apollo and Relay implementations.

- - {`import OpenReplay from '@openreplay/tracker'; +
Installation
+ {`npm i @openreplay/tracker-graphql --save`} + +
Usage
+

+ The plugin call will return the function, which receives four variables operationKind, operationName, variables and result. It + returns result without changes. +

+ +
+ + + {`import OpenReplay from '@openreplay/tracker'; import trackerGraphQL from '@openreplay/tracker-graphql'; //... const tracker = new OpenReplay({ @@ -33,11 +39,11 @@ const tracker = new OpenReplay({ tracker.start(); //... export const recordGraphQL = tracker.use(trackerGraphQL());`} - - } - second={ - - {`import OpenReplay from '@openreplay/tracker/cjs'; + + } + second={ + + {`import OpenReplay from '@openreplay/tracker/cjs'; import trackerGraphQL from '@openreplay/tracker-graphql/cjs'; //... const tracker = new OpenReplay({ @@ -51,15 +57,16 @@ function SomeFunctionalComponent() { } //... export const recordGraphQL = tracker.use(trackerGraphQL());`} - - } - /> + + } + /> - -
- ) + +
+
+ ); }; -GraphQLDoc.displayName = "GraphQLDoc"; +GraphQLDoc.displayName = 'GraphQLDoc'; export default GraphQLDoc; diff --git a/frontend/app/components/Client/Integrations/IntegrationForm.js b/frontend/app/components/Client/Integrations/IntegrationForm.js index aeb28fe31..ad6689f3b 100644 --- a/frontend/app/components/Client/Integrations/IntegrationForm.js +++ b/frontend/app/components/Client/Integrations/IntegrationForm.js @@ -1,144 +1,147 @@ import React from 'react'; import { connect } from 'react-redux'; -import { Input, Form, Button, Checkbox } from 'UI'; +import { Input, Form, Button, Checkbox, Loader } from 'UI'; import SiteDropdown from 'Shared/SiteDropdown'; import { save, init, edit, remove, fetchList } from 'Duck/integrations/actions'; +import { fetchIntegrationList } from 'Duck/integrations/integrations'; -@connect((state, { name, customPath }) => ({ - sites: state.getIn([ 'site', 'list' ]), - initialSiteId: state.getIn([ 'site', 'siteId' ]), - list: state.getIn([ name, 'list' ]), - config: state.getIn([ name, 'instance']), - saving: state.getIn([ customPath || name, 'saveRequest', 'loading']), - removing: state.getIn([ name, 'removeRequest', 'loading']), -}), { - save, - init, - edit, - remove, - fetchList -}) +@connect( + (state, { name, customPath }) => ({ + sites: state.getIn(['site', 'list']), + initialSiteId: state.getIn(['site', 'siteId']), + list: state.getIn([name, 'list']), + config: state.getIn([name, 'instance']), + loading: state.getIn([name, 'fetchRequest', 'loading']), + saving: state.getIn([customPath || name, 'saveRequest', 'loading']), + removing: state.getIn([name, 'removeRequest', 'loading']), + siteId: state.getIn(['integrations', 'siteId']), + }), + { + save, + init, + edit, + remove, + fetchList, + fetchIntegrationList, + } +) export default class IntegrationForm extends React.PureComponent { - constructor(props) { - super(props); - const currentSiteId = this.props.initialSiteId; - this.state = { currentSiteId }; - this.init(currentSiteId); - } - - write = ({ target: { value, name: key, type, checked } }) => { - if (type === 'checkbox') - this.props.edit(this.props.name, { [ key ]: checked }) - else - this.props.edit(this.props.name, { [ key ]: value }) - }; + constructor(props) { + super(props); + // const currentSiteId = this.props.initialSiteId; + // this.state = { currentSiteId }; + // this.init(currentSiteId); + } - onChangeSelect = ({ value }) => { - const { sites, list, name } = this.props; - const site = sites.find(s => s.id === value.value); - this.setState({ currentSiteId: site.id }) - this.init(value.value); - } + write = ({ target: { value, name: key, type, checked } }) => { + if (type === 'checkbox') this.props.edit(this.props.name, { [key]: checked }); + else this.props.edit(this.props.name, { [key]: value }); + }; - init = (siteId) => { - const { list, name } = this.props; - const config = (parseInt(siteId) > 0) ? list.find(s => s.projectId === siteId) : undefined; - this.props.init(name, config ? config : list.first()); - } + // onChangeSelect = ({ value }) => { + // const { sites, list, name } = this.props; + // const site = sites.find((s) => s.id === value.value); + // this.setState({ currentSiteId: site.id }); + // this.init(value.value); + // }; - save = () => { - const { config, name, customPath } = this.props; - const isExists = config.exists(); - const { currentSiteId } = this.state; - const { ignoreProject } = this.props; - this.props.save(customPath || name, (!ignoreProject ? currentSiteId : null), config) - .then(() => { - this.props.fetchList(name) - this.props.onClose(); - if (isExists) return; - }); - } + // init = (siteId) => { + // const { list, name } = this.props; + // const config = parseInt(siteId) > 0 ? list.find((s) => s.projectId === siteId) : undefined; + // this.props.init(name, config ? config : list.first()); + // }; - remove = () => { - const { name, config, ignoreProject } = this.props; - this.props.remove(name, !ignoreProject ? config.projectId : null).then(function() { - this.props.onClose(); - this.props.fetchList(name) - }.bind(this)); - } + save = () => { + const { config, name, customPath, ignoreProject } = this.props; + const isExists = config.exists(); + // const { currentSiteId } = this.state; + this.props.save(customPath || name, !ignoreProject ? this.props.siteId : null, config).then(() => { + // this.props.fetchList(name); + this.props.onClose(); + if (isExists) return; + }); + }; - render() { - const { config, saving, removing, formFields, name, loading, ignoreProject } = this.props; - const { currentSiteId } = this.state; + remove = () => { + const { name, config, ignoreProject } = this.props; + this.props.remove(name, !ignoreProject ? config.projectId : null).then( + function () { + this.props.onClose(); + this.props.fetchList(name); + }.bind(this) + ); + }; - return ( -
-
- {!ignoreProject && - - - - - } + render() { + const { config, saving, removing, formFields, name, loading, ignoreProject } = this.props; + // const { currentSiteId } = this.state; - { formFields.map(({ - key, - label, - placeholder=label, - component: Component = 'input', - type = "text", - checkIfDisplayed, - autoFocus=false - }) => (typeof checkIfDisplayed !== 'function' || checkIfDisplayed(config)) && - ((type === 'checkbox') ? - - - - : - - - - - ) - )} - - + return ( + +
+ + {/* {!ignoreProject && ( + + + + + )} */} - {config.exists() && ( - - )} - -
- ); - } + {formFields.map( + ({ + key, + label, + placeholder = label, + component: Component = 'input', + type = 'text', + checkIfDisplayed, + autoFocus = false, + }) => + (typeof checkIfDisplayed !== 'function' || checkIfDisplayed(config)) && + (type === 'checkbox' ? ( + + + + ) : ( + + + + + )) + )} + + + + {config.exists() && ( + + )} + +
+ + ); + } } diff --git a/frontend/app/components/Client/Integrations/IntegrationItem.js b/frontend/app/components/Client/Integrations/IntegrationItem.js deleted file mode 100644 index b0bfa258a..000000000 --- a/frontend/app/components/Client/Integrations/IntegrationItem.js +++ /dev/null @@ -1,27 +0,0 @@ -import React from 'react'; -import cn from 'classnames'; -import { Icon } from 'UI'; -import stl from './integrationItem.module.css'; - -const onDocLinkClick = (e, link) => { - e.stopPropagation(); - window.open(link, '_blank'); -} - -const IntegrationItem = ({ - deleteHandler = null, icon, url = null, title = '', description = '', onClick = null, dockLink = '', integrated = false -}) => { - return ( -
onClick(e, url) }> - {integrated && ( -
- -
- )} - integration -

{ title }

-
- ) -}; - -export default IntegrationItem; diff --git a/frontend/app/components/Client/Integrations/IntegrationItem.tsx b/frontend/app/components/Client/Integrations/IntegrationItem.tsx new file mode 100644 index 000000000..f1b69c029 --- /dev/null +++ b/frontend/app/components/Client/Integrations/IntegrationItem.tsx @@ -0,0 +1,39 @@ +import React from 'react'; +import cn from 'classnames'; +import { Icon, Popup } from 'UI'; +import stl from './integrationItem.module.css'; +import { connect } from 'react-redux'; + +interface Props { + 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 +
+

{integration.title}

+ {/*

{integration.subtitle && integration.subtitle}

*/} +
+
+ ); +}; + +export default connect((state: any, props: Props) => { + const list = state.getIn([props.integration.slug, 'list']) || []; + return { + // integrated: props.integration.slug === 'issues' ? !!(list.first() && list.first().token) : list.size > 0, + }; +})(IntegrationItem); diff --git a/frontend/app/components/Client/Integrations/Integrations.js b/frontend/app/components/Client/Integrations/Integrations.js_ similarity index 100% rename from frontend/app/components/Client/Integrations/Integrations.js rename to frontend/app/components/Client/Integrations/Integrations.js_ diff --git a/frontend/app/components/Client/Integrations/Integrations.tsx b/frontend/app/components/Client/Integrations/Integrations.tsx new file mode 100644 index 000000000..92c050ea0 --- /dev/null +++ b/frontend/app/components/Client/Integrations/Integrations.tsx @@ -0,0 +1,173 @@ +import { useModal } from 'App/components/Modal'; +import React, { useEffect } from 'react'; +import BugsnagForm from './BugsnagForm'; +import CloudwatchForm from './CloudwatchForm'; +import DatadogForm from './DatadogForm'; +import ElasticsearchForm from './ElasticsearchForm'; +import GithubForm from './GithubForm'; +import IntegrationItem from './IntegrationItem'; +import JiraForm from './JiraForm'; +import NewrelicForm from './NewrelicForm'; +import RollbarForm from './RollbarForm'; +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 FetchDoc from './FetchDoc'; +import ProfilerDoc from './ProfilerDoc'; +import AxiosDoc from './AxiosDoc'; +import AssistDoc from './AssistDoc'; +import { PageTitle, Loader } from 'UI'; +import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG'; + +interface Props { + fetch: (name: string, siteId: string) => void; + init: () => void; + fetchIntegrationList: (siteId: any) => void; + integratedList: any; + initialSiteId: string; + setSiteId: (siteId: string) => void; + siteId: string; + hideHeader?: boolean; + loading?: boolean; +} +function Integrations(props: Props) { + const { initialSiteId, hideHeader = false, loading = false } = props; + const { showModal } = useModal(); + const [integratedList, setIntegratedList] = React.useState([]); + + useEffect(() => { + const list = props.integratedList.filter((item: any) => item.integrated).map((item: any) => item.name); + setIntegratedList(list); + }, [props.integratedList]); + + useEffect(() => { + if (!props.siteId) { + props.setSiteId(initialSiteId); + props.fetchIntegrationList(initialSiteId); + } else { + props.fetchIntegrationList(props.siteId); + } + }, []); + + const onClick = (integration: any) => { + if (integration.slug) { + props.fetch(integration.slug, props.siteId); + } + showModal(integration.component, { right: true }); + }; + + const onChangeSelect = ({ value }: any) => { + props.setSiteId(value.value); + props.fetchIntegrationList(value.value); + }; + + return ( +
+ {!hideHeader && Integrations
} />} + {integrations.map((cat: any) => ( +
+
+

{cat.title}

+ {cat.isProject && ( +
+
+ +
+ {loading && cat.isProject && } +
+ )} +
+
{cat.description}
+ +
+ {/* */} + {cat.integrations.map((integration: any) => ( + onClick(integration)} + hide={ + (integration.slug === 'github' && integratedList.includes('jira')) || + (integration.slug === 'jira' && integratedList.includes('github')) + } + /> + ))} + {/* */} +
+
+ ))} +
+ ); +} + +export default connect( + (state: any) => ({ + initialSiteId: state.getIn(['site', 'siteId']), + integratedList: state.getIn(['integrations', 'list']) || [], + loading: state.getIn(['integrations', 'fetchRequest', 'loading']), + siteId: state.getIn(['integrations', 'siteId']), + }), + { fetch, init, fetchIntegrationList, setSiteId } +)(Integrations); + +const integrations = [ + { + title: 'Issue Reporting and Collaborations', + description: 'Seamlessly report issues or share issues with your team right from OpenReplay.', + isProject: false, + integrations: [ + { title: 'Jira', slug: 'jira', category: 'Errors', icon: 'integrations/jira', component: }, + { title: 'Github', slug: 'github', category: 'Errors', icon: 'integrations/github', component: }, + { title: 'Slack', category: 'Errors', icon: 'integrations/slack', component: }, + ], + }, + { + title: 'Backend Logging', + isProject: true, + description: 'Sync your backend errors with sessions replays and see what happened front-to-back.', + integrations: [ + { title: 'Sentry', slug: 'sentry', icon: 'integrations/sentry', component: }, + { title: 'Datadog', slug: 'datadog', icon: 'integrations/datadog', component: }, + { title: 'Rollbar', slug: 'rollbar', icon: 'integrations/rollbar', component: }, + { title: 'Elasticsearch', slug: 'elasticsearch', icon: 'integrations/elasticsearch', component: }, + { title: 'Datadog', slug: 'datadog', icon: 'integrations/datadog', component: }, + { title: 'Sumo Logic', slug: 'sumologic', icon: 'integrations/sumologic', component: }, + { + title: 'Stackdriver', + slug: 'stackdriver', + icon: 'integrations/google-cloud', + component: , + }, + { title: 'CloudWatch', slug: 'cloudwatch', icon: 'integrations/aws', component: }, + { title: 'Newrelic', slug: 'newrelic', icon: 'integrations/newrelic', component: }, + ], + }, + { + title: 'Plugins', + isProject: false, + description: + "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', slug: '', icon: 'integrations/redux', component: }, + { title: 'VueX', slug: '', icon: 'integrations/vuejs', component: }, + { title: 'GraphQL', slug: '', icon: 'integrations/graphql', component: }, + { title: 'NgRx', slug: '', icon: 'integrations/ngrx', component: }, + { title: 'MobX', slug: '', icon: 'integrations/mobx', component: }, + { title: 'Fetch', slug: '', icon: 'integrations/openreplay', component: }, + { title: 'Profiler', slug: '', icon: 'integrations/openreplay', component: }, + { title: 'Axios', slug: '', icon: 'integrations/openreplay', component: }, + { title: 'Assist', slug: '', icon: 'integrations/openreplay', component: }, + ], + }, +]; diff --git a/frontend/app/components/Client/Integrations/JiraForm/JiraForm.js b/frontend/app/components/Client/Integrations/JiraForm/JiraForm.js index dc4585872..b17bbc460 100644 --- a/frontend/app/components/Client/Integrations/JiraForm/JiraForm.js +++ b/frontend/app/components/Client/Integrations/JiraForm/JiraForm.js @@ -1,37 +1,41 @@ import React from 'react'; -import IntegrationForm from '../IntegrationForm'; +import IntegrationForm from '../IntegrationForm'; import DocLink from 'Shared/DocLink/DocLink'; const JiraForm = (props) => ( - <> -
-
How to integrate Jira Cloud with OpenReplay.
-
- -
+
+

Jira

+
+
How to integrate Jira Cloud with OpenReplay.
+
+ +
+
+
- - ); -JiraForm.displayName = "JiraForm"; +JiraForm.displayName = 'JiraForm'; -export default JiraForm; \ No newline at end of file +export default JiraForm; diff --git a/frontend/app/components/Client/Integrations/MobxDoc/MobxDoc.js b/frontend/app/components/Client/Integrations/MobxDoc/MobxDoc.js index 320e1a742..bbe36d45b 100644 --- a/frontend/app/components/Client/Integrations/MobxDoc/MobxDoc.js +++ b/frontend/app/components/Client/Integrations/MobxDoc/MobxDoc.js @@ -1,29 +1,35 @@ import React from 'react'; -import Highlight from 'react-highlight' -import ToggleContent from 'Shared/ToggleContent' +import Highlight from 'react-highlight'; +import ToggleContent from 'Shared/ToggleContent'; import DocLink from 'Shared/DocLink/DocLink'; const MobxDoc = (props) => { - const { projectKey } = props; - return ( -
-
This plugin allows you to capture MobX events and inspect them later on while replaying session recordings. This is very useful for understanding and fixing issues.
- -
Installation
- - {`npm i @openreplay/tracker-mobx --save`} - - -
Usage
-

Initialize the @openreplay/tracker package as usual and load the plugin into it. Then put the generated middleware into your Redux chain.

-
+ const { projectKey } = props; + return ( +
+

MobX

+
+
+ This plugin allows you to capture MobX events and inspect them later on while replaying session recordings. This is very useful + for understanding and fixing issues. +
-
Usage
- - {`import OpenReplay from '@openreplay/tracker'; +
Installation
+ {`npm i @openreplay/tracker-mobx --save`} + +
Usage
+

+ Initialize the @openreplay/tracker package as usual and load the plugin into it. Then put the generated middleware into your Redux + chain. +

+
+ +
Usage
+ + {`import OpenReplay from '@openreplay/tracker'; import trackerMobX from '@openreplay/tracker-mobx'; //... const tracker = new OpenReplay({ @@ -31,11 +37,11 @@ const tracker = new OpenReplay({ }); tracker.use(trackerMobX()); // check list of available options below tracker.start();`} - - } - second={ - - {`import OpenReplay from '@openreplay/tracker/cjs'; + + } + second={ + + {`import OpenReplay from '@openreplay/tracker/cjs'; import trackerMobX from '@openreplay/tracker-mobx/cjs'; //... const tracker = new OpenReplay({ @@ -48,15 +54,16 @@ function SomeFunctionalComponent() { tracker.start(); }, []) }`} - - } - /> + + } + /> - -
- ) + +
+
+ ); }; -MobxDoc.displayName = "MobxDoc"; +MobxDoc.displayName = 'MobxDoc'; export default MobxDoc; diff --git a/frontend/app/components/Client/Integrations/NewrelicForm/NewrelicForm.js b/frontend/app/components/Client/Integrations/NewrelicForm/NewrelicForm.js index d7ce557e8..670656583 100644 --- a/frontend/app/components/Client/Integrations/NewrelicForm/NewrelicForm.js +++ b/frontend/app/components/Client/Integrations/NewrelicForm/NewrelicForm.js @@ -1,32 +1,36 @@ import React from 'react'; -import IntegrationForm from '../IntegrationForm'; +import IntegrationForm from '../IntegrationForm'; import DocLink from 'Shared/DocLink/DocLink'; const NewrelicForm = (props) => ( - <> -
-
How to integrate NewRelic with OpenReplay and see backend errors alongside session recordings.
- +
+

New Relic

+
+
How to integrate NewRelic with OpenReplay and see backend errors alongside session recordings.
+ +
+
- - ); -NewrelicForm.displayName = "NewrelicForm"; +NewrelicForm.displayName = 'NewrelicForm'; -export default NewrelicForm; \ No newline at end of file +export default NewrelicForm; diff --git a/frontend/app/components/Client/Integrations/NgRxDoc/NgRxDoc.js b/frontend/app/components/Client/Integrations/NgRxDoc/NgRxDoc.js index 385b0d4e4..956e4f57e 100644 --- a/frontend/app/components/Client/Integrations/NgRxDoc/NgRxDoc.js +++ b/frontend/app/components/Client/Integrations/NgRxDoc/NgRxDoc.js @@ -1,29 +1,32 @@ import React from 'react'; -import Highlight from 'react-highlight' -import ToggleContent from 'Shared/ToggleContent' +import Highlight from 'react-highlight'; +import ToggleContent from 'Shared/ToggleContent'; import DocLink from 'Shared/DocLink/DocLink'; const NgRxDoc = (props) => { - const { projectKey } = props; - return ( -
-
This plugin allows you to capture NgRx actions/state and inspect them later on while replaying session recordings. This is very useful for understanding and fixing issues.
- -
Installation
- - {`npm i @openreplay/tracker-ngrx --save`} - - -
Usage
-

Add the generated meta-reducer into your imports. See NgRx documentation for more details.

-
+ const { projectKey } = props; + return ( +
+

NgRx

+
+
+ This plugin allows you to capture NgRx actions/state and inspect them later on while replaying session recordings. This is very + useful for understanding and fixing issues. +
-
Usage
- - {`import { StoreModule } from '@ngrx/store'; +
Installation
+ {`npm i @openreplay/tracker-ngrx --save`} + +
Usage
+

Add the generated meta-reducer into your imports. See NgRx documentation for more details.

+
+ +
Usage
+ + {`import { StoreModule } from '@ngrx/store'; import { reducers } from './reducers'; import OpenReplay from '@openreplay/tracker'; import trackerNgRx from '@openreplay/tracker-ngrx'; @@ -39,11 +42,11 @@ const metaReducers = [tracker.use(trackerNgRx())]; // check list of ava imports: [StoreModule.forRoot(reducers, { metaReducers })] }) export class AppModule {}`} - - } - second={ - - {`import { StoreModule } from '@ngrx/store'; + + } + second={ + + {`import { StoreModule } from '@ngrx/store'; import { reducers } from './reducers'; import OpenReplay from '@openreplay/tracker/cjs'; import trackerNgRx from '@openreplay/tracker-ngrx/cjs'; @@ -64,15 +67,16 @@ const metaReducers = [tracker.use(trackerNgRx())]; // check list of ava }) export class AppModule {} }`} - - } - /> + + } + /> - -
- ) + +
+
+ ); }; -NgRxDoc.displayName = "NgRxDoc"; +NgRxDoc.displayName = 'NgRxDoc'; export default NgRxDoc; diff --git a/frontend/app/components/Client/Integrations/ProfilerDoc/ProfilerDoc.js b/frontend/app/components/Client/Integrations/ProfilerDoc/ProfilerDoc.js index 9cada092b..f5ffab724 100644 --- a/frontend/app/components/Client/Integrations/ProfilerDoc/ProfilerDoc.js +++ b/frontend/app/components/Client/Integrations/ProfilerDoc/ProfilerDoc.js @@ -1,29 +1,32 @@ import React from 'react'; -import Highlight from 'react-highlight' -import ToggleContent from 'Shared/ToggleContent' +import Highlight from 'react-highlight'; +import ToggleContent from 'Shared/ToggleContent'; import DocLink from 'Shared/DocLink/DocLink'; const ProfilerDoc = (props) => { - const { projectKey } = props; - return ( -
-
The profiler plugin allows you to measure your JS functions' performance and capture both arguments and result for each function call.
- -
Installation
- - {`npm i @openreplay/tracker-profiler --save`} - - -
Usage
-

Initialize the tracker and load the plugin into it. Then decorate any function inside your code with the generated function.

-
+ const { projectKey } = props; + return ( +
+

Profiler

+
+
+ The profiler plugin allows you to measure your JS functions' performance and capture both arguments and result for each function + call. +
-
Usage
- - {`import OpenReplay from '@openreplay/tracker'; +
Installation
+ {`npm i @openreplay/tracker-profiler --save`} + +
Usage
+

Initialize the tracker and load the plugin into it. Then decorate any function inside your code with the generated function.

+
+ +
Usage
+ + {`import OpenReplay from '@openreplay/tracker'; import trackerProfiler from '@openreplay/tracker-profiler'; //... const tracker = new OpenReplay({ @@ -36,11 +39,11 @@ export const profiler = tracker.use(trackerProfiler()); const fn = profiler('call_name')(() => { //... }, thisArg); // thisArg is optional`} - - } - second={ - - {`import OpenReplay from '@openreplay/tracker/cjs'; + + } + second={ + + {`import OpenReplay from '@openreplay/tracker/cjs'; import trackerProfiler from '@openreplay/tracker-profiler/cjs'; //... const tracker = new OpenReplay({ @@ -58,15 +61,16 @@ const fn = profiler('call_name')(() => { //... }, thisArg); // thisArg is optional }`} - - } - /> + + } + /> - -
- ) + +
+
+ ); }; -ProfilerDoc.displayName = "ProfilerDoc"; +ProfilerDoc.displayName = 'ProfilerDoc'; export default ProfilerDoc; diff --git a/frontend/app/components/Client/Integrations/ReduxDoc/ReduxDoc.js b/frontend/app/components/Client/Integrations/ReduxDoc/ReduxDoc.js index 8e3b12432..e16eecbba 100644 --- a/frontend/app/components/Client/Integrations/ReduxDoc/ReduxDoc.js +++ b/frontend/app/components/Client/Integrations/ReduxDoc/ReduxDoc.js @@ -1,28 +1,31 @@ import React from 'react'; -import Highlight from 'react-highlight' +import Highlight from 'react-highlight'; import ToggleContent from '../../../shared/ToggleContent'; import DocLink from 'Shared/DocLink/DocLink'; const ReduxDoc = (props) => { - const { projectKey } = props; - return ( -
-
This plugin allows you to capture Redux actions/state and inspect them later on while replaying session recordings. This is very useful for understanding and fixing issues.
- -
Installation
- - {`npm i @openreplay/tracker-redux --save`} - - + const { projectKey } = props; + return ( +
+

Redux

-
Usage
-

Initialize the tracker then put the generated middleware into your Redux chain.

-
- - {`import { applyMiddleware, createStore } from 'redux'; +
+
+ This plugin allows you to capture Redux actions/state and inspect them later on while replaying session recordings. This is very + useful for understanding and fixing issues. +
+ +
Installation
+ {`npm i @openreplay/tracker-redux --save`} + +
Usage
+

Initialize the tracker then put the generated middleware into your Redux chain.

+
+ + {`import { applyMiddleware, createStore } from 'redux'; import OpenReplay from '@openreplay/tracker'; import trackerRedux from '@openreplay/tracker-redux'; //... @@ -35,11 +38,11 @@ const store = createStore( reducer, applyMiddleware(tracker.use(trackerRedux())) // check list of available options below );`} - - } - second={ - - {`import { applyMiddleware, createStore } from 'redux'; + + } + second={ + + {`import { applyMiddleware, createStore } from 'redux'; import OpenReplay from '@openreplay/tracker/cjs'; import trackerRedux from '@openreplay/tracker-redux/cjs'; //... @@ -57,15 +60,16 @@ const store = createStore( applyMiddleware(tracker.use(trackerRedux())) // check list of available options below ); }`} - - } - /> + + } + /> - -
- ) + +
+
+ ); }; -ReduxDoc.displayName = "ReduxDoc"; +ReduxDoc.displayName = 'ReduxDoc'; export default ReduxDoc; diff --git a/frontend/app/components/Client/Integrations/RollbarForm.js b/frontend/app/components/Client/Integrations/RollbarForm.js index 3b8830423..441819323 100644 --- a/frontend/app/components/Client/Integrations/RollbarForm.js +++ b/frontend/app/components/Client/Integrations/RollbarForm.js @@ -1,25 +1,27 @@ import React from 'react'; -import IntegrationForm from './IntegrationForm'; +import IntegrationForm from './IntegrationForm'; import DocLink from 'Shared/DocLink/DocLink'; const RollbarForm = (props) => ( - <> -
-
How to integrate Rollbar with OpenReplay and see backend errors alongside session replays.
- +
+

Rollbar

+
+
How to integrate Rollbar with OpenReplay and see backend errors alongside session replays.
+ +
+
- - ); -RollbarForm.displayName = "RollbarForm"; +RollbarForm.displayName = 'RollbarForm'; -export default RollbarForm; \ No newline at end of file +export default RollbarForm; diff --git a/frontend/app/components/Client/Integrations/SentryForm.js b/frontend/app/components/Client/Integrations/SentryForm.js index fd7bf1f11..bd119ba31 100644 --- a/frontend/app/components/Client/Integrations/SentryForm.js +++ b/frontend/app/components/Client/Integrations/SentryForm.js @@ -1,31 +1,35 @@ import React from 'react'; -import IntegrationForm from './IntegrationForm'; +import IntegrationForm from './IntegrationForm'; import DocLink from 'Shared/DocLink/DocLink'; const SentryForm = (props) => ( - <> -
-
How to integrate Sentry with OpenReplay and see backend errors alongside session recordings.
- +
+

Sentry

+
+
How to integrate Sentry with OpenReplay and see backend errors alongside session recordings.
+ +
+
- - ); -SentryForm.displayName = "SentryForm"; +SentryForm.displayName = 'SentryForm'; -export default SentryForm; \ No newline at end of file +export default SentryForm; diff --git a/frontend/app/components/Client/Integrations/SlackAddForm/SlackAddForm.js b/frontend/app/components/Client/Integrations/SlackAddForm/SlackAddForm.js index 8e1bb121e..f018da3e5 100644 --- a/frontend/app/components/Client/Integrations/SlackAddForm/SlackAddForm.js +++ b/frontend/app/components/Client/Integrations/SlackAddForm/SlackAddForm.js @@ -1,101 +1,91 @@ -import React from 'react' -import { connect } from 'react-redux' -import { edit, save, init, update } from 'Duck/integrations/slack' -import { Form, Input, Button, Message } from 'UI' +import React from 'react'; +import { connect } from 'react-redux'; +import { edit, save, init, update } from 'Duck/integrations/slack'; +import { Form, Input, Button, Message } from 'UI'; import { confirm } from 'UI'; -import { remove } from 'Duck/integrations/slack' +import { remove } from 'Duck/integrations/slack'; class SlackAddForm extends React.PureComponent { - componentWillUnmount() { - this.props.init({}); - } - - save = () => { - const instance = this.props.instance; - if(instance.exists()) { - this.props.update(this.props.instance) - } else { - this.props.save(this.props.instance) - } - } - - remove = async (id) => { - if (await confirm({ - header: 'Confirm', - confirmButton: 'Yes, delete', - confirmation: `Are you sure you want to permanently delete this channel?` - })) { - this.props.remove(id); + componentWillUnmount() { + this.props.init({}); } - } - write = ({ target: { name, value } }) => this.props.edit({ [ name ]: value }); - - render() { - const { instance, saving, errors, onClose } = this.props; - return ( -
-
- - - - - - - - -
-
- - - -
- - -
-
- - { errors && -
- { errors.map(error => { error }) } -
+ save = () => { + const instance = this.props.instance; + if (instance.exists()) { + this.props.update(this.props.instance); + } else { + this.props.save(this.props.instance); } -
- ) - } + }; + + remove = async (id) => { + if ( + await confirm({ + header: 'Confirm', + confirmButton: 'Yes, delete', + confirmation: `Are you sure you want to permanently delete this channel?`, + }) + ) { + this.props.remove(id); + } + }; + + write = ({ target: { name, value } }) => this.props.edit({ [name]: value }); + + render() { + const { instance, saving, errors, onClose } = this.props; + return ( +
+
+ + + + + + + + +
+
+ + + +
+ + +
+
+ + {errors && ( +
+ {errors.map((error) => ( + + {error} + + ))} +
+ )} +
+ ); + } } -export default connect(state => ({ - instance: state.getIn(['slack', 'instance']), - saving: state.getIn(['slack', 'saveRequest', 'loading']), - errors: state.getIn([ 'slack', 'saveRequest', 'errors' ]), -}), { edit, save, init, remove, update })(SlackAddForm) \ No newline at end of file +export default connect( + (state) => ({ + instance: state.getIn(['slack', 'instance']), + saving: state.getIn(['slack', 'saveRequest', 'loading']), + errors: state.getIn(['slack', 'saveRequest', 'errors']), + }), + { edit, save, init, remove, update } +)(SlackAddForm); diff --git a/frontend/app/components/Client/Integrations/SlackChannelList/SlackChannelList.js b/frontend/app/components/Client/Integrations/SlackChannelList/SlackChannelList.js index f78527204..07f1aa123 100644 --- a/frontend/app/components/Client/Integrations/SlackChannelList/SlackChannelList.js +++ b/frontend/app/components/Client/Integrations/SlackChannelList/SlackChannelList.js @@ -1,49 +1,47 @@ -import React from 'react' -import { connect } from 'react-redux' +import React from 'react'; +import { connect } from 'react-redux'; import { NoContent } from 'UI'; -import { remove, edit } from 'Duck/integrations/slack' +import { remove, edit, init } from 'Duck/integrations/slack'; import DocLink from 'Shared/DocLink/DocLink'; function SlackChannelList(props) { - const { list } = props; + const { list } = props; - const onEdit = (instance) => { - props.edit(instance) - props.onEdit() - } + const onEdit = (instance) => { + props.edit(instance); + props.onEdit(); + }; - return ( -
- -
Integrate Slack with OpenReplay and share insights with the rest of the team, directly from the recording page.
- {/* */} - -
- } - size="small" - show={ list.size === 0 } - > - {list.map(c => ( -
onEdit(c)} - > -
-
{c.name}
-
- {c.endpoint} -
-
-
- ))} - -
- ) + return ( +
+ +
+ Integrate Slack with OpenReplay and share insights with the rest of the team, directly from the recording page. +
+ +
+ } + size="small" + show={list.size === 0} + > + {list.map((c) => ( +
onEdit(c)}> +
+
{c.name}
+
{c.endpoint}
+
+
+ ))} + +
+ ); } -export default connect(state => ({ - list: state.getIn(['slack', 'list']) -}), { remove, edit })(SlackChannelList) +export default connect( + (state) => ({ + list: state.getIn(['slack', 'list']), + }), + { remove, edit, init } +)(SlackChannelList); diff --git a/frontend/app/components/Client/Integrations/SlackForm.js b/frontend/app/components/Client/Integrations/SlackForm.js deleted file mode 100644 index 986af20ab..000000000 --- a/frontend/app/components/Client/Integrations/SlackForm.js +++ /dev/null @@ -1,15 +0,0 @@ -import React from 'react'; -import SlackChannelList from './SlackChannelList/SlackChannelList'; - -const SlackForm = (props) => { - const { onEdit } = props; - return ( - <> - - - ) -} - -SlackForm.displayName = "SlackForm"; - -export default SlackForm; \ No newline at end of file diff --git a/frontend/app/components/Client/Integrations/SlackForm.tsx b/frontend/app/components/Client/Integrations/SlackForm.tsx new file mode 100644 index 000000000..207f9b765 --- /dev/null +++ b/frontend/app/components/Client/Integrations/SlackForm.tsx @@ -0,0 +1,48 @@ +import React, { useEffect } from 'react'; +import SlackChannelList from './SlackChannelList/SlackChannelList'; +import { fetchList } from 'Duck/integrations/slack'; +import { connect } from 'react-redux'; +import SlackAddForm from './SlackAddForm'; +import { useModal } from 'App/components/Modal'; + +interface Props { + onEdit: (integration: any) => void; + istance: any; + fetchList: any; +} +const SlackForm = (props: Props) => { + const { istance } = props; + const { hideModal } = useModal(); + const [active, setActive] = React.useState(false); + + const onEdit = () => { + setActive(true); + }; + + useEffect(() => { + props.fetchList(); + }, []); + + return ( +
+
+

Slack

+ +
+ {active && ( +
+ setActive(false)} /> +
+ )} +
+ ); +}; + +SlackForm.displayName = 'SlackForm'; + +export default connect( + (state: any) => ({ + istance: state.getIn(['slack', 'instance']), + }), + { fetchList } +)(SlackForm); diff --git a/frontend/app/components/Client/Integrations/StackdriverForm.js b/frontend/app/components/Client/Integrations/StackdriverForm.js index b8e29fa3c..ce137bd99 100644 --- a/frontend/app/components/Client/Integrations/StackdriverForm.js +++ b/frontend/app/components/Client/Integrations/StackdriverForm.js @@ -1,29 +1,32 @@ import React from 'react'; -import IntegrationForm from './IntegrationForm'; +import IntegrationForm from './IntegrationForm'; import DocLink from 'Shared/DocLink/DocLink'; const StackdriverForm = (props) => ( - <> -
-
How to integrate Stackdriver with OpenReplay and see backend errors alongside session recordings.
- +
+

Stackdriver

+
+
How to integrate Stackdriver with OpenReplay and see backend errors alongside session recordings.
+ +
+
- - ); -StackdriverForm.displayName = "StackdriverForm"; +StackdriverForm.displayName = 'StackdriverForm'; export default StackdriverForm; diff --git a/frontend/app/components/Client/Integrations/SumoLogicForm/SumoLogicForm.js b/frontend/app/components/Client/Integrations/SumoLogicForm/SumoLogicForm.js index 0a807edb6..6aea9fe6e 100644 --- a/frontend/app/components/Client/Integrations/SumoLogicForm/SumoLogicForm.js +++ b/frontend/app/components/Client/Integrations/SumoLogicForm/SumoLogicForm.js @@ -4,30 +4,34 @@ import RegionDropdown from './RegionDropdown'; import DocLink from 'Shared/DocLink/DocLink'; const SumoLogicForm = (props) => ( - <> -
-
How to integrate SumoLogic with OpenReplay and see backend errors alongside session recordings.
- +
+

Sumologic

+
+
How to integrate SumoLogic with OpenReplay and see backend errors alongside session recordings.
+ +
+
- - ); -SumoLogicForm.displayName = "SumoLogicForm"; +SumoLogicForm.displayName = 'SumoLogicForm'; export default SumoLogicForm; diff --git a/frontend/app/components/Client/Integrations/VueDoc/VueDoc.js b/frontend/app/components/Client/Integrations/VueDoc/VueDoc.js index e00d1c0ad..cece7c01e 100644 --- a/frontend/app/components/Client/Integrations/VueDoc/VueDoc.js +++ b/frontend/app/components/Client/Integrations/VueDoc/VueDoc.js @@ -1,29 +1,34 @@ import React from 'react'; -import Highlight from 'react-highlight' +import Highlight from 'react-highlight'; import ToggleContent from '../../../shared/ToggleContent'; import DocLink from 'Shared/DocLink/DocLink'; const VueDoc = (props) => { - const { projectKey } = props; - return ( -
-
This plugin allows you to capture VueX mutations/state and inspect them later on while replaying session recordings. This is very useful for understanding and fixing issues.
- -
Installation
- - {`npm i @openreplay/tracker-vuex --save`} - - -
Usage
-

Initialize the @openreplay/tracker package as usual and load the plugin into it. Then put the generated plugin into your plugins field of your store.

-
+ const { projectKey } = props; + return ( +
+

VueX

+
+
+ This plugin allows you to capture VueX mutations/state and inspect them later on while replaying session recordings. This is very + useful for understanding and fixing issues. +
- - - {`import Vuex from 'vuex' +
Installation
+ {`npm i @openreplay/tracker-vuex --save`} + +
Usage
+

+ Initialize the @openreplay/tracker package as usual and load the plugin into it. Then put the generated plugin into your plugins + field of your store. +

+
+ + + {`import Vuex from 'vuex' import OpenReplay from '@openreplay/tracker'; import trackerVuex from '@openreplay/tracker-vuex'; //... @@ -36,11 +41,11 @@ const store = new Vuex.Store({ //... plugins: [tracker.use(trackerVuex())] // check list of available options below });`} - - } - second={ - - {`import Vuex from 'vuex' + + } + second={ + + {`import Vuex from 'vuex' import OpenReplay from '@openreplay/tracker/cjs'; import trackerVuex from '@openreplay/tracker-vuex/cjs'; //... @@ -58,15 +63,16 @@ const store = new Vuex.Store({ plugins: [tracker.use(trackerVuex())] // check list of available options below }); }`} - - } - /> + + } + /> - -
- ) + +
+
+ ); }; -VueDoc.displayName = "VueDoc"; +VueDoc.displayName = 'VueDoc'; export default VueDoc; diff --git a/frontend/app/components/Client/Integrations/_IntegrationItem .js_old b/frontend/app/components/Client/Integrations/_IntegrationItem .js_old deleted file mode 100644 index 962135633..000000000 --- a/frontend/app/components/Client/Integrations/_IntegrationItem .js_old +++ /dev/null @@ -1,42 +0,0 @@ -import React from 'react'; -import cn from 'classnames'; -import { Icon } from 'UI'; -import styles from './integrationItem.module.css'; - -const onDocLinkClick = (e, link) => { - e.stopPropagation(); - window.open(link, '_blank'); -} - -const IntegrationItem = ({ - deleteHandler = null, icon, url = null, title = '', description = '', onClick = null, dockLink = '', integrated = false -}) => { - return ( -
onClick(e, url) }> - -

{ title }

-

{ description }

-
-
- {deleteHandler && ( -
- - { 'Remove' } -
- )} - { dockLink && ( -
onDocLinkClick(e, dockLink) }> - - { 'Documentation' } -
- )} -
- - { 'Integrated' } -
-
-
- ) -}; - -export default IntegrationItem; diff --git a/frontend/app/components/Client/Integrations/integrationItem.module.css b/frontend/app/components/Client/Integrations/integrationItem.module.css index 94ab26726..fca162909 100644 --- a/frontend/app/components/Client/Integrations/integrationItem.module.css +++ b/frontend/app/components/Client/Integrations/integrationItem.module.css @@ -9,7 +9,7 @@ display: flex; flex-direction: column; align-items: center; - justify-content: center; + justify-content: flex-start; /* min-height: 250px; */ /* min-width: 260px; */ /* max-width: 300px; */ diff --git a/frontend/app/components/Client/Notifications/Notifications.js b/frontend/app/components/Client/Notifications/Notifications.js index 15d6b9b4d..d01b12456 100644 --- a/frontend/app/components/Client/Notifications/Notifications.js +++ b/frontend/app/components/Client/Notifications/Notifications.js @@ -1,46 +1,50 @@ -import React, { useEffect } from 'react' -import cn from 'classnames' -import stl from './notifications.module.css' -import { Checkbox } from 'UI' -import { connect } from 'react-redux' -import { withRequest } from 'HOCs' -import { fetch as fetchConfig, edit as editConfig, save as saveConfig } from 'Duck/config' +import React, { useEffect } from 'react'; +import cn from 'classnames'; +import stl from './notifications.module.css'; +import { Checkbox, Toggler } from 'UI'; +import { connect } from 'react-redux'; +import { withRequest } from 'HOCs'; +import { fetch as fetchConfig, edit as editConfig, save as saveConfig } from 'Duck/config'; import withPageTitle from 'HOCs/withPageTitle'; function Notifications(props) { - const { config } = props; + const { config } = props; - useEffect(() => { - props.fetchConfig(); - }, []) + useEffect(() => { + props.fetchConfig(); + }, []); - const onChange = () => { - const _config = { 'weeklyReport' : !config.weeklyReport }; - props.editConfig(_config); - props.saveConfig(_config) - } + const onChange = () => { + const _config = { weeklyReport: !config.weeklyReport }; + props.editConfig(_config); + props.saveConfig(_config); + }; - return ( -
-
- {

{ 'Notifications' }

} -
-
- - -
-
- ) + return ( +
+
{

{'Notifications'}

}
+
+
Weekly project summary
+
Receive wekly report for each project on email.
+ + {/* */} + {/* */} +
+
+ ); } -export default connect(state => ({ - config: state.getIn(['config', 'options']) -}), { fetchConfig, editConfig, saveConfig })(withPageTitle('Notifications - OpenReplay Preferences')(Notifications)); +export default connect( + (state) => ({ + config: state.getIn(['config', 'options']), + }), + { fetchConfig, editConfig, saveConfig } +)(withPageTitle('Notifications - OpenReplay Preferences')(Notifications)); diff --git a/frontend/app/components/Client/PreferencesMenu/PreferencesMenu.js b/frontend/app/components/Client/PreferencesMenu/PreferencesMenu.js index 820fe14e4..8314e521a 100644 --- a/frontend/app/components/Client/PreferencesMenu/PreferencesMenu.js +++ b/frontend/app/components/Client/PreferencesMenu/PreferencesMenu.js @@ -13,14 +13,14 @@ function PreferencesMenu({ account, activeTab, history, isEnterprise }) { }; return ( -
+
Preferences
-
+
-
+
-
+
{ -
+
} -
+
{isEnterprise && isAdmin && ( -
+
+
- setTab(CLIENT_TABS.MANAGE_USERS)} - /> -
+
+ setTab(CLIENT_TABS.MANAGE_USERS)} + /> +
)} -
+
newPasswordRepeat.length > 0 && newPasswordRepeat !== newPassword; const defaultState = { - oldPassword: '', - newPassword: '', - newPasswordRepeat: '', - success: false, + oldPassword: '', + newPassword: '', + newPasswordRepeat: '', + success: false, + show: false, }; -@connect(state => ({ - passwordErrors: state.getIn(['user', 'passwordErrors']), - loading: state.getIn(['user', 'updatePasswordRequest', 'loading']) -}), { - updatePassword -}) +@connect( + (state) => ({ + passwordErrors: state.getIn(['user', 'passwordErrors']), + loading: state.getIn(['user', 'updatePasswordRequest', 'loading']), + }), + { + updatePassword, + } +) export default class ChangePassword extends React.PureComponent { - state = defaultState + state = defaultState; - write = ({ target: { name, value } }) => { - this.setState({ - [ name ]: value, - }); - } - - handleSubmit = (e) => { - e.preventDefault(); - if (this.isSubmitDisabled()) return; - - const { oldPassword, newPassword } = this.state; - this.setState({ - success: false, - }); - - this.props.updatePassword({ - oldPassword, - newPassword, - }).then(() => { - if (this.props.passwordErrors.size === 0) { + write = ({ target: { name, value } }) => { this.setState({ - ...defaultState, - success: true, + [name]: value, }); - } - }); - } + }; - isSubmitDisabled() { - const { oldPassword, newPassword, newPasswordRepeat } = this.state; - if (newPassword !== newPasswordRepeat || - newPassword.length < MIN_LENGTH || - oldPassword.length < MIN_LENGTH) return true; - return false; - } + handleSubmit = (e) => { + e.preventDefault(); + if (this.isSubmitDisabled()) return; - render() { - const { - oldPassword, newPassword, newPasswordRepeat, success - } = this.state; - const { loading, passwordErrors } = this.props; + const { oldPassword, newPassword } = this.state; + this.setState({ + success: false, + }); - const doesntMatch = checkDoesntMatch(newPassword, newPasswordRepeat); - return ( -
- - - - - - - -
- { PASSWORD_POLICY } -
-
- - - - - { passwordErrors.map(err => ( - - { err } - - ))} - - - -
- ); - } + this.props + .updatePassword({ + oldPassword, + newPassword, + }) + .then(() => { + if (this.props.passwordErrors.size === 0) { + this.setState({ + ...defaultState, + success: true, + }); + } + }); + }; + + isSubmitDisabled() { + const { oldPassword, newPassword, newPasswordRepeat } = this.state; + if (newPassword !== newPasswordRepeat || newPassword.length < MIN_LENGTH || oldPassword.length < MIN_LENGTH) return true; + return false; + } + + render() { + const { oldPassword, newPassword, newPasswordRepeat, success, show } = this.state; + const { loading, passwordErrors } = this.props; + + const doesntMatch = checkDoesntMatch(newPassword, newPasswordRepeat); + return show ? ( +
+ + + + + + + +
{PASSWORD_POLICY}
+
+ + + + + {passwordErrors.map((err) => ( + {err} + ))} + +
+ + + +
+ +
+ ) : ( +
this.setState({ show: true })}> + +
+ ); + } } diff --git a/frontend/app/components/Client/ProfileSettings/ProfileSettings.js b/frontend/app/components/Client/ProfileSettings/ProfileSettings.js index 375e3ba8e..7e4ec5fb2 100644 --- a/frontend/app/components/Client/ProfileSettings/ProfileSettings.js +++ b/frontend/app/components/Client/ProfileSettings/ProfileSettings.js @@ -8,90 +8,105 @@ import TenantKey from './TenantKey'; import OptOut from './OptOut'; import Licenses from './Licenses'; import { connect } from 'react-redux'; +import { PageTitle } from 'UI'; @withPageTitle('Account - OpenReplay Preferences') -@connect(state => ({ - account: state.getIn([ 'user', 'account' ]), - isEnterprise: state.getIn([ 'user', 'account', 'edition' ]) === 'ee', +@connect((state) => ({ + account: state.getIn(['user', 'account']), + isEnterprise: state.getIn(['user', 'account', 'edition']) === 'ee', })) -export default class ProfileSettings extends React.PureComponent { - render() { - const { account, isEnterprise } = this.props; - return ( - -
-
-

{ 'Profile' }

-
{ 'Your email address is your identity on OpenReplay and is used to login.' }
-
-
-
+export default class ProfileSettings extends React.PureComponent { + render() { + const { account, isEnterprise } = this.props; + return ( + + Account
} /> +
+
+

{'Profile'}

+
{'Your email address is your identity on OpenReplay and is used to login.'}
+
+
+ +
+
-
+
- { account.hasPassword && ( - <> -
-
-

{ 'Change Password' }

-
{ 'Updating your password from time to time enhances your account’s security.' }
-
-
-
- + {account.hasPassword && ( + <> +
+
+

{'Change Password'}

+
{'Updating your password from time to time enhances your account’s security.'}
+
+
+ +
+
-
- - )} +
+ + )} -
-
-

{ 'Organization API Key' }

-
{ 'Your API key gives you access to an extra set of services.' }
-
-
-
+
+
+

{'Organization API Key'}

+
{'Your API key gives you access to an extra set of services.'}
+
+
+ +
+
- { isEnterprise && ( - <> -
-
-
-

{ 'Tenant Key' }

-
{ 'For SSO (SAML) authentication.' }
-
-
-
- - )} + {isEnterprise && ( + <> +
+
+
+

{'Tenant Key'}

+
{'For SSO (SAML) authentication.'}
+
+
+ +
+
+ + )} - { !isEnterprise && ( - <> -
-
-
-

{ 'Data Collection' }

-
{ 'Enables you to control how OpenReplay captures data on your organization’s usage to improve our product.' }
-
-
-
- - )} + {!isEnterprise && ( + <> +
+
+
+

{'Data Collection'}

+
+ {'Enables you to control how OpenReplay captures data on your organization’s usage to improve our product.'} +
+
+
+ +
+
+ + )} - { account.license && ( - <> -
+ {account.license && ( + <> +
-
-
-

{ 'License' }

-
{ 'License key and expiration date.' }
-
-
-
- - )} - - ); - } +
+
+

{'License'}

+
{'License key and expiration date.'}
+
+
+ +
+
+ + )} + + ); + } } diff --git a/frontend/app/components/Client/Roles/Roles.tsx b/frontend/app/components/Client/Roles/Roles.tsx index f9b9ef072..00b0bb6fd 100644 --- a/frontend/app/components/Client/Roles/Roles.tsx +++ b/frontend/app/components/Client/Roles/Roles.tsx @@ -109,7 +109,7 @@ function Roles(props: Props) { icon >
-
+
Title
Project Access
Feature Access
diff --git a/frontend/app/components/Client/Roles/components/RoleItem/RoleItem.tsx b/frontend/app/components/Client/Roles/components/RoleItem/RoleItem.tsx index 845811f77..8cad11ae8 100644 --- a/frontend/app/components/Client/Roles/components/RoleItem/RoleItem.tsx +++ b/frontend/app/components/Client/Roles/components/RoleItem/RoleItem.tsx @@ -27,7 +27,7 @@ interface Props { } function RoleItem({ role, deleteHandler, editHandler, isAdmin, permissions, projects }: Props) { return ( -
+
{ role.name } diff --git a/frontend/app/components/Client/Sites/AddProjectButton/AddUserButton.tsx b/frontend/app/components/Client/Sites/AddProjectButton/AddUserButton.tsx index 7371056fd..e938b391f 100644 --- a/frontend/app/components/Client/Sites/AddProjectButton/AddUserButton.tsx +++ b/frontend/app/components/Client/Sites/AddProjectButton/AddUserButton.tsx @@ -2,27 +2,29 @@ import React from 'react'; import { Popup, Button, IconButton } from 'UI'; import { useStore } from 'App/mstore'; import { useObserver } from 'mobx-react-lite'; +import { init, remove, fetchGDPR } from 'Duck/site'; +import { connect } from 'react-redux'; +import { useModal } from 'App/components/Modal'; +import NewSiteForm from '../NewSiteForm'; const PERMISSION_WARNING = 'You don’t have the permissions to perform this action.'; const LIMIT_WARNING = 'You have reached site limit.'; -function AddProjectButton({ isAdmin = false, onClick }: any) { +function AddProjectButton({ isAdmin = false, init = () => {} }: any) { const { userStore } = useStore(); + const { showModal, hideModal } = useModal(); const limtis = useObserver(() => userStore.limits); const canAddProject = useObserver(() => isAdmin && (limtis.projects === -1 || limtis.projects > 0)); + + const onClick = () => { + init(); + showModal(, { right: true }); + }; return ( - {/* */} ); } -export default AddProjectButton; +export default connect(null, { init, remove, fetchGDPR })(AddProjectButton); diff --git a/frontend/app/components/Client/Sites/InstallButton/InstallButton.tsx b/frontend/app/components/Client/Sites/InstallButton/InstallButton.tsx new file mode 100644 index 000000000..c6d04f2f4 --- /dev/null +++ b/frontend/app/components/Client/Sites/InstallButton/InstallButton.tsx @@ -0,0 +1,25 @@ +import { useModal } from 'App/components/Modal'; +import React from 'react'; +import TrackingCodeModal from 'Shared/TrackingCodeModal'; +import { Button } from 'UI'; + +interface Props { + site: any; +} +function InstallButton(props: Props) { + const { site } = props; + const { showModal, hideModal } = useModal(); + const onClick = () => { + showModal( + , + { right: true } + ); + }; + return ( + + ); +} + +export default InstallButton; diff --git a/frontend/app/components/Client/Sites/InstallButton/index.ts b/frontend/app/components/Client/Sites/InstallButton/index.ts new file mode 100644 index 000000000..c64b2ff6c --- /dev/null +++ b/frontend/app/components/Client/Sites/InstallButton/index.ts @@ -0,0 +1 @@ +export { default } from './InstallButton' \ No newline at end of file diff --git a/frontend/app/components/Client/Sites/NewSiteForm.js b/frontend/app/components/Client/Sites/NewSiteForm.js index c6633b73b..0a9dc81c7 100644 --- a/frontend/app/components/Client/Sites/NewSiteForm.js +++ b/frontend/app/components/Client/Sites/NewSiteForm.js @@ -1,121 +1,122 @@ import React from 'react'; import { connect } from 'react-redux'; import { Form, Input, Button, Icon } from 'UI'; -import { save, edit, update , fetchList, remove } from 'Duck/site'; +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 styles from './siteForm.module.css'; import { confirm } from 'UI'; -@connect(state => ({ - site: state.getIn([ 'site', 'instance' ]), - sites: state.getIn([ 'site', 'list' ]), - siteList: state.getIn([ 'site', 'list' ]), - loading: state.getIn([ 'site', 'save', 'loading' ]) || state.getIn([ 'site', 'remove', 'loading' ]), -}), { - save, - remove, - edit, - update, - pushNewSite, - fetchList, - setSiteId -}) +@connect( + (state) => ({ + site: state.getIn(['site', 'instance']), + sites: state.getIn(['site', 'list']), + siteList: state.getIn(['site', 'list']), + loading: state.getIn(['site', 'save', 'loading']) || state.getIn(['site', 'remove', 'loading']), + }), + { + save, + remove, + edit, + update, + pushNewSite, + fetchList, + setSiteId, + } +) @withRouter export default class NewSiteForm extends React.PureComponent { - state = { - existsError: false, - } + state = { + existsError: false, + }; - componentDidMount() { - const { location: { pathname }, match: { params: { siteId } } } = this.props; - if (pathname.includes('onboarding')) { - this.props.setSiteId(siteId); - } - } + componentDidMount() { + const { + location: { pathname }, + match: { + params: { siteId }, + }, + } = this.props; + if (pathname.includes('onboarding')) { + this.props.setSiteId(siteId); + } + } - onSubmit = e => { - e.preventDefault(); - const { site, siteList, location: { pathname } } = this.props; - if (!site.exists() && siteList.some(({ name }) => name === site.name)) { - return this.setState({ existsError: true }); - } - if (site.exists()) { - this.props.update(this.props.site, this.props.site.id).then(() => { - this.props.onClose(null) - this.props.fetchList(); - }) - } else { - this.props.save(this.props.site).then(() => { - this.props.fetchList().then(() => { - const { sites } = this.props; - const site = sites.last(); - if (!pathname.includes('/client')) { - this.props.setSiteId(site.get('id')) - } - this.props.onClose(null, site) - }) - - // this.props.pushNewSite(site) - }); - } - } + onSubmit = (e) => { + e.preventDefault(); + const { + site, + siteList, + location: { pathname }, + } = this.props; + if (!site.exists() && siteList.some(({ name }) => name === site.name)) { + return this.setState({ existsError: true }); + } + if (site.exists()) { + this.props.update(this.props.site, this.props.site.id).then(() => { + this.props.onClose(null); + this.props.fetchList(); + }); + } else { + this.props.save(this.props.site).then(() => { + this.props.fetchList().then(() => { + const { sites } = this.props; + const site = sites.last(); + if (!pathname.includes('/client')) { + this.props.setSiteId(site.get('id')); + } + this.props.onClose(null, site); + }); - 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).then(() => { - this.props.onClose(null) - }); - } - }; + // this.props.pushNewSite(site) + }); + } + }; - edit = ({ target: { name, value } }) => { - this.setState({ existsError: false }); - this.props.edit({ [ name ]: value }); - } + 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).then(() => { + this.props.onClose(null); + }); + } + }; - render() { - const { site, loading } = this.props; - return ( -
-
- - - - -
- - {site.exists() && ( - - )} -
- { this.state.existsError && -
- { "Site exists already. Please choose another one." } -
- } -
-
- ); - } -} \ No newline at end of file + edit = ({ target: { name, value } }) => { + this.setState({ existsError: false }); + this.props.edit({ [name]: value }); + }; + + render() { + const { site, loading } = this.props; + return ( +
+

{site.exists() ? 'Edit Project' : 'New Project'}

+
+
+ + + + +
+ + {site.exists() && ( + + )} +
+ {this.state.existsError &&
{'Site exists already. Please choose another one.'}
} +
+
+
+ ); + } +} diff --git a/frontend/app/components/Client/Sites/ProjectKey.tsx b/frontend/app/components/Client/Sites/ProjectKey.tsx new file mode 100644 index 000000000..d53b336f8 --- /dev/null +++ b/frontend/app/components/Client/Sites/ProjectKey.tsx @@ -0,0 +1,8 @@ +import { withCopy } from 'HOCs'; +import React from 'react'; + +function ProjectKey({ value, tooltip }: any) { + return
{value}
; +} + +export default withCopy(ProjectKey); diff --git a/frontend/app/components/Client/Sites/Sites.js b/frontend/app/components/Client/Sites/Sites.js index 1c96c0b3c..4158a57ea 100644 --- a/frontend/app/components/Client/Sites/Sites.js +++ b/frontend/app/components/Client/Sites/Sites.js @@ -1,18 +1,18 @@ import React from 'react'; import { connect } from 'react-redux'; -import cn from 'classnames'; import withPageTitle from 'HOCs/withPageTitle'; -import { Loader, SlideModal, Icon, Button, Popup, TextLink } from 'UI'; +import { Loader, Button, Popup, TextLink } from 'UI'; import { init, remove, fetchGDPR } from 'Duck/site'; import { RED, YELLOW, GREEN, STATUS_COLOR_MAP } from 'Types/site'; import stl from './sites.module.css'; import NewSiteForm from './NewSiteForm'; -import GDPRForm from './GDPRForm'; -import TrackingCodeModal from 'Shared/TrackingCodeModal'; -import BlockedIps from './BlockedIps'; 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'; const STATUS_MESSAGE_MAP = { [RED]: ' There seems to be an issue (please verify your installation)', @@ -20,11 +20,7 @@ const STATUS_MESSAGE_MAP = { [GREEN]: 'All good!', }; -const BLOCKED_IPS = 'BLOCKED_IPS'; -const NONE = 'NONE'; - const NEW_SITE_FORM = 'NEW_SITE_FORM'; -const GDPR_FORM = 'GDPR_FORM'; @connect( (state) => ({ @@ -43,20 +39,9 @@ const GDPR_FORM = 'GDPR_FORM'; @withPageTitle('Projects - OpenReplay Preferences') class Sites extends React.PureComponent { state = { - showTrackingCode: false, - modalContent: NONE, - detailContent: NONE, searchQuery: '', }; - toggleBlockedIp = () => { - this.setState({ - detailContent: this.state.detailContent === BLOCKED_IPS ? NONE : BLOCKED_IPS, - }); - }; - - closeModal = () => this.setState({ modalContent: NONE, detailContent: NONE }); - edit = (site) => { this.props.init(site); this.setState({ modalContent: NEW_SITE_FORM }); @@ -73,128 +58,59 @@ class Sites extends React.PureComponent { } }; - showGDPRForm = (site) => { - this.props.init(site); - this.setState({ modalContent: GDPR_FORM }); - }; - - showNewSiteForm = () => { - this.props.init(); - this.setState({ modalContent: NEW_SITE_FORM }); - }; - - showTrackingCode = (site) => { - this.props.init(site); - this.setState({ showTrackingCode: true }); - }; - - getModalTitle() { - switch (this.state.modalContent) { - case NEW_SITE_FORM: - return this.props.site.exists() ? 'Update Project' : 'New Project'; - case GDPR_FORM: - return 'Project Settings'; - default: - return ''; - } - } - - renderModalContent() { - switch (this.state.modalContent) { - case NEW_SITE_FORM: - return ; - case GDPR_FORM: - return ; - default: - return null; - } - } - - renderModalDetailContent() { - switch (this.state.detailContent) { - case BLOCKED_IPS: - return ; - default: - return null; - } - } - render() { - const { loading, sites, site, user, account } = this.props; - const { modalContent, showTrackingCode } = this.state; + 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 ( - this.setState({ showTrackingCode: false })} - site={site} - /> -
- Projects
} - actionButton={} - /> + Projects
} actionButton={} />
- + this.setState({ searchQuery: value })} />
-
Name
+
Project Name
Key
{filteredSites.map((_site) => (
- -
- + +
+
+
+ {getInitials(_site.name)} +
{_site.host}
- {_site.projectKey} +
- +
- + this.props.init(_site)} />
@@ -207,3 +123,12 @@ class Sites extends React.PureComponent { } export default Sites; + +function EditButton({ isAdmin, onClick }) { + const { showModal, hideModal } = useModal(); + const _onClick = () => { + onClick(); + showModal(); + }; + return + + + +
- +
); } -export default UserListItem; \ No newline at end of file +export default UserListItem; diff --git a/frontend/app/components/Client/Webhooks/ListItem.js b/frontend/app/components/Client/Webhooks/ListItem.js index c493cc176..e10d3905c 100644 --- a/frontend/app/components/Client/Webhooks/ListItem.js +++ b/frontend/app/components/Client/Webhooks/ListItem.js @@ -3,22 +3,19 @@ import { Icon } from 'UI'; import styles from './listItem.module.css'; const ListItem = ({ webhook, onEdit, onDelete }) => { - return ( -
-
- { webhook.name } -
{ webhook.endpoint }
-
-
-
{ e.stopPropagation(); onDelete(webhook) } }> - + return ( +
+
+ {webhook.name} +
{webhook.endpoint}
+
+
+
+ +
+
-
- -
-
-
- ); + ); }; -export default ListItem; \ No newline at end of file +export default ListItem; diff --git a/frontend/app/components/Client/Webhooks/WebhookForm.js b/frontend/app/components/Client/Webhooks/WebhookForm.js index 6ea5737ea..b64a63af8 100644 --- a/frontend/app/components/Client/Webhooks/WebhookForm.js +++ b/frontend/app/components/Client/Webhooks/WebhookForm.js @@ -4,80 +4,91 @@ import { edit, save } from 'Duck/webhook'; import { Form, Button, Input } from 'UI'; import styles from './webhookForm.module.css'; -@connect(state => ({ - webhook: state.getIn(['webhooks', 'instance']), - loading: state.getIn(['webhooks', 'saveRequest', 'loading']), -}), { - edit, - save, -}) +@connect( + (state) => ({ + webhook: state.getIn(['webhooks', 'instance']), + loading: state.getIn(['webhooks', 'saveRequest', 'loading']), + }), + { + edit, + save, + } +) class WebhookForm extends React.PureComponent { - setFocus = () => this.focusElement.focus(); - onChangeSelect = (event, { name, value }) => this.props.edit({ [ name ]: value }); - write = ({ target: { value, name } }) => this.props.edit({ [ name ]: value }); + setFocus = () => this.focusElement.focus(); + onChangeSelect = (event, { name, value }) => this.props.edit({ [name]: value }); + write = ({ target: { value, name } }) => this.props.edit({ [name]: value }); - save = () => { - this.props.save(this.props.webhook).then(() => { - this.props.onClose(); - }); - }; + save = () => { + this.props.save(this.props.webhook).then(() => { + this.props.onClose(); + }); + }; - render() { - const { webhook, loading } = this.props; - return ( -
- - - { this.focusElement = ref; } } - name="name" - value={ webhook.name } - onChange={ this.write } - placeholder="Name" - /> - + render() { + const { webhook, loading } = this.props; + return ( +
+

{webhook.exists() ? 'Update' : 'Add'} Webhook

+ + + + { + this.focusElement = ref; + }} + name="name" + value={webhook.name} + onChange={this.write} + placeholder="Name" + /> + - - - { this.focusElement = ref; } } - name="endpoint" - value={ webhook.endpoint } - onChange={ this.write } - placeholder="Endpoint" - /> - + + + { + this.focusElement = ref; + }} + name="endpoint" + value={webhook.endpoint} + onChange={this.write} + placeholder="Endpoint" + /> + - - - { this.focusElement = ref; } } - name="authHeader" - value={ webhook.authHeader } - onChange={ this.write } - placeholder="Auth Header" - /> - + + + { + this.focusElement = ref; + }} + name="authHeader" + value={webhook.authHeader} + onChange={this.write} + placeholder="Auth Header" + /> + - - { webhook.exists() && ( - - )} - - ); - } +
+
+ + {webhook.exists() && } +
+ {webhook.exists() && } +
+ +
+ ); + } } export default WebhookForm; diff --git a/frontend/app/components/Client/Webhooks/Webhooks.js b/frontend/app/components/Client/Webhooks/Webhooks.js index eb5306aa6..076ed0587 100644 --- a/frontend/app/components/Client/Webhooks/Webhooks.js +++ b/frontend/app/components/Client/Webhooks/Webhooks.js @@ -1,8 +1,8 @@ -import React from 'react'; +import React, { useEffect } from 'react'; import { connect } from 'react-redux'; import cn from 'classnames'; import withPageTitle from 'HOCs/withPageTitle'; -import { IconButton, SlideModal, Loader, NoContent } from 'UI'; +import { Button, Loader, NoContent } from 'UI'; import { init, fetchList, remove } from 'Duck/webhook'; import WebhookForm from './WebhookForm'; import ListItem from './ListItem'; @@ -10,87 +10,74 @@ import styles from './webhooks.module.css'; import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG'; import { confirm } from 'UI'; import { toast } from 'react-toastify'; +import { useModal } from 'App/components/Modal'; -@connect(state => ({ - webhooks: state.getIn(['webhooks', 'list']), - loading: state.getIn(['webhooks', 'loading']), -}), { - init, - fetchList, - remove, -}) -@withPageTitle('Webhooks - OpenReplay Preferences') -class Webhooks extends React.PureComponent { - state = { showModal: false }; +function Webhooks(props) { + const { webhooks, loading } = props; + const { showModal, hideModal } = useModal(); - componentWillMount() { - this.props.fetchList(); - } + const noSlackWebhooks = webhooks.filter((hook) => hook.type !== 'slack'); + useEffect(() => { + props.fetchList(); + }, []); - closeModal = () => this.setState({ showModal: false }); - init = (v) => { - this.props.init(v); - this.setState({ showModal: true }); - } + const init = (v) => { + props.init(v); + showModal(); + }; - removeWebhook = async (id) => { - if (await confirm({ - header: 'Confirm', - confirmButton: 'Yes, delete', - confirmation: `Are you sure you want to remove this webhook?` - })) { - this.props.remove(id).then(() => { - toast.success('Webhook removed successfully'); - }); - } - } + const removeWebhook = async (id) => { + if ( + await confirm({ + header: 'Confirm', + confirmButton: 'Yes, delete', + confirmation: `Are you sure you want to remove this webhook?`, + }) + ) { + props.remove(id).then(() => { + toast.success('Webhook removed successfully'); + }); + hideModal(); + } + }; - render() { - const { webhooks, loading } = this.props; - const { showModal } = this.state; - - const noSlackWebhooks = webhooks.filter(hook => hook.type !== 'slack'); return ( -
- } - onClose={ this.closeModal } - /> -
-

{ 'Webhooks' }

- this.init() } /> -
- - - - -
No webhooks available.
-
- } - size="small" - show={ noSlackWebhooks.size === 0 } - // animatedIcon="no-results" - > -
- { noSlackWebhooks.map(webhook => ( - this.init(webhook) } - onDelete={ () => this.removeWebhook(webhook.webhookId) } - /> - ))} +
+
+

{'Webhooks'}

+
- - -
+ + + + +
No webhooks available.
+
+ } + size="small" + show={noSlackWebhooks.size === 0} + > +
+ {noSlackWebhooks.map((webhook) => ( + init(webhook)} /> + ))} +
+ + +
); - } } -export default Webhooks; \ No newline at end of file +export default connect( + (state) => ({ + webhooks: state.getIn(['webhooks', 'list']), + loading: state.getIn(['webhooks', 'loading']), + }), + { + init, + fetchList, + remove, + } +)(withPageTitle('Webhooks - OpenReplay Preferences')(Webhooks)); diff --git a/frontend/app/components/Client/client.module.css b/frontend/app/components/Client/client.module.css index 8e69458ef..43d311b31 100644 --- a/frontend/app/components/Client/client.module.css +++ b/frontend/app/components/Client/client.module.css @@ -7,7 +7,7 @@ .main { max-height: 100%; display: flex; - min-height: calc(100vh - 81px); + /* min-height: calc(100vh - 81px); */ & .tabMenu { width: 240px; diff --git a/frontend/app/components/Modal/Modal.tsx b/frontend/app/components/Modal/Modal.tsx index d14f6411a..9dc622a18 100644 --- a/frontend/app/components/Modal/Modal.tsx +++ b/frontend/app/components/Modal/Modal.tsx @@ -3,14 +3,14 @@ import ReactDOM from 'react-dom'; import ModalOverlay from './ModalOverlay'; export default function Modal({ component, props, hideModal }: any) { - return component ? ReactDOM.createPortal( - - {component} - , - document.querySelector("#modal-root"), - ) : <>; -} \ No newline at end of file + return component ? ( + ReactDOM.createPortal( + + {component} + , + document.querySelector('#modal-root') + ) + ) : ( + <> + ); +} diff --git a/frontend/app/components/Modal/ModalOverlay.tsx b/frontend/app/components/Modal/ModalOverlay.tsx index 398e27f2f..5b2a9edab 100644 --- a/frontend/app/components/Modal/ModalOverlay.tsx +++ b/frontend/app/components/Modal/ModalOverlay.tsx @@ -1,18 +1,14 @@ import React from 'react'; -import stl from './ModalOverlay.module.css' +import stl from './ModalOverlay.module.css'; import cn from 'classnames'; function ModalOverlay({ hideModal, children, left = false, right = false }: any) { return (
-
-
{children}
+
+
{children}
); } -export default ModalOverlay; \ No newline at end of file +export default ModalOverlay; diff --git a/frontend/app/components/Modal/index.tsx b/frontend/app/components/Modal/index.tsx index 04e2acd91..920cb2d14 100644 --- a/frontend/app/components/Modal/index.tsx +++ b/frontend/app/components/Modal/index.tsx @@ -3,60 +3,59 @@ import React, { Component, createContext } from 'react'; import Modal from './Modal'; const ModalContext = createContext({ - component: null, - props: { - right: false, - onClose: () => {}, - }, - showModal: (component: any, props: any) => {}, - hideModal: () => {} + component: null, + props: { + right: true, + onClose: () => {}, + }, + showModal: (component: any, props: any) => {}, + hideModal: () => {}, }); export class ModalProvider extends Component { - - handleKeyDown = (e: any) => { - if (e.keyCode === 27) { - this.hideModal(); - } - } - - showModal = (component, props = { }) => { - this.setState({ - component, - props - }); - document.addEventListener('keydown', this.handleKeyDown); - document.querySelector("body").style.overflow = 'hidden'; - }; - - hideModal = () => { - document.removeEventListener('keydown', this.handleKeyDown); - document.querySelector("body").style.overflow = 'visible'; - const { props } = this.state; - if (props.onClose) { - props.onClose(); + handleKeyDown = (e: any) => { + if (e.keyCode === 27) { + this.hideModal(); + } }; - this.setState({ - component: null, - props: {} - }); - } - state = { - component: null, - props: {}, - showModal: this.showModal, - hideModal: this.hideModal - }; + showModal = (component, props = { right: true }) => { + this.setState({ + component, + props, + }); + document.addEventListener('keydown', this.handleKeyDown); + document.querySelector('body').style.overflow = 'hidden'; + }; - render() { - return ( - - - {this.props.children} - - ); - } + hideModal = () => { + document.removeEventListener('keydown', this.handleKeyDown); + document.querySelector('body').style.overflow = 'visible'; + const { props } = this.state; + if (props.onClose) { + props.onClose(); + } + this.setState({ + component: null, + props: {}, + }); + }; + + state = { + component: null, + props: {}, + showModal: this.showModal, + hideModal: this.hideModal, + }; + + render() { + return ( + + + {this.props.children} + + ); + } } export const ModalConsumer = ModalContext.Consumer; diff --git a/frontend/app/components/Onboarding/components/IntegrationsTab/IntegrationsTab.js b/frontend/app/components/Onboarding/components/IntegrationsTab/IntegrationsTab.js index df05ca807..db679f220 100644 --- a/frontend/app/components/Onboarding/components/IntegrationsTab/IntegrationsTab.js +++ b/frontend/app/components/Onboarding/components/IntegrationsTab/IntegrationsTab.js @@ -17,17 +17,17 @@ function IntegrationsTab() {

🔌 -
Plugins
+
Integrations

- + -
+ {/*

🔌
Integrations

- + */} {/*
How are you handling store management?
diff --git a/frontend/app/components/hocs/index.js b/frontend/app/components/hocs/index.js index 444ad0180..5f08b86f0 100644 --- a/frontend/app/components/hocs/index.js +++ b/frontend/app/components/hocs/index.js @@ -1,2 +1,3 @@ export { default as withRequest } from './withRequest'; -export { default as withToggle } from './withToggle'; \ No newline at end of file +export { default as withToggle } from './withToggle'; +export { default as withCopy } from './withCopy' \ No newline at end of file diff --git a/frontend/app/components/hocs/withCopy.tsx b/frontend/app/components/hocs/withCopy.tsx new file mode 100644 index 000000000..2b3a9d541 --- /dev/null +++ b/frontend/app/components/hocs/withCopy.tsx @@ -0,0 +1,27 @@ +import React from 'react'; +import copy from 'copy-to-clipboard'; +import { Tooltip } from 'react-tippy'; + +const withCopy = (WrappedComponent: React.ComponentType) => { + const ComponentWithCopy = (props: any) => { + const [copied, setCopied] = React.useState(false); + const { value, tooltip } = props; + const copyToClipboard = (text: string) => { + copy(text); + setCopied(true); + setTimeout(() => { + setCopied(false); + }, 1000); + }; + return ( +
copyToClipboard(value)} className="w-fit"> + + + +
+ ); + }; + return ComponentWithCopy; +}; + +export default withCopy; diff --git a/frontend/app/components/hocs/withRequest.js b/frontend/app/components/hocs/withRequest.js index 80dfaccf3..992b0ce4e 100644 --- a/frontend/app/components/hocs/withRequest.js +++ b/frontend/app/components/hocs/withRequest.js @@ -2,66 +2,66 @@ import React from 'react'; import APIClient from 'App/api_client'; export default ({ - initialData = null, - endpoint = '', - method = 'GET', - requestName = "request", - loadingName = "loading", - errorName = "requestError", - dataName = "data", - dataWrapper = data => data, - loadOnInitialize = false, - resetBeforeRequest = false, // Probably use handler? -}) => BaseComponent => class extends React.PureComponent { - constructor(props) { - super(props); - this.state = { - data: typeof initialData === 'function' ? initialData(props) : initialData, - loading: loadOnInitialize, - error: false, - }; - if (loadOnInitialize) { - this.request(); - } - } + initialData = null, + endpoint = '', + method = 'GET', + requestName = 'request', + loadingName = 'loading', + errorName = 'requestError', + dataName = 'data', + dataWrapper = (data) => data, + loadOnInitialize = false, + resetBeforeRequest = false, // Probably use handler? + }) => + (BaseComponent) => + class extends React.PureComponent { + constructor(props) { + super(props); + this.state = { + data: typeof initialData === 'function' ? initialData(props) : initialData, + loading: loadOnInitialize, + error: false, + }; + if (loadOnInitialize) { + this.request(); + } + } - request = (params, edpParams) => { - this.setState({ - loading: true, - error: false, - data: resetBeforeRequest - ? (typeof initialData === 'function' ? initialData(this.props) : initialData) - : this.state.data, - }); - const edp = typeof endpoint === 'function' - ? endpoint(this.props, edpParams) - : endpoint; - return new APIClient()[ method.toLowerCase() ](edp, params) - .then(response => response.json()) - .then(({ errors, data }) => { - if (errors) { - return this.setError(); - } - this.setState({ - data: dataWrapper(data, this.state.data), - loading: false, - }); - }) - .catch(this.setError); - } + request = (params, edpParams) => { + this.setState({ + loading: true, + error: false, + data: resetBeforeRequest ? (typeof initialData === 'function' ? initialData(this.props) : initialData) : this.state.data, + }); + const edp = typeof endpoint === 'function' ? endpoint(this.props, edpParams) : endpoint; + return new APIClient() + [method.toLowerCase()](edp, params) + .then((response) => response.json()) + .then(({ errors, data }) => { + if (errors) { + return this.setError(); + } + this.setState({ + data: dataWrapper(data, this.state.data), + loading: false, + }); + }) + .catch(this.setError); + }; - setError = () => this.setState({ - loading: false, - error: true, - }) + setError = () => + this.setState({ + loading: false, + error: true, + }); - render() { - const ownProps = { - [ requestName ]: this.request, - [ loadingName ]: this.state.loading, - [ dataName ]: this.state.data, - [ errorName ]: this.state.error, - }; - return - } -} \ No newline at end of file + render() { + const ownProps = { + [requestName]: this.request, + [loadingName]: this.state.loading, + [dataName]: this.state.data, + [errorName]: this.state.error, + }; + return ; + } + }; diff --git a/frontend/app/components/shared/Select/Select.tsx b/frontend/app/components/shared/Select/Select.tsx index cc9e2cbbc..b58f99132 100644 --- a/frontend/app/components/shared/Select/Select.tsx +++ b/frontend/app/components/shared/Select/Select.tsx @@ -104,7 +104,7 @@ export default function({ placeholder='Select', name const opacity = state.isDisabled ? 0.5 : 1; const transition = 'opacity 300ms'; - return { ...provided, opacity, transition, fontWeight: 'bold' }; + return { ...provided, opacity, transition, fontWeight: '500' }; }, input: (provided: any) => ({ ...provided, diff --git a/frontend/app/components/shared/TrackingCodeModal/TrackingCodeModal.js b/frontend/app/components/shared/TrackingCodeModal/TrackingCodeModal.js index cd8c23707..586cc8742 100644 --- a/frontend/app/components/shared/TrackingCodeModal/TrackingCodeModal.js +++ b/frontend/app/components/shared/TrackingCodeModal/TrackingCodeModal.js @@ -3,65 +3,79 @@ import { Modal, Icon, Tabs } from 'UI'; import styles from './trackingCodeModal.module.css'; import { editGDPR, saveGDPR } from 'Duck/site'; import { connect } from 'react-redux'; -import ProjectCodeSnippet from './ProjectCodeSnippet'; +import ProjectCodeSnippet from './ProjectCodeSnippet'; import InstallDocs from './InstallDocs'; import cn from 'classnames'; const PROJECT = 'Using Script'; const DOCUMENTATION = 'Using NPM'; const TABS = [ - { key: DOCUMENTATION, text: DOCUMENTATION }, - { key: PROJECT, text: PROJECT }, + { key: DOCUMENTATION, text: DOCUMENTATION }, + { key: PROJECT, text: PROJECT }, ]; class TrackingCodeModal extends React.PureComponent { - state = { copied: false, changed: false, activeTab: DOCUMENTATION }; + state = { copied: false, changed: false, activeTab: DOCUMENTATION }; - setActiveTab = (tab) => { - this.setState({ activeTab: tab }); - } + setActiveTab = (tab) => { + this.setState({ activeTab: tab }); + }; - renderActiveTab = () => { - const { site } = this.props; - switch (this.state.activeTab) { - case PROJECT: - return ; - case DOCUMENTATION: - return ; + renderActiveTab = () => { + const { site } = this.props; + switch (this.state.activeTab) { + case PROJECT: + return ; + case DOCUMENTATION: + return ; + } + return null; + }; + + render() { + const { site, displayed, onClose, title = '', subTitle } = this.props; + const { activeTab } = this.state; + return ( +
+

+ {title} {subTitle && {subTitle}} +

+ +
+ +
{this.renderActiveTab()}
+
+
+ // displayed && + // + // + //
{ title } { subTitle && {subTitle}}
+ //
+ // + //
+ //
+ // + // + //
+ // { this.renderActiveTab() } + //
+ //
+ //
+ ); } - return null; - } - - render() { - const { site, displayed, onClose, title = '', subTitle } = this.props; - const { activeTab } = this.state; - return ( - displayed && - - -
{ title } { subTitle && {subTitle}}
-
- -
-
- - -
- { this.renderActiveTab() } -
-
-
- ); - } } -export default connect(state => ({ - site: state.getIn([ 'site', 'instance' ]), - gdpr: state.getIn([ 'site', 'instance', 'gdpr' ]), - saving: state.getIn([ 'site', 'saveGDPR', 'loading' ]), -}), { - editGDPR, saveGDPR -})(TrackingCodeModal); \ No newline at end of file +export default connect( + (state) => ({ + site: state.getIn(['site', 'instance']), + gdpr: state.getIn(['site', 'instance', 'gdpr']), + saving: state.getIn(['site', 'saveGDPR', 'loading']), + }), + { + editGDPR, + saveGDPR, + } +)(TrackingCodeModal); diff --git a/frontend/app/components/ui/Form/Form.tsx b/frontend/app/components/ui/Form/Form.tsx index c9ab7c036..a85af0b23 100644 --- a/frontend/app/components/ui/Form/Form.tsx +++ b/frontend/app/components/ui/Form/Form.tsx @@ -2,16 +2,15 @@ import React from 'react'; interface Props { children: React.ReactNode; - onSubmit?: any - [x: string]: any + onSubmit?: any; + [x: string]: any; } - interface FormFieldProps { children: React.ReactNode; - [x: string]: any + [x: string]: any; } -function FormField (props: FormFieldProps) { +function FormField(props: FormFieldProps) { const { children, ...rest } = props; return (
@@ -20,16 +19,18 @@ function FormField (props: FormFieldProps) { ); } - function Form(props: Props) { const { children, ...rest } = props; return ( -
{ - e.preventDefault(); - if (props.onSubmit) { - props.onSubmit(e); - } - }}> + { + e.preventDefault(); + if (props.onSubmit) { + props.onSubmit(e); + } + }} + > {children}
); @@ -37,4 +38,4 @@ function Form(props: Props) { Form.Field = FormField; -export default Form; \ No newline at end of file +export default Form; diff --git a/frontend/app/components/ui/Input/Input.tsx b/frontend/app/components/ui/Input/Input.tsx index 1897ece13..1c36f7a8a 100644 --- a/frontend/app/components/ui/Input/Input.tsx +++ b/frontend/app/components/ui/Input/Input.tsx @@ -11,13 +11,14 @@ interface Props { rows?: number; [x: string]: any; } -function Input(props: Props) { +const Input = React.forwardRef((props: Props, ref: any) => { const { className = '', leadingButton = '', wrapperClassName = '', icon = '', type = 'text', rows = 4, ...rest } = props; return (
{icon && } {type === 'textarea' ? (