diff --git a/frontend/src/component/admin/instance-admin/InstanceStats/InstanceStats.tsx b/frontend/src/component/admin/instance-admin/InstanceStats/InstanceStats.tsx index 3584c1f30a..ba132eb20c 100644 --- a/frontend/src/component/admin/instance-admin/InstanceStats/InstanceStats.tsx +++ b/frontend/src/component/admin/instance-admin/InstanceStats/InstanceStats.tsx @@ -13,7 +13,6 @@ import { useInstanceStats } from '../../../../hooks/api/getters/useInstanceStats import { formatApiPath } from '../../../../utils/formatPath'; import { PageContent } from '../../../common/PageContent/PageContent'; import { PageHeader } from '../../../common/PageHeader/PageHeader'; -import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; export const InstanceStats: VFC = () => { const { stats } = useInstanceStats(); diff --git a/frontend/src/component/common/FormTemplate/FormTemplate.tsx b/frontend/src/component/common/FormTemplate/FormTemplate.tsx index fa7a84b130..9e049463a3 100644 --- a/frontend/src/component/common/FormTemplate/FormTemplate.tsx +++ b/frontend/src/component/common/FormTemplate/FormTemplate.tsx @@ -13,13 +13,13 @@ import { ConditionallyRender } from 'component/common/ConditionallyRender/Condit import Loader from '../Loader/Loader'; import copy from 'copy-to-clipboard'; import useToast from 'hooks/useToast'; -import React, { useState } from 'react'; +import React, { ReactNode, useState } from 'react'; import { ReactComponent as MobileGuidanceBG } from 'assets/img/mobileGuidanceBg.svg'; import { formTemplateSidebarWidth } from './FormTemplate.styles'; import { relative } from 'themes/themeStyles'; interface ICreateProps { - title?: string; + title?: ReactNode; description: string; documentationLink: string; documentationLinkLabel: string; @@ -27,6 +27,7 @@ interface ICreateProps { modal?: boolean; disablePadding?: boolean; formatApiCode?: () => string; + footer?: ReactNode; } const StyledContainer = styled('section', { @@ -46,6 +47,17 @@ const StyledContainer = styled('section', { const StyledRelativeDiv = styled('div')(({ theme }) => relative); +const StyledMain = styled('div')(({ theme }) => ({ + display: 'flex', + flexDirection: 'column', + flexGrow: 1, + flexShrink: 1, + width: '100%', + [theme.breakpoints.down(1100)]: { + width: '100%', + }, +})); + const StyledFormContent = styled('div', { shouldForwardProp: prop => prop !== 'disablePadding', })<{ disablePadding?: boolean }>(({ theme, disablePadding }) => ({ @@ -65,6 +77,17 @@ const StyledFormContent = styled('div', { }, })); +const StyledFooter = styled('div')(({ theme }) => ({ + backgroundColor: theme.palette.background.paper, + padding: theme.spacing(4, 6), + [theme.breakpoints.down('lg')]: { + padding: theme.spacing(4), + }, + [theme.breakpoints.down(500)]: { + padding: theme.spacing(4, 2), + }, +})); + const StyledTitle = styled('h1')(({ theme }) => ({ marginBottom: theme.fontSizes.mainHeader, fontWeight: 'normal', @@ -161,6 +184,7 @@ const FormTemplate: React.FC = ({ modal, formatApiCode, disablePadding, + footer, }) => { const { setToastData } = useToast(); const smallScreen = useMediaQuery(`(max-width:${1099}px)`); @@ -219,21 +243,32 @@ const FormTemplate: React.FC = ({ } /> - + + + } + elseShow={ + <> + {title}} + /> + {children} + + } + /> + } - elseShow={ + condition={footer !== undefined} + show={() => ( <> - {title}} - /> - {children} + + {footer} - } - />{' '} - + )} + /> + ({ + margin: theme.spacing(1, 0, 6), + display: 'flex', + justifyContent: 'flex-end', +})); + +export const IntegrationDelete: VFC = ({ id }) => { + const [isOpen, setIsOpen] = useState(false); + const { removeAddon } = useAddonsApi(); + const { refetchAddons } = useAddons(); + const { setToastData, setToastApiError } = useToast(); + const navigate = useNavigate(); + const onSubmit = async () => { + try { + await removeAddon(id); + refetchAddons(); + setToastData({ + type: 'success', + title: 'Success', + text: 'Deleted addon successfully', + }); + } catch (error: unknown) { + setToastApiError(formatUnknownError(error)); + } + navigate('/integrations'); + }; + + return ( + <> + Delete integration + + Deleting an integration it will delete the entire configuration + and it will automatically disable the integration + + + { + e.preventDefault(); + setIsOpen(true); + }} + > + Delete integration + + + setIsOpen(false)} + title="Confirm deletion" + > +
Are you sure you want to delete this Addon?
+
+ + ); +}; diff --git a/frontend/src/component/integrations/IntegrationForm/IntegrationDeleteDialog/IntegrationDeleteDialog.tsx b/frontend/src/component/integrations/IntegrationForm/IntegrationDeleteDialog/IntegrationDeleteDialog.tsx deleted file mode 100644 index e2c4c40611..0000000000 --- a/frontend/src/component/integrations/IntegrationForm/IntegrationDeleteDialog/IntegrationDeleteDialog.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import { type VFC } from 'react'; -import { Dialogue } from 'component/common/Dialogue/Dialogue'; -import type { AddonSchema } from 'openapi'; -import useAddonsApi from 'hooks/api/actions/useAddonsApi/useAddonsApi'; -import useAddons from 'hooks/api/getters/useAddons/useAddons'; -import useToast from 'hooks/useToast'; -import { formatUnknownError } from 'utils/formatUnknownError'; -import { useNavigate } from 'react-router-dom'; - -interface IIntegrationDeleteDialogProps { - id: AddonSchema['id']; - isOpen: boolean; - onClose: () => void; -} - -export const IntegrationDeleteDialog: VFC = ({ - id, - isOpen, - onClose, -}) => { - const { removeAddon } = useAddonsApi(); - const { refetchAddons } = useAddons(); - const { setToastData, setToastApiError } = useToast(); - const navigate = useNavigate(); - const onSubmit = async () => { - try { - await removeAddon(id); - refetchAddons(); - setToastData({ - type: 'success', - title: 'Success', - text: 'Deleted addon successfully', - }); - } catch (error: unknown) { - setToastApiError(formatUnknownError(error)); - } - onClose(); - navigate('/integrations'); - }; - - return ( - -
Are you sure you want to delete this Addon?
-
- ); -}; diff --git a/frontend/src/component/integrations/IntegrationForm/IntegrationForm.styles.tsx b/frontend/src/component/integrations/IntegrationForm/IntegrationForm.styles.tsx index 85828fd8d0..8c7584f3da 100644 --- a/frontend/src/component/integrations/IntegrationForm/IntegrationForm.styles.tsx +++ b/frontend/src/component/integrations/IntegrationForm/IntegrationForm.styles.tsx @@ -1,33 +1,34 @@ -import { styled } from '@mui/system'; -import { FormControlLabel, TextField } from '@mui/material'; +import { Paper, styled } from '@mui/material'; +import { FormControlLabel, TextField, Typography } from '@mui/material'; +import { forwardRef, type FC, type ReactNode, ComponentProps } from 'react'; -export const StyledForm = styled('form')({ +export const StyledForm = styled('form')(({ theme }) => ({ display: 'flex', flexDirection: 'column', height: '100%', - gap: '1rem', -}); + gap: theme.spacing(2), + marginTop: theme.spacing(1), +})); -export const StyledFormSection = styled('section')({ - marginBottom: '36px', -}); - -export const StyledAlerts = styled(StyledFormSection)(({ theme }) => ({ +export const StyledAlerts = styled('section')(({ theme }) => ({ display: 'flex', flexDirection: 'column', gap: theme.spacing(2), })); -export const StyledHelpText = styled('p')({ - marginBottom: '0.5rem', -}); +export const StyledHelpText = styled('p')(({ theme }) => ({ + marginBottom: theme.spacing(1), + color: theme.palette.text.secondary, + fontSize: theme.typography.body2.fontSize, +})); -export const StyledContainer = styled('div')({ - maxWidth: '600px', -}); +export const StyledContainer = styled('div')(({ theme }) => ({ + display: 'flex', + flexDirection: 'column', + gap: theme.spacing(4), +})); export const StyledButtonContainer = styled('div')({ - marginTop: 'auto', display: 'flex', justifyContent: 'flex-end', }); @@ -35,24 +36,63 @@ export const StyledButtonContainer = styled('div')({ export const StyledButtonSection = styled('section')(({ theme }) => ({ display: 'flex', justifyContent: 'flex-end', - paddingTop: theme.spacing(2), - gap: theme.spacing(1), + gap: theme.spacing(2), })); -export const StyledTextField = styled(TextField)({ +export const StyledTextField = styled(TextField)(({ theme }) => ({ width: '100%', - marginBottom: '1rem', - marginTop: '0px', -}); +})); -export const StyledSelectAllFormControlLabel = styled(FormControlLabel)({ - paddingBottom: '16px', -}); +export const StyledSelectAllFormControlLabel = styled(FormControlLabel)( + ({ theme }) => ({ + paddingBottom: theme.spacing(1), + }) +); -export const StyledTitle = styled('h4')({ - marginBottom: '8px', -}); +export const StyledTitle = forwardRef< + HTMLHeadingElement, + { children: ReactNode } +>(({ children }, ref) => ( + ({ + margin: theme.spacing(1, 0), + })} + > + {children} + +)); export const StyledAddonParameterContainer = styled('div')({ marginTop: '25px', }); + +export const StyledConfigurationSection = styled('section')(({ theme }) => ({ + borderWidth: '1px', + borderStyle: 'solid', + borderColor: theme.palette.neutral.border, + borderRadius: `${theme.shape.borderRadiusLarge}px`, + padding: theme.spacing(3), + display: 'flex', + flexDirection: 'column', + gap: theme.spacing(3), +})); + +export const StyledRaisedSection: FC> = ({ + ...props +}) => ( + ({ + background: theme.palette.background.elevation1, + padding: theme.spacing(2, 3), + display: 'flex', + flexDirection: 'column', + width: '100%', + borderRadius: `${theme.shape.borderRadiusLarge}px`, + })} + {...props} + /> +); diff --git a/frontend/src/component/integrations/IntegrationForm/IntegrationForm.tsx b/frontend/src/component/integrations/IntegrationForm/IntegrationForm.tsx index 6056beb14e..756284226c 100644 --- a/frontend/src/component/integrations/IntegrationForm/IntegrationForm.tsx +++ b/frontend/src/component/integrations/IntegrationForm/IntegrationForm.tsx @@ -6,13 +6,7 @@ import { useState, VFC, } from 'react'; -import { - Alert, - Button, - Divider, - FormControlLabel, - Switch, -} from '@mui/material'; +import { Alert, Button, Divider, Typography } from '@mui/material'; import produce from 'immer'; import { trim } from 'component/common/util'; import type { AddonSchema, AddonTypeSchema } from 'openapi'; @@ -31,23 +25,24 @@ import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; import PermissionButton from 'component/common/PermissionButton/PermissionButton'; import { CREATE_ADDON, - DELETE_ADDON, UPDATE_ADDON, } from '../../providers/AccessProvider/permissions'; import { StyledForm, - StyledFormSection, StyledAlerts, - StyledHelpText, StyledTextField, StyledContainer, StyledButtonContainer, StyledButtonSection, + StyledConfigurationSection, + StyledTitle, + StyledRaisedSection, } from './IntegrationForm.styles'; -import { useTheme } from '@mui/system'; import { GO_BACK } from 'constants/navigate'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; -import { IntegrationDeleteDialog } from './IntegrationDeleteDialog/IntegrationDeleteDialog'; +import { IntegrationDelete } from './IntegrationDelete/IntegrationDelete'; +import { IntegrationStateSwitch } from './IntegrationStateSwitch/IntegrationStateSwitch'; +import { capitalizeFirst } from 'utils/capitalizeFirst'; type IntegrationFormProps = { provider?: AddonTypeSchema; @@ -65,8 +60,6 @@ export const IntegrationForm: VFC = ({ const { createAddon, updateAddon } = useAddonsApi(); const { setToastData, setToastApiError } = useToast(); const navigate = useNavigate(); - const theme = useTheme(); - const [isDeleteOpen, setDeleteOpen] = useState(false); const { projects: availableProjects } = useProjects(); const selectableProjects = availableProjects.map(project => ({ value: project.id, @@ -260,113 +253,19 @@ export const IntegrationForm: VFC = ({ return ( + {submitText} {name ? capitalizeFirst(`${name} `) : ''} + integration + + } description={description || ''} documentationLink={documentationUrl} documentationLinkLabel="Addon documentation" formatApiCode={formatApiCode} - > - - - - {alerts?.map(({ type, text }) => ( - {text} - ))} - - ( - - )} - /> - - - - - What is your addon description? - - - - - - - - - - - - - - - - - - - + footer={ - + = ({ - ( - <> - { - e.preventDefault(); - setDeleteOpen(true); - }} - > - Delete - - { - setDeleteOpen(false); - }} - /> - - )} - /> + } + > + + + ( + + {alerts?.map(({ type, text }) => ( + {text} + ))} + + )} + /> + ); diff --git a/frontend/src/component/integrations/IntegrationForm/IntegrationInstall/IntegrationInstall.tsx b/frontend/src/component/integrations/IntegrationForm/IntegrationInstall/IntegrationInstall.tsx index 972c38926c..3e945af42e 100644 --- a/frontend/src/component/integrations/IntegrationForm/IntegrationInstall/IntegrationInstall.tsx +++ b/frontend/src/component/integrations/IntegrationForm/IntegrationInstall/IntegrationInstall.tsx @@ -1,10 +1,5 @@ -import React from 'react'; -import { - StyledFormSection, - StyledHelpText, - StyledTitle, -} from '../IntegrationForm.styles'; -import { Button } from '@mui/material'; +import { StyledHelpText, StyledTitle } from '../IntegrationForm.styles'; +import { Box, Button, styled } from '@mui/material'; import { Link } from 'react-router-dom'; export interface IAddonInstallProps { @@ -13,27 +8,37 @@ export interface IAddonInstallProps { helpText?: string; } +const StyledBox = styled(Box)(({ theme }) => ({ + display: 'flex', + columnGap: theme.spacing(3), + [theme.breakpoints.down('sm')]: { flexDirection: 'column' }, +})); + export const IntegrationInstall = ({ url, title = 'Install addon', helpText = 'Click this button to install this addon.', }: IAddonInstallProps) => { return ( - - - {title} - {helpText} - - - + + {title} + + + {helpText} + + + + + + ); }; diff --git a/frontend/src/component/integrations/IntegrationForm/IntegrationMultiSelector/IntegrationMultiSelector.tsx b/frontend/src/component/integrations/IntegrationForm/IntegrationMultiSelector/IntegrationMultiSelector.tsx index 8aa157854d..8a71e76bb7 100644 --- a/frontend/src/component/integrations/IntegrationForm/IntegrationMultiSelector/IntegrationMultiSelector.tsx +++ b/frontend/src/component/integrations/IntegrationForm/IntegrationMultiSelector/IntegrationMultiSelector.tsx @@ -147,7 +147,7 @@ export const IntegrationMultiSelector: VFC = ({ return ( - {capitalize(entityName)}s + {capitalize(`${entityName}s`)} {required ? ( * @@ -167,7 +167,7 @@ export const IntegrationMultiSelector: VFC = ({ show={} /> - Parameters {editMode ? (

Sensitive parameters will be masked with value "***** diff --git a/frontend/src/component/integrations/IntegrationForm/IntegrationStateSwitch/IntegrationStateSwitch.tsx b/frontend/src/component/integrations/IntegrationForm/IntegrationStateSwitch/IntegrationStateSwitch.tsx new file mode 100644 index 0000000000..1983d7c77e --- /dev/null +++ b/frontend/src/component/integrations/IntegrationForm/IntegrationStateSwitch/IntegrationStateSwitch.tsx @@ -0,0 +1,42 @@ +import { + Box, + FormControlLabel, + Switch, + Typography, + styled, +} from '@mui/material'; +import { MouseEventHandler, VFC } from 'react'; + +interface IIntegrationStateSwitchProps { + checked: boolean; + onClick: MouseEventHandler; +} + +const StyledContainer = styled('div')({ + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + width: '100%', +}); + +export const IntegrationStateSwitch: VFC = ({ + checked, + onClick, +}) => { + return ( + + Integration status + } + label={ + ({ marginLeft: theme.spacing(0.5) })} + > + {checked ? 'Enabled' : 'Disabled'} + + } + /> + + ); +}; diff --git a/frontend/src/themes/theme.ts b/frontend/src/themes/theme.ts index c207c73d4a..6a0dd9b6db 100644 --- a/frontend/src/themes/theme.ts +++ b/frontend/src/themes/theme.ts @@ -39,6 +39,10 @@ const theme = { fontSize: '1rem', fontWeight: '700', }, + h4: { + fontSize: '1rem', + fontWeight: '400', + }, caption: { fontSize: `${12 / 16}rem`, }, diff --git a/src/lib/addons/datadog-definition.ts b/src/lib/addons/datadog-definition.ts index 96833455bd..9f9077016a 100644 --- a/src/lib/addons/datadog-definition.ts +++ b/src/lib/addons/datadog-definition.ts @@ -53,11 +53,10 @@ const dataDogDefinition: IAddonDefinition = { { name: 'customHeaders', displayName: 'Extra HTTP Headers', - placeholder: `{ - "ISTIO_USER_KEY": "hunter2", - "SOME_OTHER_CUSTOM_HTTP_HEADER": "SOMEVALUE" - }`, - description: `(Optional) Used to add extra HTTP Headers to the request the plugin fires off. Format here needs to be a valid json object of key value pairs where both key and value are strings`, + placeholder: + '{\n"ISTIO_USER_KEY": "hunter2",\n"SOME_OTHER_CUSTOM_HTTP_HEADER": "SOMEVALUE"\n}', + description: + '(Optional) Used to add extra HTTP Headers to the request the plugin fires off. Format here needs to be a valid json object of key value pairs where both key and value are strings', required: false, sensitive: true, type: 'textfield', diff --git a/src/lib/addons/slack-definition.ts b/src/lib/addons/slack-definition.ts index c5e0e28f4d..2b163af111 100644 --- a/src/lib/addons/slack-definition.ts +++ b/src/lib/addons/slack-definition.ts @@ -63,10 +63,8 @@ const slackDefinition: IAddonDefinition = { { name: 'customHeaders', displayName: 'Extra HTTP Headers', - placeholder: `{ - "ISTIO_USER_KEY": "hunter2", - "SOME_OTHER_CUSTOM_HTTP_HEADER": "SOMEVALUE" - }`, + placeholder: + '{\n"ISTIO_USER_KEY": "hunter2",\n"SOME_OTHER_CUSTOM_HTTP_HEADER": "SOMEVALUE"\n}', description: `(Optional) Used to add extra HTTP Headers to the request the plugin fires off. Format here needs to be a valid json object of key value pairs where both key and value are strings`, required: false, sensitive: true, diff --git a/src/lib/addons/teams-definition.ts b/src/lib/addons/teams-definition.ts index 707607ebef..2c6bc0224b 100644 --- a/src/lib/addons/teams-definition.ts +++ b/src/lib/addons/teams-definition.ts @@ -34,10 +34,8 @@ const teamsDefinition: IAddonDefinition = { { name: 'customHeaders', displayName: 'Extra HTTP Headers', - placeholder: `{ - "ISTIO_USER_KEY": "hunter2", - "SOME_OTHER_CUSTOM_HTTP_HEADER": "SOMEVALUE" - }`, + placeholder: + '{\n"ISTIO_USER_KEY": "hunter2",\n"SOME_OTHER_CUSTOM_HTTP_HEADER": "SOMEVALUE"\n}', description: `(Optional) Used to add extra HTTP Headers to the request the plugin fires off. Format here needs to be a valid json object of key value pairs where both key and value are strings`, required: false, sensitive: true, diff --git a/src/lib/addons/webhook-definition.ts b/src/lib/addons/webhook-definition.ts index 9ee4bec4b0..7674674752 100644 --- a/src/lib/addons/webhook-definition.ts +++ b/src/lib/addons/webhook-definition.ts @@ -82,10 +82,8 @@ const webhookDefinition: IAddonDefinition = { { name: 'customHeaders', displayName: 'Extra HTTP Headers', - placeholder: `{ - "ISTIO_USER_KEY": "hunter2", - "SOME_OTHER_CUSTOM_HTTP_HEADER": "SOMEVALUE" - }`, + placeholder: + '{\n"ISTIO_USER_KEY": "hunter2",\n"SOME_OTHER_CUSTOM_HTTP_HEADER": "SOMEVALUE"\n}', description: `(Optional) Used to add extra HTTP Headers to the request the plugin fires off. Format here needs to be a valid json object of key value pairs where both key and value are strings`, required: false, sensitive: true,