Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions docs/analytics/analytics-events.md
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,69 @@ Tracks when an alert is deleted successfully
| ---- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------- |
| name | `"ProbeFailedExecutionsTooHigh" \| "TLSTargetCertificateCloseToExpiring" \| "HTTPRequestDurationTooHighAvg" \| "PingRequestDurationTooHighAvg" \| "DNSRequestDurationTooHighAvg"` | The name of the alert |

### secrets_management

#### synthetic-monitoring_secrets_management_create_secret_button_clicked

Tracks when the create secret button is clicked.

##### Properties

| name | type | description |
| -------- | -------------------------------------------------------------------- | ----------------------------------------------------------------- |
| source | `"check_editor_sidepanel_feature_tabs" \| "config_page_secrets_tab"` | The source context where the secrets management UI is being used. |
| location | `"empty_state" \| "header_action"` | The location where the create button was clicked. |

#### synthetic-monitoring_secrets_management_edit_secret_button_clicked

Tracks when the edit secret button is clicked.

##### Properties

| name | type | description |
| ------ | -------------------------------------------------------------------- | ----------------------------------------------------------------- |
| source | `"check_editor_sidepanel_feature_tabs" \| "config_page_secrets_tab"` | The source context where the secrets management UI is being used. |

#### synthetic-monitoring_secrets_management_delete_secret_button_clicked

Tracks when the delete secret button is clicked.

##### Properties

| name | type | description |
| ------ | -------------------------------------------------------------------- | ----------------------------------------------------------------- |
| source | `"check_editor_sidepanel_feature_tabs" \| "config_page_secrets_tab"` | The source context where the secrets management UI is being used. |

#### synthetic-monitoring_secrets_management_secret_created

Tracks when a secret is successfully created.

##### Properties

| name | type | description |
| ------ | -------------------------------------------------------------------- | ----------------------------------------------------------------- |
| source | `"check_editor_sidepanel_feature_tabs" \| "config_page_secrets_tab"` | The source context where the secrets management UI is being used. |

#### synthetic-monitoring_secrets_management_secret_updated

Tracks when a secret is successfully updated.

##### Properties

| name | type | description |
| ------ | -------------------------------------------------------------------- | ----------------------------------------------------------------- |
| source | `"check_editor_sidepanel_feature_tabs" \| "config_page_secrets_tab"` | The source context where the secrets management UI is being used. |

#### synthetic-monitoring_secrets_management_secret_deleted

Tracks when a secret is successfully deleted.

##### Properties

| name | type | description |
| ------ | -------------------------------------------------------------------- | ----------------------------------------------------------------- |
| source | `"check_editor_sidepanel_feature_tabs" \| "config_page_secrets_tab"` | The source context where the secrets management UI is being used. |

### timepoint_explorer

