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:
parent
ea0f2fa7ce
commit
86da11015c
@ -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',
|
||||
|
@ -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>
|
||||
</>
|
||||
}
|
||||
>
|
||||
|
@ -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>
|
||||
<ConditionallyRender
|
||||
condition={Boolean(selectedScore)}
|
||||
show={
|
||||
<StyledButton
|
||||
variant='contained'
|
||||
color='primary'
|
||||
type='submit'
|
||||
onClick={closeFeedback}
|
||||
>
|
||||
Send Feedback
|
||||
</StyledButton>
|
||||
}
|
||||
/>
|
||||
</StyledForm>
|
||||
</StyledContent>
|
||||
</StyledContainer>
|
||||
|
@ -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,
|
||||
);
|
||||
|
@ -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);
|
||||
};
|
||||
|
||||
|
@ -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,
|
||||
};
|
||||
};
|
Loading…
Reference in New Issue
Block a user