Skip to content

Conversation

@yaelleC
Copy link
Contributor

@yaelleC yaelleC commented Mar 7, 2022

Fixes #61

Description

When listing datasources, a card is composed of a Heading ('Amazon Redshift'), a Figure (the logo) and some meta info. Meta include the datasource typeName, url and isDefault.

This PR uses the endpoint created by #129 to use a dropdown instead of an input field and populate the url field.

It also upgrades aws-sdk to use allowCustomValue in case the user does not have permission to fetch the clusters

On the managed Secret tab, the dropdown is hidden and a plain text field is shown. The reason for that is that since the dropdown gets its options populated dynamically, when selecting a secret it would not show the clusterID associated with it (as it's a disabled dropdown with no valid options)

Screenshots

No change to the managed secret UI
Dropdown instead of text input for Temp credential UI
Details added to card in datasources list view

Before After
Screenshot 2022-02-22 at 10 27 20 Screenshot 2022-02-22 at 10 27 45
Screenshot 2022-03-07 at 20 03 24 Screenshot 2022-03-07 at 20 03 36
Screenshot 2022-03-17 at 12 47 28 Screenshot 2022-03-17 at 12 45 41
If the user does not have permission to fetch the clusters, she can create one And the AWS Secret Manager view works as expected
Screenshot 2022-03-18 at 09 54 39 Screenshot 2022-03-18 at 09 54 53

Test coverage

Running: yarn test:coverage:change

-------------------|---------|----------|---------|---------|-------------------
File               | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 
-------------------|---------|----------|---------|---------|-------------------
  ConfigEditor.tsx |   91.23 |    79.31 |   94.74 |      90 | 39-44,95-96 
-------------------|---------|----------|---------|---------|-------------------

@yaelleC yaelleC requested a review from a team as a code owner March 7, 2022 19:06
@yaelleC yaelleC requested review from andresmgot and fridgepoet and removed request for a team March 7, 2022 19:06
Comment on lines -1 to -12
import React, { useState, useEffect, FormEvent } from 'react';
import { ConfigSelect, ConnectionConfig, InlineInput } from '@grafana/aws-sdk';
import { DataSourcePluginOptionsEditorProps, SelectableValue } from '@grafana/data';
import { getBackendSrv } from '@grafana/runtime';
import React, { FormEvent, useEffect, useState } from 'react';
import { selectors } from 'selectors';

import {
RedshiftDataSourceOptions,
RedshiftDataSourceSecureJsonData,
RedshiftDataSourceSettings,
RedshiftManagedSecret,
} from '../types';
import { InlineInput, ConfigSelect, ConnectionConfig } from '@grafana/aws-sdk';
import { getBackendSrv } from '@grafana/runtime';
import { AuthTypeSwitch } from './AuthTypeSwitch';
import { selectors } from 'selectors';
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

auto re-org

return res.map((c) => ({
label: c.clusterIdentifier,
value: c.clusterIdentifier,
description: `${c.endpoint.address}:${c.endpoint.port}/${c.database}`,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this returns a database, should we make the input disable and fill automatically based on clusterId selected?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we should, that's the default database, it's possible to create a different one in the same cluster and use that one instead. I would also not display this URL in this selector as the description for the same reason.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!
So I should use the database entered in the db field to create the url in the datasource card instead of this default one?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, that would make sense

Copy link
Contributor

@andresmgot andresmgot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! Just a couple of minor comments here.

Also, would you able to create a PR for the SDK allowing to set other properties? (context) We can open a different issue for that if you don't have time now to do it atm.

return res.map((c) => ({
label: c.clusterIdentifier,
value: c.clusterIdentifier,
description: `${c.endpoint.address}:${c.endpoint.port}/${c.database}`,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we should, that's the default database, it's possible to create a different one in the same cluster and use that one instead. I would also not display this URL in this selector as the description for the same reason.

data-testid={selectors.components.ConfigEditor.ClusterID.testID}
disabled={useManagedSecret}
saveOptions={saveOptions}
hidden={useManagedSecret}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should still show this field as disabled when useManagedSecret

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

weird, when I have disabled={useManagedSecret} it does not disable the select.

When I inspect, I see that although ConfigSelect has disabled=true it's child ResourceSelector has disabled=false.

It seems to be because we override disable there: https://github.com/grafana/grafana-aws-sdk-react/blob/6a051089daf5c577297f4bf695909b4da0194f9f/src/sql/ConfigEditor/ConfigSelect.tsx#L47

I will fix that as part of the SDK PR 🙂

@andresmgot
Copy link
Contributor

BTW, you need to adapt the e2e test (smoke.test.ts) to this change. See how it's done for e2eSelectors.ConfigEditor.ManagedSecret as an example. If you need any help running the e2e tests locally (or you have any question) let me know!

Copy link
Contributor

@andresmgot andresmgot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! but it would be better if we fix the bugs in the SDK before merging this PR (so main is still releaseable, just in case)

.click({ force: true })
.type(datasource.jsonData.defaultRegion)
.type('{enter}');
e2eSelectors.ConfigEditor.ClusterID.testID()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you need to click first, wait for the clusters to load and then type here (see lines 113:115 for the example with secrets)

@yaelleC
Copy link
Contributor Author

yaelleC commented Mar 8, 2022

LGTM! but it would be better if we fix the bugs in the SDK before merging this PR (so main is still releaseable, just in case)

Agreed!
PR: grafana/grafana-aws-sdk-react#13

Comment on lines +40 to +56
expect(screen.getByText(selectors.components.ConfigEditor.ManagedSecret.input)).not.toBeVisible();
expect(screen.getByTestId(selectors.components.ConfigEditor.ClusterIDText.testID)).not.toBeVisible();
expect(screen.getByTestId(selectors.components.ConfigEditor.ClusterID.testID)).toBeVisible();
expect(screen.getByTestId(selectors.components.ConfigEditor.ClusterID.testID)).not.toBeDisabled();
expect(screen.getByText(selectors.components.ConfigEditor.DatabaseUser.input)).not.toBeDisabled();
expect(screen.getByText(selectors.components.ConfigEditor.Database.input)).not.toBeDisabled();
});

it('should switch to use the Secret Manager', () => {
render(<ConfigEditor {...props} />);
screen.getByText('AWS Secrets Manager').click();
expect(screen.getByText(selectors.components.ConfigEditor.ManagedSecret.input)).toBeInTheDocument();
expect(screen.getByText(selectors.components.ConfigEditor.ClusterID.input)).toBeInTheDocument();
expect(screen.getByText(selectors.components.ConfigEditor.Database.input)).toBeInTheDocument();
expect(screen.getByText(selectors.components.ConfigEditor.ManagedSecret.input)).toBeVisible();
expect(screen.getByTestId(selectors.components.ConfigEditor.ClusterIDText.testID)).toBeVisible();
expect(screen.getByTestId(selectors.components.ConfigEditor.ClusterIDText.testID)).toBeDisabled();
expect(screen.getByTestId(selectors.components.ConfigEditor.ClusterID.testID)).not.toBeVisible();
expect(screen.getByTestId(selectors.components.ConfigEditor.DatabaseUser.testID)).toBeDisabled();
expect(screen.getByText(selectors.components.ConfigEditor.Database.input)).not.toBeDisabled();
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These tests would always pass as even hidden or disabled components will be in the document 😄

I've made them stricter to check which inputs are disabled and which are visible

Comment on lines 99 to 106
const getClusterUrl = async (clusterID: string) => {
const { jsonData } = props.options;
if (clusterID != jsonData.clusterIdentifier) {
const clusters = await fetchClusters();
return `${clusters.find((c) => c.value === clusterID)?.description || clusterID}/${jsonData.database || ''}`;
}
return props.options.url;
};
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried to use memoize here to avoid fetching unnecessary clusters (if clusterID is still the same) but I could not get it to work so I added the memo check myself 😉

hidden={!useManagedSecret}
/>
<InlineInput
<ConfigSelect
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A dropdown for better use experience in temporary Creds

saveOptions={saveOptions}
hidden={useManagedSecret}
/>
<InlineInput
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A text input to display clusterID for managed Secret

e2eSelectors.ConfigEditor.ManagedSecret.input().click({ force: true });
// wait for it to load
e2eSelectors.ConfigEditor.ManagedSecret.testID().contains(datasource.jsonData.managedSecret.name);
// e2eSelectors.ConfigEditor.ManagedSecret.testID().contains(datasource.jsonData.managedSecret.name);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

any idea why this line fails? 🤔
I figured we don't need it as the next few ones types in the expected secret and selects it but leaving as commented out for visibility

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mmh, it should work, if not we are risking that the options loads after start typing the name so it messes it up. Are you able to reproduce the error locally? If not it could be the case that the CI account doesn't have enough permissions after the last changes(?).

Copy link
Contributor Author

@yaelleC yaelleC Mar 17, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's managedSecret, not clusters so permissions should not be an issue 🤷‍♀️
I can replicate locally, it is set up exactly the same way as the clusterID dropdown (which passes) - some puzzling behaviour! 🤔

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mmh, weird, anyway it seems stable now

Copy link
Contributor

@andresmgot andresmgot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some comments but mostly minor. Good job!

<InlineInput
{...props}
value={props.options.jsonData.clusterIdentifier ?? ''}
onChange={() => {}}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hehe we should make this property optional (not needed now)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

onChange={() => {}}
label={selectors.components.ConfigEditor.ClusterIDText.input}
data-testid={selectors.components.ConfigEditor.ClusterIDText.testID}
disabled={useManagedSecret}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: disabled={true} since it will always be disabled

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well spotted 😄

expect(onChange).toHaveBeenCalledTimes(1);
expect(onChange).toHaveBeenCalledWith({
...props.options,
jsonData: { ...props.options.jsonData, database: 'abcd' },
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you also cover the case in which the url is set here? I believe it's not modifying it in this test because the url was empty before

clusterIdentifier: s.dbClusterIdentifier,
dbUser: s.username,
},
getClusterUrl(s.dbClusterIdentifier).then((url) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can add a ".catch" for the getClusterUrl so even if the user doesn't have permissions to get clusters, we set the identifier and the db user as we did before.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added inside fetchClusters to return an empty array in catch

};
const onChange = (resource: InputResourceType) => (e: FormEvent<HTMLInputElement>) => {
const value = e.currentTarget.value;
const url = resource === 'database' ? props.options.url.replace(/\/.*$/, `/${value}`) : props.options.url;
Copy link
Contributor

@andresmgot andresmgot Mar 17, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this regex is a bit risky because it assumes that the URL will contain one and only one /. One option would be to store the current cluster URL (without the database) in the state, so this URL can be re-calculated on every change but up to you since it's not that critical.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, I agree, I could not find a workaround but using state seems like a good idea, I'll try that

e2eSelectors.ConfigEditor.ManagedSecret.input().click({ force: true });
// wait for it to load
e2eSelectors.ConfigEditor.ManagedSecret.testID().contains(datasource.jsonData.managedSecret.name);
// e2eSelectors.ConfigEditor.ManagedSecret.testID().contains(datasource.jsonData.managedSecret.name);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mmh, it should work, if not we are risking that the options loads after start typing the name so it messes it up. Are you able to reproduce the error locally? If not it could be the case that the CI account doesn't have enough permissions after the last changes(?).

Copy link
Contributor

@andresmgot andresmgot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 👍

@yaelleC yaelleC merged commit 01ca096 into main Mar 21, 2022
@yaelleC yaelleC deleted the yaelle_61_clusterID-in-dropdown branch March 21, 2022 09:04
@fridgepoet fridgepoet mentioned this pull request Mar 24, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add datasource details in the list

2 participants