1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-05-08 01:15:49 +02:00

feat: ui now connects to backend, full e2e (#5736)

Added API hooks and now frontend actually sends data to database.
This commit is contained in:
Jaanus Sellin 2023-12-28 14:31:53 +02:00 committed by GitHub
parent ea0f2fa7ce
commit 86da11015c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 205 additions and 55 deletions

View File

@ -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(<FeatureToggleListTable />);
render(
<FeedbackProvider>
<FeatureToggleListTable />
</FeedbackProvider>,
);
await verifyTableFeature({
name: 'Operational Feature',

View File

@ -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<FeatureSearchResponseSchema>();
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 (
<PageContent
bodyClass='no-padding'
@ -300,6 +328,14 @@ const FeatureToggleListTableComponent: VFC = () => {
<FeatureToggleListActions
onExportClick={() => setShowExportDialog(true)}
/>
<Tooltip title='Provide feedback' arrow>
<IconButton
onClick={createFeedbackContext}
size='large'
>
<ReviewsOutlined />
</IconButton>
</Tooltip>
</>
}
>

View File

@ -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<HTMLFormElement>) => {
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<string | null>(null);
const onScoreChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setSelectedScore(event.target.value);
};
return (
<ConditionallyRender
condition={showFeedback}
show={
<ParentContainer>
<StyledContainer>
<Tooltip title='Close' arrow>
<StyledCloseButton
onClick={closeFeedback}
size='large'
>
<CloseIcon />
</StyledCloseButton>
</Tooltip>
<StyledContent>
<StyledTitle>
Help us to improve Unleash
</StyledTitle>
<StyledForm>
<FormTitle>
How easy wasy it to configure the strategy?
</FormTitle>
<StyledForm onSubmit={onSubmission}>
<input
type='hidden'
name='category'
value={feedbackData.category}
/>
<input
type='hidden'
name='userType'
value={feedbackData.userType}
/>
<FormTitle>{feedbackData.title}</FormTitle>
<StyledScoreContainer>
<StyledScoreInput>
{[1, 2, 3, 4, 5, 6, 7].map((score) => (
<StyledScoreValue key={score}>
<input
type='radio'
name='score'
name='difficultyScore'
value={score}
onChange={onScoreChange}
/>
<span>{score}</span>
</StyledScoreValue>
@ -174,12 +256,12 @@ export const FeedbackComponent = () => {
</StyledScoreContainer>
<Box>
<FormSubTitle>
What do you like most about the strategy
configuration?
{feedbackData.positiveLabel}
</FormSubTitle>
<TextField
label='Your answer here'
style={{ width: '100%' }}
name='positive'
multiline
rows={3}
variant='outlined'
@ -193,13 +275,13 @@ export const FeedbackComponent = () => {
</Box>
<Box>
<FormSubTitle>
What should be improved in the strategy
configuration?
{feedbackData.areasForImprovementsLabel}
</FormSubTitle>
<TextField
label='Your answer here'
style={{ width: '100%' }}
multiline
name='areasForImprovement'
rows={3}
InputLabelProps={{
style: {
@ -210,14 +292,18 @@ export const FeedbackComponent = () => {
size='small'
/>
</Box>
<StyledButton
variant='contained'
color='primary'
type='submit'
onClick={closeFeedback}
>
Send Feedback
</StyledButton>
<ConditionallyRender
condition={Boolean(selectedScore)}
show={
<StyledButton
variant='contained'
color='primary'
type='submit'
>
Send Feedback
</StyledButton>
}
/>
</StyledForm>
</StyledContent>
</StyledContainer>

View File

@ -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<IFeedbackContext>({
feedbackData: DEFAULT_FEEDBACK_DATA,
showFeedback: true,
setShowFeedback: setShowFeedback,
openFeedback: openFeedback,
closeFeedback: closeFeedback,
});
export const FeedbackContext = createContext<IFeedbackContext | undefined>(
undefined,
);

View File

@ -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<ProvideFeedbackSchema>(
DEFAULT_FEEDBACK_DATA,
const [feedbackData, setFeedbackData] = useState<IFeedbackData | undefined>(
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);
};

View File

@ -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,
};
};