diff --git a/frontend/src/js/components/common/dialogs/feedback.js b/frontend/src/js/components/common/dialogs/feedback.js
new file mode 100644
index 00000000..9cf51127
--- /dev/null
+++ b/frontend/src/js/components/common/dialogs/feedback.js
@@ -0,0 +1,165 @@
+// Copyright 2024 Northern.tech AS
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+import React, { useEffect, useRef, useState } from 'react';
+import { useDispatch } from 'react-redux';
+
+import {
+ Close as CloseIcon,
+ SentimentVeryDissatisfied as DissatisfiedIcon,
+ SentimentNeutral as NeutralIcon,
+ SentimentSatisfiedAlt as SatisfiedIcon,
+ SentimentVeryDissatisfiedOutlined as VeryDissatisfiedIcon,
+ SentimentVerySatisfiedOutlined as VerySatisfiedIcon
+} from '@mui/icons-material';
+import {
+ Button,
+ Dialog,
+ DialogContent,
+ DialogTitle,
+ IconButton,
+ TextField,
+ dialogClasses,
+ dialogTitleClasses,
+ iconButtonClasses,
+ lighten,
+ textFieldClasses
+} from '@mui/material';
+import { makeStyles } from 'tss-react/mui';
+
+import { dismissFeedbackDialog, submitFeedback } from '../../../actions/userActions';
+import { TIMEOUTS } from '../../../constants/appConstants';
+
+const useStyles = makeStyles()(theme => ({
+ root: {
+ [`.${dialogClasses.paper}`]: { width: 350, bottom: 0, right: 0, position: 'absolute' },
+ [`.${dialogTitleClasses.root}`]: {
+ alignSelf: 'flex-end',
+ padding: 0,
+ [`.${iconButtonClasses.root}`]: { paddingBottom: 0 }
+ }
+ },
+ columns: { gap: theme.spacing(2) },
+ rating: {
+ [`.${iconButtonClasses.root}`]: {
+ borderRadius: theme.shape.borderRadius,
+ height: theme.spacing(6),
+ width: theme.spacing(6),
+ backgroundColor: lighten(theme.palette.primary.main, 0.85),
+ color: theme.palette.primary.main,
+ '&:hover': {
+ backgroundColor: theme.palette.primary.main,
+ color: lighten(theme.palette.primary.main, 0.85)
+ }
+ }
+ },
+ text: { [`.${textFieldClasses.root}`]: { marginTop: 0 }, '.submitButton': { alignSelf: 'start' } }
+}));
+
+const satisfactionLevels = [
+ { Icon: VeryDissatisfiedIcon, title: 'Very Dissatisfied' },
+ { Icon: DissatisfiedIcon, title: 'Dissatisfied' },
+ { Icon: NeutralIcon, title: 'Neutral' },
+ { Icon: SatisfiedIcon, title: 'Satisfied' },
+ { Icon: VerySatisfiedIcon, title: 'Very Satisfied' }
+];
+const explanations = ['Very unsatisfied', 'Very satisfied'];
+
+const SatisfactionGauge = ({ classes, setSatisfaction }) => {
+ return (
+
+
How satisfied are you with Mender?
+
+ {satisfactionLevels.map(({ Icon, title }, index) => (
+ setSatisfaction(index)} title={title}>
+
+
+ ))}
+
+
+ {explanations.map((explanation, index) => (
+
+ {explanation}
+
+ ))}
+
+
+ );
+};
+
+const TextEntry = ({ classes, feedback, onChangeFeedback, onSubmit }) => (
+
+
What do you think is the most important thing to improve in Mender? (optional)
+
onChangeFeedback(value)} value={feedback} variant="outlined" />
+
+
+);
+
+const AppreciationNote = () =>
Thank you for taking the time to share your thoughts!
;
+
+const progressionLevels = [SatisfactionGauge, TextEntry, AppreciationNote];
+
+export const FeedbackDialog = () => {
+ const [progress, setProgress] = useState(0);
+ const [satisfaction, setSatisfaction] = useState(2);
+ const [feedback, setFeedback] = useState('');
+ const dispatch = useDispatch();
+ const isInitialized = useRef(false);
+
+ const { classes } = useStyles();
+
+ useEffect(() => {
+ if (!isInitialized.current) {
+ return;
+ }
+ setProgress(current => current + 1);
+ }, [satisfaction]);
+
+ useEffect(() => {
+ setTimeout(() => (isInitialized.current = true), TIMEOUTS.threeSeconds);
+ }, []);
+
+ const onCloseClick = () => dispatch(dismissFeedbackDialog());
+
+ const onAdvance = () => setProgress(current => current + 1);
+
+ const onSubmit = () => {
+ onAdvance();
+ dispatch(submitFeedback({ satisfaction: satisfactionLevels[satisfaction].title, feedback }));
+ };
+
+ const Component = progressionLevels[progress];
+ return (
+
+ );
+};
+
+export default FeedbackDialog;
diff --git a/frontend/src/js/constants/userConstants.js b/frontend/src/js/constants/userConstants.js
index 96888189..9dc6223a 100644
--- a/frontend/src/js/constants/userConstants.js
+++ b/frontend/src/js/constants/userConstants.js
@@ -391,6 +391,7 @@ export const SET_SHOW_CONNECT_DEVICE = 'SET_SHOW_CONNECT_DEVICE';
export const SET_TOOLTIP_STATE = 'SET_TOOLTIP_STATE';
export const SET_TOOLTIPS_STATE = 'SET_TOOLTIPS_STATE';
export const SET_SHOW_STARTUP_NOTIFICATION = 'SET_SHOW_STARTUP_NOTIFICATION';
+export const SET_SHOW_FEEDBACK_DIALOG = 'SET_SHOW_FEEDBACK_DIALOG';
export const OWN_USER_ID = 'me';
diff --git a/frontend/src/js/reducers/userReducer.js b/frontend/src/js/reducers/userReducer.js
index 3af12c7a..5e774f36 100644
--- a/frontend/src/js/reducers/userReducer.js
+++ b/frontend/src/js/reducers/userReducer.js
@@ -34,6 +34,7 @@ export const initialState = {
...UserConstants.rolesById
},
showConnectDeviceDialog: false,
+ showFeedbackDialog: true,
showStartupNotification: false,
tooltips: {
byId: {
@@ -183,6 +184,11 @@ const userReducer = (state = initialState, action) => {
byId: action.value
}
};
+ case UserConstants.SET_SHOW_FEEDBACK_DIALOG:
+ return {
+ ...state,
+ showFeedbackDialog: action.value
+ };
default:
return state;
}