From 598289df4e3cac2f3a399ce483e2a4cbb4fbd2b2 Mon Sep 17 00:00:00 2001 From: David Zane Date: Wed, 27 May 2026 09:06:31 -0700 Subject: [PATCH 1/2] Move WLM description to per-rule field (#525) Signed-off-by: David Zane --- .../e2e/wlm-no-security/7_WLM_details.cy.js | 22 +- cypress/e2e/wlm/7_WLM_details.cy.js | 68 +-- .../WLMCreate/WLMCreate.tsx | 230 +++++----- .../WLMDetails/WLMDetails.test.tsx | 8 +- .../WLMDetails/WLMDetails.tsx | 405 +++++++++--------- 5 files changed, 373 insertions(+), 360 deletions(-) diff --git a/cypress/e2e/wlm-no-security/7_WLM_details.cy.js b/cypress/e2e/wlm-no-security/7_WLM_details.cy.js index 1b2d9724..22beef8a 100644 --- a/cypress/e2e/wlm-no-security/7_WLM_details.cy.js +++ b/cypress/e2e/wlm-no-security/7_WLM_details.cy.js @@ -81,7 +81,7 @@ describe('WLM Details Page', () => { cy.get('[data-testid="wlm-tab-settings"]').click(); cy.contains('+ Add another rule').click(); - cy.get('textarea[data-testid="indexInput"]').last().type(i1); + cy.get('[data-testid="indexInput"]').last().type(i1); cy.contains('button', /^Apply Changes$/) .should('not.be.disabled') @@ -92,13 +92,12 @@ describe('WLM Details Page', () => { cy.wait('@listRules'); cy.get('[data-testid="wlm-tab-settings"]').click(); - cy.contains(i1, { timeout: 20000 }).should('exist'); + cy.get('[data-testid="indexInput"]', { timeout: 20000 }).should(($els) => { + const values = Array.from($els, (el) => (el.value || '').trim()); + expect(values).to.include(i1); + }); - cy.get('textarea[data-testid="indexInput"]') - .last() - .type('{selectAll}{backspace}') - .type(i2) - .blur(); + cy.get('[data-testid="indexInput"]').last().type('{selectAll}{backspace}').type(i2).blur(); cy.contains('button', /^Apply Changes$/) .should('not.be.disabled') @@ -109,8 +108,11 @@ describe('WLM Details Page', () => { cy.wait('@listRules'); cy.get('[data-testid="wlm-tab-settings"]').click(); - cy.contains(i2, { timeout: 20000 }).should('exist'); - cy.contains(i1).should('not.exist'); + cy.get('[data-testid="indexInput"]', { timeout: 20000 }).should(($els) => { + const values = Array.from($els, (el) => (el.value || '').trim()); + expect(values).to.include(i2); + expect(values).to.not.include(i1); + }); cy.get('button[aria-label="Delete rule"]', { timeout: 20000 }).last().click({ force: true }); @@ -123,7 +125,7 @@ describe('WLM Details Page', () => { cy.wait('@listRules'); cy.get('[data-testid="wlm-tab-settings"]').click(); - cy.get('textarea[data-testid="indexInput"]').should(($areas) => { + cy.get('[data-testid="indexInput"]').should(($areas) => { const values = Array.from($areas, (el) => (el.value || '').trim()); expect(values).to.not.include(i2); }); diff --git a/cypress/e2e/wlm/7_WLM_details.cy.js b/cypress/e2e/wlm/7_WLM_details.cy.js index 307321f5..db8b22a8 100644 --- a/cypress/e2e/wlm/7_WLM_details.cy.js +++ b/cypress/e2e/wlm/7_WLM_details.cy.js @@ -77,9 +77,9 @@ describe('WLM Details Page', () => { cy.get('[data-testid="wlm-tab-settings"]').click(); cy.contains('+ Add another rule').click(); - cy.get('textarea[placeholder="Enter username"]').last().type(u1); - cy.get('textarea[placeholder="Enter role"]').last().type(r1); - cy.get('textarea[data-testid="indexInput"]').last().type(i1); + cy.get('[placeholder="Enter username"]').last().type(u1); + cy.get('[placeholder="Enter role"]').last().type(r1); + cy.get('[data-testid="indexInput"]').last().type(i1); cy.contains('button', /^Apply Changes$/) .should('not.be.disabled') @@ -90,25 +90,22 @@ describe('WLM Details Page', () => { cy.wait('@listRules'); cy.get('[data-testid="wlm-tab-settings"]').click(); - cy.contains(u1, { timeout: 20000 }).should('exist'); - cy.contains(r1, { timeout: 20000 }).should('exist'); - cy.contains(i1, { timeout: 20000 }).should('exist'); - - cy.get('textarea[placeholder="Enter username"]') - .last() - .type('{selectAll}{backspace}') - .type(u2) - .blur(); - cy.get('textarea[placeholder="Enter role"]') - .last() - .type('{selectAll}{backspace}') - .type(r2) - .blur(); - cy.get('textarea[data-testid="indexInput"]') - .last() - .type('{selectAll}{backspace}') - .type(i2) - .blur(); + cy.get('[placeholder="Enter username"]', { timeout: 20000 }).should(($els) => { + const values = Array.from($els, (el) => (el.value || '').trim()); + expect(values).to.include(u1); + }); + cy.get('[placeholder="Enter role"]', { timeout: 20000 }).should(($els) => { + const values = Array.from($els, (el) => (el.value || '').trim()); + expect(values).to.include(r1); + }); + cy.get('[data-testid="indexInput"]', { timeout: 20000 }).should(($els) => { + const values = Array.from($els, (el) => (el.value || '').trim()); + expect(values).to.include(i1); + }); + + cy.get('[placeholder="Enter username"]').last().type('{selectAll}{backspace}').type(u2).blur(); + cy.get('[placeholder="Enter role"]').last().type('{selectAll}{backspace}').type(r2).blur(); + cy.get('[data-testid="indexInput"]').last().type('{selectAll}{backspace}').type(i2).blur(); cy.contains('button', /^Apply Changes$/) .should('not.be.disabled') @@ -119,12 +116,21 @@ describe('WLM Details Page', () => { cy.wait('@listRules'); cy.get('[data-testid="wlm-tab-settings"]').click(); - cy.contains(u2, { timeout: 20000 }).should('exist'); - cy.contains(r2, { timeout: 20000 }).should('exist'); - cy.contains(i2, { timeout: 20000 }).should('exist'); - cy.contains(u1).should('not.exist'); - cy.contains(r1).should('not.exist'); - cy.contains(i1).should('not.exist'); + cy.get('[placeholder="Enter username"]', { timeout: 20000 }).should(($els) => { + const values = Array.from($els, (el) => (el.value || '').trim()); + expect(values).to.include(u2); + expect(values).to.not.include(u1); + }); + cy.get('[placeholder="Enter role"]', { timeout: 20000 }).should(($els) => { + const values = Array.from($els, (el) => (el.value || '').trim()); + expect(values).to.include(r2); + expect(values).to.not.include(r1); + }); + cy.get('[data-testid="indexInput"]', { timeout: 20000 }).should(($els) => { + const values = Array.from($els, (el) => (el.value || '').trim()); + expect(values).to.include(i2); + expect(values).to.not.include(i1); + }); cy.get('button[aria-label="Delete rule"]', { timeout: 20000 }).last().click({ force: true }); @@ -137,15 +143,15 @@ describe('WLM Details Page', () => { cy.wait('@listRules'); cy.get('[data-testid="wlm-tab-settings"]').click(); - cy.get('textarea[placeholder="Enter username"]').should(($areas) => { + cy.get('[placeholder="Enter username"]').should(($areas) => { const values = Array.from($areas, (el) => (el.value || '').trim()); expect(values).to.not.include(u2); }); - cy.get('textarea[placeholder="Enter role"]').should(($areas) => { + cy.get('[placeholder="Enter role"]').should(($areas) => { const values = Array.from($areas, (el) => (el.value || '').trim()); expect(values).to.not.include(r2); }); - cy.get('textarea[data-testid="indexInput"]').should(($areas) => { + cy.get('[data-testid="indexInput"]').should(($areas) => { const values = Array.from($areas, (el) => (el.value || '').trim()); expect(values).to.not.include(i2); }); diff --git a/public/pages/WorkloadManagement/WLMCreate/WLMCreate.tsx b/public/pages/WorkloadManagement/WLMCreate/WLMCreate.tsx index 0709768a..322202ff 100644 --- a/public/pages/WorkloadManagement/WLMCreate/WLMCreate.tsx +++ b/public/pages/WorkloadManagement/WLMCreate/WLMCreate.tsx @@ -8,7 +8,6 @@ import { EuiTitle, EuiSpacer, EuiFieldText, - EuiTextArea, EuiFormRow, EuiButton, EuiPanel, @@ -16,6 +15,8 @@ import { EuiFieldNumber, EuiText, EuiButtonIcon, + EuiFlexGroup, + EuiFlexItem, } from '@elastic/eui'; import { useHistory } from 'react-router-dom'; import { CoreStart, AppMountParameters } from 'opensearch-dashboards/public'; @@ -34,6 +35,7 @@ interface Rule { index: string; username: string; role: string; + description: string; } export const WLMCreate = ({ @@ -50,11 +52,12 @@ export const WLMCreate = ({ const history = useHistory(); const [name, setName] = useState(''); - const [description, setDescription] = useState(''); const [resiliencyMode, setResiliencyMode] = useState<'soft' | 'enforced'>(); const [cpuThreshold, setCpuThreshold] = useState(); const [memThreshold, setMemThreshold] = useState(); - const [rules, setRules] = useState([{ index: '', username: '', role: '' }]); + const [rules, setRules] = useState([ + { index: '', username: '', role: '', description: '' }, + ]); const [indexErrors, setIndexErrors] = useState>([]); const [usernameErrors, setUsernameErrors] = useState>([]); const [roleErrors, setRoleErrors] = useState>([]); @@ -158,7 +161,7 @@ export const WLMCreate = ({ if (!hasIndexes && !hasUsernames && !hasRoles) return null; return { - description: (description || '-').trim(), + description: (rule.description || '-').trim(), ...(hasUsernames || hasRoles ? { principal: { @@ -288,22 +291,6 @@ export const WLMCreate = ({ /> - - - <> - - Description – Optional - - - Describe the purpose of the workload group. - - setDescription(e.target.value)} - /> - - @@ -345,90 +332,35 @@ export const WLMCreate = ({

Define your rule using any combination of username, role, or index.

- - <> - {/* ---- Username / Role (gated by showSecurity) ---- */} -
- - Username - - - { - const value = e.target.value; - - const updatedRules = [...rules]; - const updatedErrors = [...usernameErrors]; - - updatedRules[idx].username = value; - updatedErrors[idx] = - value.length > 100 ? 'Maximum total length is 100 characters.' : null; - - setRules(updatedRules); - setUsernameErrors(updatedErrors); - }} - disabled={!showSecurity} - isInvalid={Boolean(usernameErrors[idx])} - /> - - - {!showSecurity - ? 'Username rules require data source ≥ 3.3.' - : 'You can use (,) to add multiple usernames.'} - -
- -
- - Role - - - { - const value = e.target.value; - - const updatedRules = [...rules]; - const updatedErrors = [...roleErrors]; - - updatedRules[idx].role = value; - updatedErrors[idx] = - value.length > 100 ? 'Maximum total length is 100 characters.' : null; - - setRules(updatedRules); - setRoleErrors(updatedErrors); - }} - disabled={!showSecurity} - isInvalid={Boolean(roleErrors[idx])} - /> - - - {!showSecurity - ? 'Role rules require data source ≥ 3.3.' - : 'You can use (,) to add multiple roles.'} - -
- - {/* ---- Index (always available) ---- */} -
- - Index wildcard - - - + + {/* ---- Description (per-rule) ---- */} + + Description – Optional + + { + const updatedRules = [...rules]; + updatedRules[idx].description = e.target.value; + setRules(updatedRules); + }} + /> + + + {/* ---- Index wildcard ---- */} + + Index wildcard + + + - - You can use (,) to add multiple indexes. - -
- -
+ + + + + + + + + {/* ---- Username (gated by showSecurity) ---- */} + + Username + + + { + const value = e.target.value; + + const updatedRules = [...rules]; + const updatedErrors = [...usernameErrors]; + + updatedRules[idx].username = value; + updatedErrors[idx] = + value.length > 100 ? 'Maximum total length is 100 characters.' : null; + + setRules(updatedRules); + setUsernameErrors(updatedErrors); + }} + disabled={!showSecurity} + isInvalid={Boolean(usernameErrors[idx])} + /> + + + + {/* ---- Role (gated by showSecurity) ---- */} + + Role + + + { + const value = e.target.value; + + const updatedRules = [...rules]; + const updatedErrors = [...roleErrors]; + + updatedRules[idx].role = value; + updatedErrors[idx] = + value.length > 100 ? 'Maximum total length is 100 characters.' : null; + + setRules(updatedRules); + setRoleErrors(updatedErrors); + }} + disabled={!showSecurity} + isInvalid={Boolean(roleErrors[idx])} + /> + + + @@ -469,7 +477,7 @@ export const WLMCreate = ({ { - setRules((prev) => [...prev, { index: '', username: '', role: '' }]); + setRules((prev) => [...prev, { index: '', username: '', role: '', description: '' }]); setIndexErrors((prev) => [...prev, null]); }} disabled={rules.length >= 5} diff --git a/public/pages/WorkloadManagement/WLMDetails/WLMDetails.test.tsx b/public/pages/WorkloadManagement/WLMDetails/WLMDetails.test.tsx index 7e60ef90..4579daf6 100644 --- a/public/pages/WorkloadManagement/WLMDetails/WLMDetails.test.tsx +++ b/public/pages/WorkloadManagement/WLMDetails/WLMDetails.test.tsx @@ -195,7 +195,7 @@ describe('WLMDetails Component', () => { renderComponent(); expect(screen.getByText(/Workload group name/i)).toBeInTheDocument(); - expect(screen.getByText(/Description/i)).toBeInTheDocument(); + expect(screen.getByText(/Resiliency mode/i)).toBeInTheDocument(); expect(screen.getByText(/CPU usage limit/i)).toBeInTheDocument(); expect(screen.getByText(/Memory usage limit/i)).toBeInTheDocument(); }); @@ -520,7 +520,7 @@ describe('WLMDetails Component', () => { ); await waitFor(() => { - expect(screen.getByText('System default workload group')).toBeInTheDocument(); + expect(screen.getAllByText('DEFAULT_WORKLOAD_GROUP').length).toBeGreaterThan(0); expect(screen.getByText('soft')).toBeInTheDocument(); }); }); @@ -725,7 +725,7 @@ describe('WLMDetails Component', () => { ); const body = JSON.parse(options.body); expect(body).toEqual({ - description: '-', + description: 'd', index_pattern: ['keep-updated-*'], workload_group: 'wg-123', }); @@ -778,7 +778,7 @@ describe('WLMDetails Component', () => { // Make unsaved change -> isSaved = false stops interval fireEvent.click(screen.getByTestId('wlm-tab-settings')); await waitFor(() => expect(screen.getByText(/Workload group settings/i)).toBeInTheDocument()); - fireEvent.change(screen.getByPlaceholderText(/Describe the workload group/i), { + fireEvent.change(screen.getAllByPlaceholderText(/Describe the rule/i)[0], { target: { value: 'changed' }, }); diff --git a/public/pages/WorkloadManagement/WLMDetails/WLMDetails.tsx b/public/pages/WorkloadManagement/WLMDetails/WLMDetails.tsx index d5b114de..8b552e33 100644 --- a/public/pages/WorkloadManagement/WLMDetails/WLMDetails.tsx +++ b/public/pages/WorkloadManagement/WLMDetails/WLMDetails.tsx @@ -23,7 +23,6 @@ import { EuiRadioGroup, EuiFieldNumber, EuiConfirmModal, - EuiTextArea, EuiButtonIcon, } from '@elastic/eui'; import { useHistory, useLocation } from 'react-router-dom'; @@ -124,6 +123,7 @@ interface Rule { indexId: string; role: string; username: string; + description: string; } // === Main Component === @@ -152,10 +152,11 @@ export const WLMDetails = ({ const [memoryLimit, setMemoryLimit] = useState(); const [originalCpuLimit, setOriginalCpuLimit] = useState(undefined); const [originalMemoryLimit, setOriginalMemoryLimit] = useState(undefined); - const [description, setDescription] = useState(); - const [rules, setRules] = useState([{ index: '', indexId: '', role: '', username: '' }]); + const [rules, setRules] = useState([ + { index: '', indexId: '', role: '', username: '', description: '' }, + ]); const [existingRules, setExistingRules] = useState([ - { index: '', indexId: '', role: '', username: '' }, + { index: '', indexId: '', role: '', username: '', description: '' }, ]); const [indexErrors, setIndexErrors] = useState>([]); const [isSaved, setIsSaved] = useState(true); @@ -331,31 +332,19 @@ export const WLMDetails = ({ const allRules = rulesRes?.rules ?? []; const matchedRules = allRules.filter((rule: any) => rule.workload_group === groupId); - setRules( - matchedRules.map((rule: any) => ({ - index: (rule?.index_pattern ?? []).join(','), - indexId: rule.id, - role: (rule?.principal?.role ?? []).join(','), - username: (rule?.principal?.username ?? []).join(','), - })) - ); - - setExistingRules( - matchedRules.map((rule: any) => ({ - index: (rule?.index_pattern ?? []).join(','), - indexId: rule.id, - role: (rule?.principal?.role ?? []).join(','), - username: (rule?.principal?.username ?? []).join(','), - })) - ); - - extractDescriptionFromRules(rulesRes, groupId); + const toRule = (rule: any): Rule => ({ + index: (rule?.index_pattern ?? []).join(','), + indexId: rule.id, + role: (rule?.principal?.role ?? []).join(','), + username: (rule?.principal?.username ?? []).join(','), + description: rule?.description ?? '', + }); + setRules(matchedRules.map(toRule)); + setExistingRules(matchedRules.map(toRule)); } catch (err) { console.error('Failed to fetch group stats', err); core.notifications.toasts.addDanger('Could not load rules.', err); } - } else { - setDescription('System default workload group'); } try { @@ -390,14 +379,6 @@ export const WLMDetails = ({ } }; - const extractDescriptionFromRules = (rulesRes: any, groupId: string) => { - const allRules = rulesRes?.body?.rules ?? []; - - const matchedRule = allRules.find((rule: any) => rule.workload_group === groupId); - - setDescription(matchedRule?.description ?? '-'); - }; - // === Actions === const splitCSV = (v?: string | null) => (v ?? '') @@ -414,8 +395,12 @@ export const WLMDetails = ({ const buildRulePayload = ( currentGroupId: string, - rule: { index?: string | null; username?: string | null; role?: string | null }, - currentDescription?: string | null + rule: { + index?: string | null; + username?: string | null; + role?: string | null; + description?: string | null; + } ): RulePayload | null => { const indexPattern = splitCSV(rule.index); const usernames = showSecurity ? splitCSV(rule.username) : []; @@ -428,7 +413,7 @@ export const WLMDetails = ({ if (!hasIndexes && !hasUsernames && !hasRoles) return null; // skip blank rule return { - description: (currentDescription || '-').trim(), + description: (rule.description || '-').trim(), ...(hasUsernames || hasRoles ? { principal: { @@ -493,7 +478,7 @@ export const WLMDetails = ({ } for (const rule of rulesToCreate) { - const response = buildRulePayload(currentId!, rule, description); + const response = buildRulePayload(currentId!, rule); if (!response) continue; await core.http.put(`/api/_rules/workload_group`, { @@ -504,7 +489,7 @@ export const WLMDetails = ({ } for (const rule of rulesToUpdate) { - const response = buildRulePayload(currentId!, rule, description); + const response = buildRulePayload(currentId!, rule); if (!response) continue; await core.http.put(`/api/_rules/workload_group/${rule.indexId}`, { @@ -660,12 +645,6 @@ export const WLMDetails = ({ {workloadGroup.name} - - - Description - - {description} - Resiliency mode @@ -836,25 +815,6 @@ export const WLMDetails = ({ - - <> - - Description – Optional - - - Describe the purpose of the workload group. - - { - setDescription(e.target.value); - setIsSaved(false); - }} - /> - - - {/* Resiliency Mode */} <> @@ -891,159 +851,193 @@ export const WLMDetails = ({ Define your rule using any combination of username, role, or index. -
-
+ + + - Username + Description – Optional - { const updated = [...rules]; - updated[idx].username = e.target.value; + updated[idx].description = e.target.value; setRules(updated); setIsSaved(false); }} - onBlur={(e) => { - const originallyNonEmpty = !!existingRules[idx]?.username?.trim(); - const nowEmpty = - (e.target.value ?? '') - .split(',') - .map((s) => s.trim()) - .filter(Boolean).length === 0; - - if (originallyNonEmpty && nowEmpty) { - // revert to original and warn - setRules((prev) => { - const next = [...prev]; - next[idx] = { - ...next[idx], - username: existingRules[idx]?.username ?? '', - }; - return next; - }); - core.notifications.toasts.addWarning( - 'Username cannot be cleared once set.' - ); - } - }} - disabled={!showSecurity} /> - - {!showSecurity - ? 'Username rules require data source ≥ 3.3.' - : 'You can use (,) to add multiple usernames.'} - -
- - - - - Role - - { - const updated = [...rules]; - updated[idx].role = e.target.value; - setRules(updated); - setIsSaved(false); - }} - onBlur={(e) => { - const originallyNonEmpty = !!existingRules[idx]?.role?.trim(); - const nowEmpty = - (e.target.value ?? '') - .split(',') - .map((s) => s.trim()) - .filter(Boolean).length === 0; - - if (originallyNonEmpty && nowEmpty) { - // revert to original and warn - setRules((prev) => { - const next = [...prev]; - next[idx] = { ...next[idx], role: existingRules[idx]?.role ?? '' }; - return next; - }); - core.notifications.toasts.addWarning('Role cannot be cleared once set.'); - } - }} - disabled={!showSecurity} - /> - - {!showSecurity - ? 'Role rules require data source ≥ 3.3.' - : 'You can use (,) to add multiple roles.'} - -
- - - - {/* Index */} - - <> +
+ Index wildcard - - { - const value = e.target.value; - - const updatedRules = [...rules]; - const updatedErrors = [...indexErrors]; - updatedRules[idx].index = value; - - let error: string | null = null; - - // split on commas, trim each segment - const items = value.split(',').map((s) => s.trim()); - - // 1) Any item too long? - if (items.some((item) => item.length > 100)) { - error = 'Index names must be 100 characters or fewer.'; - } - // 2) Too many items? - else if (items.length > 10) { - error = 'You can specify at most 10 indexes per rule.'; - } - - updatedErrors[idx] = error; - setRules(updatedRules); - setIndexErrors(updatedErrors); - setIsSaved(false); - }} - onBlur={(e) => { - const originallyNonEmpty = !!existingRules[idx]?.index?.trim(); - const nowEmpty = - (e.target.value ?? '') - .split(',') - .map((s) => s.trim()) - .filter(Boolean).length === 0; - - if (originallyNonEmpty && nowEmpty) { - // revert to original and warn - setRules((prev) => { - const next = [...prev]; - next[idx] = { ...next[idx], index: existingRules[idx]?.index ?? '' }; - return next; - }); - core.notifications.toasts.addWarning( - 'Index cannot be cleared once set.' - ); - } - }} + - - You can use (,) to add multiple indexes. + error={indexErrors[idx] || undefined} + helpText="You can use (,) to add multiple indexes." + > + { + const value = e.target.value; + + const updatedRules = [...rules]; + const updatedErrors = [...indexErrors]; + updatedRules[idx].index = value; + + let error: string | null = null; + + // split on commas, trim each segment + const items = value.split(',').map((s) => s.trim()); + + // 1) Any item too long? + if (items.some((item) => item.length > 100)) { + error = 'Index names must be 100 characters or fewer.'; + } + // 2) Too many items? + else if (items.length > 10) { + error = 'You can specify at most 10 indexes per rule.'; + } + + updatedErrors[idx] = error; + setRules(updatedRules); + setIndexErrors(updatedErrors); + setIsSaved(false); + }} + onBlur={(e) => { + const originallyNonEmpty = !!existingRules[idx]?.index?.trim(); + const nowEmpty = + (e.target.value ?? '') + .split(',') + .map((s) => s.trim()) + .filter(Boolean).length === 0; + + if (originallyNonEmpty && nowEmpty) { + // revert to original and warn + setRules((prev) => { + const next = [...prev]; + next[idx] = { + ...next[idx], + index: existingRules[idx]?.index ?? '', + }; + return next; + }); + core.notifications.toasts.addWarning( + 'Index cannot be cleared once set.' + ); + } + }} + isInvalid={Boolean(indexErrors[idx])} + /> + + + + + + + + + + Username - - + + { + const updated = [...rules]; + updated[idx].username = e.target.value; + setRules(updated); + setIsSaved(false); + }} + onBlur={(e) => { + const originallyNonEmpty = !!existingRules[idx]?.username?.trim(); + const nowEmpty = + (e.target.value ?? '') + .split(',') + .map((s) => s.trim()) + .filter(Boolean).length === 0; + + if (originallyNonEmpty && nowEmpty) { + // revert to original and warn + setRules((prev) => { + const next = [...prev]; + next[idx] = { + ...next[idx], + username: existingRules[idx]?.username ?? '', + }; + return next; + }); + core.notifications.toasts.addWarning( + 'Username cannot be cleared once set.' + ); + } + }} + disabled={!showSecurity} + /> + + + + + Role + + + { + const updated = [...rules]; + updated[idx].role = e.target.value; + setRules(updated); + setIsSaved(false); + }} + onBlur={(e) => { + const originallyNonEmpty = !!existingRules[idx]?.role?.trim(); + const nowEmpty = + (e.target.value ?? '') + .split(',') + .map((s) => s.trim()) + .filter(Boolean).length === 0; + + if (originallyNonEmpty && nowEmpty) { + // revert to original and warn + setRules((prev) => { + const next = [...prev]; + next[idx] = { + ...next[idx], + role: existingRules[idx]?.role ?? '', + }; + return next; + }); + core.notifications.toasts.addWarning( + 'Role cannot be cleared once set.' + ); + } + }} + disabled={!showSecurity} + /> + + + @@ -1064,7 +1058,10 @@ export const WLMDetails = ({ ))} { - setRules([...rules, { index: '', indexId: '', role: '', username: '' }]); + setRules([ + ...rules, + { index: '', indexId: '', role: '', username: '', description: '' }, + ]); setIndexErrors([...indexErrors, null]); setIsSaved(false); }} From 583d3b59fee31f0589b74fe43949e5b3d9fca442 Mon Sep 17 00:00:00 2001 From: David Zane Date: Wed, 27 May 2026 11:20:28 -0700 Subject: [PATCH 2/2] Make text boxes auto expand Signed-off-by: David Zane --- .../WLMCreate/WLMCreate.tsx | 9 ++--- .../WLMDetails/WLMDetails.tsx | 9 ++--- .../auto_size_text_area.tsx | 35 +++++++++++++++++++ 3 files changed, 45 insertions(+), 8 deletions(-) create mode 100644 public/pages/WorkloadManagement/auto_size_text_area.tsx diff --git a/public/pages/WorkloadManagement/WLMCreate/WLMCreate.tsx b/public/pages/WorkloadManagement/WLMCreate/WLMCreate.tsx index 322202ff..580f1138 100644 --- a/public/pages/WorkloadManagement/WLMCreate/WLMCreate.tsx +++ b/public/pages/WorkloadManagement/WLMCreate/WLMCreate.tsx @@ -30,6 +30,7 @@ import { resolveDataSourceVersion, isSecurityAttributesSupported, } from '../../../utils/datasource-utils'; +import { AutoSizeTextArea } from '../auto_size_text_area'; interface Rule { index: string; @@ -338,7 +339,7 @@ export const WLMCreate = ({ Description – Optional - - - - Description – Optional - - - { @@ -1001,7 +1002,7 @@ export const WLMDetails = ({ : 'You can use (,) to add multiple roles.' } > - { diff --git a/public/pages/WorkloadManagement/auto_size_text_area.tsx b/public/pages/WorkloadManagement/auto_size_text_area.tsx new file mode 100644 index 00000000..fdb89620 --- /dev/null +++ b/public/pages/WorkloadManagement/auto_size_text_area.tsx @@ -0,0 +1,35 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useLayoutEffect, useRef } from 'react'; +import { EuiTextArea, EuiTextAreaProps } from '@elastic/eui'; + +const MAX_HEIGHT_PX = 120; + +export const AutoSizeTextArea: React.FC = (props) => { + const ref = useRef(null); + + useLayoutEffect(() => { + const el = ref.current; + if (!el) return; + el.style.height = 'auto'; + const next = Math.min(el.scrollHeight, MAX_HEIGHT_PX); + el.style.height = `${next}px`; + el.style.overflowY = el.scrollHeight > MAX_HEIGHT_PX ? 'auto' : 'hidden'; + }, [props.value]); + + return ( + { + ref.current = node; + if (typeof props.inputRef === 'function') props.inputRef(node); + }} + rows={1} + resize="none" + style={{ minHeight: 40, ...(props.style || {}) }} + /> + ); +};