diff --git a/jest.config.js b/jest.config.js index ed3d33f3aa1..6f14ed6b306 100644 --- a/jest.config.js +++ b/jest.config.js @@ -23,6 +23,7 @@ module.exports = { '\\.(css|less)$': '/test_utils/__mocks__/styleMock.js', '^browser-styles(.*)$': '/src/browser/styles$1', '^browser-components(.*)$': '/src/browser/components$1', + '^browser-services(.*)$': '/src/browser/services$1', '^browser-hooks(.*)$': '/src/browser/hooks$1', 'worker-loader': '/test_utils/__mocks__/workerLoaderMock.js', 'project-root(.*)$': '$1', diff --git a/src/browser/AppInit.tsx b/src/browser/AppInit.tsx index 035f056da19..777e5b45840 100644 --- a/src/browser/AppInit.tsx +++ b/src/browser/AppInit.tsx @@ -25,7 +25,8 @@ import { combineReducers, applyMiddleware, compose, - AnyAction + AnyAction, + StoreEnhancer } from 'redux' import { Provider } from 'react-redux' import { @@ -42,7 +43,9 @@ import * as Sentry from '@sentry/react' import { Integrations } from '@sentry/tracing' import { CaptureConsole } from '@sentry/integrations' +import { CannySDK } from 'browser-services/canny' import { createReduxMiddleware, getAll, applyKeys } from 'services/localstorage' +import { GlobalState } from 'shared/globalState' import { APP_START } from 'shared/modules/app/appDuck' import { detectRuntimeEnv } from 'services/utils' import { NEO4J_CLOUD_DOMAINS } from 'shared/modules/settings/settingsDuck' @@ -70,13 +73,20 @@ const suberMiddleware = createSuberReduxMiddleware(bus) const epicMiddleware = createEpicMiddleware(epics) const localStorageMiddleware = createReduxMiddleware() -const reducer = combineReducers({ ...(reducers as any) }) +const reducer = combineReducers({ ...(reducers as any) }) -const enhancer = compose( +declare global { + interface Window { + __REDUX_DEVTOOLS_EXTENSION__?: ( + ...args: unknown[] + ) => StoreEnhancer + } +} + +const enhancer: StoreEnhancer = compose( applyMiddleware(suberMiddleware, epicMiddleware, localStorageMiddleware), - process.env.NODE_ENV !== 'production' && - (window as any).__REDUX_DEVTOOLS_EXTENSION__ - ? (window as any).__REDUX_DEVTOOLS_EXTENSION__({ + process.env.NODE_ENV !== 'production' && window.__REDUX_DEVTOOLS_EXTENSION__ + ? window.__REDUX_DEVTOOLS_EXTENSION__({ actionSanitizer: (action: AnyAction) => action.type === 'requests/UPDATED' ? { @@ -88,37 +98,35 @@ const enhancer = compose( } } : action, - stateSanitizer: (state: any) => ({ + stateSanitizer: (state: GlobalState) => ({ ...state, requests: Object.assign( {}, - ...Object.entries(state.requests).map( - ([id, request]: [string, any]) => ({ - [id]: { - ...request, - result: { - ...request.result, - records: - 'REQUEST RECORDS OMITTED FROM REDUX DEVTOOLS TO PREVENT OUT OF MEMORY ERROR' - } + ...Object.entries(state.requests).map(([id, request]) => ({ + [id]: { + ...request, + result: { + ...request.result, + records: + 'REQUEST RECORDS OMITTED FROM REDUX DEVTOOLS TO PREVENT OUT OF MEMORY ERROR' } - }) - ) + } + })) ) }) }) - : (f: any) => f + : (f: unknown) => f ) -const store: any = createStore( +const store = createStore( reducer, - getAll(), // rehydrate from local storage on app start + getAll() as GlobalState, // rehydrate from local storage on app start enhancer ) // Send everything from suber into Redux bus.applyMiddleware( - (_, origin) => (channel: any, message: any, source: any) => { + (_, origin) => (channel: string, message: AnyAction, source: string) => { // No loop-backs if (source === 'redux') return // Send to Redux with the channel as the action type @@ -126,14 +134,14 @@ bus.applyMiddleware( } ) -function scrubQueryparams(event: Sentry.Event): Sentry.Event { +function scrubQueryParams(event: Sentry.Event): Sentry.Event { if (event.request?.query_string) { event.request.query_string = '' } return event } -export function setupSentry() { +export function setupSentry(): void { if (process.env.NODE_ENV === 'production') { Sentry.init({ dsn: @@ -146,7 +154,7 @@ export function setupSentry() { tracesSampleRate: 0.2, beforeSend: event => allowOutgoingConnections(store.getState()) - ? scrubQueryparams(event) + ? scrubQueryParams(event) : null, environment: 'unset' }) @@ -166,7 +174,7 @@ export function setupSentry() { })) ) }) - .catch(() => {}) + .catch(() => undefined) } } @@ -231,9 +239,17 @@ const client = new ApolloClient({ link: uploadLink }) -const AppInit = () => { +CannySDK.init() + .then(() => { + window.CannyIsLoaded = true + }) + .catch(() => { + window.CannyIsLoaded = false + }) + +const AppInit = (): JSX.Element => { return ( - + diff --git a/src/browser/components/TabNavigation/Navigation.tsx b/src/browser/components/TabNavigation/Navigation.tsx index 5f28555ae08..ead93c35fc4 100644 --- a/src/browser/components/TabNavigation/Navigation.tsx +++ b/src/browser/components/TabNavigation/Navigation.tsx @@ -21,8 +21,10 @@ import React, { Component } from 'react' import { StyledNavigationButton, - NavigationButtonContainer + NavigationButtonContainer, + StyledCannyBadgeAnchor } from 'browser-components/buttons' +import { cannyOptions } from 'browser-services/canny' import { StyledSidebar, StyledDrawer, @@ -36,26 +38,61 @@ const Closed = 'CLOSED' const Open = 'OPEN' const Opening = 'OPENING' -type State = any +export interface NavItem { + name: string + title: string + icon: (isOpen: boolean) => JSX.Element + content: any + enableCannyBadge?: boolean +} + +interface NavigationProps { + openDrawer: string + onNavClick: (name: string) => void + topNavItems: NavItem[] + bottomNavItems?: NavItem[] +} + +type TransitionState = + | typeof Closing + | typeof Closed + | typeof Open + | typeof Opening + +interface NavigationState { + transitionState?: TransitionState + drawerContent?: null | string +} -class Navigation extends Component { - _onTransitionEnd: any - transitionState: any - state: any = {} - constructor(props: {}) { +class Navigation extends Component { + _onTransitionEnd: () => void + transitionState?: TransitionState + state: NavigationState = {} + constructor(props: NavigationProps) { super(props) this._onTransitionEnd = this.onTransitionEnd.bind(this) } - componentDidMount() { + componentDidMount(): void { this.setState({ transitionState: Closed }) + + window.Canny && window.Canny('initChangelog', cannyOptions) + } + + componentWillUnmount(): void { + if (window.Canny) { + window.Canny('closeChangelog') + } } - componentDidUpdate(prevProps: any, prevState: State) { + componentDidUpdate( + prevProps: NavigationProps, + prevState: NavigationState + ): void { if (prevProps.openDrawer !== this.props.openDrawer) { - const newState: any = {} + const newState: NavigationState = {} if (this.props.openDrawer) { newState.drawerContent = this.props.openDrawer if ( @@ -77,7 +114,7 @@ class Navigation extends Component { } } - onTransitionEnd() { + onTransitionEnd(): void { if (this.transitionState === Closing) { this.setState({ transitionState: Closed, @@ -91,32 +128,37 @@ class Navigation extends Component { } } - render() { + render(): JSX.Element { const { onNavClick, topNavItems, bottomNavItems = [] } = this.props - const buildNavList = (list: any, selected: any) => - list.map((item: any) => { + const buildNavList = (list: NavItem[], selected?: null | string) => + list.map(item => { const isOpen = item.name.toLowerCase() === selected return ( - onNavClick(item.name.toLowerCase())} - isOpen={isOpen} - > - - {item.icon(isOpen)} - - + <> + {item.enableCannyBadge ? ( + + ) : null} + onNavClick(item.name.toLowerCase())} + isOpen={isOpen} + > + + {item.icon(isOpen)} + + + ) }) - const getContentToShow = (openDrawer: any) => { + const getContentToShow = (openDrawer?: null | string) => { if (openDrawer) { const filteredList = topNavItems .concat(bottomNavItems) - .filter((item: any) => item.name.toLowerCase() === openDrawer) + .filter(item => item.name.toLowerCase() === openDrawer) const TabContent = filteredList[0].content return } diff --git a/src/browser/components/buttons/index.tsx b/src/browser/components/buttons/index.tsx index 6440d67208c..7d680864752 100644 --- a/src/browser/components/buttons/index.tsx +++ b/src/browser/components/buttons/index.tsx @@ -28,11 +28,11 @@ import { hexToRgba } from '../../styles/utils' import styles from './style.css' -export const CloseButton = (props: any) => { +export const CloseButton = (props: any): JSX.Element => { return } -export const EditorButton = (props: any) => { +export const EditorButton = (props: any): JSX.Element => { const { icon, title, color, width, onClick, ...rest } = props const overrideColor = { ...(color ? { color } : {}) } return ( @@ -77,6 +77,24 @@ const BaseButton: any = styled.span` } ` +export const StyledCannyBadgeAnchor = styled.div` + pointer-events: none; + + .Canny_BadgeContainer { + pointer-events: none; + + .Canny_Badge { + pointer-events: none; + top: 10px; + right: 10px; + border-radius: 10px; + background-color: red; + padding: 4px; + border: 1px solid red; + } + } +` + export const StyledNavigationButton = styled.button` background: transparent; border: 0; @@ -229,7 +247,7 @@ interface ButtonTypeProps { className?: any } -export const FormButton = (props: ButtonTypeProps) => { +export const FormButton = (props: ButtonTypeProps): JSX.Element => { const { icon, label, children, ...rest } = props const ButtonType = buttonTypes[props.buttonType as string] || buttonTypes.primary @@ -262,7 +280,7 @@ export const FormButton = (props: ButtonTypeProps) => { ) } -export const CypherFrameButton = (props: any) => { +export const CypherFrameButton = (props: any): JSX.Element => { const { selected, ...rest } = props return selected ? ( @@ -296,7 +314,7 @@ const StyledSelectedCypherFrameButton = styled(StyledCypherFrameButton)` color: ${props => props.theme.secondaryButtonTextHover}; fill: ${props => props.theme.secondaryButtonTextHover}; ` -export const FrameButton = (props: any) => { +export const FrameButton = (props: any): JSX.Element => { const { pressed, children, ...rest } = props return pressed ? ( {children} @@ -361,7 +379,7 @@ export const FrameButtonAChild = styled(DefaultA)` } ` -export const ActionButton = (props: any) => { +export const ActionButton = (props: any): JSX.Element => { const { className, ...rest } = props return