From 0e563b4371d003cd36532236d62ff36e47eb149e Mon Sep 17 00:00:00 2001 From: Theophile Sandoz Date: Thu, 25 Apr 2024 13:45:37 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=A5=20Hotfix:=20Unresponsive=20Proxy?= =?UTF-8?q?=20API=20(#4842)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Do not share a cold observable between queries e.g all `api.derive` were sharing the same cold observable. Which resulted in all other payloads being deserialize on every emitted message (from other `api.derive` queries). Same thing for `api.query` and `api.rpc`. * Revert "Debug failing test" This reverts commit 7337a7d72dcde81370985aa159c49a1df0e8f5c6. * Fix the multiple recipient funding request proposal * Bump version to `3.5.1` --- CHANGELOG.md | 9 +- packages/ui/package.json | 2 +- .../Proposals/CurrentProposals.stories.tsx | 184 +++++++++--------- packages/ui/src/proposals/constants/regExp.ts | 1 - .../SpecificParameters/FundingRequest.tsx | 34 ++-- .../modals/AddNewProposal/helpers.ts | 6 +- packages/ui/src/proposals/model/isValidCsv.ts | 14 ++ packages/ui/src/proposals/model/validation.ts | 20 -- packages/ui/src/proxyApi/client/query.ts | 5 +- 9 files changed, 141 insertions(+), 134 deletions(-) delete mode 100644 packages/ui/src/proposals/constants/regExp.ts create mode 100644 packages/ui/src/proposals/model/isValidCsv.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index db671e91d7..a007d1be5d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,8 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [3.5.1] - 2024-04-25 -## [3.5.0 (Luxor)][3.5.0] - 2024-03-18 +### Fixed +- Proxy API becoming unresponsive. + +## [3.5.0 (Luxor)][3.5.0] - 2024-04-18 ### Added - Decrease council budget proposal. @@ -380,7 +384,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [0.1.1] - 2022-12-02 -[unreleased]: https://github.com/Joystream/pioneer/compare/v3.5.0...HEAD +[unreleased]: https://github.com/Joystream/pioneer/compare/v3.5.1...HEAD +[3.5.1]: https://github.com/Joystream/pioneer/compare/v3.5.0...v3.5.1 [3.5.0]: https://github.com/Joystream/pioneer/compare/v3.4.0...v3.5.0 [3.4.0]: https://github.com/Joystream/pioneer/compare/v3.3.1...v3.4.0 [3.3.1]: https://github.com/Joystream/pioneer/compare/v3.3.0...v3.3.1 diff --git a/packages/ui/package.json b/packages/ui/package.json index f362158df9..2b4d79a87d 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@joystream/pioneer", - "version": "3.5.0", + "version": "3.5.1", "license": "GPL-3.0-only", "scripts": { "build": "node --max_old_space_size=4096 ./build.js", diff --git a/packages/ui/src/app/pages/Proposals/CurrentProposals.stories.tsx b/packages/ui/src/app/pages/Proposals/CurrentProposals.stories.tsx index 60c2fcf857..b82357e779 100644 --- a/packages/ui/src/app/pages/Proposals/CurrentProposals.stories.tsx +++ b/packages/ui/src/app/pages/Proposals/CurrentProposals.stories.tsx @@ -804,98 +804,98 @@ export const SpecificParametersFundingRequest: Story = { }), } -// export const SpecificParametersMultipleFundingRequest: Story = { -// play: specificParametersTest('Funding Request', async ({ args, createProposal, modal, step }) => { -// const aliceAddress = alice.controllerAccount -// const bobAddress = member('bob').controllerAccount -// const charlieAddress = member('charlie').controllerAccount - -// await createProposal(async () => { -// const nextButton = getButtonByText(modal, 'Create proposal') -// expect(nextButton).toBeDisabled() - -// await userEvent.click(modal.getByTestId('pay-multiple')) - -// const csvField = modal.getByTestId('accounts-amounts') - -// // Invalid -// await userEvent.clear(csvField) -// await userEvent.type(csvField, `${aliceAddress},500${bobAddress},500`) -// expect(await modal.findByText(/Not valid CSV format/)) -// // ensure its not being open-able while the CSV syntax is valid -// const previewButton = getButtonByText(modal, 'Preview and Validate') -// expect(previewButton).toBeDisabled() -// await waitFor(() => expect(modal.queryByTestId('sidePanel-overlay')).toBeNull()) -// expect(nextButton).toBeDisabled() - -// // Invalid Accounts error -// await userEvent.clear(csvField) -// await userEvent.type(csvField, `5GNJqTPy,500\n${bobAddress},500`) - -// await waitFor(() => expect(modal.queryByText(/Not valid CSV format/)).toBeNull()) -// expect(await modal.findByText(/Please preview and validate the inputs to proceed/)) -// expect(nextButton).toBeDisabled() -// expect(previewButton).toBeEnabled() - -// await userEvent.click(previewButton) -// expect(await modal.findByText(/Incorrect destination accounts detected/)) -// await userEvent.click(modal.getByTestId('sidePanel-overlay')) - -// // Max Amount error -// await userEvent.clear(csvField) -// await userEvent.type(csvField, `${aliceAddress},166667\n${bobAddress},500`) -// expect(await modal.findByText(/Please preview and validate the inputs to proceed/)) -// expect(nextButton).toBeDisabled() -// await waitFor(() => expect(previewButton).toBeEnabled()) -// await waitFor( -// async () => { -// await userEvent.click(previewButton) -// expect(await modal.findByText(/Max payment amount is exceeded/)) -// }, -// { timeout: 8000 } -// ) -// await userEvent.click(modal.getByTestId('sidePanel-overlay')) //ensure create proposal is still disabled -// expect(nextButton).toBeDisabled() - -// // Max Allowed Accounts error -// await userEvent.clear(csvField) -// await userEvent.type(csvField, `${aliceAddress},400\n${bobAddress},500\n${charlieAddress},500`) -// expect(await modal.findByText(/Please preview and validate the inputs to proceed/)) -// expect(nextButton).toBeDisabled() -// await waitFor(() => expect(previewButton).toBeEnabled()) -// await userEvent.click(previewButton) -// expect(await modal.findByText(/Maximum allowed accounts exceeded/)) -// await userEvent.click(modal.getByTestId('sidePanel-overlay')) //ensure create proposal is still disabled -// expect(nextButton).toBeDisabled() - -// // delete one account from the list' -// await waitFor(() => expect(previewButton).toBeEnabled()) -// await userEvent.click(previewButton) -// await userEvent.click(modal.getByTestId('removeAccount-2')) -// await waitFor(() => expect(modal.queryByText(/Maximum allowed accounts exceeded/)).toBeNull()) -// await userEvent.click(modal.getByTestId('sidePanel-overlay')) - -// // Valid -// await userEvent.clear(csvField) -// await userEvent.type(csvField, `${aliceAddress},500\n${bobAddress},500`) -// expect(nextButton).toBeDisabled() - -// await waitFor(() => expect(previewButton).toBeEnabled()) -// await userEvent.click(previewButton) -// await userEvent.click(modal.getByTestId('sidePanel-overlay')) -// }) - -// await step('Transaction parameters', () => { -// const [, , specificParameters] = args.onCreateProposal.mock.calls.at(-1) -// expect(specificParameters.toJSON()).toEqual({ -// fundingRequest: [ -// { account: aliceAddress, amount: 500_0000000000 }, -// { account: bobAddress, amount: 500_0000000000 }, -// ], -// }) -// }) -// }), -// } +export const SpecificParametersMultipleFundingRequest: Story = { + play: specificParametersTest('Funding Request', async ({ args, createProposal, modal, step }) => { + const aliceAddress = alice.controllerAccount + const bobAddress = member('bob').controllerAccount + const charlieAddress = member('charlie').controllerAccount + + await createProposal(async () => { + const nextButton = getButtonByText(modal, 'Create proposal') + expect(nextButton).toBeDisabled() + + await userEvent.click(modal.getByTestId('pay-multiple')) + + const csvField = modal.getByTestId('accounts-amounts') + + // Invalid + await userEvent.clear(csvField) + await userEvent.type(csvField, `${aliceAddress},500${bobAddress},500`) + expect(await modal.findByText(/Not valid CSV format/)) + // ensure its not being open-able while the CSV syntax is valid + const previewButton = getButtonByText(modal, 'Preview and Validate') + expect(previewButton).toBeDisabled() + await waitFor(() => expect(modal.queryByTestId('sidePanel-overlay')).toBeNull()) + expect(nextButton).toBeDisabled() + + // Invalid Accounts error + await userEvent.clear(csvField) + await userEvent.type(csvField, `5GNJqTPy,500\n${bobAddress},500`) + + await waitFor(() => expect(modal.queryByText(/Not valid CSV format/)).toBeNull()) + expect(await modal.findByText(/Please preview and validate the inputs to proceed/)) + expect(nextButton).toBeDisabled() + expect(previewButton).toBeEnabled() + + await userEvent.click(previewButton) + expect(await modal.findByText(/Incorrect destination accounts detected/)) + await userEvent.click(modal.getByTestId('sidePanel-overlay')) + + // Max Amount error + await userEvent.clear(csvField) + await userEvent.type(csvField, `${aliceAddress},166667\n${bobAddress},500`) + expect(await modal.findByText(/Please preview and validate the inputs to proceed/)) + expect(nextButton).toBeDisabled() + await waitFor(() => expect(previewButton).toBeEnabled()) + await waitFor( + async () => { + await userEvent.click(previewButton) + expect(await modal.findByText(/Max payment amount is exceeded/)) + }, + { timeout: 8000 } + ) + await userEvent.click(modal.getByTestId('sidePanel-overlay')) //ensure create proposal is still disabled + expect(nextButton).toBeDisabled() + + // Max Allowed Accounts error + await userEvent.clear(csvField) + await userEvent.type(csvField, `${aliceAddress},400\n${bobAddress},500\n${charlieAddress},500`) + expect(await modal.findByText(/Please preview and validate the inputs to proceed/)) + expect(nextButton).toBeDisabled() + await waitFor(() => expect(previewButton).toBeEnabled()) + await userEvent.click(previewButton) + expect(await modal.findByText(/Maximum allowed accounts exceeded/)) + await userEvent.click(modal.getByTestId('sidePanel-overlay')) //ensure create proposal is still disabled + expect(nextButton).toBeDisabled() + + // delete one account from the list' + await waitFor(() => expect(previewButton).toBeEnabled()) + await userEvent.click(previewButton) + await userEvent.click(modal.getByTestId('removeAccount-2')) + await waitFor(() => expect(modal.queryByText(/Maximum allowed accounts exceeded/)).toBeNull()) + await userEvent.click(modal.getByTestId('sidePanel-overlay')) + + // Valid + await userEvent.clear(csvField) + await userEvent.type(csvField, `${aliceAddress},500\n${bobAddress},500`) + expect(nextButton).toBeDisabled() + + await waitFor(() => expect(previewButton).toBeEnabled()) + await userEvent.click(previewButton) + await userEvent.click(modal.getByTestId('sidePanel-overlay')) + }) + + await step('Transaction parameters', () => { + const [, , specificParameters] = args.onCreateProposal.mock.calls.at(-1) + expect(specificParameters.toJSON()).toEqual({ + fundingRequest: [ + { account: aliceAddress, amount: 500_0000000000 }, + { account: bobAddress, amount: 500_0000000000 }, + ], + }) + }) + }), +} export const SpecificParametersSetReferralCut: Story = { play: specificParametersTest('Set Referral Cut', async ({ args, createProposal, modal, step }) => { diff --git a/packages/ui/src/proposals/constants/regExp.ts b/packages/ui/src/proposals/constants/regExp.ts deleted file mode 100644 index 2223a831e0..0000000000 --- a/packages/ui/src/proposals/constants/regExp.ts +++ /dev/null @@ -1 +0,0 @@ -export const CSV_PATTERN = /^([^,:;]+),([^,:;]+)(\n[^,:;]+,[^,:;]+)*(\n)?$/ diff --git a/packages/ui/src/proposals/modals/AddNewProposal/components/SpecificParameters/FundingRequest.tsx b/packages/ui/src/proposals/modals/AddNewProposal/components/SpecificParameters/FundingRequest.tsx index 8cee121b0b..285fc854b5 100644 --- a/packages/ui/src/proposals/modals/AddNewProposal/components/SpecificParameters/FundingRequest.tsx +++ b/packages/ui/src/proposals/modals/AddNewProposal/components/SpecificParameters/FundingRequest.tsx @@ -20,25 +20,36 @@ import { RowGapBlock } from '@/common/components/page/PageContent' import { Tooltip, TooltipDefault } from '@/common/components/Tooltip' import { TextMedium, TextSmall, TextInlineSmall } from '@/common/components/typography' import { useResponsive } from '@/common/hooks/useResponsive' +import { isValidCSV } from '@/proposals/model/isValidCsv' import { PreviewAndValidateModal } from './modals/PreviewAndValidate' import { ErrorPrompt, Prompt } from './Prompt' export const FundingRequest = () => { - const { watch, setValue, getFieldState } = useFormContext() + const { watch, setValue } = useFormContext() const { isMobile, size } = useResponsive() const [isPreviewModalShown, setIsPreviewModalShown] = useState(false) const [payMultiple] = watch(['fundingRequest.payMultiple']) - const [hasPreviewedInput] = watch(['fundingRequest.hasPreviewedInput'], { 'fundingRequest.hasPreviewedInput': true }) - const csvInput = watch('fundingRequest.csvInput') + + const [hasPreviewedInput, setHasPreviewedInput] = useState(false) + const [canPreviewInput, setCanPreviewInput] = useState(false) + useEffect(() => { - if (getFieldState('fundingRequest.accountsAndAmounts')) { - setValue('fundingRequest.accountsAndAmounts', undefined, { shouldValidate: true }) - setValue('fundingRequest.hasPreviewedInput', false, { shouldValidate: true }) - } - }, [csvInput]) - const canPreviewInput = - getFieldState('fundingRequest.csvInput').isDirty && !getFieldState('fundingRequest.csvInput').error + const subscription = watch((data, info) => { + if (info.name !== 'fundingRequest.csvInput' || info.type !== 'change') return + + const isCsvInpuValid = isValidCSV(data.fundingRequest.csvInput) + setCanPreviewInput(isCsvInpuValid !== canPreviewInput) + + if (hasPreviewedInput) { + setHasPreviewedInput(false) + setValue('fundingRequest.accountsAndAmounts', undefined, { shouldValidate: true }) + } + }) + + return () => subscription.unsubscribe() + }, [watch]) + return ( @@ -90,7 +101,8 @@ export const FundingRequest = () => { { .test('previewedinput', 'Please preview', (value) => typeof value !== 'undefined' && value) .required('Field is required'), }), - csvInput: Yup.string().when('payMultiple', { - is: true, - then: (schema) => schema.test(isValidCSV('Not valid CSV format')).required('Field is required'), - }), accountsAndAmounts: Yup.array().when('payMultiple', { is: true, then: (schema) => schema.required(), diff --git a/packages/ui/src/proposals/model/isValidCsv.ts b/packages/ui/src/proposals/model/isValidCsv.ts new file mode 100644 index 0000000000..0327659f5c --- /dev/null +++ b/packages/ui/src/proposals/model/isValidCsv.ts @@ -0,0 +1,14 @@ +export const CSV_PATTERN = /^([^,:;]+),([^,:;]+)(\n[^,:;]+,[^,:;]+)*(\n)?$/ + +export const isValidCSV = (value: string) => { + if (!CSV_PATTERN.test(value)) return false + + const pairs = value.split('\n') + + for (const pair of pairs) { + const [, amount] = pair.split(',') + if (!Number(amount)) return false + } + + return true +} diff --git a/packages/ui/src/proposals/model/validation.ts b/packages/ui/src/proposals/model/validation.ts index 4281a4068d..eaa88e18c9 100644 --- a/packages/ui/src/proposals/model/validation.ts +++ b/packages/ui/src/proposals/model/validation.ts @@ -2,26 +2,6 @@ import { get } from 'lodash' import * as Yup from 'yup' import { AnyObject } from 'yup/lib/types' -import { CSV_PATTERN } from '../constants/regExp' - -export const isValidCSV = (message: string): Yup.TestConfig => ({ - message, - name: 'isValidCSV', - exclusive: false, - test(value: string) { - if (!CSV_PATTERN.test(value)) return false - - const pairs = value.split('\n') - - for (const pair of pairs) { - const [, amount] = pair.split(',') - if (!Number(amount)) return false - } - - return true - }, -}) - export const equalToContext = ( msg: (value: any) => string, contextPath: string, diff --git a/packages/ui/src/proxyApi/client/query.ts b/packages/ui/src/proxyApi/client/query.ts index e67d50b634..7d4629fcfc 100644 --- a/packages/ui/src/proxyApi/client/query.ts +++ b/packages/ui/src/proxyApi/client/query.ts @@ -1,6 +1,6 @@ import { AnyTuple } from '@polkadot/types/types' import { uniqueId } from 'lodash' -import { filter, Observable, map } from 'rxjs' +import { filter, Observable, map, share } from 'rxjs' import { deserializeMessage } from '../models/payload' import { ApiKinds, PostMessage, RawWorkerMessageEvent } from '../types' @@ -35,7 +35,8 @@ export const query = ( ) => { const queryMessages = messages.pipe( filter(({ data }) => data.messageType === apiKind), - deserializeMessage>() + deserializeMessage>(), + share() ) return apiInterfaceProxy(