feat(explorer,governance): always allow selecting a node, add node guard (#3678)

This commit is contained in:
Matthew Russell 2023-05-09 12:58:09 -07:00 committed by GitHub
parent 76ddf45f4c
commit bded1d32ba
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 318 additions and 168 deletions

View File

@ -1,9 +1,18 @@
import { NetworkLoader, useInitializeEnv } from '@vegaprotocol/environment'; import {
AppFailure,
NetworkLoader,
NodeGuard,
NodeSwitcherDialog,
useEnvironment,
useInitializeEnv,
useNodeSwitcherStore,
} from '@vegaprotocol/environment';
import { TendermintWebsocketProvider } from './contexts/websocket/tendermint-websocket-provider'; import { TendermintWebsocketProvider } from './contexts/websocket/tendermint-websocket-provider';
import { Loader, Splash } from '@vegaprotocol/ui-toolkit'; import { Loader, Splash } from '@vegaprotocol/ui-toolkit';
import { DEFAULT_CACHE_CONFIG } from '@vegaprotocol/apollo-client'; import { DEFAULT_CACHE_CONFIG } from '@vegaprotocol/apollo-client';
import { RouterProvider } from 'react-router-dom'; import { RouterProvider } from 'react-router-dom';
import { router } from './routes/router-config'; import { router } from './routes/router-config';
import { t } from '@vegaprotocol/i18n';
const splashLoading = ( const splashLoading = (
<Splash> <Splash>
@ -12,10 +21,23 @@ const splashLoading = (
); );
function App() { function App() {
const { VEGA_URL } = useEnvironment();
const [nodeSwitcherOpen, setNodeSwitcherOpen] = useNodeSwitcherStore(
(store) => [store.dialogOpen, store.setDialogOpen]
);
return ( return (
<TendermintWebsocketProvider> <TendermintWebsocketProvider>
<NetworkLoader cache={DEFAULT_CACHE_CONFIG}> <NetworkLoader cache={DEFAULT_CACHE_CONFIG}>
<NodeGuard
skeleton={<div>{t('Loading')}</div>}
failure={<AppFailure title={t(`Node: ${VEGA_URL} is unsuitable`)} />}
>
<RouterProvider router={router} fallbackElement={splashLoading} /> <RouterProvider router={router} fallbackElement={splashLoading} />
</NodeGuard>
<NodeSwitcherDialog
open={nodeSwitcherOpen}
setOpen={setNodeSwitcherOpen}
/>
</NetworkLoader> </NetworkLoader>
</TendermintWebsocketProvider> </TendermintWebsocketProvider>
); );

View File

@ -1,13 +1,19 @@
import { NodeSwitcherDialog, useEnvironment } from '@vegaprotocol/environment'; import {
useEnvironment,
useNodeSwitcherStore,
} from '@vegaprotocol/environment';
import { t } from '@vegaprotocol/i18n'; import { t } from '@vegaprotocol/i18n';
import { useScreenDimensions } from '@vegaprotocol/react-helpers'; import { useScreenDimensions } from '@vegaprotocol/react-helpers';
import { ExternalLink, Link } from '@vegaprotocol/ui-toolkit'; import { ExternalLink, Link } from '@vegaprotocol/ui-toolkit';
import { useMemo, useState } from 'react'; import { useMemo } from 'react';
import { ENV } from '../../config/env'; import { ENV } from '../../config/env';
export const Footer = () => { export const Footer = () => {
const { VEGA_URL, GIT_COMMIT_HASH, GIT_ORIGIN_URL } = useEnvironment(); const { VEGA_URL, GIT_COMMIT_HASH, GIT_ORIGIN_URL } = useEnvironment();
const [nodeSwitcherOpen, setNodeSwitcherOpen] = useState(false); const setNodeSwitcherOpen = useNodeSwitcherStore(
(store) => store.setDialogOpen
);
const { screenSize } = useScreenDimensions(); const { screenSize } = useScreenDimensions();
const showFullFeedbackLabel = useMemo( const showFullFeedbackLabel = useMemo(
() => ['md', 'lg', 'xl', 'xxl', 'xxxl'].includes(screenSize), () => ['md', 'lg', 'xl', 'xxl', 'xxxl'].includes(screenSize),
@ -15,7 +21,6 @@ export const Footer = () => {
); );
return ( return (
<>
<footer className="grid grid-rows-2 grid-cols-[1fr_auto] text-xs md:text-md md:flex md:col-span-2 px-4 py-2 gap-4 border-t border-vega-light-200 dark:border-vega-dark-200"> <footer className="grid grid-rows-2 grid-cols-[1fr_auto] text-xs md:text-md md:flex md:col-span-2 px-4 py-2 gap-4 border-t border-vega-light-200 dark:border-vega-dark-200">
<div className="flex justify-between gap-2 align-middle"> <div className="flex justify-between gap-2 align-middle">
{GIT_COMMIT_HASH && ( {GIT_COMMIT_HASH && (
@ -50,11 +55,6 @@ export const Footer = () => {
</div> </div>
</div> </div>
</footer> </footer>
<NodeSwitcherDialog
open={nodeSwitcherOpen}
setOpen={setNodeSwitcherOpen}
/>
</>
); );
}; };

View File

@ -37,6 +37,10 @@ import {
useEnvironment, useEnvironment,
NetworkLoader, NetworkLoader,
useInitializeEnv, useInitializeEnv,
NodeGuard,
AppFailure,
NodeSwitcherDialog,
useNodeSwitcherStore,
} from '@vegaprotocol/environment'; } from '@vegaprotocol/environment';
import { ENV } from './config'; import { ENV } from './config';
import type { InMemoryCacheConfig } from '@apollo/client'; import type { InMemoryCacheConfig } from '@apollo/client';
@ -48,6 +52,7 @@ import {
TELEMETRY_ON, TELEMETRY_ON,
} from './components/telemetry-dialog/telemetry-dialog'; } from './components/telemetry-dialog/telemetry-dialog';
import { useLocalStorage } from '@vegaprotocol/react-helpers'; import { useLocalStorage } from '@vegaprotocol/react-helpers';
import { useTranslation } from 'react-i18next';
const cache: InMemoryCacheConfig = { const cache: InMemoryCacheConfig = {
typePolicies: { typePolicies: {
@ -181,9 +186,19 @@ const ScrollToTop = () => {
const AppContainer = () => { const AppContainer = () => {
const { config, loading, error } = useEthereumConfig(); const { config, loading, error } = useEthereumConfig();
const { VEGA_ENV, GIT_COMMIT_HASH, GIT_BRANCH, ETHEREUM_PROVIDER_URL } = const {
useEnvironment(); VEGA_ENV,
VEGA_URL,
GIT_COMMIT_HASH,
GIT_BRANCH,
ETHEREUM_PROVIDER_URL,
} = useEnvironment();
const [telemetryOn] = useLocalStorage(TELEMETRY_ON); const [telemetryOn] = useLocalStorage(TELEMETRY_ON);
const { t } = useTranslation();
const [nodeSwitcherOpen, setNodeSwitcher] = useNodeSwitcherStore((store) => [
store.dialogOpen,
store.setDialogOpen,
]);
useEffect(() => { useEffect(() => {
if (ENV.dsn && telemetryOn) { if (ENV.dsn && telemetryOn) {
@ -219,6 +234,12 @@ const AppContainer = () => {
<ScrollToTop /> <ScrollToTop />
<AppStateProvider> <AppStateProvider>
<div className="grid min-h-full text-white"> <div className="grid min-h-full text-white">
<NodeGuard
skeleton={<div>{t('Loading')}</div>}
failure={
<AppFailure title={t('NodeUnsuitable', { url: VEGA_URL })} />
}
>
<AsyncRenderer<EthereumConfig | null> <AsyncRenderer<EthereumConfig | null>
loading={loading} loading={loading}
data={config} data={config}
@ -232,8 +253,10 @@ const AppContainer = () => {
) )
} }
/> />
</NodeGuard>
</div> </div>
</AppStateProvider> </AppStateProvider>
<NodeSwitcherDialog open={nodeSwitcherOpen} setOpen={setNodeSwitcher} />
</Router> </Router>
); );
}; };

View File

@ -811,5 +811,6 @@
"OptOutOfTelemetry": "You can opt out any time via settings", "OptOutOfTelemetry": "You can opt out any time via settings",
"NoThanks": "No thanks", "NoThanks": "No thanks",
"ShareData": "Share data", "ShareData": "Share data",
"ContinueSharingData": "Continue sharing data" "ContinueSharingData": "Continue sharing data",
"NodeUnsuitable": "Node: {{url}} is unsuitable"
} }

View File

@ -12,6 +12,7 @@ import {
createMarketsDataFragment, createMarketsDataFragment,
assetQuery, assetQuery,
networkParamsQuery, networkParamsQuery,
nodeGuardQuery,
} from '@vegaprotocol/mock'; } from '@vegaprotocol/mock';
import { import {
addDecimalsFormatNumber, addDecimalsFormatNumber,
@ -158,6 +159,7 @@ describe('Closed markets', { tags: '@smoke' }, () => {
cy.mockGQL((req) => { cy.mockGQL((req) => {
aliasGQLQuery(req, 'ChainId', chainIdQuery()); aliasGQLQuery(req, 'ChainId', chainIdQuery());
aliasGQLQuery(req, 'Statistics', statisticsQuery()); aliasGQLQuery(req, 'Statistics', statisticsQuery());
aliasGQLQuery(req, 'NodeGuard', nodeGuardQuery());
aliasGQLQuery(req, 'NetworkParams', networkParamsQuery()); aliasGQLQuery(req, 'NetworkParams', networkParamsQuery());
aliasGQLQuery( aliasGQLQuery(
req, req,

View File

@ -20,6 +20,7 @@ import {
marketsDataQuery, marketsDataQuery,
marketsQuery, marketsQuery,
networkParamsQuery, networkParamsQuery,
nodeGuardQuery,
ordersQuery, ordersQuery,
positionsQuery, positionsQuery,
proposalListQuery, proposalListQuery,
@ -82,6 +83,7 @@ const mockTradingPage = (
) => { ) => {
aliasGQLQuery(req, 'ChainId', chainIdQuery()); aliasGQLQuery(req, 'ChainId', chainIdQuery());
aliasGQLQuery(req, 'Statistics', statisticsQuery()); aliasGQLQuery(req, 'Statistics', statisticsQuery());
aliasGQLQuery(req, 'NodeGuard', nodeGuardQuery());
aliasGQLQuery( aliasGQLQuery(
req, req,
'Markets', 'Markets',

View File

@ -1,5 +1,6 @@
import type { InMemoryCacheConfig } from '@apollo/client'; import type { InMemoryCacheConfig } from '@apollo/client';
import { import {
AppFailure,
NetworkLoader, NetworkLoader,
NodeGuard, NodeGuard,
useEnvironment, useEnvironment,
@ -9,7 +10,6 @@ import { MaintenancePage } from '@vegaprotocol/ui-toolkit';
import { VegaWalletProvider } from '@vegaprotocol/wallet'; import { VegaWalletProvider } from '@vegaprotocol/wallet';
import dynamic from 'next/dynamic'; import dynamic from 'next/dynamic';
import type { ReactNode } from 'react'; import type { ReactNode } from 'react';
import { AppFailure } from './app-failure';
import { Web3Provider } from './web3-provider'; import { Web3Provider } from './web3-provider';
export const DynamicLoader = dynamic(() => import('../preloader/preloader'), { export const DynamicLoader = dynamic(() => import('../preloader/preloader'), {

View File

@ -1,3 +1,2 @@
export * from './app-failure';
export * from './app-loader'; export * from './app-loader';
export * from './web3-provider'; export * from './web3-provider';

View File

@ -1,11 +1,14 @@
import { useCallback } from 'react'; import { useCallback } from 'react';
import { useEnvironment, useNodeHealth } from '@vegaprotocol/environment'; import {
useEnvironment,
useNodeHealth,
useNodeSwitcherStore,
} from '@vegaprotocol/environment';
import { t } from '@vegaprotocol/i18n'; import { t } from '@vegaprotocol/i18n';
import type { Intent } from '@vegaprotocol/ui-toolkit'; import type { Intent } from '@vegaprotocol/ui-toolkit';
import { Indicator, ExternalLink } from '@vegaprotocol/ui-toolkit'; import { Indicator, ExternalLink } from '@vegaprotocol/ui-toolkit';
import classNames from 'classnames'; import classNames from 'classnames';
import type { ButtonHTMLAttributes, ReactNode } from 'react'; import type { ButtonHTMLAttributes, ReactNode } from 'react';
import { useGlobalStore } from '../../stores';
export const Footer = () => { export const Footer = () => {
return ( return (
@ -20,9 +23,7 @@ export const Footer = () => {
export const NodeHealth = () => { export const NodeHealth = () => {
const { VEGA_URL, VEGA_INCIDENT_URL } = useEnvironment(); const { VEGA_URL, VEGA_INCIDENT_URL } = useEnvironment();
const setNodeSwitcher = useGlobalStore( const setNodeSwitcher = useNodeSwitcherStore((store) => store.setDialogOpen);
(store) => (open: boolean) => store.update({ nodeSwitcherDialog: open })
);
const { datanodeBlockHeight, text, intent } = useNodeHealth(); const { datanodeBlockHeight, text, intent } = useNodeHealth();
const onClick = useCallback(() => { const onClick = useCallback(() => {
setNodeSwitcher(true); setNodeSwitcher(true);

View File

@ -21,9 +21,10 @@ import {
NodeSwitcherDialog, NodeSwitcherDialog,
useEnvironment, useEnvironment,
useInitializeEnv, useInitializeEnv,
useNodeSwitcherStore,
} from '@vegaprotocol/environment'; } from '@vegaprotocol/environment';
import './styles.css'; import './styles.css';
import { useGlobalStore, usePageTitleStore } from '../stores'; import { usePageTitleStore } from '../stores';
import { Footer } from '../components/footer'; import { Footer } from '../components/footer';
import DialogsContainer from './dialogs-container'; import DialogsContainer from './dialogs-container';
import ToastsManager from './toasts-manager'; import ToastsManager from './toasts-manager';
@ -115,11 +116,10 @@ function AppBody({ Component }: AppProps) {
function VegaTradingApp(props: AppProps) { function VegaTradingApp(props: AppProps) {
const status = useEnvironment((store) => store.status); const status = useEnvironment((store) => store.status);
const { nodeSwitcherOpen, setNodeSwitcher } = useGlobalStore((store) => ({ const [nodeSwitcherOpen, setNodeSwitcher] = useNodeSwitcherStore((store) => [
nodeSwitcherOpen: store.nodeSwitcherDialog, store.dialogOpen,
setNodeSwitcher: (open: boolean) => store.setDialogOpen,
store.update({ nodeSwitcherDialog: open }), ]);
}));
useInitializeEnv(); useInitializeEnv();

View File

@ -3,7 +3,6 @@ import { create } from 'zustand';
import produce from 'immer'; import produce from 'immer';
interface GlobalStore { interface GlobalStore {
nodeSwitcherDialog: boolean;
marketId: string | null; marketId: string | null;
update: (store: Partial<Omit<GlobalStore, 'update'>>) => void; update: (store: Partial<Omit<GlobalStore, 'update'>>) => void;
shouldDisplayWelcomeDialog: boolean; shouldDisplayWelcomeDialog: boolean;
@ -15,7 +14,6 @@ interface PageTitleStore {
} }
export const useGlobalStore = create<GlobalStore>()((set) => ({ export const useGlobalStore = create<GlobalStore>()((set) => ({
nodeSwitcherDialog: false,
marketId: LocalStorage.getItem('marketId') || null, marketId: LocalStorage.getItem('marketId') || null,
shouldDisplayWelcomeDialog: false, shouldDisplayWelcomeDialog: false,
update: (newState) => { update: (newState) => {

View File

@ -7,6 +7,7 @@ export * from '../candles-chart/src/lib/chart.mock';
export * from '../deal-ticket/src/hooks/estimate-order.mock'; export * from '../deal-ticket/src/hooks/estimate-order.mock';
export * from '../deposits/src/lib/deposit.mock'; export * from '../deposits/src/lib/deposit.mock';
export * from '../environment/src/utils/node.mock'; export * from '../environment/src/utils/node.mock';
export * from '../environment/src/components/node-guard/node-guard.mock';
export * from '../fills/src/lib/fills.mock'; export * from '../fills/src/lib/fills.mock';
export * from '../proposals/src/lib/proposals-data-provider/proposals.mock'; export * from '../proposals/src/lib/proposals-data-provider/proposals.mock';
export * from '../ledger/src/lib/ledger-entries.mock'; export * from '../ledger/src/lib/ledger-entries.mock';

View File

@ -1,6 +1,6 @@
import { t } from '@vegaprotocol/i18n'; import { t } from '@vegaprotocol/i18n';
import { Button } from '@vegaprotocol/ui-toolkit'; import { Button } from '@vegaprotocol/ui-toolkit';
import { useGlobalStore } from '../../stores'; import { useNodeSwitcherStore } from '../../hooks/use-node-switcher-store';
export const AppFailure = ({ export const AppFailure = ({
title, title,
@ -9,17 +9,13 @@ export const AppFailure = ({
title: string; title: string;
error?: string | null; error?: string | null;
}) => { }) => {
const { setNodeSwitcher } = useGlobalStore((store) => ({ const setNodeSwitcher = useNodeSwitcherStore((store) => store.setDialogOpen);
nodeSwitcherOpen: store.nodeSwitcherDialog,
setNodeSwitcher: (open: boolean) =>
store.update({ nodeSwitcherDialog: open }),
}));
const nonIdealWrapperClasses = const nonIdealWrapperClasses =
'h-full min-h-screen flex items-center justify-center'; 'h-full min-h-screen flex items-center justify-center';
return ( return (
<div className={nonIdealWrapperClasses}> <div className={nonIdealWrapperClasses}>
<div className="text-center"> <div className="text-center">
<h1 className="text-xl mb-4">{title}</h1> <p className="text-xl mb-4">{title}</p>
{error && <p className="text-sm mb-8">{error}</p>} {error && <p className="text-sm mb-8">{error}</p>}
<Button onClick={() => setNodeSwitcher(true)}> <Button onClick={() => setNodeSwitcher(true)}>
{t('Change node')} {t('Change node')}

View File

@ -0,0 +1 @@
export * from './app-failure';

View File

@ -1,3 +1,4 @@
export * from './app-failure';
export * from './network-loader'; export * from './network-loader';
export * from './network-switcher'; export * from './network-switcher';
export * from './node-guard'; export * from './node-guard';

View File

@ -0,0 +1,11 @@
query NodeGuard {
lastBlockHeight
networkParametersConnection {
edges {
node {
key
value
}
}
}
}

View File

@ -0,0 +1,51 @@
import * as Types from '@vegaprotocol/types';
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
const defaultOptions = {} as const;
export type NodeGuardQueryVariables = Types.Exact<{ [key: string]: never; }>;
export type NodeGuardQuery = { __typename?: 'Query', lastBlockHeight: string, networkParametersConnection: { __typename?: 'NetworkParametersConnection', edges?: Array<{ __typename?: 'NetworkParameterEdge', node: { __typename?: 'NetworkParameter', key: string, value: string } } | null> | null } };
export const NodeGuardDocument = gql`
query NodeGuard {
lastBlockHeight
networkParametersConnection {
edges {
node {
key
value
}
}
}
}
`;
/**
* __useNodeGuardQuery__
*
* To run a query within a React component, call `useNodeGuardQuery` and pass it any options that fit your needs.
* When your component renders, `useNodeGuardQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useNodeGuardQuery({
* variables: {
* },
* });
*/
export function useNodeGuardQuery(baseOptions?: Apollo.QueryHookOptions<NodeGuardQuery, NodeGuardQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<NodeGuardQuery, NodeGuardQueryVariables>(NodeGuardDocument, options);
}
export function useNodeGuardLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<NodeGuardQuery, NodeGuardQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<NodeGuardQuery, NodeGuardQueryVariables>(NodeGuardDocument, options);
}
export type NodeGuardQueryHookResult = ReturnType<typeof useNodeGuardQuery>;
export type NodeGuardLazyQueryHookResult = ReturnType<typeof useNodeGuardLazyQuery>;
export type NodeGuardQueryResult = Apollo.QueryResult<NodeGuardQuery, NodeGuardQueryVariables>;

View File

@ -0,0 +1,53 @@
import type { NodeGuardQuery } from './__generated__/NodeGuard';
import merge from 'lodash/merge';
import type { PartialDeep } from 'type-fest';
export const nodeGuardQuery = (
override?: PartialDeep<NodeGuardQuery>
): NodeGuardQuery => {
const defaultResult: NodeGuardQuery = {
lastBlockHeight: '11',
networkParametersConnection: {
__typename: 'NetworkParametersConnection',
edges: [
{
__typename: 'NetworkParameterEdge',
node: {
__typename: 'NetworkParameter' as const,
key: 'governance.proposal.market.requiredMajority',
value: '0.66',
},
},
{
__typename: 'NetworkParameterEdge',
node: {
__typename: 'NetworkParameter' as const,
key: 'blockchains.ethereumConfig',
value: JSON.stringify({
network_id: '3',
chain_id: '3',
collateral_bridge_contract: {
address: '0x7fe27d970bc8Afc3B11Cc8d9737bfB66B1efd799',
},
multisig_control_contract: {
address: '0x6eBc32d66277D94DB8FF2ccF86E36f37F29a52D3',
deployment_block_height: 12341882,
},
staking_bridge_contract: {
address: '0xFFb0A0d4806502ceF491aF1141f66669A1Bd0D03',
deployment_block_height: 11177313,
},
token_vesting_contract: {
address: '0x680fF88252FA7071CAce7398e77872d54D781d0B',
deployment_block_height: 11177353,
},
confirmations: 3,
}),
},
},
],
},
};
return merge(defaultResult, override);
};

View File

@ -1,5 +1,5 @@
import type { ReactNode } from 'react'; import type { ReactNode } from 'react';
import { useStatisticsQuery } from '../../utils/__generated__/Node'; import { useNodeGuardQuery } from './__generated__/NodeGuard';
export const NodeGuard = ({ export const NodeGuard = ({
children, children,
@ -10,14 +10,19 @@ export const NodeGuard = ({
failure: ReactNode; failure: ReactNode;
skeleton: ReactNode; skeleton: ReactNode;
}) => { }) => {
const { error, loading } = useStatisticsQuery(); const { data, error, loading } = useNodeGuardQuery();
const wrapperClasses = 'h-full min-h-screen flex items-center justify-center'; const wrapperClasses =
'h-full min-h-screen flex items-center justify-center text-black dark:text-white';
if (loading) { if (loading) {
return <div className={wrapperClasses}>{skeleton}</div>; return <div className={wrapperClasses}>{skeleton}</div>;
} }
if (error) { // It is possible for nodes to have a functioning datanode, but not return
// any net params. The app cannot safely function without net params
const netParamEdges = data?.networkParametersConnection.edges;
if (error || !netParamEdges || !netParamEdges.length) {
return <div className={wrapperClasses}>{failure}</div>; return <div className={wrapperClasses}>{failure}</div>;
} }

View File

@ -124,29 +124,6 @@ export const RowData = ({
return false; return false;
}; };
const getIsNodeDisabled = () => {
if (!isValidUrl(url)) {
return true;
}
// if still waiting or query errored disable node
if (loading || error) {
return true;
}
if (subLoading || subError) {
return true;
}
// if we are still waiting for a header entry for this
// url disable the node
if (!headers) {
return true;
}
return false;
};
const getSubFailed = ( const getSubFailed = (
subError: ApolloError | undefined, subError: ApolloError | undefined,
subFailed: boolean subFailed: boolean
@ -160,12 +137,7 @@ export const RowData = ({
<> <>
{id !== CUSTOM_NODE_KEY && ( {id !== CUSTOM_NODE_KEY && (
<div className="break-all" data-testid="node"> <div className="break-all" data-testid="node">
<Radio <Radio id={`node-url-${id}`} value={url} label={url} />
id={`node-url-${id}`}
value={url}
label={url}
disabled={getIsNodeDisabled()}
/>
</div> </div>
)} )}
<LayoutCell <LayoutCell

View File

@ -1,5 +1,6 @@
export * from './use-environment'; export * from './use-environment';
export * from './use-links'; export * from './use-links';
export * from './use-node-health'; export * from './use-node-health';
export * from './use-node-switcher-store';
export * from './use-vega-releases'; export * from './use-vega-releases';
export * from './use-vega-release'; export * from './use-vega-release';

View File

@ -0,0 +1,11 @@
import { create } from 'zustand';
export const useNodeSwitcherStore = create<{
dialogOpen: boolean;
setDialogOpen: (isOpen: boolean) => void;
}>()((set) => ({
dialogOpen: false,
setDialogOpen: (isOpen) => {
set({ dialogOpen: isOpen });
},
}));

View File

@ -1,7 +1,11 @@
import { Fragment, useState } from 'react'; import { Fragment } from 'react';
import { t } from '@vegaprotocol/i18n'; import { t } from '@vegaprotocol/i18n';
import { Link, Lozenge } from '@vegaprotocol/ui-toolkit'; import { Link, Lozenge } from '@vegaprotocol/ui-toolkit';
import { NodeSwitcherDialog, useEnvironment } from '@vegaprotocol/environment'; import {
NodeSwitcherDialog,
useEnvironment,
useNodeSwitcherStore,
} from '@vegaprotocol/environment';
const getFeedbackLinks = (gitOriginUrl?: string) => const getFeedbackLinks = (gitOriginUrl?: string) =>
[ [
@ -19,18 +23,18 @@ export const NetworkInfo = () => {
GITHUB_FEEDBACK_URL, GITHUB_FEEDBACK_URL,
ETHEREUM_PROVIDER_URL, ETHEREUM_PROVIDER_URL,
} = useEnvironment(); } = useEnvironment();
const [nodeSwitcherOpen, setNodeSwitcherOpen] = useState(false);
const setNodeSwitcher = useNodeSwitcherStore((store) => store.setDialogOpen);
const feedbackLinks = getFeedbackLinks(GITHUB_FEEDBACK_URL); const feedbackLinks = getFeedbackLinks(GITHUB_FEEDBACK_URL);
return ( return (
<>
<div data-testid="git-info"> <div data-testid="git-info">
<p data-testid="git-network-data" className="mb-2"> <p data-testid="git-network-data" className="mb-2">
{t('Reading network data from')}{' '} {t('Reading network data from')}{' '}
<Lozenge className="bg-neutral-300 dark:bg-neutral-700"> <Lozenge className="bg-neutral-300 dark:bg-neutral-700">
{VEGA_URL} {VEGA_URL}
</Lozenge> </Lozenge>
. <Link onClick={() => setNodeSwitcherOpen(true)}>{t('Edit')}</Link> . <Link onClick={() => setNodeSwitcher(true)}>{t('Edit')}</Link>
</p> </p>
<p data-testid="git-eth-data" className="mb-2 break-all"> <p data-testid="git-eth-data" className="mb-2 break-all">
{t('Reading Ethereum data from')}{' '} {t('Reading Ethereum data from')}{' '}
@ -73,10 +77,5 @@ export const NetworkInfo = () => {
</p> </p>
)} )}
</div> </div>
<NodeSwitcherDialog
open={nodeSwitcherOpen}
setOpen={setNodeSwitcherOpen}
/>
</>
); );
}; };