Skip to content

Commit

Permalink
Merge branch 'master' into enh/mfp-hackaton
Browse files Browse the repository at this point in the history
* master:
  Update dependency vite to v5.3.3 (#1954)
  Update dependency eslint-plugin-vue to v9.27.0 (#1948)
  Update dependency eslint-plugin-promise to v6.4.0 (#1945)
  Update dependency jose to v5.6.2 (#1941)
  Update dependency vite to v5.3.2 (#1942)
  including the iss parameter in server code exchange (#1939)
  Update dependency vue to v3.4.31 (#1944)
  Fix issue when no costObject is defined (#1943)
  Update dependency path-to-regexp to v7 (#1930)
  Update Yarn to v4.3.1 (#1931)
  Update dependency prom-client to v15.1.3 (#1940)
  Update dependency vue to v3.4.30 (#1935)
  Update dependency jose to v5.5.0 (#1938)
  Update dependency vue-router to v4.4.0 (#1932)
  Add Custom Field Configuration Editor for Shoot Clusters (#1926)

# Conflicts:
#	.pnp.cjs
#	.yarn/cache/@vue-compiler-core-npm-3.4.29-29bc9e7853-9d68fd1a0c.zip
#	.yarn/cache/@vue-compiler-core-npm-3.4.30-f4933d9063-f0109b472d.zip
#	.yarn/cache/@vue-compiler-core-npm-3.4.31-f79d05324a-17833fa55a.zip
#	.yarn/cache/@vue-compiler-dom-npm-3.4.29-a117217369-c98620b718.zip
#	.yarn/cache/@vue-compiler-dom-npm-3.4.30-7742f540f5-b975fcb1a6.zip
#	.yarn/cache/@vue-compiler-dom-npm-3.4.31-6d2d250445-136b220868.zip
#	.yarn/cache/@vue-compiler-sfc-npm-3.4.29-25de7bdaef-4db562793d.zip
#	.yarn/cache/@vue-compiler-sfc-npm-3.4.30-7854a51719-63b09e7d9d.zip
#	.yarn/cache/@vue-compiler-sfc-npm-3.4.31-25353c4cc2-b8983a52dd.zip
#	.yarn/cache/@vue-compiler-ssr-npm-3.4.29-acc329a1f4-a12cc3ecc0.zip
#	.yarn/cache/@vue-compiler-ssr-npm-3.4.30-bf06ebff88-f7ba4bde96.zip
#	.yarn/cache/@vue-compiler-ssr-npm-3.4.31-9533893acb-8083959c21.zip
#	.yarn/cache/@vue-reactivity-npm-3.4.29-60fd993ecd-cc465ba167.zip
#	.yarn/cache/@vue-reactivity-npm-3.4.30-241edc7af3-b6ca8281f4.zip
#	.yarn/cache/@vue-reactivity-npm-3.4.31-6fb2cecc5c-974ce9c9f2.zip
#	.yarn/cache/@vue-runtime-core-npm-3.4.29-2bd370acac-1580ac9dae.zip
#	.yarn/cache/@vue-runtime-core-npm-3.4.30-a2886d20a5-f496a9bd99.zip
#	.yarn/cache/@vue-runtime-core-npm-3.4.31-fb7fdb78b9-446711364e.zip
#	.yarn/cache/@vue-runtime-dom-npm-3.4.29-dff1aa2f2a-b307e9a166.zip
#	.yarn/cache/@vue-runtime-dom-npm-3.4.30-6ed8273a18-69fa19e5a7.zip
#	.yarn/cache/@vue-runtime-dom-npm-3.4.31-18c8027dfb-4c0b20f16a.zip
#	.yarn/cache/@vue-server-renderer-npm-3.4.29-b7dad78c9d-c414447049.zip
#	.yarn/cache/@vue-server-renderer-npm-3.4.30-98a18cf281-6313e7f71c.zip
#	.yarn/cache/@vue-server-renderer-npm-3.4.31-a7fc49ff3c-1e01142c2f.zip
#	.yarn/cache/@vue-shared-npm-3.4.29-f059414627-7569bb841f.zip
#	.yarn/cache/@vue-shared-npm-3.4.30-8d6e063bf4-39e128f9b2.zip
#	.yarn/cache/@vue-shared-npm-3.4.31-07b999feaf-45643c0c7a.zip
#	.yarn/cache/jose-npm-5.4.1-82d453ef52-51234a7383.zip
#	.yarn/cache/jose-npm-5.5.0-f42a134c93-e240caa9f7.zip
#	.yarn/cache/jose-npm-5.6.2-6dd6e106fc-86df192545.zip
#	.yarn/cache/vue-npm-3.4.29-5618bba5e0-1a84802f74.zip
#	.yarn/cache/vue-npm-3.4.30-52ebe7d44e-0d0f5d8841.zip
#	.yarn/cache/vue-npm-3.4.31-375a256262-d9d7ac45f2.zip
#	backend/lib/security/index.js
#	frontend/src/views/GAdministration.vue
#	yarn.lock
  • Loading branch information
holgerkoser committed Jul 4, 2024
2 parents 788b070 + 9a0f414 commit c4f9304
Show file tree
Hide file tree
Showing 72 changed files with 2,586 additions and 1,200 deletions.
300 changes: 150 additions & 150 deletions .pnp.cjs

Large diffs are not rendered by default.

Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
434 changes: 217 additions & 217 deletions .yarn/releases/yarn-4.3.0.cjs → .yarn/releases/yarn-4.3.1.cjs

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion .yarnrc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,4 +61,4 @@ supportedArchitectures:
- linux
- darwin

yarnPath: .yarn/releases/yarn-4.3.0.cjs
yarnPath: .yarn/releases/yarn-4.3.1.cjs
1 change: 1 addition & 0 deletions backend/image-version.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
europe-docker.pkg.dev/gardener-project/snapshots/gardener/dashboard:1.76.0-dev-d752df12003dc165dcb2d75ca98b7ad3333674e7
1 change: 1 addition & 0 deletions backend/lib/security/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,7 @@ async function authorizationCallback (req, res) {
redirectOrigin,
state
} = stateObject

const client = await exports.getIssuerClient()
const parameters = client.callbackParams(req)
const backendRedirectUri = getBackendRedirectUri(redirectOrigin)
Expand Down
4 changes: 2 additions & 2 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@
"fast-json-patch": "^3.1.1",
"jest": "^29.7.0",
"p-event": "^4.2.0",
"path-to-regexp": "^6.2.1",
"path-to-regexp": "^7.0.0",
"set-cookie-parser": "^2.6.0",
"socket.io-client": "^4.7.5",
"supertest": "^7.0.0"
Expand Down Expand Up @@ -154,7 +154,7 @@
"<rootDir>/jest.setup.js"
]
},
"packageManager": "[email protected].0",
"packageManager": "[email protected].1",
"engines": {
"node": "^20.5.0"
}
Expand Down
2 changes: 1 addition & 1 deletion charts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@
"<rootDir>/jest.setup.js"
]
},
"packageManager": "[email protected].0",
"packageManager": "[email protected].1",
"engines": {
"node": "^20.5.0"
}
Expand Down
Binary file modified docs/images/custom-fields-1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/images/custom-fields-2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/images/custom-fields-3.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/images/custom-fields-4.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/custom-fields-5.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/custom-fields-6.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/custom-fields-7.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
35 changes: 28 additions & 7 deletions docs/usage/custom-fields.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
# Custom Shoot Fields

The Dashboard supports custom shoot fields, that can be defined per project by specifying `metadata.annotations["dashboard.gardener.cloud/shootCustomFields"]`.
The fields can be configured to be displayed on the cluster list and cluster details page.
Custom fields do not show up on the `ALL_PROJECTS` page.
The Dashboard supports custom shoot fields, which can be configured to be displayed on the cluster list and cluster details page. Custom fields do not show up on the `ALL_PROJECTS` page.

## Project administration page:
Each custom field configuration is shown with it's own chip.
Each custom field configuration is shown with its own chip.

<img width="800" src="../images/custom-fields-1.png">

Expand All @@ -32,16 +30,39 @@ Custom fields can be shown in a dedicated card (`Custom Fields`) on the cluster
| defaultValue | String/Number | | | Default value, in case there is no value for the given `path` |
| showColumn | Bool | true | | Field shall appear as column in the cluster list |
| columnSelectedByDefault | Bool | true | | Indicates if field shall be selected by default on the cluster list (not hidden by default) |
| weight | Number | 0 | | Defines the order of the column. The standard columns start with weight 100 and continue in 100 increments (200, 300, ..) |
| weight | Number | 0 | | Defines the order of the column. The built-in columns start with a weight of 100, increasing by 100 (200, 300, etc.) |
| sortable | Bool | true | | Indicates if column is sortable on the cluster list |
| searchable | Bool | true | | Indicates if column is searchable on the cluster list |
| showDetails | Bool | true | | Indicates if field shall appear in a dedicated card (`Custom Fields`) on the cluster details page |

As there is currently no way to configure the custom shoot fields for a project in the gardener dashboard, you have to use `kubectl` to update the `project` resource. See [Project Operations](./project-operations.md#download-kubeconfig-for-a-user) on how to get a `kubeconfig` for the `garden` cluster in order to edit the `project`.
## Editor for Custom Shoot Fields

The Gardener Dashboard now includes an editor for custom shoot fields, allowing users to configure these fields directly from the dashboard without needing to use `kubectl`. This editor can be accessed from the project administration page.

### Accessing the Editor

1. Navigate to the project administration page.
2. Scroll down to the `Custom Fields for Shoots` section.
3. Click on the gear icon to open the configuration panel for custom fields.

<img width="800" src="../images/custom-fields-5.png">

### Adding a New Custom Field


1. In the `Configure Custom Fields for Shoot Clusters` panel, click on the `+ ADD NEW FIELD` button.

<img width="800" src="../images/custom-fields-6.png">

2. Fill in the details for the new custom field in the `Add New Field` form. Refer to the [Configuration](#configuration) section for detailed descriptions of each field.

3. Click the `ADD` button to save the new custom field.

<img width="800" src="../images/custom-fields-7.png">

### Example

The following is an example project yaml:
Custom shoot fields can be defined per project by specifying `metadata.annotations["dashboard.gardener.cloud/shootCustomFields"]`. The following is an example project yaml:
```yaml
apiVersion: core.gardener.cloud/v1beta1
kind: Project
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`useProjectShootCustomFields > should add a custom field 1`] = `
[
{
"columnSelectedByDefault": true,
"name": "Field1",
"path": "path1",
"searchable": true,
"showColumn": true,
"showDetails": true,
"sortable": true,
"weight": 0,
},
]
`;

exports[`useProjectShootCustomFields > should ensure the key is not part of the rendered JSON.stringify annotation when adding a new entry 1`] = `"[{"name":"Field1","path":"path1"}]"`;

exports[`useProjectShootCustomFields > should initialize shootCustomFields correctly with legacy data 1`] = `
[
{
"columnSelectedByDefault": true,
"defaultValue": "unknown",
"icon": "mdi-heart-pulse",
"name": "Shoot Status",
"path": "metadata.labels["shoot.gardener.cloud/status"]",
"searchable": true,
"showColumn": true,
"showDetails": true,
"sortable": true,
"tooltip": "Indicates the health status of the cluster",
"weight": 950,
},
{
"columnSelectedByDefault": true,
"icon": "mdi-table-network",
"name": "Networking Type",
"path": "spec.networking.type",
"searchable": true,
"showColumn": false,
"showDetails": true,
"sortable": true,
"weight": 0,
},
]
`;

exports[`useProjectShootCustomFields > should skip custom fields with invalid values 1`] = `
[
{
"columnSelectedByDefault": true,
"name": "Valid Field",
"path": "path1",
"searchable": true,
"showColumn": true,
"showDetails": true,
"sortable": true,
"weight": 0,
},
]
`;
218 changes: 218 additions & 0 deletions frontend/__tests__/composables/useProjectShootCustomFields.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
//
// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Gardener contributors
//
// SPDX-License-Identifier: Apache-2.0
//

import {
ref,
reactive,
} from 'vue'
import {
describe,
it,
expect,
vi,
beforeEach,
} from 'vitest'

import { useProjectShootCustomFields } from '@/composables/useProjectShootCustomFields'
import {
formatValue,
isCustomField,
} from '@/composables/useProjectShootCustomFields/helper'

// Mock dependencies
vi.mock('@/composables/useLogger', () => ({
useLogger: () => ({
error: vi.fn(),
}),
}))

let annotations

beforeEach(() => {
annotations = reactive({})
})

vi.mock('@/composables/useProjectMetadata', () => ({
useProjectMetadata: projectItem => ({
getProjectAnnotation: vi.fn(key => annotations[key] || null),
setProjectAnnotation: vi.fn((key, value) => {
annotations[key] = value
}),
unsetProjectAnnotation: vi.fn(key => {
delete annotations[key]
}),
}),
}))

describe('useProjectShootCustomFields', () => {
let projectItem
let options

beforeEach(() => {
projectItem = ref({})
options = {}
})

it('should initialize shootCustomFields correctly with legacy data', () => {
annotations['dashboard.gardener.cloud/shootCustomFields'] = JSON.stringify({
shootStatus: {
name: 'Shoot Status',
path: 'metadata.labels["shoot.gardener.cloud/status"]',
icon: 'mdi-heart-pulse',
tooltip: 'Indicates the health status of the cluster',
defaultValue: 'unknown',
showColumn: true,
columnSelectedByDefault: true,
weight: 950,
searchable: true,
sortable: true,
showDetails: true,
},
networking: {
name: 'Networking Type',
path: 'spec.networking.type',
icon: 'mdi-table-network',
showColumn: false,
},
})
const { shootCustomFields } = useProjectShootCustomFields(projectItem, options)
expect(shootCustomFields.value.length).toBe(2)
expect(shootCustomFields.value).toMatchSnapshot()
})

it('should add a custom field', async () => {
const { shootCustomFields, addShootCustomField } = useProjectShootCustomFields(projectItem, options)
const customField = { name: 'Field1', path: 'path1' }
expect(shootCustomFields.value.length).toBe(0)
addShootCustomField(customField)
expect(shootCustomFields.value.length).toBe(1)
expect(shootCustomFields.value).toMatchSnapshot()
})

it('should delete a custom field', async () => {
const { shootCustomFields, addShootCustomField, deleteShootCustomField } = useProjectShootCustomFields(projectItem, options)
const customField = { name: 'Field1', path: 'path1' }
addShootCustomField(customField)
expect(shootCustomFields.value.length).toBe(1)
deleteShootCustomField(customField)
expect(shootCustomFields.value.length).toBe(0)
})

it('should replace a custom field', async () => {
const { shootCustomFields, addShootCustomField, replaceShootCustomField } = useProjectShootCustomFields(projectItem, options)
const oldCustomField = { name: 'Field1', path: 'path1' }
const newCustomField = { name: 'Field2', path: 'path2' }
addShootCustomField(oldCustomField)
expect(shootCustomFields.value.length).toBe(1)
replaceShootCustomField(oldCustomField, newCustomField)
expect(shootCustomFields.value.length).toBe(1)
expect(shootCustomFields.value).toContainEqual(expect.objectContaining(newCustomField))
expect(shootCustomFields.value).not.toContainEqual(expect.objectContaining(oldCustomField))
})

it('should check if custom field name is unique', async () => {
const { addShootCustomField, isShootCustomFieldNameUnique } = useProjectShootCustomFields(projectItem, options)
const customField = { name: 'Field1', path: 'path1' }
addShootCustomField(customField)
expect(isShootCustomFieldNameUnique('Field1')).toBe(false)
expect(isShootCustomFieldNameUnique('Field2')).toBe(true)
})

it('should get custom field by key', async () => {
const { addShootCustomField, getCustomFieldByKey } = useProjectShootCustomFields(projectItem, options)
const customField = { name: 'Field1', path: 'path1' }
addShootCustomField(customField)
addShootCustomField({ name: 'Field2', path: 'path2' })
const key = 'Z_field1'
const result = getCustomFieldByKey(key)
expect(result).toEqual(expect.objectContaining(customField))
})

it('should handle empty custom fields', async () => {
const { shootCustomFields, rawShootCustomFields } = useProjectShootCustomFields(projectItem, options)
rawShootCustomFields.value = null
expect(shootCustomFields.value).toEqual([])
})

it('should handle invalid JSON in custom fields', async () => {
const { shootCustomFields, rawShootCustomFields } = useProjectShootCustomFields(projectItem, options)
rawShootCustomFields.value = 'invalid JSON'
expect(shootCustomFields.value).toEqual([])
})

it('should generate key from name correctly', () => {
const { generateKeyFromName } = useProjectShootCustomFields(projectItem, options)
const name = 'Custom Field Name'
const key = generateKeyFromName(name)
expect(key).toBe('Z_customFieldName')
})

it('should skip custom fields with invalid values', () => {
annotations = reactive({
'dashboard.gardener.cloud/shootCustomFields': JSON.stringify([
{ name: 'Valid Field', path: 'path1' },
{ name: 'Invalid Field1', path: 'path2', value: { key: 'value' } }, // ignored - no objects allowed as values of custom field properties
{ name: 'Invalid Field2', path: { foo: 'bar' } }, // no objects allowed as values of custom field properties
{ name: 'Invalid Field3' }, // ignored, missing required property path
{ path: 'path3' }, // ignored, missing required property name
{}, // ignored
null, // ignored
]),
})

const { shootCustomFields } = useProjectShootCustomFields(projectItem, options)
expect(shootCustomFields.value.length).toBe(1)
expect(shootCustomFields.value).toMatchSnapshot()
})

it('should ensure the key is not part of the rendered JSON.stringify annotation when adding a new entry', () => {
const { shootCustomFields, addShootCustomField } = useProjectShootCustomFields(projectItem, options)
const customField = { name: 'Field1', path: 'path1' }

// Add the custom field
addShootCustomField(customField)

// Verify the key is set correctly
const addedField = shootCustomFields.value.find(field => field.name === 'Field1')
expect(addedField.key).toBe('Z_field1')

// Verify the key is not part of the JSON stringified output in annotations
const annotationString = annotations['dashboard.gardener.cloud/shootCustomFields']
expect(annotationString).not.toContain('key')
expect(annotationString).toMatchSnapshot()
})

describe('helper', () => {
describe('formatValue', () => {
it('should join array elements with a separator', () => {
const result = formatValue(['a', 'b', 'c'], ',')
expect(result).toBe('a,b,c')
})

it('should return the value as is for non-array values', () => {
const result = formatValue('string', ',')
expect(result).toBe('string')
})

it('should return undefined for object values', () => {
const result = formatValue({ key: 'value' }, ',')
expect(result).toBeUndefined()
})
})

describe('isCustomField', () => {
it('should return true for keys starting with "Z_"', () => {
const result = isCustomField('Z_customField')
expect(result).toBe(true)
})

it('should return false for keys not starting with "Z_"', () => {
const result = isCustomField('customField')
expect(result).toBe(false)
})
})
})
})
Loading

0 comments on commit c4f9304

Please sign in to comment.