浏览代码

Add ability to change joystream node (#1235)

* Add ability to change joystream node

* PR FIX

* PR FIX 2

* lint fix
Rafał Pawłow 3 年之前
父节点
当前提交
861e1fdd94

+ 1 - 1
.env

@@ -13,8 +13,8 @@ REACT_APP_DEVELOPMENT_OFFICIAL_JOYSTREAM_CHANNEL_ID=12
 REACT_APP_PRODUCTION_QUERY_NODE_URL=https://hydra.joystream.org/graphql
 REACT_APP_PRODUCTION_QUERY_NODE_SUBSCRIPTION_URL=wss://hydra.joystream.org/graphql
 REACT_APP_PRODUCTION_ORION_URL=https://orion.joystream.org/graphql
+REACT_APP_PRODUCTION_NODE_URL=wss://rome-rpc-endpoint.joystream.org:9944
 REACT_APP_PRODUCTION_ASSET_LOGS_URL=https://orion.joystream.org/logs
-REACT_APP_PRODUCTION_NODE_URL=wss://rome-rpc-endpoint.joystream.org:9944/
 REACT_APP_PRODUCTION_FAUCET_URL=https://member-faucet.joystream.org/register
 REACT_APP_PRODUCTION_OFFICIAL_JOYSTREAM_CHANNEL_ID=3
 

+ 30 - 0
src/config/availableNodes.ts

@@ -0,0 +1,30 @@
+export const availableNodes = [
+  {
+    name: 'Joystream (Europe/Germany - High Availabitliy)',
+    value: 'wss://rome-rpc-endpoint.joystream.org:9944',
+  },
+  {
+    name: 'Joystream (JoystreamStats.Live)',
+    value: 'wss://joystreamstats.live:9945',
+  },
+  {
+    name: 'Joystream (Europe/UK)',
+    value: 'wss://testnet-rpc-3-uk.joystream.org',
+  },
+  {
+    name: 'Joystream (US/East)',
+    value: 'wss://testnet-rpc-1-us.joystream.org',
+  },
+  {
+    name: 'Joystream (Singapore)',
+    value: 'wss://testnet-rpc-2-singapore.joystream.org',
+  },
+  {
+    name: 'Sumer Dev',
+    value: 'wss://sumer-dev-2.joystream.app/rpc',
+  },
+  {
+    name: 'Local node',
+    value: 'ws://127.0.0.1:9944',
+  },
+]

+ 3 - 8
src/config/envs.ts

@@ -1,16 +1,11 @@
+import { useEnvironmentStore } from '@/providers/environment/store'
+
 type BuildEnv = 'production' | 'development'
 
 export const BUILD_ENV = (process.env.REACT_APP_ENV || 'production') as BuildEnv
-const target_env = window.localStorage.getItem('target_env')
-export const TARGET_DEV_ENV = target_env ? JSON.parse(target_env) : 'development'
+export const TARGET_DEV_ENV = useEnvironmentStore.getState().targetEnv
 export const ENV_PREFIX = 'REACT_APP'
 
-export const setEnvInLocalStorage = (value: string) => {
-  if (availableEnvs().includes(value)) {
-    window.localStorage.setItem('target_env', JSON.stringify(value))
-  }
-}
-
 export const availableEnvs = () => {
   return Array.from(
     new Set(

+ 1 - 0
src/providers/environment/index.ts

@@ -0,0 +1 @@
+export type { EnvironmentState } from './store'

+ 44 - 0
src/providers/environment/store.ts

@@ -0,0 +1,44 @@
+import { createStore } from '@/store'
+
+const LOCAL_STORAGE_KEY = 'environment'
+
+export type EnvironmentState = {
+  selectedNode: string | null
+  targetEnv: string
+}
+
+const INITIAL_STATE: EnvironmentState = {
+  targetEnv: 'development',
+  selectedNode: null,
+}
+
+export type EnvironmentStoreActions = {
+  setSelectedNode: (node: string) => void
+  setTargetEnv: (env: string) => void
+}
+
+export const useEnvironmentStore = createStore<EnvironmentState, EnvironmentStoreActions>(
+  {
+    state: INITIAL_STATE,
+    actionsFactory: (set) => ({
+      setSelectedNode: (node) => {
+        set((state) => {
+          state.selectedNode = node || state.selectedNode
+        })
+      },
+      setTargetEnv: (env) => {
+        set((state) => {
+          state.targetEnv = env || state.targetEnv
+        })
+      },
+    }),
+  },
+  {
+    persist: {
+      key: LOCAL_STORAGE_KEY,
+      version: 0,
+      whitelist: ['selectedNode', 'targetEnv'],
+      migrate: () => null,
+    },
+  }
+)

+ 4 - 2
src/providers/joystream/provider.tsx

@@ -3,6 +3,7 @@ import React, { useCallback, useEffect, useState } from 'react'
 
 import { NODE_URL } from '@/config/urls'
 import { JoystreamJs } from '@/joystream-lib'
+import { useEnvironmentStore } from '@/providers/environment/store'
 import { SentryLogger } from '@/utils/logs'
 
 import { useConnectionStatusStore } from '../connectionStatus'
@@ -16,6 +17,7 @@ JoystreamContext.displayName = 'JoystreamContext'
 
 export const JoystreamProvider: React.FC = ({ children }) => {
   const { activeAccountId, accounts } = useUser()
+  const { selectedNode } = useEnvironmentStore((state) => state)
   const setNodeConnection = useConnectionStatusStore((state) => state.actions.setNodeConnection)
 
   const [joystream, setJoystream] = useState<JoystreamJs | null>(null)
@@ -33,7 +35,7 @@ export const JoystreamProvider: React.FC = ({ children }) => {
     const init = async () => {
       try {
         setNodeConnection('connecting')
-        joystream = new JoystreamJs(NODE_URL)
+        joystream = new JoystreamJs(selectedNode || NODE_URL)
         setJoystream(joystream)
 
         joystream.onNodeConnectionUpdate = handleNodeConnectionUpdate
@@ -48,7 +50,7 @@ export const JoystreamProvider: React.FC = ({ children }) => {
     return () => {
       joystream?.destroy()
     }
-  }, [handleNodeConnectionUpdate, setNodeConnection])
+  }, [handleNodeConnectionUpdate, selectedNode, setNodeConnection])
 
   useEffect(() => {
     if (!joystream || !activeAccountId || !accounts) {

+ 3 - 0
src/shared/components/TextField/TextField.tsx

@@ -15,6 +15,7 @@ export type TextFieldProps = {
   required?: boolean
   className?: string
   placeholder?: string
+  defaultValue?: string
 } & InputBaseProps
 
 const TextFieldComponent: React.ForwardRefRenderFunction<HTMLInputElement, TextFieldProps> = (
@@ -30,6 +31,7 @@ const TextFieldComponent: React.ForwardRefRenderFunction<HTMLInputElement, TextF
     disabled,
     required,
     placeholder,
+    defaultValue,
     ...inputBaseProps
   },
   ref
@@ -49,6 +51,7 @@ const TextFieldComponent: React.ForwardRefRenderFunction<HTMLInputElement, TextF
         type={type}
         required={required}
         tabIndex={disabled ? -1 : 0}
+        defaultValue={defaultValue}
       />
     </InputBase>
   )

+ 66 - 10
src/views/admin/AdminView.tsx

@@ -1,33 +1,70 @@
-import React, { useState } from 'react'
+import React, { useRef, useState } from 'react'
 import { Link } from 'react-router-dom'
 
-import { TARGET_DEV_ENV, availableEnvs, setEnvInLocalStorage } from '@/config/envs'
+import { availableNodes } from '@/config/availableNodes'
+import { TARGET_DEV_ENV, availableEnvs } from '@/config/envs'
 import { absoluteRoutes } from '@/config/routes'
+import { NODE_URL } from '@/config/urls'
+import { useEnvironmentStore } from '@/providers/environment/store'
 import { useSnackbar } from '@/providers/snackbars'
 import { Button } from '@/shared/components/Button'
+import { Checkbox } from '@/shared/components/Checkbox'
 import { Select } from '@/shared/components/Select'
 import { Text } from '@/shared/components/Text'
+import { TextField } from '@/shared/components/TextField'
 import { SentryLogger } from '@/utils/logs'
 
+type SelectValue = string | null
+
 const items = availableEnvs().map((item) => ({ name: item, value: item }))
 
 export const AdminView = () => {
-  const env = availableEnvs().includes(TARGET_DEV_ENV) ? TARGET_DEV_ENV : null
-  const [value, setValue] = useState<null | string>(env)
+  const [customUrlError, setCustomUrlError] = useState<string | null>(null)
+  const { setSelectedNode, selectedNode, setTargetEnv, targetEnv } = useEnvironmentStore((state) => ({
+    ...state.actions,
+    ...state,
+  }))
+  const determinedNode = selectedNode || NODE_URL
+  const isCustomUrl = availableNodes.find(({ value }) => value === determinedNode)
+  const [customUrlChecked, setCustomUrlChecked] = useState(!isCustomUrl)
   const { displaySnackbar } = useSnackbar()
+  const customNodeRef = useRef<HTMLInputElement>(null)
 
-  const handleEnvironmentChange = (value?: string | null | undefined) => {
+  const handleEnvironmentChange = (value?: SelectValue) => {
     if (!value) {
       return
     }
-    setEnvInLocalStorage(value)
+    setTargetEnv(value)
 
     if (TARGET_DEV_ENV) {
-      setValue(TARGET_DEV_ENV)
       window.location.reload()
     }
   }
 
+  const handleNodeChange = (value?: SelectValue) => {
+    setSelectedNode(value as string)
+  }
+
+  const handleCustomNodeChange = () => {
+    if (customNodeRef.current) {
+      if (customNodeRef.current.value.length) {
+        setSelectedNode(customNodeRef.current.value)
+      } else {
+        setCustomUrlError('Custom url cannot be empty')
+      }
+    }
+  }
+
+  const toggleCustomUrl = () => {
+    setCustomUrlError(null)
+    if (!customUrlChecked) {
+      setCustomUrlChecked(true)
+    } else {
+      setCustomUrlChecked(false)
+      setSelectedNode(isCustomUrl ? availableNodes[0].value : determinedNode)
+    }
+  }
+
   const handleExportClick = () => {
     const storageKeys = Object.keys(window.localStorage)
     const storage = storageKeys.reduce((acc, key) => {
@@ -77,10 +114,29 @@ export const AdminView = () => {
   }
 
   return (
-    <>
+    <div style={{ padding: '40px' }}>
       <div>
         <Text variant="h2">Choose environment</Text>
-        <Select items={items} onChange={handleEnvironmentChange} value={value} />
+        <Select items={items} onChange={handleEnvironmentChange} value={targetEnv} />
+      </div>
+      <div style={{ margin: '20px 0' }}>
+        <Text variant="h2">
+          Choose node url <Checkbox value={customUrlChecked} label="Custom URL" onChange={toggleCustomUrl} />
+        </Text>
+        {customUrlChecked ? (
+          <>
+            <TextField
+              ref={customNodeRef}
+              placeholder="Type your Node URL"
+              defaultValue={determinedNode}
+              error={!!customUrlError}
+              helperText={customUrlError}
+            />
+            <Button onClick={handleCustomNodeChange}>Update node</Button>
+          </>
+        ) : (
+          <Select items={availableNodes} onChange={handleNodeChange} value={determinedNode} />
+        )}
       </div>
       <div>
         <Text variant="h2">Import/Export Local state</Text>
@@ -93,6 +149,6 @@ export const AdminView = () => {
         {' '}
         <Link to={absoluteRoutes.viewer.index()}>Back to homepage</Link>
       </div>
-    </>
+    </div>
   )
 }