#### synthetic-monitoring_timepoint_explorer_view_toggle
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export function SecretsPanel() {
padding: ${theme.spacing(2)};
`}
>
<SecretsManagementUI />
<SecretsManagementUI source="check_editor_sidepanel_feature_tabs" />
</div>
);
}
38 changes: 38 additions & 0 deletions src/features/tracking/secretsManagementEvents.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { createSMEventFactory, TrackingEventProps } from 'features/tracking/utils';

import { SecretsManagementSource } from 'page/ConfigPageLayout/tabs/SecretsManagementTab/types';

const secretsManagementEvents = createSMEventFactory('secrets_management');

interface CreateSecretButtonClicked extends TrackingEventProps {
/** The source context where the secrets management UI is being used. */
source: SecretsManagementSource;
/** The location where the create button was clicked. */
location: 'empty_state' | 'header_action';
}

/** Tracks when the create secret button is clicked. */
export const trackCreateSecretButtonClicked =
secretsManagementEvents<CreateSecretButtonClicked>('create_secret_button_clicked');

interface SecretsManagementEvent extends TrackingEventProps {
/** The source context where the secrets management UI is being used. */
source: SecretsManagementSource;
}

/** Tracks when the edit secret button is clicked. */
export const trackEditSecretButtonClicked =
secretsManagementEvents<SecretsManagementEvent>('edit_secret_button_clicked');

/** Tracks when the delete secret button is clicked. */
export const trackDeleteSecretButtonClicked =
secretsManagementEvents<SecretsManagementEvent>('delete_secret_button_clicked');

/** Tracks when a secret is successfully created. */
export const trackSecretCreated = secretsManagementEvents<SecretsManagementEvent>('secret_created');

/** Tracks when a secret is successfully updated. */
export const trackSecretUpdated = secretsManagementEvents<SecretsManagementEvent>('secret_updated');

/** Tracks when a secret is successfully deleted. */
export const trackSecretDeleted = secretsManagementEvents<SecretsManagementEvent>('secret_deleted');
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ describe('SecretEditModal', () => {
name: SECRETS_EDIT_MODE_ADD,
onDismiss: jest.fn(),
open: true,
};
source: 'config_page_secrets_tab',
} as const;

it('should not render when open is false', async () => {
await render(<SecretEditModal {...defaultProps} open={false} />);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ import { GrafanaTheme2 } from '@grafana/data';
import { Alert, Button, Field, IconButton, Input, Modal, TextLink, useStyles2 } from '@grafana/ui';
import { css } from '@emotion/css';
import { standardSchemaResolver } from '@hookform/resolvers/standard-schema';
import { trackSecretCreated, trackSecretUpdated } from 'features/tracking/secretsManagementEvents';

import { Secret } from './types';
import { Secret, SecretsManagementSource } from './types';
import { getErrorMessage } from 'utils';
import { useSaveSecret, useSecret } from 'data/useSecrets';

Expand All @@ -19,6 +20,8 @@ interface SecretEditModalProps {
onDismiss: () => void;
open?: boolean;
existingNames?: string[];
/** The source context where the secrets management UI is being used. */
source: SecretsManagementSource;
}

function getDefaultValues(isNew = true): SecretFormValues & { plaintext?: string } {
Expand Down Expand Up @@ -62,7 +65,7 @@ function createGetFieldError(errors: FormErrorMap) {
};
}

export function SecretEditModal({ open, name, onDismiss, existingNames = [] }: SecretEditModalProps) {
export function SecretEditModal({ open, name, onDismiss, existingNames = [], source }: SecretEditModalProps) {
const { data: secret, isLoading, isError: hasFetchError, error: fetchError } = useSecret(name);
const saveSecret = useSaveSecret();
const isNewSecret = name === SECRETS_EDIT_MODE_ADD;
Expand Down Expand Up @@ -123,6 +126,11 @@ export function SecretEditModal({ open, name, onDismiss, existingNames = [] }: S
},
onSuccess() {
setSaveError(null);
if (isNewSecret) {
trackSecretCreated({ source });
} else {
trackSecretUpdated({ source });
}
onDismiss();
},
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export function SecretsManagementTab() {

return (
<QueryErrorBoundary>
<SecretsManagementUI />
<SecretsManagementUI source="config_page_secrets_tab" />
</QueryErrorBoundary>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
runTestAsSecretsReadOnly,
} from 'test/utils';

import { SecretsManagementSource } from './types';
import { useDeleteSecret, useSecrets } from 'data/useSecrets';

import { MOCKED_SECRETS } from '../../../../test/fixtures/secrets';
Expand All @@ -16,7 +17,17 @@ import { SecretsManagementTab } from './SecretsManagementTab';
import { SecretsManagementUI } from './SecretsManagementUI';

jest.mock('./SecretEditModal', () => ({
SecretEditModal: ({ onDismiss, open, name }: { onDismiss?: () => void; name: string; open?: boolean }) => {
SecretEditModal: ({
onDismiss,
open,
name,
source,
}: {
onDismiss?: () => void;
name: string;
open?: boolean;
source: SecretsManagementSource;
}) => {
if (!open) {
return null;
}
Expand Down Expand Up @@ -70,14 +81,14 @@ describe('SecretsManagementUI', () => {

it('should render empty state when no secrets exist', async () => {
runTestAsSecretsFullAccess();
render(<SecretsManagementUI />);
render(<SecretsManagementUI source="config_page_secrets_tab" />);

expect(screen.getByText(/you don't have any secrets yet/i)).toBeInTheDocument();
});

it('should have create button for users with create permissions', async () => {
runTestAsSecretsFullAccess();
render(<SecretsManagementUI />);
render(<SecretsManagementUI source="config_page_secrets_tab" />);

const addButton = screen.getByRole('button', { name: /create secret/i });
await userEvent.click(addButton);
Expand All @@ -88,22 +99,22 @@ describe('SecretsManagementUI', () => {

it('should have create button for users with creator permissions', async () => {
runTestAsSecretsCreator();
render(<SecretsManagementUI />);
render(<SecretsManagementUI source="config_page_secrets_tab" />);

expect(screen.getByRole('button', { name: /create secret/i })).toBeInTheDocument();
});

it('should not have create button for read-only users', async () => {
runTestAsSecretsReadOnly();
render(<SecretsManagementUI />);
render(<SecretsManagementUI source="config_page_secrets_tab" />);

expect(screen.queryByRole('button', { name: /create secret/i })).not.toBeInTheDocument();
expect(screen.getByText(/Contact an admin to create secrets/)).toBeInTheDocument();
});

it('should not have create button for editor-only users', async () => {
runTestAsSecretsEditor();
render(<SecretsManagementUI />);
render(<SecretsManagementUI source="config_page_secrets_tab" />);

expect(screen.queryByRole('button', { name: /create secret/i })).not.toBeInTheDocument();
expect(screen.getByText(/Contact an admin to create secrets/)).toBeInTheDocument();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
import React, { useState } from 'react';
import { Button, ConfirmModal, EmptyState } from '@grafana/ui';
import { css } from '@emotion/css';
import {
trackCreateSecretButtonClicked,
trackDeleteSecretButtonClicked,
trackEditSecretButtonClicked,
trackSecretDeleted,
} from 'features/tracking/secretsManagementEvents';

import { SecretWithUuid } from './types';
import { SecretsManagementSource, SecretWithUuid } from './types';
import { getUserPermissions } from 'data/permissions';
import { useDeleteSecret, useSecrets } from 'data/useSecrets';
import { CenteredSpinner } from 'components/CenteredSpinner';
Expand All @@ -12,7 +18,12 @@ import { SECRETS_EDIT_MODE_ADD } from './constants';
import { SecretCard } from './SecretCard';
import { SecretEditModal } from './SecretEditModal';

export function SecretsManagementUI() {
interface SecretsManagementUIProps {
/** The source context where the secrets management UI is being used. */
source: SecretsManagementSource;
}

export function SecretsManagementUI({ source }: SecretsManagementUIProps) {
const [editMode, setEditMode] = useState<string | false>(false);
const [deleteMode, setDeleteMode] = useState<SecretWithUuid | undefined>();
const { canCreateSecrets, canReadSecrets } = getUserPermissions();
Expand All @@ -22,15 +33,20 @@ export function SecretsManagementUI() {

const existingNames = secrets?.map((secret) => secret.name) ?? [];

const handleAddSecret = () => {
const handleAddSecret = (location: 'empty_state' | 'header_action') => {
trackCreateSecretButtonClicked({ source, location });
setEditMode(SECRETS_EDIT_MODE_ADD);
};

const handleEditSecret = (name?: string) => {
if (name) {
trackEditSecretButtonClicked({ source });
}
setEditMode(name ?? false);
};

const handleDeleteSecret = (name: string) => {
trackDeleteSecretButtonClicked({ source });
const secret = secrets?.find((s) => s.name === name);
if (secret) {
setDeleteMode(secret);
Expand All @@ -50,7 +66,7 @@ export function SecretsManagementUI() {
message="You don't have any secrets yet."
button={
canCreateSecrets ? (
<Button onClick={handleAddSecret} icon="plus">
<Button onClick={() => handleAddSecret('empty_state')} icon="plus">
Create secret
</Button>
) : undefined
Expand All @@ -70,7 +86,7 @@ export function SecretsManagementUI() {
actions={
canCreateSecrets ? (
<div>
<Button size="sm" icon="plus" onClick={handleAddSecret}>
<Button size="sm" icon="plus" onClick={() => handleAddSecret('header_action')}>
Create secret
</Button>
</div>
Expand All @@ -94,7 +110,13 @@ export function SecretsManagementUI() {
)}

{editMode && (
<SecretEditModal name={editMode} existingNames={existingNames} open onDismiss={() => handleEditSecret()} />
<SecretEditModal
name={editMode}
existingNames={existingNames}
open
source={source}
onDismiss={() => handleEditSecret()}
/>
)}

<ConfirmModal
Expand All @@ -114,7 +136,13 @@ export function SecretsManagementUI() {
</div>
}
onConfirm={() => {
deleteMode && deleteSecret.mutate(deleteMode.name);
if (deleteMode) {
deleteSecret.mutate(deleteMode.name, {
onSuccess: () => {
trackSecretDeleted({ source });
},
});
}
setDeleteMode(undefined);
}}
onDismiss={() => setDeleteMode(undefined)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,6 @@ export interface SecretWithUuid extends Omit<Secret, 'uuid'> {
}

export interface SecretWithMetadata extends SecretWithUuid, SecretMetadata {}

/** The source context where the secrets management UI is being used. */
export type SecretsManagementSource = 'check_editor_sidepanel_feature_tabs' | 'config_page_secrets_tab';