Skip to content

Commit

Permalink
PMM-11148 Service management - Add/Edit service (#599)
Browse files Browse the repository at this point in the history
* PMM-1148 initial inventory management rework

* PMM-11148 Refactor AddInstance

* PMM-11148 remove unnecessary tooltip icon

* PMM-11148 Fix discovery tests

* PMM-11148 Fix add instance submission

* PMM-11148 Integrate service editing api

* PMM-11148 fix custom labels field

* PMM-11148 add success modal

* PMM-11148 fix inventory test

* PMM-11148 add labels tooltip & fix description

* PMM-11148 refactor

* PMM-11148 add required property

* PMM-11148 add feature check

* PMM-11148 adjust services delete

* PMM-11148 remove documentation links

* PMM-11148 Wording, boolean state, etc.

* PMM-11148 update tablestat options name

* PMM-11148 Extract messages for service tab

* PMM-11148 Add warning when changing cluster label

* PMM-11148 Add docs link to confirm edit service dialog

* PMM-11148 Update api endpoint name

* PMM-11148 Move delete services modal to component

* PMM-11148 Fix bugs

* PMM-11148 Call onSucces when single service is deleted

* PMM-11148 Import FC directly

* PMM-11148 Fix spacing

* PMM-11148 Fix errors from component library integration

* PMM-11148 Fix DeleteServicesModal tests

* PMM-11148 Use correct urls for services

* PMM-11148 Use shortened docs link

* PMM-11148 Increase width for azure instance form

* PMM-11148 Jump straight to add instance page & reorder
  • Loading branch information
matejkubinec authored Aug 28, 2023
1 parent 6c59e50 commit 3737244
Show file tree
Hide file tree
Showing 70 changed files with 1,786 additions and 662 deletions.
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
export const Messages = {
sectionTitle: 'Select service type',
description: 'Select the service type you want to configure and then add it to your inventory.',
titles: {
rds: 'Amazon RDS',
azure: 'Microsoft Azure MySQL or PostgreSQL',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,52 +1,30 @@
import { css } from '@emotion/css';

import { GrafanaTheme } from '@grafana/data';
import { GrafanaTheme2 } from '@grafana/data';

export const getStyles = ({ border, colors, spacing, typography }: GrafanaTheme) => ({
navigationButton: css`
export const getStyles = (theme: GrafanaTheme2) => ({
Content: css`
display: flex;
flex-direction: column;
justify-content: flex-end;
align-items: center;
padding-bottom: 1.2em;
margin: ${spacing.sm};
border-radius: ${border.radius.md};
width: 230px;
height: 160px;
text-align: center;
background-color: transparent;
border: ${border.width.sm} dashed ${colors.border2};
:hover {
cursor: pointer;
background-color: ${colors.dropdownOptionHoverBg};
border: ${border.width.sm} solid ${colors.border2};
}
align-items: flex-start;
`,
navigationPanel: css`
NavigationPanel: css`
display: flex;
flex-direction: row;
justify-content: center;
justify-content: flex-start;
flex-wrap: wrap;
max-width: 800px;
max-width: 825px;
width: 100%;
overflow: hidden;
gap: ${theme.spacing(2)};
padding: 3px;
margin: -3px;
`,
content: css`
display: flex;
flex-direction: column;
align-items: center;
margin-top: 2em;
InstanceCard: css`
width: 375px;
margin: 0;
`,
addInstance: css`
margin-top: ${spacing.sm};
font-size: ${typography.size.sm};
`,
addInstanceTitle: css`
margin-top: ${spacing.sm};
overflow: hidden;
line-height: ${typography.lineHeight.md};
width: 65%;
height: 3em;
Description: css`
color: ${theme.colors.text.secondary};
`,
});
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,20 @@ import { Provider } from 'react-redux';
import { configureStore } from 'app/store/configureStore';
import { StoreState } from 'app/types';

import { InstanceAvailable } from '../../panel.types';

import { AddInstance } from './AddInstance';
import { instanceList } from './AddInstance.constants';

jest.mock('app/percona/settings/Settings.service');

const selectedInstanceType: InstanceAvailable = { type: '' };

describe('AddInstance page::', () => {
it('should render a given number of links', async () => {
const ui = withStore(<AddInstance showAzure={false} onSelectInstanceType={() => {}} />);
const ui = withStore(
<AddInstance showAzure={false} onSelectInstanceType={() => {}} selectedInstanceType={selectedInstanceType} />
);
await waitFor(() => render(ui));

expect(screen.getAllByRole('button')).toHaveLength(instanceList.length);
Expand All @@ -22,7 +28,9 @@ describe('AddInstance page::', () => {
});

it('should render azure option', async () => {
const ui = withStore(<AddInstance showAzure onSelectInstanceType={() => {}} />);
const ui = withStore(
<AddInstance showAzure onSelectInstanceType={() => {}} selectedInstanceType={selectedInstanceType} />
);
await waitFor(() => render(ui));

expect(screen.getAllByRole('button')).toHaveLength(instanceList.length + 1);
Expand All @@ -35,13 +43,15 @@ describe('AddInstance page::', () => {
it('should invoke a callback with a proper instance type', async () => {
const onSelectInstanceType = jest.fn();

const ui = withStore(<AddInstance showAzure onSelectInstanceType={onSelectInstanceType} />);
const ui = withStore(
<AddInstance showAzure onSelectInstanceType={onSelectInstanceType} selectedInstanceType={selectedInstanceType} />
);
render(ui);

expect(onSelectInstanceType).toBeCalledTimes(0);

const button = await screen.findByTestId('rds-instance');
fireEvent.click(button);
const button = (await screen.findByTestId('rds-instance')).querySelector('button');
fireEvent.click(button!);

expect(onSelectInstanceType).toBeCalledTimes(1);
expect(onSelectInstanceType.mock.calls[0][0]).toStrictEqual({ type: 'rds' });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@
import React, { FC, useMemo } from 'react';
import { v4 as uuidv4 } from 'uuid';

import { useStyles } from '@grafana/ui';
import { Database } from 'app/percona/shared/components/Elements/Icons/Database';
import { Card, Icon, useStyles2 } from '@grafana/ui';
import { Databases } from 'app/percona/shared/core';
import * as UserFlow from 'app/percona/shared/core/reducers/userFlow';
import { useDispatch } from 'app/types';
Expand All @@ -12,37 +11,34 @@ import { InstanceAvailableType, InstanceTypesExtra } from '../../panel.types';

import { Messages } from './AddInstance.messages';
import { getStyles } from './AddInstance.styles';
import { AddInstanceProps, SelectInstanceProps } from './AddInstance.types';
import { AddInstanceProps, InstanceListItem, SelectInstanceProps } from './AddInstance.types';

export const SelectInstance: FC<SelectInstanceProps> = ({ type, selectInstanceType, title }) => {
const styles = useStyles(getStyles);
export const SelectInstance: FC<SelectInstanceProps> = ({ type, isSelected, icon, selectInstanceType, title }) => {
const styles = useStyles2(getStyles);

return (
<button
className={styles.navigationButton}
data-testid={`${type}-instance`}
onClick={selectInstanceType(type)}
type="button"
>
<Database />
<span className={styles.addInstanceTitle}>{title}</span>
<span className={styles.addInstance}>{Messages.titles.addInstance}</span>
</button>
<Card data-testid={`${type}-instance`} onClick={selectInstanceType(type)} className={styles.InstanceCard}>
<Card.Heading>{title}</Card.Heading>
<Card.Description>{Messages.titles.addInstance}</Card.Description>
<Card.Figure>
<Icon size="xxxl" name={icon ? icon : 'database'} />
</Card.Figure>
</Card>
);
};

export const AddInstance: FC<AddInstanceProps> = ({ onSelectInstanceType, showAzure }) => {
const styles = useStyles(getStyles);
const instanceList = useMemo(
export const AddInstance: FC<AddInstanceProps> = ({ selectedInstanceType, onSelectInstanceType, showAzure }) => {
const styles2 = useStyles2(getStyles);
const instanceList = useMemo<InstanceListItem[]>(
() => [
{ type: InstanceTypesExtra.rds, title: Messages.titles.rds },
{ type: InstanceTypesExtra.azure, title: Messages.titles.azure, isHidden: !showAzure },
{ type: Databases.postgresql, title: Messages.titles.postgresql },
{ type: Databases.mysql, title: Messages.titles.mysql },
{ type: Databases.mongodb, title: Messages.titles.mongodb },
{ type: Databases.proxysql, title: Messages.titles.proxysql },
{ type: Databases.mysql, title: Messages.titles.mysql, icon: 'percona-database-mysql' },
{ type: Databases.mongodb, title: Messages.titles.mongodb, icon: 'percona-database-mongodb' },
{ type: Databases.postgresql, title: Messages.titles.postgresql, icon: 'percona-database-postgresql' },
{ type: Databases.proxysql, title: Messages.titles.proxysql, icon: 'percona-database-proxysql' },
{ type: Databases.haproxy, title: Messages.titles.haproxy, icon: 'percona-database-haproxy' },
{ type: InstanceTypesExtra.external, title: Messages.titles.external },
{ type: Databases.haproxy, title: Messages.titles.haproxy },
{ type: InstanceTypesExtra.azure, title: Messages.titles.azure, isHidden: !showAzure },
],
[showAzure]
);
Expand All @@ -61,14 +57,18 @@ export const AddInstance: FC<AddInstanceProps> = ({ onSelectInstanceType, showAz
};

return (
<section className={styles.content}>
<nav className={styles.navigationPanel}>
<section className={styles2.Content}>
<h2>{Messages.sectionTitle}</h2>
<p className={styles2.Description}>{Messages.description}</p>
<nav className={styles2.NavigationPanel}>
{instanceList
.filter(({ isHidden }) => !isHidden)
.map((item) => (
<SelectInstance
isSelected={item.type === selectedInstanceType.type}
selectInstanceType={selectInstanceType}
type={item.type}
icon={item.icon}
title={item.title}
key={item.type}
/>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,21 @@
import { InstanceAvailable } from '../../panel.types';
import { IconName } from '@grafana/data';

export interface SelectInstanceProps {
type: string;
title: string;
selectInstanceType: (type: string) => () => void;
import { InstanceAvailable, InstanceAvailableType } from '../../panel.types';

export interface SelectInstanceProps extends InstanceListItem {
isSelected: boolean;
selectInstanceType: (type: InstanceAvailableType) => () => void;
}

export interface AddInstanceProps {
selectedInstanceType: InstanceAvailable;
onSelectInstanceType: (arg: InstanceAvailable) => void;
showAzure: boolean;
}

export interface InstanceListItem {
type: InstanceAvailableType;
icon?: IconName;
title: string;
isHidden?: boolean;
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,15 @@
export const Messages = {
pageTitleSelection: 'Inventory / Add service / Step 1 of 2',
pageTitleConfiguration: 'Inventory / Add service / Step 2 of 2',
selectionStep: {
cancel: 'Cancel',
next: 'Next step: Configuration',
},
configurationStep: {
cancel: 'Cancel',
next: 'Add service',
discover: 'Discover',
},
form: {
trackingOptions: {
none: "Don't track",
Expand All @@ -22,4 +33,8 @@ export const Messages = {
addRemoteInstance: 'Add remote instance',
},
},
success: {
title: (service: string) => `Service “${service}” added to your inventory`,
description: (serviceType: string) => `Your ${serviceType} service instance is now ready to be monitored.`,
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export const getStyles = ({ spacing }: GrafanaTheme) => ({
white-space: nowrap;
`,
addRemoteInstanceTitle: css`
text-align: center;
text-align: left;
`,
addRemoteInstanceButtons: css`
margin-top: ${spacing.md};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ jest.mock('app/percona/shared/helpers/logger', () => {
describe('Add remote instance:: ', () => {
it('should render correct for mysql and postgres and highlight empty mandatory fields on submit', async () => {
const type = Databases.mysql;
render(<AddRemoteInstance instance={{ type, credentials: {} }} selectInstance={jest.fn()} />);
render(<AddRemoteInstance onSubmit={jest.fn()} instance={{ type, credentials: {} }} selectInstance={jest.fn()} />);

expect(screen.getByTestId('address-text-input').classList.contains('invalid')).toBe(false);
expect(screen.getByTestId('username-text-input').classList.contains('invalid')).toBe(false);
Expand All @@ -35,7 +35,7 @@ describe('Add remote instance:: ', () => {

it('should render for external service and highlight empty mandatory fields on submit', async () => {
const type = InstanceTypesExtra.external;
render(<AddRemoteInstance instance={{ type, credentials: {} }} selectInstance={jest.fn()} />);
render(<AddRemoteInstance onSubmit={jest.fn()} instance={{ type, credentials: {} }} selectInstance={jest.fn()} />);

expect(screen.getByTestId('address-text-input').classList.contains('invalid')).toBe(false);
expect(screen.getByTestId('metrics_path-text-input').classList.contains('invalid')).toBe(false);
Expand All @@ -55,7 +55,7 @@ describe('Add remote instance:: ', () => {
it('should render correct for HAProxy and highlight empty mandatory fields on submit', async () => {
const type = Databases.haproxy;

render(<AddRemoteInstance instance={{ type, credentials: {} }} selectInstance={jest.fn()} />);
render(<AddRemoteInstance onSubmit={jest.fn()} instance={{ type, credentials: {} }} selectInstance={jest.fn()} />);

expect(screen.getByTestId('address-text-input').classList.contains('invalid')).toBe(false);
expect(screen.getByTestId('username-text-input').classList.contains('invalid')).toBe(false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,16 @@ import { FormApi } from 'final-form';
import React, { FC, useCallback, useMemo, useState } from 'react';
import { Form as FormFinal } from 'react-final-form';

import { Button, useStyles } from '@grafana/ui';
import { AppEvents } from '@grafana/data';
import { useStyles } from '@grafana/ui';
import appEvents from 'app/core/app_events';
import { useCancelToken } from 'app/percona/shared/components/hooks/cancelToken.hook';
import { Databases } from 'app/percona/shared/core';
import { isApiCancelError } from 'app/percona/shared/helpers/api';
import { logger } from 'app/percona/shared/helpers/logger';

import { InstanceAvailableType, InstanceTypes, InstanceTypesExtra, INSTANCE_TYPES_LABELS } from '../../panel.types';
import { ADD_INSTANCE_FORM_NAME } from '../../panel.constants';
import { InstanceTypesExtra, InstanceTypes, INSTANCE_TYPES_LABELS, InstanceAvailableType } from '../../panel.types';

import { ADD_AZURE_CANCEL_TOKEN, ADD_RDS_CANCEL_TOKEN } from './AddRemoteInstance.constants';
import { Messages } from './AddRemoteInstance.messages';
Expand All @@ -35,7 +38,10 @@ import {
import { ExternalServiceConnectionDetails } from './FormParts/ExternalServiceConnectionDetails/ExternalServiceConnectionDetails';
import { HAProxyConnectionDetails } from './FormParts/HAProxyConnectionDetails/HAProxyConnectionDetails';

const AddRemoteInstance: FC<AddRemoteInstanceProps> = ({ instance: { type, credentials }, selectInstance }) => {
const AddRemoteInstance: FC<AddRemoteInstanceProps> = ({
instance: { type, credentials },
onSubmit: submitWrapper,
}) => {
const styles = useStyles(getStyles);

const { remoteInstanceCredentials, discoverName } = getInstanceData(type, credentials);
Expand Down Expand Up @@ -70,7 +76,10 @@ const AddRemoteInstance: FC<AddRemoteInstanceProps> = ({ instance: { type, crede
} else {
await AddRemoteInstanceService.addRemote(type, values, generateToken(remoteToken(type)));
}

appEvents.emit(AppEvents.alertSuccess, [
Messages.success.title(values.serviceName || values.address || ''),
Messages.success.description(INSTANCE_TYPES_LABELS[type as Databases]),
]);
window.location.href = '/graph/inventory/';
} catch (e) {
if (isApiCancelError(e)) {
Expand Down Expand Up @@ -130,37 +139,23 @@ const AddRemoteInstance: FC<AddRemoteInstanceProps> = ({ instance: { type, crede
if (databaseType === '') {
return Messages.form.titles.addRemoteInstance;
}
return `Add remote ${INSTANCE_TYPES_LABELS[databaseType]} Instance`;
return `Configuring ${INSTANCE_TYPES_LABELS[databaseType]} service`;
};

return (
<div className={styles.formWrapper}>
<FormFinal
onSubmit={onSubmit}
onSubmit={(values) => submitWrapper(onSubmit(values))}
initialValues={initialValues}
mutators={{
setValue: ([field, value], state, { changeValue }) => {
changeValue(state, field, () => value);
},
}}
render={({ form, handleSubmit }) => (
<form onSubmit={handleSubmit} data-testid="add-remote-instance-form">
<h4 className={styles.addRemoteInstanceTitle}>{getHeader(type)}</h4>
<form id={ADD_INSTANCE_FORM_NAME} onSubmit={handleSubmit} data-testid="add-remote-instance-form">
<h3 className={styles.addRemoteInstanceTitle}>{getHeader(type)}</h3>
{formParts(form)}
<div className={styles.addRemoteInstanceButtons}>
<Button id="addInstance" disabled={loading} type="submit">
{Messages.form.buttons.addService}
</Button>
<Button
variant="secondary"
onClick={() => selectInstance({ type: '' })}
disabled={loading}
className={styles.returnButton}
icon="arrow-left"
>
{Messages.form.buttons.toMenu}
</Button>
</div>
</form>
)}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export interface InstanceData {
export interface AddRemoteInstanceProps {
instance: InstanceAvailable;
selectInstance: SelectInstance;
onSubmit: (submitPromise: Promise<void>) => void;
}

export enum DefaultPorts {
Expand Down
Loading

0 comments on commit 3737244

Please sign in to comment.