From a5232786947ae9525ac084aa1766818af6046edb Mon Sep 17 00:00:00 2001 From: Yash Oswal Date: Tue, 21 May 2024 20:33:23 +0530 Subject: [PATCH] feat(feedback): upgrade to mui v5 - switch to backstage `AlertApi` instead of snackbars - use mui 5 styled components - add `themeId` prop to GlobalFeedback component - updated tests Signed-off-by: Yash Oswal --- plugins/feedback/package.json | 5 +- .../CreateFeedbackModal.tsx | 571 +++++++++--------- .../CustomEmptyState/CustomEmptyState.tsx | 118 ++-- .../EntityFeedbackPage.test.tsx | 14 +- .../EntityFeedbackPage/EntityFeedbackPage.tsx | 100 ++- .../FeedbackDetailsModal.tsx | 172 +++--- .../FeedbackTable/FeedbackTable.tsx | 52 +- .../GlobalFeedbackPage/GlobalFeedbackPage.tsx | 6 +- .../OpcFeedbackComponent.tsx | 67 +- plugins/feedback/src/index.ts | 2 +- 10 files changed, 525 insertions(+), 582 deletions(-) diff --git a/plugins/feedback/package.json b/plugins/feedback/package.json index 4cfa55c9065..5cecdc01b11 100644 --- a/plugins/feedback/package.json +++ b/plugins/feedback/package.json @@ -31,9 +31,8 @@ "@backstage/core-plugin-api": "^1.9.2", "@backstage/plugin-catalog-react": "^1.11.3", "@backstage/theme": "^0.5.3", - "@material-ui/core": "^4.9.13", - "@material-ui/icons": "^4.9.1", - "@material-ui/lab": "^4.0.0-alpha.61", + "@mui/icons-material": "^5.15.18", + "@mui/material": "^5.15.18", "@one-platform/opc-feedback": "0.1.1-alpha", "axios": "^1.6.4", "react-use": "^17.2.4" diff --git a/plugins/feedback/src/components/CreateFeedbackModal/CreateFeedbackModal.tsx b/plugins/feedback/src/components/CreateFeedbackModal/CreateFeedbackModal.tsx index 9917597ae8e..faffa0c8fcd 100644 --- a/plugins/feedback/src/components/CreateFeedbackModal/CreateFeedbackModal.tsx +++ b/plugins/feedback/src/components/CreateFeedbackModal/CreateFeedbackModal.tsx @@ -2,64 +2,74 @@ import React, { useState } from 'react'; import { configApiRef, useAnalytics, useApi } from '@backstage/core-plugin-api'; -import { - Button, - Chip, - createStyles, - DialogActions, - DialogContent, - DialogTitle, - FormControlLabel, - Grid, - IconButton, - makeStyles, - Paper, - Radio, - RadioGroup, - TextField, - Theme, - Typography, -} from '@material-ui/core'; -import BugReportOutlined from '@material-ui/icons/BugReportOutlined'; -import BugReportTwoToneIcon from '@material-ui/icons/BugReportTwoTone'; -import CloseRounded from '@material-ui/icons/CloseRounded'; -import SmsOutlined from '@material-ui/icons/SmsOutlined'; -import SmsTwoTone from '@material-ui/icons/SmsTwoTone'; -import { Alert } from '@material-ui/lab'; +import BugReportOutlined from '@mui/icons-material/BugReportOutlined'; +import BugReportTwoToneIcon from '@mui/icons-material/BugReportTwoTone'; +import CloseRounded from '@mui/icons-material/CloseRounded'; +import SmsOutlined from '@mui/icons-material/SmsOutlined'; +import SmsTwoTone from '@mui/icons-material/SmsTwoTone'; +import Alert from '@mui/material/Alert'; +import Button from '@mui/material/Button'; +import Chip from '@mui/material/Chip'; +import DialogActions from '@mui/material/DialogActions'; +import DialogContent from '@mui/material/DialogContent'; +import DialogTitle from '@mui/material/DialogTitle'; +import FormControlLabel from '@mui/material/FormControlLabel'; +import Grid from '@mui/material/Grid'; +import IconButton from '@mui/material/IconButton'; +import Paper from '@mui/material/Paper'; +import Radio from '@mui/material/Radio'; +import RadioGroup from '@mui/material/RadioGroup'; +import { styled, Theme } from '@mui/material/styles'; +import TextField from '@mui/material/TextField'; +import Typography from '@mui/material/Typography'; import { feedbackApiRef } from '../../api'; import { FeedbackCategory } from '../../models/feedback.model'; -const useStyles = makeStyles((theme: Theme) => - createStyles({ - root: { - '& > * > *': { - margin: theme.spacing(1), - width: '100%', - }, - padding: '0.5rem', - }, - actions: { - '& > *': { - margin: theme.spacing(1), - }, - paddingRight: '1rem', - }, - container: { - padding: '1rem', - }, - dialogTitle: {}, - closeButton: { - position: 'absolute', - right: theme.spacing(1), - top: theme.spacing(1), - color: theme.palette.grey[500], +const PREFIX = 'CreateFeedbackModal'; + +const classes = { + root: `${PREFIX}-root`, + actions: `${PREFIX}-actions`, + container: `${PREFIX}-container`, + dialogTitle: `${PREFIX}-dialogTitle`, + closeButton: `${PREFIX}-closeButton`, + radioGroup: `${PREFIX}-radioGroup`, +}; + +const StyledPaper = styled(Paper)(({ theme }: { theme: Theme }) => ({ + [`& .${classes.root}`]: { + '& > * > *': { + margin: theme.spacing(1), + width: '100%', }, - radioGroup: { - gap: theme.spacing(3), + padding: '0.5rem', + }, + + [`& .${classes.actions}`]: { + '& > *': { + margin: theme.spacing(1), }, - }), -); + paddingRight: '1rem', + }, + + [`& .${classes.container}`]: { + padding: '1rem', + }, + + [`& .${classes.dialogTitle}`]: {}, + + [`& .${classes.closeButton}`]: { + position: 'absolute', + right: theme.spacing(1), + top: theme.spacing(1), + color: theme.palette.grey[500], + }, + + [`& .${classes.radioGroup}`]: { + gap: theme.spacing(3), + }, +})); const issueTags = [ 'Slow Loading', @@ -70,255 +80,250 @@ const issueTags = [ ]; const feedbackTags = ['Excellent', 'Good', 'Needs Improvement', 'Other']; -export const CreateFeedbackModal = React.forwardRef( - ( - props: { - projectEntity: string; - userEntity: string; - serverType: string; - handleModalCloseFn: (respObj?: any) => void; - }, - ref, - ) => { - const classes = useStyles(); - const api = useApi(feedbackApiRef); - const analytics = useAnalytics(); - const [feedbackType, setFeedbackType] = useState('BUG'); - const [selectedTag, setSelectedTag] = useState(issueTags[0]); - const app = useApi(configApiRef); - const summaryLimit = app.getOptionalNumber('feedback.summaryLimit') ?? 240; +export const CreateFeedbackModal = (props: { + projectEntity: string; + userEntity: string; + serverType: string; + handleModalCloseFn: (respObj?: any) => void; +}) => { + const api = useApi(feedbackApiRef); + const analytics = useAnalytics(); + const [feedbackType, setFeedbackType] = useState('BUG'); + const [selectedTag, setSelectedTag] = useState(issueTags[0]); + const app = useApi(configApiRef); + const summaryLimit = app.getOptionalNumber('feedback.summaryLimit') ?? 240; - const [summary, setSummary] = useState({ - value: '', - error: false, - errorMessage: 'Enter some summary', - }); - const [description, setDescription] = useState({ - value: '', - error: false, - errorMessage: 'Enter some description', - }); + const [summary, setSummary] = useState({ + value: '', + error: false, + errorMessage: 'Enter some summary', + }); + const [description, setDescription] = useState({ + value: '', + error: false, + errorMessage: 'Enter some description', + }); - const projectEntity = props.projectEntity; - const userEntity = props.userEntity; + const projectEntity = props.projectEntity; + const userEntity = props.userEntity; - function handleCategoryClick(event: any) { - setFeedbackType(event.target.value); - setSelectedTag( - event.target.value === 'FEEDBACK' ? feedbackTags[0] : issueTags[0], - ); - } + function handleCategoryClick(event: any) { + setFeedbackType(event.target.value); + setSelectedTag( + event.target.value === 'FEEDBACK' ? feedbackTags[0] : issueTags[0], + ); + } - function handleChipSlection(tag: string) { - if (tag === selectedTag) { - return; - } - setSelectedTag(tag); + function handleChipSlection(tag: string) { + if (tag === selectedTag) { + return; } + setSelectedTag(tag); + } - async function handleSubmitClick() { - const resp = await api.createFeedback({ - summary: summary.value, - description: description.value, - projectId: projectEntity, - url: window.location.href, - userAgent: navigator.userAgent, - createdBy: userEntity, - feedbackType: - feedbackType === 'BUG' - ? FeedbackCategory.BUG - : FeedbackCategory.FEEDBACK, - tag: selectedTag, - }); - props.handleModalCloseFn(resp); - analytics.captureEvent('click', `submit - ${summary.value}`); - } + async function handleSubmitClick() { + const resp = await api.createFeedback({ + summary: summary.value, + description: description.value, + projectId: projectEntity, + url: window.location.href, + userAgent: navigator.userAgent, + createdBy: userEntity, + feedbackType: + feedbackType === 'BUG' + ? FeedbackCategory.BUG + : FeedbackCategory.FEEDBACK, + tag: selectedTag, + }); + props.handleModalCloseFn(resp); + analytics.captureEvent('click', `submit - ${summary.value}`); + } - function handleInputChange( - event: React.FocusEvent, - ) { - if (event.target.id === 'summary') { - const _summary = event.target.value; - if (_summary.trim().length === 0) { - return setSummary({ - ...summary, - value: '', - errorMessage: 'Provide summary', - error: true, - }); - } else if (_summary.length > summaryLimit) { - return setSummary({ - ...summary, - value: _summary, - error: true, - errorMessage: `Summary should be less than ${summaryLimit} characters.`, - }); - } - return setSummary({ ...summary, value: _summary, error: false }); - } - if (event.target.id === 'description') { - return setDescription({ - ...description, - value: event.target.value, - error: false, + function handleInputChange( + event: React.FocusEvent, + ) { + if (event.target.id === 'summary') { + const _summary = event.target.value; + if (_summary.trim().length === 0) { + return setSummary({ + ...summary, + value: '', + errorMessage: 'Provide summary', + error: true, }); - } - return 0; - } - - function handleValidation( - event: React.FocusEvent, - ) { - if (event.target.id === 'summary') { - if (event.target.value.length === 0) { - return setSummary({ ...summary, error: true }); - } + } else if (_summary.length > summaryLimit) { return setSummary({ ...summary, - value: event.target.value.trim(), - error: event.target.value.trim().length > summaryLimit, + value: _summary, + error: true, + errorMessage: `Summary should be less than ${summaryLimit} characters.`, }); } - if (event.target.id === 'description' && event.target.value.length > 0) { - setDescription({ ...description, value: description.value.trim() }); + return setSummary({ ...summary, value: _summary, error: false }); + } + if (event.target.id === 'description') { + return setDescription({ + ...description, + value: event.target.value, + error: false, + }); + } + return 0; + } + + function handleValidation( + event: React.FocusEvent, + ) { + if (event.target.id === 'summary') { + if (event.target.value.length === 0) { + return setSummary({ ...summary, error: true }); } - return 0; + return setSummary({ + ...summary, + value: event.target.value.trim(), + error: event.target.value.trim().length > summaryLimit, + }); + } + if (event.target.id === 'description' && event.target.value.length > 0) { + setDescription({ ...description, value: description.value.trim() }); } + return 0; + } - return ( - - - {`Feedback for ${projectEntity.split('/').pop()}`} - {props.handleModalCloseFn ? ( - - - - ) : null} - - - - - Select type - - } - checkedIcon={} + return ( + + + {`Feedback for ${projectEntity.split('/').pop()}`} + {props.handleModalCloseFn ? ( + + + + ) : null} + + + + + Select type + + } + checkedIcon={} + color="secondary" + /> + } + /> + } + checkedIcon={} + color="primary" + /> + } + /> + + + + + Select {feedbackType === 'FEEDBACK' ? 'Feedback' : 'Bug'} + :  + {feedbackType === 'BUG' + ? issueTags.map(issueTitle => ( + handleChipSlection(issueTitle)} + label={issueTitle} /> - } - /> - } - checkedIcon={} + )) + : feedbackTags.map(feedbackTitle => ( + handleChipSlection(feedbackTitle)} + label={feedbackTitle} /> - } - /> - - - - - Select {feedbackType === 'FEEDBACK' ? 'Feedback' : 'Bug'} - :  - {feedbackType === 'BUG' - ? issueTags.map(issueTitle => ( - handleChipSlection(issueTitle)} - label={issueTitle} - /> - )) - : feedbackTags.map(feedbackTitle => ( - handleChipSlection(feedbackTitle)} - label={feedbackTitle} - /> - ))} - - - - - + ))} + + + + + - - - + + - - - - - - {props.serverType.toLowerCase() !== 'mail' && - !selectedTag.match(/(Excellent|Good)/g) ? ( - - Note: By submitting  - {feedbackType === 'FEEDBACK' ? 'feedback' : 'bug'} with this tag, it - will create an issue in {props.serverType.toLowerCase()} - - ) : null} - - ); - }, -); + + + + + + + {props.serverType.toLowerCase() !== 'mail' && + !selectedTag.match(/(Excellent|Good)/g) ? ( + + Note: By submitting  + {feedbackType === 'FEEDBACK' ? 'feedback' : 'bug'} with this tag, it + will create an issue in {props.serverType.toLowerCase()} + + ) : null} + + ); +}; diff --git a/plugins/feedback/src/components/EntityFeedbackPage/CustomEmptyState/CustomEmptyState.tsx b/plugins/feedback/src/components/EntityFeedbackPage/CustomEmptyState/CustomEmptyState.tsx index d728f6653cd..26a122e5bdc 100644 --- a/plugins/feedback/src/components/EntityFeedbackPage/CustomEmptyState/CustomEmptyState.tsx +++ b/plugins/feedback/src/components/EntityFeedbackPage/CustomEmptyState/CustomEmptyState.tsx @@ -2,62 +2,71 @@ import React from 'react'; import { CodeSnippet, EmptyState } from '@backstage/core-components'; -import { - Accordion, - AccordionDetails, - AccordionSummary, - createStyles, - makeStyles, - Theme, - Typography, -} from '@material-ui/core'; -import ExpandMoreRounded from '@material-ui/icons/ExpandMoreRounded'; - -const useStyles = makeStyles((theme: Theme) => - createStyles({ - code: { - width: '100%', - borderRadius: 6, - margin: theme.spacing(0, 0), - background: theme.palette.background.paper, +import ExpandMoreRounded from '@mui/icons-material/ExpandMoreRounded'; +import Accordion from '@mui/material/Accordion'; +import AccordionDetails from '@mui/material/AccordionDetails'; +import AccordionSummary from '@mui/material/AccordionSummary'; +import { styled, Theme } from '@mui/material/styles'; +import Typography from '@mui/material/Typography'; + +const PREFIX = 'CustomEmptyState'; + +const classes = { + code: `${PREFIX}-code`, + accordionGroup: `${PREFIX}-accordionGroup`, + heading: `${PREFIX}-heading`, + subHeading: `${PREFIX}-subHeading`, + embeddedVideo: `${PREFIX}-embeddedVideo`, +}; + +// TODO jss-to-styled codemod: The Fragment root was replaced by div. Change the tag if needed. +const Root = styled('div')(({ theme }: { theme: Theme }) => ({ + [`& .${classes.code}`]: { + width: '100%', + borderRadius: 6, + margin: theme.spacing(0, 0), + background: theme.palette.background.paper, + }, + + [`& .${classes.accordionGroup}`]: { + '& > *': { + marginTop: theme.spacing(2), + marginBottom: theme.spacing(1), + background: theme.palette.mode === 'dark' ? '#333' : '#eee', }, - accordionGroup: { - '& > *': { - marginTop: theme.spacing(2), - marginBottom: theme.spacing(1), - background: theme.palette.type === 'dark' ? '#333' : '#eee', + }, + + [`& .${classes.heading}`]: { + width: '50%', + fontSize: '1.2rem', + fontWeight: 600, + }, + + [`& .${classes.subHeading}`]: { + color: '#9e9e9e', + }, + + [`& .${classes.embeddedVideo}`]: { + top: '50%', + left: '50%', + zIndex: 2, + position: 'relative', + transform: 'translate(-50%, 15%)', + '& > iframe': { + [theme.breakpoints.up('sm')]: { + width: '100%', + height: '280px', }, - }, - heading: { - width: '50%', - fontSize: '1.2rem', - fontWeight: 600, - }, - subHeading: { - color: theme.palette.textSubtle, - }, - embeddedVideo: { - top: '50%', - left: '50%', - zIndex: 2, - position: 'relative', - transform: 'translate(-50%, 15%)', - '& > iframe': { - [theme.breakpoints.up('sm')]: { - width: '100%', - height: '280px', - }, - [theme.breakpoints.up('xl')]: { - width: '768px', - height: '482px', - }, - - border: '0', - borderRadius: theme.spacing(1), + [theme.breakpoints.up('xl')]: { + width: '768px', + height: '482px', }, + + border: '0', + borderRadius: theme.spacing(1), }, - }), -); + }, +})); const EMAIL_YAML = `metadata: annotations: @@ -86,7 +95,6 @@ const JIRA_YAML = `metadata: feedback/email-to: 'example@example.com';`; export const CustomEmptyState = (props: { [key: string]: string }) => { - const classes = useStyles(); const [expanded, setExpanded] = React.useState('jira'); const handleChange = @@ -106,7 +114,7 @@ export const CustomEmptyState = (props: { [key: string]: string }) => { } action={ - <> + Add the annotation to your component YAML as shown in the highlighted example below: @@ -159,7 +167,7 @@ export const CustomEmptyState = (props: { [key: string]: string }) => { - + } missing="field" /> diff --git a/plugins/feedback/src/components/EntityFeedbackPage/EntityFeedbackPage.test.tsx b/plugins/feedback/src/components/EntityFeedbackPage/EntityFeedbackPage.test.tsx index 68548bfb75a..e631b09e7b0 100644 --- a/plugins/feedback/src/components/EntityFeedbackPage/EntityFeedbackPage.test.tsx +++ b/plugins/feedback/src/components/EntityFeedbackPage/EntityFeedbackPage.test.tsx @@ -4,6 +4,7 @@ import { ConfigReader } from '@backstage/config'; import { BackstageUserIdentity, configApiRef, + fetchApiRef, IdentityApi, identityApiRef, } from '@backstage/core-plugin-api'; @@ -11,7 +12,12 @@ import { EntityProvider, entityRouteRef, } from '@backstage/plugin-catalog-react'; -import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; +import { + MockConfigApi, + MockFetchApi, + renderInTestApp, + TestApiProvider, +} from '@backstage/test-utils'; import { FeedbackAPI, feedbackApiRef } from '../../api'; import { mockEntity, mockFeedback } from '../../mocks'; @@ -41,7 +47,7 @@ describe('Entity Feedback Page', () => { }), }; - const mockConfigApi = new ConfigReader({ + const mockConfigApi = new MockConfigApi({ feedback: { integrations: { jira: [{ host: 'https://jira-server-url' }] } }, }); @@ -73,7 +79,9 @@ describe('Entity Feedback Page', () => { it('Should have buttons', async () => { const rendered = await render(); expect( - rendered.getByRole('button', { name: 'Create' }), + rendered.getByRole('button', { + name: 'Give a feedback / Report a issue', + }), ).toBeInTheDocument(); expect( rendered.getByRole('button', { name: 'Refresh' }), diff --git a/plugins/feedback/src/components/EntityFeedbackPage/EntityFeedbackPage.tsx b/plugins/feedback/src/components/EntityFeedbackPage/EntityFeedbackPage.tsx index bf5675b0ab8..56581778051 100644 --- a/plugins/feedback/src/components/EntityFeedbackPage/EntityFeedbackPage.tsx +++ b/plugins/feedback/src/components/EntityFeedbackPage/EntityFeedbackPage.tsx @@ -2,6 +2,7 @@ import React, { useEffect, useState } from 'react'; import { Progress } from '@backstage/core-components'; import { + alertApiRef, configApiRef, identityApiRef, useAnalytics, @@ -9,42 +10,41 @@ import { } from '@backstage/core-plugin-api'; import { useEntity } from '@backstage/plugin-catalog-react'; -import { - ButtonGroup, - CircularProgress, - createStyles, - Dialog, - makeStyles, - Snackbar, - Theme, - Tooltip, - Zoom, -} from '@material-ui/core'; -import Button from '@material-ui/core/Button'; -import Grid from '@material-ui/core/Grid'; -import Add from '@material-ui/icons/Add'; -import ArrowForwardRounded from '@material-ui/icons/ArrowForwardRounded'; -import Sync from '@material-ui/icons/Sync'; -import { Alert } from '@material-ui/lab'; +import Add from '@mui/icons-material/Add'; +import ArrowForwardRounded from '@mui/icons-material/ArrowForwardRounded'; +import Sync from '@mui/icons-material/Sync'; +import Button from '@mui/material/Button'; +import ButtonGroup from '@mui/material/ButtonGroup'; +import CircularProgress from '@mui/material/CircularProgress'; +import Dialog from '@mui/material/Dialog'; +import Grid from '@mui/material/Grid'; +import { styled, Theme } from '@mui/material/styles'; +import Tooltip from '@mui/material/Tooltip'; +import Zoom from '@mui/material/Zoom'; import { CreateFeedbackModal } from '../CreateFeedbackModal/CreateFeedbackModal'; import { FeedbackDetailsModal } from '../FeedbackDetailsModal'; import { FeedbackTable } from '../FeedbackTable'; import { CustomEmptyState } from './CustomEmptyState'; -const useStyles = makeStyles((theme: Theme) => - createStyles({ - buttonGroup: { - textAlign: 'center', - whiteSpace: 'nowrap', - marginTop: theme.spacing(1), - }, - }), -); +const PREFIX = 'EntityFeedbackPage'; + +const classes = { + buttonGroup: `${PREFIX}-buttonGroup`, +}; + +const StyledGrid = styled(Grid)(({ theme }: { theme: Theme }) => ({ + [`& .${classes.buttonGroup}`]: { + textAlign: 'center', + whiteSpace: 'nowrap', + marginTop: theme.spacing(1), + }, +})); export const EntityFeedbackPage = () => { const config = useApi(configApiRef); - const classes = useStyles(); + const alertApi = useApi(alertApiRef); + const { entity } = useEntity(); const user = useApi(identityApiRef); const analytics = useAnalytics(); @@ -56,19 +56,11 @@ export const EntityFeedbackPage = () => { }>(); const [modalOpen, setModalOpen] = useState(false); - const [snackbarOpen, setSnackbarOpen] = useState<{ - message: string; - open: boolean; - severity: 'success' | 'error'; - }>({ - message: '', - open: false, - severity: 'success', - }); + const [reload, setReload] = useState(false); useEffect(() => { setReload(false); - }, [snackbarOpen, reload]); + }, [reload]); const projectEntity = `${entity.kind}:${entity.metadata.namespace}/${entity.metadata.name}`.toLowerCase(); @@ -102,41 +94,30 @@ export const EntityFeedbackPage = () => { if (respObj) { setReload(true); if (respObj.error) { - setSnackbarOpen({ + alertApi.post({ message: respObj.error, severity: 'error', - open: true, + display: 'transient', }); } else if (respObj.message) { - setSnackbarOpen({ + alertApi.post({ message: respObj.message, severity: 'success', - open: true, + display: 'transient', }); } } }; - const handleSnackbarClose = ( - event?: React.SyntheticEvent, - reason?: string, - ) => { - event?.preventDefault(); - if (reason === 'clickaway') { - return; - } - setSnackbarOpen({ ...snackbarOpen, open: false }); - }; - const handleResyncClick = () => { analytics.captureEvent('click', 'refresh'); setReload(true); }; - return pluginConfig.feedbackEmailTo === undefined ? ( + return pluginConfig.feedbackType === undefined ? ( ) : ( - + { {reload ? : } - - - {snackbarOpen?.message} - - - + ); }; diff --git a/plugins/feedback/src/components/FeedbackDetailsModal/FeedbackDetailsModal.tsx b/plugins/feedback/src/components/FeedbackDetailsModal/FeedbackDetailsModal.tsx index f028061eba8..fa0103f3fa2 100644 --- a/plugins/feedback/src/components/FeedbackDetailsModal/FeedbackDetailsModal.tsx +++ b/plugins/feedback/src/components/FeedbackDetailsModal/FeedbackDetailsModal.tsx @@ -2,90 +2,91 @@ import React, { useEffect, useState } from 'react'; import { parseEntityRef } from '@backstage/catalog-model'; import { Progress, useQueryParamState } from '@backstage/core-components'; -import { useApi } from '@backstage/core-plugin-api'; +import { alertApiRef, useApi } from '@backstage/core-plugin-api'; import { EntityRefLink } from '@backstage/plugin-catalog-react'; -import { - Avatar, - Button, - Chip, - createStyles, - Dialog, - DialogActions, - DialogContent, - DialogTitle, - Grid, - IconButton, - Link, - List, - ListItem, - ListItemSecondaryAction, - ListItemText, - makeStyles, - Snackbar, - Theme, - Tooltip, - Typography, - Zoom, -} from '@material-ui/core'; -import ArrowForwardRounded from '@material-ui/icons/ArrowForwardRounded'; -import BugReportOutlined from '@material-ui/icons/BugReportOutlined'; -import CloseRounded from '@material-ui/icons/CloseRounded'; -import ExpandLessRounded from '@material-ui/icons/ExpandLessRounded'; -import ExpandMoreRounded from '@material-ui/icons/ExpandMoreRounded'; -import SmsOutlined from '@material-ui/icons/SmsOutlined'; -import { Alert } from '@material-ui/lab'; +import ArrowForwardRounded from '@mui/icons-material/ArrowForwardRounded'; +import BugReportOutlined from '@mui/icons-material/BugReportOutlined'; +import CloseRounded from '@mui/icons-material/CloseRounded'; +import ExpandLessRounded from '@mui/icons-material/ExpandLessRounded'; +import ExpandMoreRounded from '@mui/icons-material/ExpandMoreRounded'; +import SmsOutlined from '@mui/icons-material/SmsOutlined'; +import Avatar from '@mui/material/Avatar'; +import Button from '@mui/material/Button'; +import Chip from '@mui/material/Chip'; +import Dialog from '@mui/material/Dialog'; +import DialogActions from '@mui/material/DialogActions'; +import DialogContent from '@mui/material/DialogContent'; +import DialogTitle from '@mui/material/DialogTitle'; +import Grid from '@mui/material/Grid'; +import IconButton from '@mui/material/IconButton'; +import Link from '@mui/material/Link'; +import List from '@mui/material/List'; +import ListItem from '@mui/material/ListItem'; +import ListItemSecondaryAction from '@mui/material/ListItemSecondaryAction'; +import ListItemText from '@mui/material/ListItemText'; +import { styled, Theme } from '@mui/material/styles'; +import Tooltip from '@mui/material/Tooltip'; +import Typography from '@mui/material/Typography'; +import Zoom from '@mui/material/Zoom'; import { feedbackApiRef } from '../../api'; import { FeedbackType } from '../../models/feedback.model'; -const useStyles = makeStyles((theme: Theme) => - createStyles({ - closeButton: { +const PREFIX = 'FeedbackDetailsModal'; + +const classes = { + closeButton: `${PREFIX}-closeButton`, + dialogAction: `${PREFIX}-dialogAction`, + dialogTitle: `${PREFIX}-dialogTitle`, + submittedBy: `${PREFIX}-submittedBy`, + readMoreLink: `${PREFIX}-readMoreLink`, +}; + +const StyledDialog = styled(Dialog)(({ theme }: { theme: Theme }) => ({ + [`& .${classes.dialogAction}`]: { + justifyContent: 'flex-start', + paddingLeft: theme.spacing(3), + paddingBottom: theme.spacing(2), + }, + + [`& .${classes.submittedBy}`]: { + color: '#9e9e9e', + fontWeight: 500, + }, + + [`& .${classes.readMoreLink}`]: { + display: 'flex', + alignItems: 'center', + cursor: 'pointer', + }, +})); + +const StyledDialogTitle = styled(DialogTitle)( + ({ theme }: { theme: Theme }) => ({ + display: 'flex', + paddingBottom: theme.spacing(0), + marginRight: theme.spacing(2), + '& > svg': { + marginTop: theme.spacing(0.5), + marginRight: theme.spacing(1), + }, + [`& .${classes.closeButton}`]: { position: 'absolute', right: theme.spacing(1), top: theme.spacing(1), color: theme.palette.grey[500], }, - dialogAction: { - justifyContent: 'flex-start', - paddingLeft: theme.spacing(3), - paddingBottom: theme.spacing(2), - }, - dialogTitle: { - '& > *': { - display: 'flex', - alignItems: 'center', - marginRight: theme.spacing(2), - wordBreak: 'break-word', - }, - '& > * > svg': { marginRight: theme.spacing(1) }, - - paddingBottom: theme.spacing(0), - }, - submittedBy: { - color: theme.palette.textSubtle, - fontWeight: 500, - }, - readMoreLink: { - display: 'flex', - alignItems: 'center', - cursor: 'pointer', - }, }), ); export const FeedbackDetailsModal = () => { - const classes = useStyles(); const api = useApi(feedbackApiRef); + const alertApi = useApi(alertApiRef); const [queryState, setQueryState] = useQueryParamState( 'id', ); const [modalData, setModalData] = useState(); - const [snackbarOpen, setSnackbarOpen] = useState({ - message: '', - open: false, - }); const [ticketDetails, setTicketDetails] = useState<{ status: string | null; @@ -148,26 +149,18 @@ export const FeedbackDetailsModal = () => { if (!modalData && queryState) { api.getFeedbackById(queryState).then(resp => { if (resp?.error !== undefined) { - setSnackbarOpen({ message: resp.error, open: true }); + alertApi.post({ + message: resp.error, + display: 'transient', + severity: 'error', + }); } else { const respData: FeedbackType = resp?.data!; setModalData(respData); } }); } - }, [modalData, api, queryState]); - - const handleSnackbarClose = ( - event?: React.SyntheticEvent, - reason?: string, - ) => { - event?.preventDefault(); - if (reason === 'clickaway') { - return; - } - setSnackbarOpen({ ...snackbarOpen, open: false }); - setQueryState(undefined); - }; + }, [modalData, api, queryState, alertApi]); const handleClose = () => { setModalData(undefined); @@ -194,7 +187,7 @@ export const FeedbackDetailsModal = () => { }; return ( - { maxWidth="sm" scroll="paper" > - {!modalData ? ( - - - {snackbarOpen.message} - - - ) : ( + {modalData ? ( <> - + { aria-label="close" className={classes.closeButton} onClick={handleClose} + size="large" > - + { ) : null} - )} - + ) : null} + ); }; diff --git a/plugins/feedback/src/components/FeedbackTable/FeedbackTable.tsx b/plugins/feedback/src/components/FeedbackTable/FeedbackTable.tsx index c036989d01a..2ed54cf0b1e 100644 --- a/plugins/feedback/src/components/FeedbackTable/FeedbackTable.tsx +++ b/plugins/feedback/src/components/FeedbackTable/FeedbackTable.tsx @@ -14,29 +14,32 @@ import { EntityRefLink, } from '@backstage/plugin-catalog-react'; -import { - Box, - Chip, - IconButton, - Link, - makeStyles, - Paper, - TableContainer, - TextField, - Theme, - Tooltip, - Typography, -} from '@material-ui/core'; -import BugReportOutlined from '@material-ui/icons/BugReportOutlined'; -import Clear from '@material-ui/icons/Clear'; -import Search from '@material-ui/icons/Search'; -import TextsmsOutlined from '@material-ui/icons/TextsmsOutlined'; +import BugReportOutlined from '@mui/icons-material/BugReportOutlined'; +import Clear from '@mui/icons-material/Clear'; +import Search from '@mui/icons-material/Search'; +import TextsmsOutlined from '@mui/icons-material/TextsmsOutlined'; +import Box from '@mui/material/Box'; +import Chip from '@mui/material/Chip'; +import IconButton from '@mui/material/IconButton'; +import Link from '@mui/material/Link'; +import Paper from '@mui/material/Paper'; +import { styled, Theme } from '@mui/material/styles'; +import TableContainer from '@mui/material/TableContainer'; +import TextField from '@mui/material/TextField'; +import Tooltip from '@mui/material/Tooltip'; +import Typography from '@mui/material/Typography'; import { feedbackApiRef } from '../../api'; import { FeedbackType } from '../../models/feedback.model'; -const useStyles = makeStyles((theme: Theme) => ({ - textField: { +const PREFIX = 'FeedbackTable'; + +const classes = { + textField: `${PREFIX}-textField`, +}; + +const StyledPaper = styled(Paper)(({ theme }: { theme: Theme }) => ({ + [`& .${classes.textField}`]: { padding: 0, margin: theme.spacing(2), width: '70%', @@ -46,9 +49,7 @@ const useStyles = makeStyles((theme: Theme) => ({ }, })); -export const FeedbackTable: React.FC<{ projectId?: string }> = (props: { - projectId?: string; -}) => { +export const FeedbackTable = (props: { projectId?: string }) => { const projectId = props.projectId ? props.projectId : 'all'; const api = useApi(feedbackApiRef); const analytics = useAnalytics(); @@ -61,7 +62,6 @@ export const FeedbackTable: React.FC<{ projectId?: string }> = (props: { const [queryState, setQueryState] = useQueryParamState('id'); const [loading, setLoading] = useState(true); const [searchText, setSearchText] = useState(''); - const classes = useStyles(); const columns: TableColumn[] = [ { @@ -248,7 +248,7 @@ export const FeedbackTable: React.FC<{ projectId?: string }> = (props: { } return ( - + = (props: { ), endAdornment: ( - setSearchText('')}> + setSearchText('')} size="medium"> @@ -293,6 +293,6 @@ export const FeedbackTable: React.FC<{ projectId?: string }> = (props: { page={tableConfig.page - 1} /> - + ); }; diff --git a/plugins/feedback/src/components/GlobalFeedbackPage/GlobalFeedbackPage.tsx b/plugins/feedback/src/components/GlobalFeedbackPage/GlobalFeedbackPage.tsx index 14d70800b30..dbf0bce3f7c 100644 --- a/plugins/feedback/src/components/GlobalFeedbackPage/GlobalFeedbackPage.tsx +++ b/plugins/feedback/src/components/GlobalFeedbackPage/GlobalFeedbackPage.tsx @@ -3,16 +3,16 @@ import React from 'react'; import { Content, Header, Page } from '@backstage/core-components'; import { configApiRef, useApi } from '@backstage/core-plugin-api'; -import { Grid } from '@material-ui/core'; +import Grid from '@mui/material/Grid'; import { FeedbackDetailsModal } from '../FeedbackDetailsModal'; import { FeedbackTable } from '../FeedbackTable'; -export const GlobalFeedbackPage = () => { +export const GlobalFeedbackPage = (props: { themeId?: string }) => { const app = useApi(configApiRef); const appTitle = app.getString('app.title'); return ( - +
diff --git a/plugins/feedback/src/components/OpcFeedbackComponent/OpcFeedbackComponent.tsx b/plugins/feedback/src/components/OpcFeedbackComponent/OpcFeedbackComponent.tsx index 849d11080c7..43c0f0de363 100644 --- a/plugins/feedback/src/components/OpcFeedbackComponent/OpcFeedbackComponent.tsx +++ b/plugins/feedback/src/components/OpcFeedbackComponent/OpcFeedbackComponent.tsx @@ -3,6 +3,7 @@ import React, { useEffect } from 'react'; import '@one-platform/opc-feedback'; import { + alertApiRef, configApiRef, identityApiRef, useAnalytics, @@ -10,9 +11,6 @@ import { useRouteRef, } from '@backstage/core-plugin-api'; -import { Snackbar } from '@material-ui/core'; -import { Alert } from '@material-ui/lab'; - import { feedbackApiRef } from '../../api'; import { FeedbackCategory } from '../../models/feedback.model'; import { rootRouteRef, viewDocsRouteRef } from '../../routes'; @@ -21,6 +19,7 @@ export const OpcFeedbackComponent = () => { const appConfig = useApi(configApiRef); const feedbackApi = useApi(feedbackApiRef); const identityApi = useApi(identityApiRef); + const alertApi = useApi(alertApiRef); const analytics = useAnalytics(); const footer = JSON.stringify({ @@ -33,34 +32,13 @@ export const OpcFeedbackComponent = () => { const docsSpa = useRouteRef(viewDocsRouteRef); const feedbackSpa = useRouteRef(rootRouteRef); - const [snackbarState, setSnackbarState] = React.useState<{ - message?: string; - open: boolean; - severity: 'success' | 'error'; - }>({ - message: '', - open: false, - severity: 'success', - }); - - const handleSnackbarClose = ( - event?: React.SyntheticEvent, - reason?: string, - ) => { - event?.preventDefault(); - if (reason === 'clickaway') { - return; - } - setSnackbarState({ ...snackbarState, open: false }); - }; - useEffect(() => { const onSubmit = async (event: any) => { if (event.detail.data.summary.trim().length < 1) { - setSnackbarState({ + alertApi.post({ message: 'Summary cannot be empty', severity: 'error', - open: true, + display: 'transient', }); throw Error('Summary cannot be empty'); } @@ -84,42 +62,31 @@ export const OpcFeedbackComponent = () => { : FeedbackCategory.FEEDBACK, }); if (resp.error) { - setSnackbarState({ + alertApi.post({ message: resp.error, severity: 'error', - open: true, + display: 'transient', }); throw new Error(resp.error); } else { - setSnackbarState({ - message: resp.message, + alertApi.post({ + message: resp.message as string, severity: 'success', - open: true, + display: 'transient', }); } }; const elem: any = document.querySelector('opc-feedback'); elem.onSubmit = onSubmit; - }, [feedbackApi, projectId, identityApi, analytics]); + }, [feedbackApi, projectId, identityApi, analytics, alertApi]); return ( - <> - - - {snackbarState?.message} - - - - + ); }; diff --git a/plugins/feedback/src/index.ts b/plugins/feedback/src/index.ts index d5a0b6b6577..86525d58d8f 100644 --- a/plugins/feedback/src/index.ts +++ b/plugins/feedback/src/index.ts @@ -1,4 +1,4 @@ -export { default as FeedbackIcon } from '@material-ui/icons/TextsmsOutlined'; +export { default as FeedbackIcon } from '@mui/icons-material/TextsmsOutlined'; export { feedbackPlugin, GlobalFeedbackPage,