feat(trading): upgrade banner (#3883)
This commit is contained in:
parent
a51ab4d98d
commit
a0eb381ab1
@ -5,6 +5,9 @@ import { ProtocolUpgradeProposalDetailInfo } from '../components/protocol-upgrad
|
||||
import { getNormalisedVotingPower } from '../../staking/shared';
|
||||
import type { NodesFragmentFragment } from '../../staking/home/__generated__/Nodes';
|
||||
import type { ProtocolUpgradeProposalFieldsFragment } from '@vegaprotocol/proposals';
|
||||
import { useVegaRelease } from '@vegaprotocol/environment';
|
||||
import { ExternalLink } from '@vegaprotocol/ui-toolkit';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export interface ProtocolUpgradeProposalProps {
|
||||
proposal: ProtocolUpgradeProposalFieldsFragment;
|
||||
@ -42,6 +45,9 @@ export const ProtocolUpgradeProposal = ({
|
||||
lastBlockHeight,
|
||||
consensusValidators,
|
||||
}: ProtocolUpgradeProposalProps) => {
|
||||
const { t } = useTranslation();
|
||||
const releaseInfo = useVegaRelease(proposal.vegaReleaseTag);
|
||||
|
||||
const consensusApprovals = useMemo(
|
||||
() => getConsensusApprovals(consensusValidators || [], proposal),
|
||||
[consensusValidators, proposal]
|
||||
@ -78,6 +84,14 @@ export const ProtocolUpgradeProposal = ({
|
||||
totalConsensusValidators={consensusValidators.length}
|
||||
/>
|
||||
)}
|
||||
|
||||
{releaseInfo && releaseInfo.htmlUrl && (
|
||||
<div className="mb-10">
|
||||
<ExternalLink href={releaseInfo.htmlUrl}>
|
||||
{t('Explore release on GitHub')}
|
||||
</ExternalLink>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
@ -14,3 +14,5 @@ NX_VEGA_REPO_URL=https://github.com/vegaprotocol/vega/releases
|
||||
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/main/announcements.json
|
||||
NX_WALLETCONNECT_PROJECT_ID=fe8091dc35738863e509fc4947525c72
|
||||
|
||||
# TAG name of the current app version - TODO: bump to the latest upon release
|
||||
NX_APP_VERSION=v0.20.7-core-0.71.4
|
||||
|
@ -1 +1,2 @@
|
||||
export * from './announcement-banner';
|
||||
export * from './upgrade-banner';
|
||||
|
77
apps/trading/components/banner/upgrade-banner.tsx
Normal file
77
apps/trading/components/banner/upgrade-banner.tsx
Normal file
@ -0,0 +1,77 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import {
|
||||
ReleasesFeed,
|
||||
useEnvironment,
|
||||
useReleases,
|
||||
Networks,
|
||||
} from '@vegaprotocol/environment';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import {
|
||||
CopyWithTooltip,
|
||||
ExternalLink,
|
||||
Intent,
|
||||
NotificationBanner,
|
||||
VegaIcon,
|
||||
VegaIconNames,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
|
||||
const CANONICAL_URL = 'https://vega.trading';
|
||||
|
||||
type UpgradeBannerProps = {
|
||||
showVersionChange: boolean;
|
||||
};
|
||||
export const UpgradeBanner = ({ showVersionChange }: UpgradeBannerProps) => {
|
||||
const [visible, setVisible] = useState(true);
|
||||
const { data } = useReleases(ReleasesFeed.FrontEnd);
|
||||
const { APP_VERSION, VEGA_ENV } = useEnvironment();
|
||||
/**
|
||||
* Filtering out the release candidates.
|
||||
*/
|
||||
const latest = useMemo(() => {
|
||||
const valid =
|
||||
VEGA_ENV === Networks.MAINNET
|
||||
? data?.filter((r) => !/-rc$/i.test(r.tagName))
|
||||
: data;
|
||||
|
||||
return valid && valid.length > 0 ? valid[0] : undefined;
|
||||
}, [VEGA_ENV, data]);
|
||||
|
||||
if (!visible || !latest || latest.tagName === APP_VERSION) return null;
|
||||
|
||||
const versionChange = (
|
||||
<span>
|
||||
<span className="line-through text-vega-light-300 dark:text-vega-dark-300">
|
||||
{APP_VERSION}
|
||||
</span>{' '}
|
||||
<VegaIcon size={14} name={VegaIconNames.ARROW_RIGHT} />{' '}
|
||||
<span className="text-vega-orange-500 dark:text-vega-yellow-500">
|
||||
<ExternalLink href={latest.htmlUrl}>{latest.tagName}</ExternalLink>
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
|
||||
return (
|
||||
<NotificationBanner
|
||||
intent={Intent.Warning}
|
||||
onClose={() => {
|
||||
setVisible(false);
|
||||
}}
|
||||
>
|
||||
<div className="uppercase ">
|
||||
{t('Upgrade to the latest version of Console')}{' '}
|
||||
{showVersionChange && versionChange}
|
||||
</div>
|
||||
<div data-testid="bookmark-message">
|
||||
{t('Bookmark')}{' '}
|
||||
<ExternalLink href={CANONICAL_URL}>{t('vega.trading')}</ExternalLink>
|
||||
<CopyWithTooltip text={CANONICAL_URL}>
|
||||
<button title={t('Copy %s', CANONICAL_URL)} className="text-white">
|
||||
<span className="sr-only">{t('Copy %s', CANONICAL_URL)}</span>
|
||||
<VegaIcon size={14} name={VegaIconNames.COPY} />
|
||||
</button>
|
||||
</CopyWithTooltip>{' '}
|
||||
{'to always see the latest version.'}
|
||||
</div>
|
||||
</NotificationBanner>
|
||||
);
|
||||
};
|
@ -31,7 +31,7 @@ import ToastsManager from './toasts-manager';
|
||||
import { HashRouter, useLocation, useSearchParams } from 'react-router-dom';
|
||||
import { Connectors } from '../lib/vega-connectors';
|
||||
import { ViewingBanner } from '../components/viewing-banner';
|
||||
import { AnnouncementBanner } from '../components/banner';
|
||||
import { AnnouncementBanner, UpgradeBanner } from '../components/banner';
|
||||
import { AppLoader, DynamicLoader } from '../components/app-loader';
|
||||
import { Navbar } from '../components/navbar';
|
||||
import { ENV } from '../lib/config';
|
||||
@ -99,6 +99,7 @@ function AppBody({ Component }: AppProps) {
|
||||
mode={ProtocolUpgradeCountdownMode.IN_ESTIMATED_TIME_REMAINING}
|
||||
/>
|
||||
<ViewingBanner />
|
||||
<UpgradeBanner showVersionChange={true} />
|
||||
</div>
|
||||
<main data-testid={location.pathname}>
|
||||
<Component />
|
||||
|
@ -4,3 +4,4 @@ export * from './use-node-health';
|
||||
export * from './use-node-switcher-store';
|
||||
export * from './use-vega-releases';
|
||||
export * from './use-vega-release';
|
||||
export * from './use-releases';
|
||||
|
File diff suppressed because one or more lines are too long
@ -296,6 +296,7 @@ function compileEnvVars() {
|
||||
GIT_ORIGIN_URL: process.env['GIT_ORIGIN_URL'],
|
||||
ANNOUNCEMENTS_CONFIG_URL: process.env['NX_ANNOUNCEMENTS_CONFIG_URL'],
|
||||
VEGA_INCIDENT_URL: process.env['NX_VEGA_INCIDENT_URL'],
|
||||
APP_VERSION: process.env['NX_APP_VERSION'],
|
||||
};
|
||||
|
||||
return env;
|
||||
|
48
libs/environment/src/hooks/use-releases.spec.ts
Normal file
48
libs/environment/src/hooks/use-releases.spec.ts
Normal file
@ -0,0 +1,48 @@
|
||||
import { renderHook, waitFor } from '@testing-library/react';
|
||||
import fetchMock from 'fetch-mock';
|
||||
import { GITHUB_VEGA_FRONTEND_RELEASES_DATA } from './mocks/github-releases';
|
||||
import {
|
||||
GITHUB_VEGA_FRONTEND_RELEASES,
|
||||
ReleasesFeed,
|
||||
useReleases,
|
||||
} from './use-releases';
|
||||
|
||||
describe('useReleases', () => {
|
||||
afterEach(() => {
|
||||
fetchMock.reset();
|
||||
});
|
||||
|
||||
it('should return an empty list when request is unsuccessful', async () => {
|
||||
fetchMock.get(GITHUB_VEGA_FRONTEND_RELEASES, 404);
|
||||
|
||||
const { result } = renderHook(() => {
|
||||
return useReleases(ReleasesFeed.FrontEnd);
|
||||
});
|
||||
expect(result.current.loading).toBeTruthy();
|
||||
await waitFor(() => {
|
||||
expect(result.current.loading).toBeFalsy();
|
||||
expect(result.current.data).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
it('should return a list of releases', async () => {
|
||||
fetchMock.get(
|
||||
GITHUB_VEGA_FRONTEND_RELEASES,
|
||||
GITHUB_VEGA_FRONTEND_RELEASES_DATA
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useReleases(ReleasesFeed.FrontEnd));
|
||||
expect(result.current.loading).toBeTruthy();
|
||||
await waitFor(() => {
|
||||
expect(result.current.loading).toBeFalsy();
|
||||
expect(result.current).not.toEqual([]);
|
||||
const data = result.current.data;
|
||||
expect(data?.[0].tagName).toEqual('v0.20.8-core-0.71.4');
|
||||
expect(data?.[0].htmlUrl).toEqual(
|
||||
'https://github.com/vegaprotocol/frontend-monorepo/releases/tag/v0.20.8-core-0.71.4'
|
||||
);
|
||||
expect(data?.[1].tagName).toEqual('v0.20.6-core-0.71.4-3');
|
||||
expect(data?.[2].tagName).toEqual('v0.20.6-core-0.71.4');
|
||||
});
|
||||
});
|
||||
});
|
121
libs/environment/src/hooks/use-releases.ts
Normal file
121
libs/environment/src/hooks/use-releases.ts
Normal file
@ -0,0 +1,121 @@
|
||||
import { localLoggerFactory } from '@vegaprotocol/utils';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import z from 'zod';
|
||||
|
||||
export const GITHUB_VEGA_RELEASES =
|
||||
'https://api.github.com/repos/vegaprotocol/vega/releases';
|
||||
export const GITHUB_VEGA_DEV_RELEASES =
|
||||
'https://api.github.com/repos/vegaprotocol/vega-dev-releases/releases';
|
||||
export const GITHUB_VEGA_FRONTEND_RELEASES =
|
||||
'https://api.github.com/repos/vegaprotocol/frontend-monorepo/releases';
|
||||
|
||||
export enum ReleasesFeed {
|
||||
Vega = GITHUB_VEGA_RELEASES,
|
||||
VegaDev = GITHUB_VEGA_DEV_RELEASES,
|
||||
FrontEnd = GITHUB_VEGA_FRONTEND_RELEASES,
|
||||
}
|
||||
|
||||
const GithubReleaseSchema = z.object({
|
||||
url: z.string(),
|
||||
html_url: z.string(),
|
||||
id: z.number(),
|
||||
tag_name: z.string(),
|
||||
name: z.string().nullable(),
|
||||
draft: z.boolean(),
|
||||
created_at: z.string().datetime(),
|
||||
});
|
||||
|
||||
const GithubReleasesSchema = z.array(GithubReleaseSchema);
|
||||
|
||||
type ReleaseInfo = {
|
||||
id: number;
|
||||
name: string;
|
||||
tagName: string;
|
||||
htmlUrl: string;
|
||||
url: string;
|
||||
isDraft: boolean;
|
||||
createdAt: Date;
|
||||
isDevRelease: boolean;
|
||||
};
|
||||
|
||||
const toReleaseInfo = (
|
||||
releaseData: z.infer<typeof GithubReleaseSchema>,
|
||||
isDevRelease: boolean
|
||||
): ReleaseInfo => ({
|
||||
id: releaseData.id,
|
||||
name: releaseData.name || '',
|
||||
tagName: releaseData.tag_name,
|
||||
htmlUrl: releaseData.html_url,
|
||||
url: releaseData.url,
|
||||
isDraft: releaseData.draft,
|
||||
createdAt: new Date(releaseData.created_at),
|
||||
isDevRelease,
|
||||
});
|
||||
|
||||
export type ReleasesState = {
|
||||
loading: boolean;
|
||||
data: ReleaseInfo[] | null;
|
||||
error: Error | null | undefined;
|
||||
};
|
||||
export const fetchReleases = async (
|
||||
feed: ReleasesFeed,
|
||||
cache: RequestCache = 'default'
|
||||
) => {
|
||||
const response = await fetch(String(feed), {
|
||||
cache,
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const json = await response.json();
|
||||
const data = GithubReleasesSchema.parse(json);
|
||||
return data.map((d) => toReleaseInfo(d, feed === ReleasesFeed.VegaDev));
|
||||
}
|
||||
|
||||
return [];
|
||||
};
|
||||
|
||||
export const useReleases = (feed: ReleasesFeed, cache?: RequestCache) => {
|
||||
const [state, setState] = useState<ReleasesState>({
|
||||
loading: true,
|
||||
data: null,
|
||||
error: null,
|
||||
});
|
||||
const logger = localLoggerFactory({ application: 'github' });
|
||||
|
||||
const fetchData = useCallback(() => {
|
||||
let mounted = true;
|
||||
|
||||
fetchReleases(feed, cache)
|
||||
.then((releases) => {
|
||||
if (mounted) {
|
||||
setState({
|
||||
loading: false,
|
||||
data: releases.sort(
|
||||
(a, b) => b.createdAt.valueOf() - a.createdAt.valueOf()
|
||||
),
|
||||
error: null,
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
if (mounted) {
|
||||
setState({
|
||||
loading: false,
|
||||
data: null,
|
||||
error: err,
|
||||
});
|
||||
}
|
||||
logger.error('get releases from GitHub API', err);
|
||||
});
|
||||
|
||||
return () => {
|
||||
mounted = false;
|
||||
};
|
||||
}, [cache, feed, logger]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, [fetchData]);
|
||||
|
||||
return state;
|
||||
};
|
@ -1,9 +1,6 @@
|
||||
import { renderHook, waitFor } from '@testing-library/react';
|
||||
import fetchMock from 'fetch-mock';
|
||||
import {
|
||||
GITHUB_VEGA_DEV_RELEASES,
|
||||
GITHUB_VEGA_RELEASES,
|
||||
} from './use-vega-releases';
|
||||
import { GITHUB_VEGA_DEV_RELEASES, GITHUB_VEGA_RELEASES } from './use-releases';
|
||||
import {
|
||||
GITHUB_VEGA_RELEASES_DATA,
|
||||
GITHUB_VEGA_DEV_RELEASES_DATA,
|
||||
|
@ -2,7 +2,7 @@ import { useMemo } from 'react';
|
||||
import { useVegaReleases } from './use-vega-releases';
|
||||
|
||||
export const useVegaRelease = (tag: string, includeDevReleases = false) => {
|
||||
const { data } = useVegaReleases();
|
||||
const { data } = useVegaReleases(includeDevReleases, 'force-cache');
|
||||
return useMemo(() => {
|
||||
return data?.find((r) => r.tagName === tag);
|
||||
}, [data, tag]);
|
||||
|
@ -1,14 +1,11 @@
|
||||
import { renderHook, waitFor } from '@testing-library/react';
|
||||
import fetchMock from 'fetch-mock';
|
||||
import {
|
||||
GITHUB_VEGA_DEV_RELEASES,
|
||||
GITHUB_VEGA_RELEASES,
|
||||
useVegaReleases,
|
||||
} from './use-vega-releases';
|
||||
import { useVegaReleases } from './use-vega-releases';
|
||||
import {
|
||||
GITHUB_VEGA_RELEASES_DATA,
|
||||
GITHUB_VEGA_DEV_RELEASES_DATA,
|
||||
} from './mocks/github-releases';
|
||||
import { GITHUB_VEGA_DEV_RELEASES, GITHUB_VEGA_RELEASES } from './use-releases';
|
||||
|
||||
describe('useVegaReleases', () => {
|
||||
afterEach(() => {
|
||||
|
@ -1,82 +1,18 @@
|
||||
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import z from 'zod';
|
||||
|
||||
export const GITHUB_VEGA_RELEASES =
|
||||
'https://api.github.com/repos/vegaprotocol/vega/releases';
|
||||
export const GITHUB_VEGA_DEV_RELEASES =
|
||||
'https://api.github.com/repos/vegaprotocol/vega-dev-releases/releases';
|
||||
|
||||
const GithubReleaseSchema = z.object({
|
||||
url: z.string(),
|
||||
html_url: z.string(),
|
||||
id: z.number(),
|
||||
tag_name: z.string(),
|
||||
name: z.string().nullable(),
|
||||
draft: z.boolean(),
|
||||
created_at: z.string().datetime(),
|
||||
});
|
||||
|
||||
const GithubReleasesSchema = z.array(GithubReleaseSchema);
|
||||
|
||||
type ReleaseInfo = {
|
||||
id: number;
|
||||
name: string;
|
||||
tagName: string;
|
||||
htmlUrl: string;
|
||||
url: string;
|
||||
isDraft: boolean;
|
||||
createdAt: Date;
|
||||
isDevRelease: boolean;
|
||||
};
|
||||
|
||||
const toReleaseInfo = (
|
||||
releaseData: z.infer<typeof GithubReleaseSchema>,
|
||||
isDevRelease: boolean
|
||||
): ReleaseInfo => ({
|
||||
id: releaseData.id,
|
||||
name: releaseData.name || '',
|
||||
tagName: releaseData.tag_name,
|
||||
htmlUrl: releaseData.html_url,
|
||||
url: releaseData.url,
|
||||
isDraft: releaseData.draft,
|
||||
createdAt: new Date(releaseData.created_at),
|
||||
isDevRelease,
|
||||
});
|
||||
|
||||
enum ReleaseFeed {
|
||||
// @ts-ignore TS18033 - allowed in 5.0
|
||||
Vega = GITHUB_VEGA_RELEASES,
|
||||
// @ts-ignore TS18033
|
||||
VegaDev = GITHUB_VEGA_DEV_RELEASES,
|
||||
}
|
||||
|
||||
const fetchReleases = async (feed: ReleaseFeed) => {
|
||||
const response = await fetch(String(feed), {
|
||||
cache: 'force-cache',
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const json = await response.json();
|
||||
const data = GithubReleasesSchema.parse(json);
|
||||
return data.map((d) => toReleaseInfo(d, feed === ReleaseFeed.VegaDev));
|
||||
}
|
||||
|
||||
return [];
|
||||
};
|
||||
|
||||
type State = {
|
||||
loading: boolean;
|
||||
data: ReleaseInfo[] | null;
|
||||
error: Error | null | undefined;
|
||||
};
|
||||
import type { ReleasesState } from './use-releases';
|
||||
import { ReleasesFeed } from './use-releases';
|
||||
import { fetchReleases } from './use-releases';
|
||||
|
||||
/**
|
||||
* Retrieves a list of vega releases from github.
|
||||
* First element is the newest.
|
||||
*/
|
||||
export const useVegaReleases = (includeDevReleases = false) => {
|
||||
const [state, setState] = useState<State>({
|
||||
export const useVegaReleases = (
|
||||
includeDevReleases = false,
|
||||
cache?: RequestCache
|
||||
) => {
|
||||
const [state, setState] = useState<ReleasesState>({
|
||||
loading: true,
|
||||
data: null,
|
||||
error: null,
|
||||
@ -87,8 +23,11 @@ export const useVegaReleases = (includeDevReleases = false) => {
|
||||
|
||||
Promise.all(
|
||||
includeDevReleases
|
||||
? [fetchReleases(ReleaseFeed.Vega), fetchReleases(ReleaseFeed.VegaDev)]
|
||||
: [fetchReleases(ReleaseFeed.Vega)]
|
||||
? [
|
||||
fetchReleases(ReleasesFeed.Vega, cache),
|
||||
fetchReleases(ReleasesFeed.VegaDev, cache),
|
||||
]
|
||||
: [fetchReleases(ReleasesFeed.Vega, cache)]
|
||||
)
|
||||
.then(([vega, vegaDev]) => {
|
||||
if (mounted) {
|
||||
@ -114,7 +53,7 @@ export const useVegaReleases = (includeDevReleases = false) => {
|
||||
return () => {
|
||||
mounted = false;
|
||||
};
|
||||
}, [includeDevReleases, setState]);
|
||||
}, [cache, includeDevReleases]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
|
@ -51,6 +51,7 @@ const schemaObject = {
|
||||
ETH_WALLET_MNEMONIC: z.optional(z.string()),
|
||||
ANNOUNCEMENTS_CONFIG_URL: z.optional(z.string()),
|
||||
VEGA_INCIDENT_URL: z.optional(z.string()),
|
||||
APP_VERSION: z.optional(z.string()),
|
||||
};
|
||||
|
||||
// combine schema above with custom rule to ensure either
|
||||
|
Loading…
Reference in New Issue
Block a user