From ef585d45cd1d133758bf89754729da8ce46e0383 Mon Sep 17 00:00:00 2001 From: Ildar Kamalov Date: Thu, 16 Jul 2026 13:02:59 +0000 Subject: [PATCH] Pull request 2712: AGDNS-4229 add textarea comments highlight Added comments highlight for all upstream fields and custom rules Squashed commit of the following: commit 2feb4d4dc199474199a234e39157b85c37fa6fd6 Merge: b5bfdc81c 7729d8c57 Author: Ildar Kamalov Date: Thu Jul 16 15:27:54 2026 +0300 Merge branch 'master' into AGDNS-4229 commit b5bfdc81c463d489d459b883a15c139ea6595b4a Author: Ildar Kamalov Date: Thu Jul 16 11:33:09 2026 +0300 support all comments for ignored domains commit decb5b6587e900dae081b01930f09f42a66c7533 Author: Ildar Kamalov Date: Wed Jul 15 17:58:39 2026 +0300 fix bootstrap and ignored domains commit bd7d342b2f8bdce0174d9a2251377eac732b9281 Author: Ildar Kamalov Date: Wed Jul 15 14:40:53 2026 +0300 remove output from test frontend commit 5c9de2362676bf247dd0f0926a5a3afcf64f4be7 Author: Ildar Kamalov Date: Wed Jul 15 14:18:50 2026 +0300 fix upstream comments commit e10354065709c2f5268c1dfff269d3aac2be27ad Author: Ildar Kamalov Date: Wed Jul 15 13:32:34 2026 +0300 add comments for clients upstreams commit a65ee2cf2a7efb568be1dece9b492d2acd3d4b7e Author: Ildar Kamalov Date: Wed Jul 15 12:30:04 2026 +0300 config commit 67207b04645fa30e1cd97fe6a6998956eabb3c62 Author: Ildar Kamalov Date: Wed Jul 15 12:06:04 2026 +0300 fix protection menu commit 380ac0797e850cb1cb8f641627017876107f433e Merge: 10945e24d c1f208ef6 Author: Ildar Kamalov Date: Wed Jul 15 11:55:28 2026 +0300 Merge branch 'master' into AGDNS-4229 commit 10945e24df4ae748e728e9d6a58fd8dc3db819f2 Author: Ildar Kamalov Date: Tue Jul 14 17:54:41 2026 +0300 fix dockerfile commit faedfede4518ceceddbd774f75c05d45fc8eab08 Author: Ildar Kamalov Date: Tue Jul 14 17:27:53 2026 +0300 makefile commit b4723ebceaa19c751b9ded3b18075135b2b1dbfa Author: Ildar Kamalov Date: Tue Jul 14 17:27:38 2026 +0300 AGDNS-4229 add textarea comments highlight commit 4923b3645bb9ea1410cbf09ec59efecf358b1baa Author: Ildar Kamalov Date: Tue Jul 14 17:13:31 2026 +0300 AGDNS-4229 add textarea comments highlight --- bamboo-specs/test.yaml | 2 - .../__tests__/clientForm/validators.test.ts | 59 ++++++++++-- .../helpers/validate-domains-per-line.test.ts | 8 ++ .../src/common/controls/Textarea/Textarea.tsx | 92 ++++++++++++++----- .../controls/Textarea/TextareaHighlight.tsx | 58 ++++++++++++ .../controls/Textarea/styles.module.pcss | 43 +++++++++ client_v2/src/common/ui/Menu/Menu.tsx | 4 +- client_v2/src/components/App/index.tsx | 55 +++++------ .../Clients/AddClient/AddClient.tsx | 1 + .../blocks/Header/Header.module.pcss | 8 ++ .../Dashboard/blocks/Header/Header.tsx | 1 + client_v2/src/components/Dhcp/Dhcp.tsx | 4 +- .../Upstream/blocks/BootstrapDnsDialog.tsx | 4 +- .../Upstream/blocks/FallbackDnsDialog.tsx | 1 + .../blocks/PrivateReverseServersDialog.tsx | 1 + .../Upstream/blocks/ServerAddressesDialog.tsx | 1 + client_v2/src/components/Routes/Paths.ts | 6 +- .../src/components/Settings/FiltersConfig.tsx | 2 +- .../IgnoredDomainsModal.tsx | 3 + .../UserRules/UserRules.module.pcss | 2 +- .../src/components/UserRules/UserRules.tsx | 9 +- .../UserRules/blocks/RulesEditor.tsx | 3 + client_v2/src/helpers/constants.ts | 26 +----- client_v2/src/helpers/helpers.tsx | 2 + .../src/helpers/highlightTextareaComments.tsx | 46 ---------- client_v2/src/helpers/validators.ts | 38 ++++++-- client_v2/src/lib/theme/Highlight.module.pcss | 24 ----- client_v2/src/lib/theme/index.ts | 2 - client_v2/src/stores/dhcp.ts | 5 +- client_v2/vitest.config.ts | 1 - 30 files changed, 331 insertions(+), 180 deletions(-) create mode 100644 client_v2/src/common/controls/Textarea/TextareaHighlight.tsx delete mode 100644 client_v2/src/helpers/highlightTextareaComments.tsx delete mode 100644 client_v2/src/lib/theme/Highlight.module.pcss diff --git a/bamboo-specs/test.yaml b/bamboo-specs/test.yaml index f78992e0a15..f08688041a8 100644 --- a/bamboo-specs/test.yaml +++ b/bamboo-specs/test.yaml @@ -62,7 +62,6 @@ --build-arg "BASE_IMAGE=${bamboo_dockerFrontend}" \ --build-arg "CACHE_BUSTER=${bamboo_cacheBuster}" \ --build-arg "CLIENT_DIR=${bamboo_clientDir}" \ - --output '.' \ --progress 'plain' \ --target 'tester' \ -f ./docker/frontend.Dockerfile \ @@ -222,7 +221,6 @@ --build-arg "BASE_IMAGE=${bamboo_dockerFrontend}" \ --build-arg "CACHE_BUSTER=${bamboo_cacheBuster}" \ --build-arg "CLIENT_DIR=${bamboo_clientDir}" \ - --output '.' \ --progress 'plain' \ --target 'e2etester' \ -f ./docker/frontend.Dockerfile \ diff --git a/client_v2/src/__tests__/clientForm/validators.test.ts b/client_v2/src/__tests__/clientForm/validators.test.ts index 48e08b1f1c0..327989177be 100644 --- a/client_v2/src/__tests__/clientForm/validators.test.ts +++ b/client_v2/src/__tests__/clientForm/validators.test.ts @@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest'; import { validateIdentifier, validateUpstreams, + validateBootstrapDns, validateRequiredValue, validateIpv4, validatePort, @@ -503,24 +504,70 @@ describe('validateRewriteNotSame', () => { }); }); -describe('validateUpstreams with bang comments', () => { - it('accepts lines starting with !', () => { - expect(validateUpstreams('! comment line\n8.8.8.8')).toBeUndefined(); +describe('validateUpstreams with bang prefix', () => { + it('rejects lines starting with ! as invalid upstreams', () => { + expect(validateUpstreams('! comment line\n8.8.8.8')).toBeTruthy(); }); - it('accepts only comment lines (!)', () => { - expect(validateUpstreams('! first\n! second')).toBeUndefined(); + it('rejects only ! lines as invalid', () => { + expect(validateUpstreams('! first\n! second')).toBeTruthy(); + }); + + it('rejects !-prefixed URLs that would otherwise pass address check', () => { + expect(validateUpstreams('! https://dns10.quad9.net/dns-query')).toBeTruthy(); + }); + + it('rejects !-prefixed upstream with dot in comment', () => { + expect(validateUpstreams('! dns.example.com:53')).toBeTruthy(); }); it('still accepts # comments', () => { expect(validateUpstreams('# comment\n8.8.8.8')).toBeUndefined(); }); - it('rejects invalid non-comment lines alongside comments', () => { + it('rejects ! and invalid non-comment lines', () => { expect(validateUpstreams('! good\ninvalidline')).toBeTruthy(); }); }); +describe('validateBootstrapDns', () => { + it('returns undefined for empty value', () => { + expect(validateBootstrapDns('')).toBeUndefined(); + }); + + it('returns undefined for valid IP', () => { + expect(validateBootstrapDns('8.8.8.8')).toBeUndefined(); + }); + + it('returns undefined for valid upstream URL', () => { + expect(validateBootstrapDns('https://dns.example.com/dns-query')).toBeUndefined(); + }); + + it('rejects # comment lines (no comment support)', () => { + expect(validateBootstrapDns('# comment\n8.8.8.8')).toBeTruthy(); + }); + + it('rejects #-prefixed URLs that would otherwise pass address check', () => { + expect(validateBootstrapDns('# https://dns.example.com')).toBeTruthy(); + }); + + it('rejects only # lines', () => { + expect(validateBootstrapDns('# first\n# second')).toBeTruthy(); + }); + + it('rejects !-prefixed lines', () => { + expect(validateBootstrapDns('! https://dns.example.com')).toBeTruthy(); + }); + + it('rejects lines without dot or colon', () => { + expect(validateBootstrapDns('not-a-server')).toBe('Invalid format'); + }); + + it('returns line-numbered error for mixed valid and # comment', () => { + expect(validateBootstrapDns('8.8.8.8\n# comment')).toBe('Invalid format on line 2'); + }); +}); + describe('validateLeaseTime', () => { it('accepts 1 (minimum)', () => { expect(validateLeaseTime(1)).toBeUndefined(); diff --git a/client_v2/src/__tests__/helpers/validate-domains-per-line.test.ts b/client_v2/src/__tests__/helpers/validate-domains-per-line.test.ts index 348e10613e4..e54610866a4 100644 --- a/client_v2/src/__tests__/helpers/validate-domains-per-line.test.ts +++ b/client_v2/src/__tests__/helpers/validate-domains-per-line.test.ts @@ -44,6 +44,14 @@ describe('validateDomainsPerLine', () => { expect(validateDomainsPerLine('# this is a comment')).toBeUndefined(); }); + it('rejects !-prefixed filter rule that would otherwise pass dot check', () => { + expect(validateDomainsPerLine('! ||example.org^')).toBeTruthy(); + }); + + it('rejects only ! lines as invalid', () => { + expect(validateDomainsPerLine('! first\n! second')).toBeTruthy(); + }); + it('returns undefined for mixed valid lines with comments', () => { expect( validateDomainsPerLine('# comment\nexample.org\n||ads.example.org^'), diff --git a/client_v2/src/common/controls/Textarea/Textarea.tsx b/client_v2/src/common/controls/Textarea/Textarea.tsx index 5fcf1a5b86f..91f3f303aa9 100644 --- a/client_v2/src/common/controls/Textarea/Textarea.tsx +++ b/client_v2/src/common/controls/Textarea/Textarea.tsx @@ -1,23 +1,47 @@ -import { type JSX, Show } from 'solid-js'; +import { type JSX, Show, createSignal, createEffect } from 'solid-js'; import cn from 'clsx'; import s from './styles.module.pcss'; +import { TextareaHighlight } from './TextareaHighlight'; + +import type { CommentLineToken } from 'panel/helpers/constants'; type TextareaChangeEvent = Event & { currentTarget: HTMLTextAreaElement; target: HTMLTextAreaElement; }; -type Props = Omit, 'onChange' | 'onBlur'> & { +type Props = Omit< + JSX.TextareaHTMLAttributes, + 'onChange' | 'onBlur' | 'onInput' | 'onScroll' +> & { label?: JSX.Element; size?: 'small' | 'medium' | 'large'; errorMessage?: string; + highlightComments?: boolean; + commentPrefixes?: readonly CommentLineToken[]; ref?: HTMLTextAreaElement | ((el: HTMLTextAreaElement) => void); onChange?: (event: TextareaChangeEvent) => void; + onInput?: (event: TextareaChangeEvent) => void; onBlur?: (event: FocusEvent) => void; + onScroll?: ( + event: Event & { currentTarget: HTMLTextAreaElement; target: HTMLTextAreaElement }, + ) => void; }; export const Textarea = (props: Props) => { + const [scrollTop, setScrollTop] = createSignal(0); + const [currentValue, setCurrentValue] = createSignal(''); + + // Sync from external prop changes (e.g. dialog open/close resets); + // also handles initial value on mount. Runs before browser paint so + // there is no visible flicker from the empty initial signal. + createEffect(() => { + setCurrentValue(props.value as string); + }); + + const highlightEnabled = () => !!props.highlightComments; + const setRef = (el: HTMLTextAreaElement) => { if (typeof props.ref === 'function') { props.ref(el); @@ -28,10 +52,22 @@ export const Textarea = (props: Props) => { props.onChange?.(e); }; + const handleInput = (e: TextareaChangeEvent) => { + setCurrentValue(e.target.value); + props.onInput?.(e); + }; + const handleBlur = (e: FocusEvent) => { props.onBlur?.(e); }; + const handleScroll = ( + e: Event & { currentTarget: HTMLTextAreaElement; target: HTMLTextAreaElement }, + ) => { + setScrollTop(e.target.scrollTop); + props.onScroll?.(e); + }; + return (
@@ -39,26 +75,38 @@ export const Textarea = (props: Props) => { {props.label} -