Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
0c00291
feat: secret management draft ui
w1kman Feb 11, 2025
e6f9897
feat: secret management draft ui
w1kman Feb 11, 2025
ace8519
Merge branch 'secret-service/feature-branch' into w1kman/secrets-mana…
w1kman Feb 12, 2025
fc79679
fix: optimistic update of local store
w1kman Feb 19, 2025
a040d19
fix: optimistic update of local store
w1kman Feb 19, 2025
956df8a
fix: copy button paddings and margins
w1kman Feb 19, 2025
aa3b2ff
fix: copy button paddings and margins
w1kman Feb 19, 2025
ac58054
Merge branch 'secret-service/feature-branch' into w1kman/secrets-mana…
w1kman Feb 19, 2025
330935c
Merge branch 'secret-service/feature-branch' into w1kman/secrets-mana…
w1kman Feb 19, 2025
6f3ba7f
fix: update external resource spec
w1kman Feb 19, 2025
c35c300
fix: update external resource spec
w1kman Feb 19, 2025
fc5ef77
Merge branch 'secret-service/feature-branch' into w1kman/secrets-mana…
w1kman Mar 3, 2025
3d288b6
Merge branch 'secret-service/feature-branch' into w1kman/secrets-mana…
w1kman Mar 3, 2025
1540179
add allowed list of descrypters
w1kman Mar 3, 2025
aaaf860
add allowed list of descrypters
w1kman Mar 3, 2025
b665671
Merge branch 'secret-service/feature-branch' into w1kman/secrets-mana…
w1kman Mar 4, 2025
15c8e7a
SecretsManagement: update allowed decrypters constant (and default ke…
macabu Mar 17, 2025
d89a140
Merge remote-tracking branch 'origin/w1kman/secrets-management-admin-…
w1kman Mar 24, 2025
8936f50
fix: build issues
w1kman Mar 24, 2025
7f11f26
update ui to handle label/value for decrypters
w1kman Mar 24, 2025
a49523e
Merge branch 'secret-service/feature-branch' into w1kman/secrets-mana…
w1kman Apr 2, 2025
e3b2c27
Merge branch 'secret-service/feature-branch' into w1kman/secrets-mana…
w1kman May 14, 2025
095b7b7
migrate to RTK query (wip)
w1kman May 15, 2025
aa424ba
Merge branch 'secret-service/feature-branch' into w1kman/secrets-mana…
w1kman May 15, 2025
ab77457
refactor/update secrets management
w1kman May 15, 2025
d7d985c
add labels to secret form
w1kman May 21, 2025
8907ad3
add util function for invalid field detection
w1kman May 21, 2025
2e4c03e
update breakpoint for wrap detection
w1kman May 22, 2025
8420d6d
SecureValues: Update decrypter format to get rid of actor_ prefix (#1…
macabu May 22, 2025
5c03e13
update translation for secrets and add tests
w1kman May 22, 2025
cf27992
Merge remote-tracking branch 'origin/w1kman/secrets-management-admin-…
w1kman May 22, 2025
96fef80
update translation for secrets and add tests
w1kman May 22, 2025
3ba1f14
fix relative imports for `app/core/internationalization`
w1kman Jun 2, 2025
e811f25
Merge branch 'secret-service/feature-branch' into w1kman/secrets-mana…
w1kman Jun 2, 2025
b8a9545
use `t` and `Trans` from `@grafana/i18n`
w1kman Jun 2, 2025
f9233f5
fix eslint error
w1kman Jun 4, 2025
9067f28
improve error handling
w1kman Jun 4, 2025
59365d2
improve error handling
w1kman Jun 4, 2025
f134eaf
change date format and add modified
w1kman Jun 5, 2025
38df06b
internally rename `audiences` to `decrypters`
w1kman Jun 5, 2025
8619b86
add additional tests
w1kman Jun 9, 2025
aa86e81
add additional tests
w1kman Jun 9, 2025
49f45e2
Merge branch 'secret-service/feature-branch' into w1kman/secrets-mana…
w1kman Jun 9, 2025
477e1be
generate translations
w1kman Jun 9, 2025
9980a20
fix `betterer` issues and style guide issues
w1kman Jun 11, 2025
01a4140
fix prettier issue
w1kman Jun 11, 2025
f3cbc89
fix `name` issue when creating and updating.
w1kman Jun 18, 2025
819af68
Merge branch 'secret-service/feature-branch' into w1kman/secrets-mana…
w1kman Jun 18, 2025
f526244
fix updated translation usage.
w1kman Jun 18, 2025
068ca60
fix updated translation usage (remove dependency @grafana/i18n/intern…
w1kman Jun 18, 2025
9f93dbd
Merge branch 'secret-service/feature-branch' into w1kman/secrets-mana…
w1kman Jun 24, 2025
efd4d92
fix change requests
w1kman Jun 24, 2025
ee13e25
Merge remote-tracking branch 'origin/secret-service/feature-branch' i…
dana-axinte Jun 25, 2025
c7458c7
Merge remote-tracking branch 'origin/secret-service/feature-branch' i…
dana-axinte Jun 25, 2025
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
Prev Previous commit
Next Next commit
improve error handling
- various smaller fixes
- move remaining translations to new namespace
  • Loading branch information
w1kman committed Jun 4, 2025
commit 9067f28e88938260e9910f6e7eea9d8e3904cd79
45 changes: 36 additions & 9 deletions public/app/features/secrets-management/SecretsManagementPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,29 @@ import { useCallback, useEffect, useState } from 'react';

import { GrafanaTheme2 } from '@grafana/data';
import { Trans, useTranslate } from '@grafana/i18n';
import { Button, FilterInput, InlineField, TextLink, useStyles2 } from '@grafana/ui';
import { Button, EmptyState, FilterInput, InlineField, TextLink, useStyles2 } from '@grafana/ui';
import { Page } from 'app/core/components/Page/Page';
import { PageContents } from 'app/core/components/Page/PageContents';

import { useDeleteSecretMutation, useListSecretsQuery } from './api/secretsManagementApi';
import { EditSecretModal } from './components/EditSecretModal';
import { SecretsList } from './components/SecretsList';
import { SecretStatusPhase } from './types';
import { getErrorMessage } from './utils';

export default function SecretsManagementPage() {
const styles = useStyles2(getStyles);
const { t } = useTranslate();

// Api test
const [pollingInterval, setPollingInterval] = useState(0);
const { data: secrets, isLoading } = useListSecretsQuery(undefined, {
const {
data: secrets,
isLoading,
isError,
error,
refetch,
} = useListSecretsQuery(undefined, {
pollingInterval,
});

Expand Down Expand Up @@ -83,13 +90,33 @@ export default function SecretsManagementPage() {
</InlineField>
</div>

<SecretsList
onEditSecret={handleEditSecret}
onDeleteSecret={deleteSecret}
secrets={secrets}
filter={filter}
onCreateSecret={handleShowCreateModal}
/>
{isError ? (
<EmptyState
variant="not-found"
message={t('secrets.error-state.message', 'Something went wrong')}
button={
<Trans i18nKey="secrets.error-state.retry">
<Button onClick={() => refetch()}>Retry</Button>
</Trans>
}
>
<Trans i18nKey="secrets.error-state.body">
<p>
This may be due to poor network conditions or a potential plugin blocking requests. Retry, and if the
problem persists, contact support.
</p>
{!!error && <p>Details: {getErrorMessage(error)}</p>}
</Trans>
</EmptyState>
) : (
<SecretsList
onEditSecret={handleEditSecret}
onDeleteSecret={deleteSecret}
secrets={secrets}
filter={filter}
onCreateSecret={handleShowCreateModal}
/>
)}

{isEditModalOpen && <EditSecretModal isOpen onDismiss={handleDismissModal} name={editTarget} />}
</PageContents>
Expand Down
34 changes: 18 additions & 16 deletions public/app/features/secrets-management/api/secretsManagementApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { transformFromSecret, transformToSecret } from '../utils';
const baseURL = getAPIBaseURL('secret.grafana.app', 'v0alpha1');

export const secretsManagementApi = createApi({
tagTypes: ['Secrets'],
tagTypes: ['Secret'],
reducerPath: 'secretsManagementApi',
baseQuery: createBaseQuery({ baseURL }),
endpoints: (builder) => ({
Expand All @@ -19,7 +19,7 @@ export const secretsManagementApi = createApi({
method: 'GET',
}),
providesTags: (result) =>
result ? [...result.map(({ name }) => ({ type: 'Secrets' as const, id: name })), 'Secrets'] : ['Secrets'],
result ? [...result.map(({ name }) => ({ type: 'Secret' as const, id: name })), 'Secret'] : ['Secret'],
transformResponse: (response: SecretsListResponse) => {
return (response?.items?.map(transformToSecret) as Secret[]) ?? [];
},
Expand All @@ -29,26 +29,17 @@ export const secretsManagementApi = createApi({
url: `/securevalues/${encodeURIComponent(name)}`,
method: 'GET',
}),
providesTags: (_result, _error, name) => [{ type: 'Secrets', id: name }],
providesTags: (_result, _error, name) => [{ type: 'Secret', id: name }],
transformResponse: (response: SecretsListResponseItem) => {
return transformToSecret(response);

// return {
// name: response.metadata.name,
// description: response.spec.description,
// audiences: response.spec.decrypters,
// uid: response.metadata.uid,
// status: response.status?.phase ?? 'Succeeded',
// ...('keeper' in response.spec ? { keeper: response.spec.keeper } : undefined),
// } as Secret;
},
}),
deleteSecret: builder.mutation({
query: (name) => ({
url: `/securevalues/${encodeURIComponent(name)}`,
method: 'DELETE',
}),
invalidatesTags: (result, error, name) => ['Secrets'],
invalidatesTags: (result, error, arg) => [{ type: 'Secret', id: arg }],
async onQueryStarted(name, { dispatch, queryFulfilled }) {
const patchResult = dispatch(
secretsManagementApi.util.updateQueryData('listSecrets', undefined, (draft) => {
Expand All @@ -71,19 +62,30 @@ export const secretsManagementApi = createApi({
method: 'POST',
body: transformFromSecret(data),
}),
invalidatesTags: ['Secrets'],
invalidatesTags: (result, error, arg, meta) => {
if (!!error) {
return [null];
}

return ['Secret'];
},
}),
updateSecret: builder.mutation({
updateSecret: builder.mutation<Secret, Partial<Secret> & Pick<Secret, 'name'>>({
query: (secret) => ({
url: `/securevalues/${encodeURIComponent(secret.name)}`,
method: 'PUT',
body: transformFromSecret(secret),
}),
invalidatesTags: ['Secrets'],
invalidatesTags: (result, error, arg) => [{ type: 'Secret', id: arg.name }],
}),
}),
});

// Secret mutation factory
export function useSecretMutation(update = false) {
return update ? useUpdateSecretMutation() : useCreateSecretMutation();
}

export const {
useListSecretsQuery,
useDeleteSecretMutation,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import { useCallback } from 'react';
import { useCallback, useEffect } from 'react';

import { AppEvents } from '@grafana/data';
import { Trans } from '@grafana/i18n';
import { t } from '@grafana/i18n/internal';
import { Modal, Spinner } from '@grafana/ui';
import { getAppEvents } from '@grafana/runtime';
import { Alert, Box, Modal, Spinner, Text } from '@grafana/ui';

import { useCreateSecretMutation, useGetSecretQuery, useUpdateSecretMutation } from '../api/secretsManagementApi';
import { useGetSecretQuery, useSecretMutation } from '../api/secretsManagementApi';
import { SecretFormValues } from '../types';
import { secretFormValuesToSecret, secretToSecretFormValues } from '../utils';
import { getErrorMessage, secretFormValuesToSecret, secretToSecretFormValues } from '../utils';

import { SecretForm } from './SecretForm';

Expand All @@ -15,6 +18,8 @@ interface EditSecretModalProps {
onDismiss: () => void;
}

const appEvents = getAppEvents();

export function EditSecretModal({ isOpen, onDismiss, name }: EditSecretModalProps) {
const {
data: secret,
Expand All @@ -23,11 +28,9 @@ export function EditSecretModal({ isOpen, onDismiss, name }: EditSecretModalProp
} = useGetSecretQuery(name as string, {
skip: !name,
});

const [createSecret] = useCreateSecretMutation();
const [updateSecret] = useUpdateSecretMutation();

const isNew = isUninitialized;
const [mutation, { data: response, error, isSuccess }] = useSecretMutation(!isNew);

const initialValues = isNew ? undefined : secretToSecretFormValues(secret);
const modalTitle = isNew
? t('secrets.edit-modal.title.create', 'Create secret')
Expand All @@ -37,34 +40,58 @@ export function EditSecretModal({ isOpen, onDismiss, name }: EditSecretModalProp
: t('secrets.edit-modal.form.button-update', 'Update');

const handleSubmit = useCallback(
async (data: SecretFormValues) => {
try {
const secretData = secretFormValuesToSecret({ ...secret, ...data });
if (isNew) {
await createSecret(secretData);
} else {
await updateSecret(secretData);
}
} catch (error) {
return Promise.reject('Unable to store secret');
} finally {
onDismiss();
}
(data: SecretFormValues) => {
const secretData = secretFormValuesToSecret({ ...secret, ...data });
return mutation(secretData);
},
[createSecret, isNew, onDismiss, secret, updateSecret]
[secret, mutation]
);

useEffect(() => {
if (isSuccess) {
const message = isNew
? t('secrets.mutation-success.create', 'Secret "{{name}}" was created successfully', response.metadata)
: t('secrets.mutation-success.update', 'Secret "{{name}}" was updated successfully', response.metadata);
appEvents.publish({
type: AppEvents.alertSuccess.name,
payload: [message],
});
onDismiss();
}
}, [isNew, isSuccess, onDismiss, response]);

return (
<Modal title={modalTitle} isOpen={isOpen} onDismiss={onDismiss} closeOnBackdropClick={false}>
{isLoading && <Spinner />}
{!isLoading && (
<SecretForm
disableNameField={!isNew}
submitText={submitText}
initialValues={initialValues}
onSubmit={handleSubmit}
onCancel={onDismiss}
/>
<div>
{!!error && (
<Alert severity="error" title={t('secrets.mutation-error.title', 'Request error')}>
{isNew
? t(
'secrets.mutation-error.create',
'Failed to create secret. Please try again, and if the problem persists, contact support'
)
: t(
'secrets.mutation-error.update',
'Failed to update secret. Please try again, and if the problem persists, contact support'
)}

<Box marginTop={1}>
<Trans i18nKey="secrets.error-state.details" values={{ message: getErrorMessage(error) }}>
<Text italic>Details: {'{{message}}'}</Text>
</Trans>
</Box>
</Alert>
)}
<SecretForm
disableNameField={!isNew}
submitText={submitText}
initialValues={initialValues}
onSubmit={handleSubmit}
onCancel={onDismiss}
/>
</div>
)}
</Modal>
);
Expand Down
Loading
Loading