From c1068e32a1387df9c0346a0c5bdd24fde6d9c0f7 Mon Sep 17 00:00:00 2001 From: Hoang Rio Date: Fri, 28 Aug 2026 14:07:44 +0700 Subject: [PATCH 1/3] Show update banner after dismissing TLS expiry warning --- .../src/__tests__/common/ui/Banners.test.tsx | 25 +++++++++++- client_v2/src/common/ui/Banners/Banners.tsx | 38 ++++++++++--------- 2 files changed, 44 insertions(+), 19 deletions(-) diff --git a/client_v2/src/__tests__/common/ui/Banners.test.tsx b/client_v2/src/__tests__/common/ui/Banners.test.tsx index 1468a289ed9..fc553b8c5c3 100644 --- a/client_v2/src/__tests__/common/ui/Banners.test.tsx +++ b/client_v2/src/__tests__/common/ui/Banners.test.tsx @@ -1,5 +1,5 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { render, screen } from '@solidjs/testing-library'; +import { fireEvent, render, screen } from '@solidjs/testing-library'; import userEvent from '@testing-library/user-event'; import { HashRouter, Route } from '@solidjs/router'; @@ -235,6 +235,29 @@ describe('Banners', () => { expect(screen.queryByTestId('banner-tls-expired')).not.toBeInTheDocument(); }); + it('shows the update banner after dismissing the TLS expiring banner', () => { + mockEncryptionState.enabled = true; + mockEncryptionState.valid_cert = true; + mockEncryptionState.not_after = new Date(Date.now() + 2 * 86400000).toISOString(); // 2 days + // Update available too, but TLS expiring takes priority + mockDashboardState.isUpdateAvailable = true; + mockDashboardState.newVersion = 'v1.0.0'; + mockDashboardState.canAutoUpdate = true; + + renderBanners(); + + expect(screen.getByTestId('banner-tls-expiring')).toBeInTheDocument(); + expect(screen.queryByTestId('banner-update-auto')).not.toBeInTheDocument(); + + // Dismiss the expiring warning while TLS is still within the warning window + const closeButton = screen.getByTestId('banner-tls-expiring-close'); + fireEvent.click(closeButton); + + // Lower-priority update banner should now appear even though TLS still takes priority + expect(screen.queryByTestId('banner-tls-expiring')).not.toBeInTheDocument(); + expect(screen.getByTestId('banner-update-auto')).toBeInTheDocument(); + }); + // ── forceBanner (dev test override) ── it('renders forced TLS expired banner regardless of store state', () => { diff --git a/client_v2/src/common/ui/Banners/Banners.tsx b/client_v2/src/common/ui/Banners/Banners.tsx index 895c1699d8d..f51be644226 100644 --- a/client_v2/src/common/ui/Banners/Banners.tsx +++ b/client_v2/src/common/ui/Banners/Banners.tsx @@ -39,15 +39,16 @@ export const Banners = (props: Props) => { return null; }); - const computeActiveBanner = (): BannerSpec | null => { + const getActiveBanners = (): BannerSpec[] => { + const active: BannerSpec[] = []; + if (encryptionState.enabled && encryptionState.valid_cert && encryptionState.not_after) { const expiry = new Date(encryptionState.not_after).getTime(); if (!Number.isNaN(expiry)) { if (Date.now() > expiry) { - return { type: 'tls_expired' }; - } - if (Date.now() > expiry - TLS_EXPIRY_WARNING_MS) { - return { type: 'tls_expiring' }; + active.push({ type: 'tls_expired' }); + } else if (Date.now() > expiry - TLS_EXPIRY_WARNING_MS) { + active.push({ type: 'tls_expiring' }); } } } @@ -57,22 +58,24 @@ export const Banners = (props: Props) => { version: dashboardState.newVersion, announcementUrl: dashboardState.announcementUrl, }; - return dashboardState.canAutoUpdate - ? { type: 'update_auto', ...spec } - : { type: 'update_manual', ...spec }; + active.push( + dashboardState.canAutoUpdate + ? { type: 'update_auto', ...spec } + : { type: 'update_manual', ...spec }, + ); } - return null; + return active; }; const banner = createMemo(() => { - const active = props.forceBanner ?? forceFromQuery() ?? computeActiveBanner(); - if (!active) return null; + const forced = props.forceBanner ?? forceFromQuery(); + const candidates = forced ? [forced] : getActiveBanners(); const dismissedValue = dismissed(); - if (dismissedValue && bannerSpecsEqual(dismissedValue, active)) { - return null; - } - return active; + return ( + candidates.find((b) => !(dismissedValue && bannerSpecsEqual(dismissedValue, b))) ?? + null + ); }); const announcementLinkHandler = (announcementUrl: string) => (text: string) => ( @@ -82,9 +85,8 @@ export const Banners = (props: Props) => { ); return ( - - {(spec) => { - const current = spec(); + + {(current) => { const renderBanner = (): JSX.Element => { switch (current.type) { From 54f65411f1ede1b98fa233b06586130939834333 Mon Sep 17 00:00:00 2001 From: Maksim Kazantsev Date: Fri, 28 Aug 2026 15:35:17 +0300 Subject: [PATCH 2/3] AGDNS-3720 Refactor TLS vol.4 --- internal/aghtls/defaultmanager.go | 38 ++++++++--------------- internal/home/web.go | 48 +++++++++++++++++------------- internal/home/web_internal_test.go | 17 ++++++----- 3 files changed, 49 insertions(+), 54 deletions(-) diff --git a/internal/aghtls/defaultmanager.go b/internal/aghtls/defaultmanager.go index de1b4b1cbe0..983e05714a5 100644 --- a/internal/aghtls/defaultmanager.go +++ b/internal/aghtls/defaultmanager.go @@ -53,15 +53,13 @@ type DefaultManager struct { logger *slog.Logger // mu protects tlsConf, extTLSConf, certLastMod, tlsCert, and pair. - mu *sync.Mutex - tlsConf *tls.Config - extTLSConf *ExtendedTLSConfig - tlsCert *tls.Certificate - rootCerts *x509.CertPool - updates chan UpdateSignal - pair TLSPair - - // TODO(m.kazantsev): Add support for dynamic updates of custom ciphers. + mu *sync.Mutex + tlsConf *tls.Config + extTLSConf *ExtendedTLSConfig + tlsCert *tls.Certificate + rootCerts *x509.CertPool + updates chan UpdateSignal + pair TLSPair customCipherIDs []uint16 } @@ -435,7 +433,11 @@ func (mgr *DefaultManager) SetExtendedTLSConfig( return false, err } - updatePlainDNS(mgr.extTLSConf, newConf, servePlainDNS) + if servePlainDNS != aghalg.NBNull { + newConf.ServePlainDNS = servePlainDNS == aghalg.NBTrue + } else { + newConf.ServePlainDNS = mgr.extTLSConf.ServePlainDNS + } if !setPrivateFieldsAndCompare(mgr.extTLSConf, newConf) { mgr.logger.InfoContext(ctx, "config has changed, restarting https server") @@ -533,22 +535,6 @@ func setPrivateFieldsAndCompare( return cmp.Equal(currentTLSConf, newTLSConf) } -// updatePlainDNS checks the old value of -// [aghtls.ExtendedTLSConfig.ServePlainDNS] in currentTLSConf and if it differs -// from servePlain, sets the value of servePlain in newTLSConf.ServePlainDNS. -// currentTLSConf and newTLSConf must not be nil. -func updatePlainDNS( - currentTLSConf *ExtendedTLSConfig, - newTLSConf *ExtendedTLSConfig, - servePlain aghalg.NullBool, -) { - if servePlain != aghalg.NBNull { - newTLSConf.ServePlainDNS = servePlain == aghalg.NBTrue - } else { - newTLSConf.ServePlainDNS = currentTLSConf.ServePlainDNS - } -} - // setCertFileTime sets [tlsManager.certLastMod] from the certificate. If there // are errors, setCertFileTime logs them. m.mu is expected to be locked. func (mgr *DefaultManager) setCertFileTime(ctx context.Context) { diff --git a/internal/home/web.go b/internal/home/web.go index c80af27d81b..a1385cc727d 100644 --- a/internal/home/web.go +++ b/internal/home/web.go @@ -227,11 +227,9 @@ type webAPI struct { // hostsContainer is used for DNS initialization on updates. hostsContainer *aghnet.HostsContainer - // httpsServer is the server that handles HTTPS traffic. If it is not nil, - // [Web.http3Server] must also not be nil. - // - // TODO(d.kolyshev): Make it a pointer. - httpsServer httpsServer + // httpsServer is the server that handles HTTPS traffic. It must not be + // nil. + httpsServer *httpsServer // pidFilePath is used for cleanup. pidFilePath string @@ -284,9 +282,11 @@ func newWebAPI(ctx context.Context, conf *webAPIConfig) (w *webAPI) { w.registerControlHandlers() } - w.httpsServer.logger = conf.baseLogger.With(slogutil.KeyPrefix, "https_server") - w.httpsServer.mu = &sync.Mutex{} - w.httpsServer.reconfigured = make(chan unit, 1) + w.httpsServer = &httpsServer{ + logger: conf.baseLogger.With(slogutil.KeyPrefix, "https_server"), + mu: &sync.Mutex{}, + reconfigured: make(chan unit, 1), + } return w } @@ -513,6 +513,7 @@ func (web *webAPI) mustStartHTTP3(ctx context.Context, address string) { } // startPprof launches the debug and profiling server on the provided port. +// baseLogger must not be nil. func startPprof(baseLogger *slog.Logger, port uint16) { addr := netip.AddrPortFrom(netutil.IPv4Localhost(), port) @@ -591,15 +592,16 @@ func (web *webAPI) handleTLSValidate(w http.ResponseWriter, r *http.Request) { status, ) resp := &tlsConfig{ - tlsConfigSettingsExt: setts, + tlsConfigSettingsExt: *setts, tlsConfigStatus: tlsConfigStatusFromConf(status), } marshalTLS(ctx, web.logger, w, r, resp) } -// validateTLSSettings returns error if the setts are not valid. -func (web *webAPI) validateTLSSettings(setts tlsConfigSettingsExt) (err error) { +// validateTLSSettings returns error if the setts are not valid. setts must not +// be nil. +func (web *webAPI) validateTLSSettings(setts *tlsConfigSettingsExt) (err error) { if !setts.Enabled { if setts.ServePlainDNS == aghalg.NBFalse { // TODO(a.garipov): Support full disabling of all DNS. @@ -785,7 +787,7 @@ func (web *webAPI) handleTLSConfigure(w http.ResponseWriter, r *http.Request) { err = aghtls.LoadTLSConfig(ctx, web.logger, web.tlsManager, conf, status) if err != nil { resp := &tlsConfig{ - tlsConfigSettingsExt: req, + tlsConfigSettingsExt: *req, tlsConfigStatus: tlsConfigStatusFromConf(status), } @@ -816,7 +818,7 @@ func (web *webAPI) handleTLSConfigure(w http.ResponseWriter, r *http.Request) { } resp := &tlsConfig{ - tlsConfigSettingsExt: req, + tlsConfigSettingsExt: *req, tlsConfigStatus: tlsConfigStatusFromConf(status), } @@ -834,7 +836,7 @@ func (web *webAPI) handleTLSConfigure(w http.ResponseWriter, r *http.Request) { } // setServePlainDNS updates the ServePlainDNS field of [config.DNS]. -func setServePlainDNS(req tlsConfigSettingsExt) { +func setServePlainDNS(req *tlsConfigSettingsExt) { if req.ServePlainDNS == aghalg.NBNull { return } @@ -909,12 +911,18 @@ func marshalTLS( aghhttp.WriteJSONResponseOK(ctx, logger, w, r, *data) } -// unmarshalTLS handles base64-encoded certificates transparently. -func unmarshalTLS(r *http.Request) (data tlsConfigSettingsExt, err error) { - data = tlsConfigSettingsExt{} +// unmarshalTLS handles base64-encoded certificates transparently. r must not +// be nil. +func unmarshalTLS(r *http.Request) (data *tlsConfigSettingsExt, err error) { + data = &tlsConfigSettingsExt{} + err = json.NewDecoder(r.Body).Decode(&data) if err != nil { - return data, fmt.Errorf("failed to parse new TLS config json: %w", err) + return data, fmt.Errorf("failed to parse new tls config json: %w", err) + } + + if data == nil { + return &tlsConfigSettingsExt{}, nil } if data.tlsConfigSettings.CertificateChain != "" { @@ -926,7 +934,7 @@ func unmarshalTLS(r *http.Request) (data tlsConfigSettingsExt, err error) { data.tlsConfigSettings.CertificateChain = string(cert) if data.tlsConfigSettings.CertificatePath != "" { - return data, fmt.Errorf("certificate data and file can't be set together") + return data, errors.Error("certificate data and file can't be set together") } } @@ -941,7 +949,7 @@ func unmarshalTLS(r *http.Request) (data tlsConfigSettingsExt, err error) { data.tlsConfigSettings.PrivateKey = string(key) if data.tlsConfigSettings.PrivateKeyPath != "" { - return data, fmt.Errorf("private key data and file can't be set together") + return data, errors.Error("private key data and file can't be set together") } return data, nil diff --git a/internal/home/web_internal_test.go b/internal/home/web_internal_test.go index 6b7778b2db6..5caa9412932 100644 --- a/internal/home/web_internal_test.go +++ b/internal/home/web_internal_test.go @@ -132,7 +132,8 @@ func TestWebAPI_HandleTLSConfigure(t *testing.T) { }) res := &tlsConfig{ - tlsConfigStatus: &tlsConfigStatus{}, + tlsConfigStatus: &tlsConfigStatus{}, + tlsConfigSettingsExt: tlsConfigSettingsExt{}, } err = json.NewDecoder(w.Body).Decode(res) @@ -223,21 +224,21 @@ func TestWebAPI_ValidateTLSSettings(t *testing.T) { testCases := []struct { name string wantErr string - setts tlsConfigSettingsExt + setts *tlsConfigSettingsExt }{{ name: "basic", wantErr: "", - setts: tlsConfigSettingsExt{}, + setts: &tlsConfigSettingsExt{}, }, { name: "disabled_all", wantErr: "plain DNS is required in case encryption protocols are disabled", - setts: tlsConfigSettingsExt{ + setts: &tlsConfigSettingsExt{ ServePlainDNS: aghalg.NBFalse, }, }, { name: "busy_https_port", wantErr: fmt.Sprintf("port %d for HTTPS is not available", busyTCPPort), - setts: tlsConfigSettingsExt{ + setts: &tlsConfigSettingsExt{ tlsConfigSettings: tlsConfigSettings{ Enabled: true, PortHTTPS: uint16(busyTCPPort), @@ -246,7 +247,7 @@ func TestWebAPI_ValidateTLSSettings(t *testing.T) { }, { name: "busy_dot_port", wantErr: fmt.Sprintf("port %d for DNS-over-TLS is not available", busyTCPPort), - setts: tlsConfigSettingsExt{ + setts: &tlsConfigSettingsExt{ tlsConfigSettings: tlsConfigSettings{ Enabled: true, PortDNSOverTLS: uint16(busyTCPPort), @@ -255,7 +256,7 @@ func TestWebAPI_ValidateTLSSettings(t *testing.T) { }, { name: "busy_doq_port", wantErr: fmt.Sprintf("port %d for DNS-over-QUIC is not available", busyUDPPort), - setts: tlsConfigSettingsExt{ + setts: &tlsConfigSettingsExt{ tlsConfigSettings: tlsConfigSettings{ Enabled: true, PortDNSOverQUIC: uint16(busyUDPPort), @@ -264,7 +265,7 @@ func TestWebAPI_ValidateTLSSettings(t *testing.T) { }, { name: "duplicate_port", wantErr: "validating tcp ports: duplicated values: [4433]", - setts: tlsConfigSettingsExt{ + setts: &tlsConfigSettingsExt{ tlsConfigSettings: tlsConfigSettings{ Enabled: true, PortHTTPS: 4433, From 02ab180f5aa031307b5d047e466e919cac823525 Mon Sep 17 00:00:00 2001 From: Ildar Kamalov Date: Fri, 28 Aug 2026 17:00:28 +0300 Subject: [PATCH 3/3] AGH-1 fix(client_v2): filter interval reset on mount and untranslated modal labels * fix(client_v2): prevent FiltersConfig from overwriting filter interval on mount The mount effect in FiltersConfig posted setFiltersConfig unconditionally, which could overwrite the backend interval (e.g. 1 hour) with the default 24 hours before getFilteringStatus resolved. Only post when the enabled toggle actually changes. * fix(client_v2): translate filter update modal radio labels reactively The radio options were built once at module load, freezing labels in English after a language switch. Rebuild them in a memo so intl resolves per current locale. --------- Co-authored-by: Hoang Rio --- .../FilterLists/filter-update-modal.test.tsx | 102 ++++++++++++++++++ .../Settings/FiltersConfig.test.tsx | 68 ++++++++++++ .../FilterUpdateModal/FilterUpdateModal.tsx | 18 ++-- .../src/components/Settings/FiltersConfig.tsx | 16 ++- 4 files changed, 190 insertions(+), 14 deletions(-) create mode 100644 client_v2/src/__tests__/components/FilterLists/filter-update-modal.test.tsx create mode 100644 client_v2/src/__tests__/components/Settings/FiltersConfig.test.tsx diff --git a/client_v2/src/__tests__/components/FilterLists/filter-update-modal.test.tsx b/client_v2/src/__tests__/components/FilterLists/filter-update-modal.test.tsx new file mode 100644 index 00000000000..d777281f975 --- /dev/null +++ b/client_v2/src/__tests__/components/FilterLists/filter-update-modal.test.tsx @@ -0,0 +1,102 @@ +import { render, screen } from '@solidjs/testing-library'; +import userEvent from '@testing-library/user-event'; +import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest'; + +import { FilterUpdateModal } from 'panel/components/FilterLists/blocks/FilterUpdateModal'; +import { getFilteringStatus } from 'panel/stores/filtering'; +import { openModal, closeModal } from 'panel/stores/modals'; +import { MODAL_TYPE } from 'panel/helpers/constants'; +import intl from 'panel/common/intl'; + +const mocks = vi.hoisted(() => ({ + apiSetFiltersConfig: vi.fn(() => Promise.resolve(undefined)), + apiGetFilteringStatus: vi.fn(() => Promise.resolve({})), +})); + +vi.mock('panel/api/generated', () => ({ + filteringConfig: mocks.apiSetFiltersConfig, + filteringStatus: mocks.apiGetFilteringStatus, +})); + +vi.mock('panel/stores/toasts', () => ({ + addErrorToast: vi.fn(), + addSuccessToast: vi.fn(), + createUndoToast: vi.fn(), +})); + +const checkedIntervalIds = () => + (screen.getAllByRole('radio') as HTMLInputElement[]) + .filter((r) => r.checked) + .map((r) => r.id); + +const statusWithInterval = (interval: number) => ({ + enabled: true, + interval, + filters: [] as unknown[], + whitelist_filters: [] as unknown[], + clients_filters: [] as unknown[], + user_rules: [] as string[], +}); + +describe('FilterUpdateModal interval', () => { + beforeEach(async () => { + vi.clearAllMocks(); + closeModal(); + mocks.apiGetFilteringStatus.mockResolvedValue(statusWithInterval(24)); + await getFilteringStatus(); + }); + + afterEach(async () => { + await intl.changeLanguage('en'); + }); + + it('shows the disabled radio label translated', async () => { + await intl.changeLanguage('vi'); + openModal(MODAL_TYPE.FILTER_UPDATE); + render(() => ); + + expect(screen.getByText('Vô hiệu')).toBeInTheDocument(); + }); + + it('shows hourly radio selected when interval is 1 hour', async () => { + mocks.apiGetFilteringStatus.mockResolvedValue(statusWithInterval(1)); + await getFilteringStatus(); + + openModal(MODAL_TYPE.FILTER_UPDATE); + render(() => ); + + expect(checkedIntervalIds()).toEqual(['interval-1']); + }); + + it('submits the selected hourly interval', async () => { + openModal(MODAL_TYPE.FILTER_UPDATE); + const { container } = render(() => ); + + const hourly = container.querySelector('#interval-1') as HTMLInputElement; + await userEvent.click(hourly); + await userEvent.click(screen.getByRole('button', { name: 'Save' })); + + expect(mocks.apiSetFiltersConfig).toHaveBeenCalledWith({ + enabled: true, + interval: 1, + }); + }); + + it('keeps hourly selection when status resolves to 1 hour after modal opens', async () => { + let resolveStatus: (v: object) => void; + mocks.apiGetFilteringStatus.mockReturnValueOnce( + new Promise((resolve) => { + resolveStatus = resolve; + }) as Promise, + ); + + const statusPromise = getFilteringStatus(); + openModal(MODAL_TYPE.FILTER_UPDATE); + render(() => ); + + resolveStatus!(statusWithInterval(1)); + await statusPromise; + + expect(checkedIntervalIds()).toEqual(['interval-1']); + }); +}); diff --git a/client_v2/src/__tests__/components/Settings/FiltersConfig.test.tsx b/client_v2/src/__tests__/components/Settings/FiltersConfig.test.tsx new file mode 100644 index 00000000000..06de9fece97 --- /dev/null +++ b/client_v2/src/__tests__/components/Settings/FiltersConfig.test.tsx @@ -0,0 +1,68 @@ +import { render, screen, fireEvent } from '@solidjs/testing-library'; +import { describe, expect, it, vi, beforeEach } from 'vitest'; + +import { FiltersConfig } from 'panel/components/Settings/FiltersConfig'; +import { getFilteringStatus } from 'panel/stores/filtering'; + +const mocks = vi.hoisted(() => ({ + apiSetFiltersConfig: vi.fn(() => Promise.resolve(undefined)), + apiGetFilteringStatus: vi.fn(() => Promise.resolve({})), +})); + +vi.mock('panel/api/generated', () => ({ + filteringConfig: mocks.apiSetFiltersConfig, + filteringStatus: mocks.apiGetFilteringStatus, +})); + +vi.mock('panel/stores/toasts', () => ({ + addErrorToast: vi.fn(), + addSuccessToast: vi.fn(), + createUndoToast: vi.fn(), +})); + +describe('FiltersConfig', () => { + beforeEach(() => vi.clearAllMocks()); + + it('does not call setFiltersConfig on mount when values did not change', () => { + render(() => ( + + )); + + expect(mocks.apiSetFiltersConfig).not.toHaveBeenCalled(); + }); + + it('does not overwrite a 1-hour interval while the status is still loading', async () => { + mocks.apiGetFilteringStatus.mockResolvedValue({ + enabled: true, + interval: 1, + filters: [], + whitelist_filters: [], + clients_filters: [], + user_rules: [], + }); + + const statusPromise = getFilteringStatus(); + + render(() => ( + + )); + + await statusPromise; + + expect(mocks.apiSetFiltersConfig).not.toHaveBeenCalled(); + expect(mocks.apiGetFilteringStatus).toHaveBeenCalled(); + }); + + it('calls setFiltersConfig when the switch is toggled', async () => { + render(() => ( + + )); + + fireEvent.click(screen.getByRole('checkbox')); + + expect(mocks.apiSetFiltersConfig).toHaveBeenCalledWith({ + interval: 1, + enabled: false, + }); + }); +}); diff --git a/client_v2/src/components/FilterLists/blocks/FilterUpdateModal/FilterUpdateModal.tsx b/client_v2/src/components/FilterLists/blocks/FilterUpdateModal/FilterUpdateModal.tsx index be813ca37f1..2231420449b 100644 --- a/client_v2/src/components/FilterLists/blocks/FilterUpdateModal/FilterUpdateModal.tsx +++ b/client_v2/src/components/FilterLists/blocks/FilterUpdateModal/FilterUpdateModal.tsx @@ -36,15 +36,15 @@ const getIntervalTitle = (interval: number) => { } }; -const RADIO_OPTIONS = [ - { text: getIntervalTitle(FILTER_INTERVALS.DISABLE), value: FILTER_INTERVALS.DISABLE }, - { text: getIntervalTitle(FILTER_INTERVALS.HOURLY), value: FILTER_INTERVALS.HOURLY }, - { text: getIntervalTitle(FILTER_INTERVALS.DAILY), value: FILTER_INTERVALS.DAILY }, - { text: getIntervalTitle(FILTER_INTERVALS.WEEKLY), value: FILTER_INTERVALS.WEEKLY }, - { text: getIntervalTitle(FILTER_INTERVALS.CUSTOM), value: FILTER_INTERVALS.CUSTOM }, -]; - export const FilterUpdateModal = () => { + const getRadioOptions = createMemo(() => [ + { text: getIntervalTitle(FILTER_INTERVALS.DISABLE), value: FILTER_INTERVALS.DISABLE }, + { text: getIntervalTitle(FILTER_INTERVALS.HOURLY), value: FILTER_INTERVALS.HOURLY }, + { text: getIntervalTitle(FILTER_INTERVALS.DAILY), value: FILTER_INTERVALS.DAILY }, + { text: getIntervalTitle(FILTER_INTERVALS.WEEKLY), value: FILTER_INTERVALS.WEEKLY }, + { text: getIntervalTitle(FILTER_INTERVALS.CUSTOM), value: FILTER_INTERVALS.CUSTOM }, + ]); + const PREDEFINED_INTERVALS: number[] = [ FILTER_INTERVALS.DISABLE, FILTER_INTERVALS.HOURLY, @@ -119,7 +119,7 @@ export const FilterUpdateModal = () => { setIntervalValue(value)} disabled={filteringState.processingSetConfig} /> diff --git a/client_v2/src/components/Settings/FiltersConfig.tsx b/client_v2/src/components/Settings/FiltersConfig.tsx index e83faf5ad41..a50f84e2b16 100644 --- a/client_v2/src/components/Settings/FiltersConfig.tsx +++ b/client_v2/src/components/Settings/FiltersConfig.tsx @@ -1,4 +1,4 @@ -import { createSignal, createEffect, untrack } from 'solid-js'; +import { createSignal, createEffect, on, untrack } from 'solid-js'; import intl from 'panel/common/intl'; import theme from 'panel/lib/theme'; @@ -20,10 +20,16 @@ type Props = { export const FiltersConfig = (props: Props) => { const [enabled, setEnabled] = createSignal(props.initialValues.enabled); - createEffect(() => { - const initial = untrack(() => props.initialValues); - setFiltersConfig({ ...initial, enabled: enabled() }); - }); + createEffect( + on( + enabled, + () => { + const initial = untrack(() => props.initialValues); + setFiltersConfig({ ...initial, enabled: enabled() }); + }, + { defer: true }, + ), + ); return (