From 86da11015cdc7184b0033e982b9713059fe3482d Mon Sep 17 00:00:00 2001 From: Jaanus Sellin Date: Thu, 28 Dec 2023 14:31:53 +0200 Subject: [PATCH] feat: ui now connects to backend, full e2e (#5736) Added API hooks and now frontend actually sends data to database. --- .../FeatureToggleListTable.test.tsx | 7 +- .../FeatureToggleListTable.tsx | 40 +++++- .../feedbackNew/FeedbackComponent.tsx | 134 ++++++++++++++---- .../component/feedbackNew/FeedbackContext.ts | 36 ++--- .../feedbackNew/FeedbackProvider.tsx | 11 +- .../useUserFeedbackApi/useUserFeedbackApi.ts | 32 +++++ 6 files changed, 205 insertions(+), 55 deletions(-) create mode 100644 frontend/src/hooks/api/actions/useUserFeedbackApi/useUserFeedbackApi.ts diff --git a/frontend/src/component/feature/FeatureToggleList/FeatureToggleListTable.test.tsx b/frontend/src/component/feature/FeatureToggleList/FeatureToggleListTable.test.tsx index 91b5e75029..de08ae46ad 100644 --- a/frontend/src/component/feature/FeatureToggleList/FeatureToggleListTable.test.tsx +++ b/frontend/src/component/feature/FeatureToggleList/FeatureToggleListTable.test.tsx @@ -2,6 +2,7 @@ import { screen } from '@testing-library/react'; import { render } from 'utils/testRenderer'; import { testServerRoute, testServerSetup } from 'utils/testServer'; import { FeatureToggleListTable } from './FeatureToggleListTable'; +import { FeedbackProvider } from '../../feedbackNew/FeedbackProvider'; type APIFeature = { name: string; @@ -123,7 +124,11 @@ test('Filter table by project', async () => { }, ], ); - render(); + render( + + + , + ); await verifyTableFeature({ name: 'Operational Feature', diff --git a/frontend/src/component/feature/FeatureToggleList/FeatureToggleListTable.tsx b/frontend/src/component/feature/FeatureToggleList/FeatureToggleListTable.tsx index 957c37544c..c9e38f93d7 100644 --- a/frontend/src/component/feature/FeatureToggleList/FeatureToggleListTable.tsx +++ b/frontend/src/component/feature/FeatureToggleList/FeatureToggleListTable.tsx @@ -1,5 +1,12 @@ import { useCallback, useEffect, useMemo, useState, VFC } from 'react'; -import { Box, Link, useMediaQuery, useTheme } from '@mui/material'; +import { + Box, + IconButton, + Link, + Tooltip, + useMediaQuery, + useTheme, +} from '@mui/material'; import { Link as RouterLink } from 'react-router-dom'; import { createColumnHelper, useReactTable } from '@tanstack/react-table'; import { PaginatedTable, TablePlaceholder } from 'component/common/Table'; @@ -47,6 +54,8 @@ import { FeatureToggleListTable as LegacyFeatureToggleListTable } from './Legacy import { FeatureToggleListActions } from './FeatureToggleListActions/FeatureToggleListActions'; import useLoading from 'hooks/useLoading'; import { usePlausibleTracker } from 'hooks/usePlausibleTracker'; +import { useFeedback } from '../../feedbackNew/useFeedback'; +import { ReviewsOutlined } from '@mui/icons-material'; export const featuresPlaceholder = Array(15).fill({ name: 'Name of the feature', @@ -60,6 +69,7 @@ const columnHelper = createColumnHelper(); const FeatureToggleListTableComponent: VFC = () => { const theme = useTheme(); + const { openFeedback } = useFeedback(); const { trackEvent } = usePlausibleTracker(); const { environments } = useEnvironments(); const enabledEnvironments = environments @@ -70,7 +80,7 @@ const FeatureToggleListTableComponent: VFC = () => { const [showExportDialog, setShowExportDialog] = useState(false); const { setToastApiError } = useToast(); - const { uiConfig } = useUiConfig(); + const { uiConfig, isPro, isOss, isEnterprise } = useUiConfig(); const stateConfig = { offset: withDefault(NumberParam, 0), @@ -259,6 +269,24 @@ const FeatureToggleListTableComponent: VFC = () => { return null; } + const createFeedbackContext = () => { + const userType = isPro() + ? 'pro' + : isOss() + ? 'oss' + : isEnterprise() + ? 'enterprise' + : 'unknown'; + openFeedback({ + category: 'search', + userType, + title: 'How easy was it to use search and filters?', + positiveLabel: 'What do you like most about search and filters?', + areasForImprovementsLabel: + 'What should be improved in search and filters page?', + }); + }; + return ( { setShowExportDialog(true)} /> + + + + + } > diff --git a/frontend/src/component/feedbackNew/FeedbackComponent.tsx b/frontend/src/component/feedbackNew/FeedbackComponent.tsx index a5f3af0d36..c630da8f66 100644 --- a/frontend/src/component/feedbackNew/FeedbackComponent.tsx +++ b/frontend/src/component/feedbackNew/FeedbackComponent.tsx @@ -1,7 +1,18 @@ -import { Box, Button, styled, TextField } from '@mui/material'; +import { + Box, + Button, + IconButton, + styled, + TextField, + Tooltip, +} from '@mui/material'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { useFeedback } from './useFeedback'; -import React from 'react'; +import React, { useState } from 'react'; +import CloseIcon from '@mui/icons-material/Close'; +import { useUserFeedbackApi } from 'hooks/api/actions/useUserFeedbackApi/useUserFeedbackApi'; +import useToast from 'hooks/useToast'; +import { ProvideFeedbackSchema } from '../../openapi'; export const ParentContainer = styled('div')(({ theme }) => ({ position: 'relative', @@ -48,7 +59,7 @@ export const StyledTitle = styled(Box)(({ theme }) => ({ lineHeight: theme.spacing(2.5), })); -export const StyledForm = styled(Box)(({ theme }) => ({ +export const StyledForm = styled('form')(({ theme }) => ({ display: 'flex', width: '400px', padding: theme.spacing(3), @@ -60,6 +71,10 @@ export const StyledForm = styled(Box)(({ theme }) => ({ borderColor: 'rgba(0, 0, 0, 0.12)', backgroundColor: '#fff', boxShadow: '0px 4px 4px 0px rgba(0, 0, 0, 0.12)', + + '& > *': { + width: '100%', + }, })); export const FormTitle = styled(Box)(({ theme }) => ({ @@ -75,7 +90,7 @@ export const FormSubTitle = styled(Box)(({ theme }) => ({ lineHeight: theme.spacing(2.5), })); -export const StyledButton = styled(Button)(({ theme }) => ({ +export const StyledButton = styled(Button)(() => ({ width: '100%', })); @@ -86,9 +101,10 @@ const StyledScoreContainer = styled('div')(({ theme }) => ({ alignItems: 'flex-start', })); -const StyledScoreInput = styled('div')(({ theme }) => ({ +const StyledScoreInput = styled('div')(() => ({ display: 'flex', - gap: theme.spacing(2), + width: '100%', + justifyContent: 'space-between', })); const StyledScoreHelp = styled('span')(({ theme }) => ({ @@ -96,7 +112,7 @@ const StyledScoreHelp = styled('span')(({ theme }) => ({ fontSize: theme.spacing(1.75), })); -const ScoreHelpContainer = styled('span')(({ theme }) => ({ +const ScoreHelpContainer = styled('span')(() => ({ display: 'flex', flexDirection: 'row', justifyContent: 'space-between', @@ -131,33 +147,99 @@ const StyledScoreValue = styled('label')(({ theme }) => ({ }, })); +const StyledCloseButton = styled(IconButton)(({ theme }) => ({ + position: 'absolute', + right: theme.spacing(2), + top: theme.spacing(2), + color: theme.palette.background.paper, +})); + export const FeedbackComponent = () => { const { feedbackData, showFeedback, closeFeedback } = useFeedback(); if (!feedbackData) return null; + const { setToastData } = useToast(); + const { addFeedback } = useUserFeedbackApi(); + + function isProvideFeedbackSchema(data: any): data is ProvideFeedbackSchema { + data.difficultyScore = data.difficultyScore + ? Number(data.difficultyScore) + : undefined; + + return ( + typeof data.category === 'string' && + typeof data.userType === 'string' && + (typeof data.difficultyScore === 'number' || + data.difficultyScore === undefined) + ); + } + + const onSubmission = async (event: React.FormEvent) => { + event.preventDefault(); + const formData = new FormData(event.currentTarget); + const data = Object.fromEntries(formData); + + if (isProvideFeedbackSchema(data)) { + await addFeedback(data as ProvideFeedbackSchema); + setToastData({ + title: 'Feedback sent', + type: 'success', + }); + } else { + setToastData({ + title: 'Feedback not sent', + type: 'error', + }); + } + closeFeedback(); + }; + + const [selectedScore, setSelectedScore] = useState(null); + + const onScoreChange = (event: React.ChangeEvent) => { + setSelectedScore(event.target.value); + }; + return ( + + + + + Help us to improve Unleash - - - How easy wasy it to configure the strategy? - + + + + {feedbackData.title} {[1, 2, 3, 4, 5, 6, 7].map((score) => ( {score} @@ -174,12 +256,12 @@ export const FeedbackComponent = () => { - What do you like most about the strategy - configuration? + {feedbackData.positiveLabel} { - What should be improved in the strategy - configuration? + {feedbackData.areasForImprovementsLabel} { size='small' /> - - Send Feedback - + + Send Feedback + + } + /> diff --git a/frontend/src/component/feedbackNew/FeedbackContext.ts b/frontend/src/component/feedbackNew/FeedbackContext.ts index 5b8e7c9dfe..6eebadfa60 100644 --- a/frontend/src/component/feedbackNew/FeedbackContext.ts +++ b/frontend/src/component/feedbackNew/FeedbackContext.ts @@ -2,33 +2,25 @@ import { createContext } from 'react'; import { ProvideFeedbackSchema } from '../../openapi'; interface IFeedbackContext { - feedbackData: ProvideFeedbackSchema; - openFeedback: (data: ProvideFeedbackSchema) => void; + feedbackData: IFeedbackData | undefined; + openFeedback: (data: IFeedbackData) => void; closeFeedback: () => void; showFeedback: boolean; setShowFeedback: (visible: boolean) => void; } -export const DEFAULT_FEEDBACK_DATA = { - category: 'general', +type IFeedbackText = { + title: string; + positiveLabel: string; + areasForImprovementsLabel: string; }; -const setShowFeedback = () => { - throw new Error('setShowFeedback called outside FeedbackContext'); -}; +export type IFeedbackData = Pick< + ProvideFeedbackSchema, + 'category' | 'userType' +> & + IFeedbackText; -const openFeedback = () => { - throw new Error('openFeedback called outside FeedbackContext'); -}; - -const closeFeedback = () => { - throw new Error('closeFeedback called outside FeedbackContext'); -}; - -export const FeedbackContext = createContext({ - feedbackData: DEFAULT_FEEDBACK_DATA, - showFeedback: true, - setShowFeedback: setShowFeedback, - openFeedback: openFeedback, - closeFeedback: closeFeedback, -}); +export const FeedbackContext = createContext( + undefined, +); diff --git a/frontend/src/component/feedbackNew/FeedbackProvider.tsx b/frontend/src/component/feedbackNew/FeedbackProvider.tsx index d06a2a7037..4f3cadf593 100644 --- a/frontend/src/component/feedbackNew/FeedbackProvider.tsx +++ b/frontend/src/component/feedbackNew/FeedbackProvider.tsx @@ -1,21 +1,20 @@ import { FeedbackComponent } from './FeedbackComponent'; -import { DEFAULT_FEEDBACK_DATA, FeedbackContext } from './FeedbackContext'; +import { FeedbackContext, IFeedbackData } from './FeedbackContext'; import { FC, useState } from 'react'; -import { ProvideFeedbackSchema } from '../../openapi'; export const FeedbackProvider: FC = ({ children }) => { - const [feedbackData, setFeedbackData] = useState( - DEFAULT_FEEDBACK_DATA, + const [feedbackData, setFeedbackData] = useState( + undefined, ); const [showFeedback, setShowFeedback] = useState(false); - const openFeedback = (data: ProvideFeedbackSchema) => { + const openFeedback = (data: IFeedbackData) => { setFeedbackData(data); setShowFeedback(true); }; const closeFeedback = () => { - setFeedbackData(DEFAULT_FEEDBACK_DATA); + setFeedbackData(undefined); setShowFeedback(false); }; diff --git a/frontend/src/hooks/api/actions/useUserFeedbackApi/useUserFeedbackApi.ts b/frontend/src/hooks/api/actions/useUserFeedbackApi/useUserFeedbackApi.ts new file mode 100644 index 0000000000..4287288fe1 --- /dev/null +++ b/frontend/src/hooks/api/actions/useUserFeedbackApi/useUserFeedbackApi.ts @@ -0,0 +1,32 @@ +import { IInternalBanner } from 'interfaces/banner'; +import useAPI from '../useApi/useApi'; +import { ProvideFeedbackSchema } from '../../../../openapi'; + +const ENDPOINT = 'api/admin/user-feedback'; + +export const useUserFeedbackApi = () => { + const { loading, makeRequest, createRequest, errors } = useAPI({ + propagateErrors: true, + }); + + const addFeedback = async (feedbackSchema: ProvideFeedbackSchema) => { + const requestId = 'addBanner'; + const req = createRequest( + ENDPOINT, + { + method: 'POST', + body: JSON.stringify(feedbackSchema), + }, + requestId, + ); + + const response = await makeRequest(req.caller, req.id); + return response.json(); + }; + + return { + addFeedback, + errors, + loading, + }; +};