) => (
{options.map((o) => (
-
-
diff --git a/client_v2/src/login/Login/Login.css b/client_v2/src/login/Login/Login.css
index 79d0203e..3cc3ad60 100644
--- a/client_v2/src/login/Login/Login.css
+++ b/client_v2/src/login/Login/Login.css
@@ -38,6 +38,12 @@
letter-spacing: 0;
}
+.login__link {
+ margin-top: 16px;
+ border: 0;
+ background: transparent;
+}
+
@media screen and (min-width: 992px) {
.login__message {
position: absolute;
diff --git a/client_v2/src/login/Login/index.tsx b/client_v2/src/login/Login/index.tsx
index 846656c7..2461e32d 100644
--- a/client_v2/src/login/Login/index.tsx
+++ b/client_v2/src/login/Login/index.tsx
@@ -1,14 +1,16 @@
import React, { useState } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { Trans } from 'react-i18next';
+import cn from 'clsx';
+
+import theme from 'panel/lib/theme';
import * as actionCreators from '../../actions/login';
-
import Toasts from '../../components/Toasts';
import Form, { LoginFormValues } from './Form';
+import { LoginState } from '../../initialState';
import './Login.css';
-import { LoginState } from '../../initialState';
export const Login = () => {
const dispatch = useDispatch();
@@ -29,7 +31,7 @@ export const Login = () => {
-
+
forgot_password
@@ -41,7 +43,9 @@ export const Login = () => {
href="https://github.com/AdguardTeam/AdGuardHome/wiki/Configuration#password-reset"
key="0"
target="_blank"
- rel="noopener noreferrer">
+ rel="noopener noreferrer"
+ className={theme.link.link}
+ >
link
,
]}>
diff --git a/client_v2/src/reducers/dashboard.ts b/client_v2/src/reducers/dashboard.ts
index 91f2b262..072f0f68 100644
--- a/client_v2/src/reducers/dashboard.ts
+++ b/client_v2/src/reducers/dashboard.ts
@@ -163,6 +163,10 @@ const dashboard = handleActions(
theme: payload.theme,
processingProfile: false,
}),
+ [actions.changeLanguageSuccess.toString()]: (state, { payload }: any) => ({
+ ...state,
+ language: payload.language,
+ }),
[actions.changeThemeSuccess.toString()]: (state, { payload }: any) => ({
...state,
theme: payload.theme,
@@ -188,6 +192,7 @@ const dashboard = handleActions(
supportedTags: [],
name: '',
theme: undefined,
+ language: undefined,
checkUpdateFlag: false,
},
);
diff --git a/client_v2/src/stories/controls/Switch.stories.tsx b/client_v2/src/stories/controls/Switch.stories.tsx
index 1ca206a1..6fc473e6 100644
--- a/client_v2/src/stories/controls/Switch.stories.tsx
+++ b/client_v2/src/stories/controls/Switch.stories.tsx
@@ -49,7 +49,7 @@ const meta: Meta = {
control: 'text',
description: 'Additional CSS class for the wrapper element',
},
- handleChange: { action: 'changed' },
+ onChange: { action: 'changed' },
},
};
diff --git a/client_v2/tests/e2e/control-panel.spec.ts b/client_v2/tests/e2e/control-panel.spec.ts
index 9a4cace1..81dede95 100644
--- a/client_v2/tests/e2e/control-panel.spec.ts
+++ b/client_v2/tests/e2e/control-panel.spec.ts
@@ -1,34 +1,15 @@
-import { test, expect } from '@playwright/test';
+import { test } from '@playwright/test';
import { ADMIN_USERNAME, ADMIN_PASSWORD } from '../constants';
test.describe('Control Panel', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/login.html');
- await page.getByTestId('username').click();
- await page.getByTestId('username').fill(ADMIN_USERNAME);
- await page.getByTestId('password').click();
- await page.getByTestId('password').fill(ADMIN_PASSWORD);
+ await page.locator('#username').click();
+ await page.locator('#username').fill(ADMIN_USERNAME);
+ await page.locator('#password').click();
+ await page.locator('#password').fill(ADMIN_PASSWORD);
await page.keyboard.press('Tab');
- await page.getByTestId('sign_in').click();
+ await page.locator('#sign_in').click();
await page.waitForURL((url) => !url.href.endsWith('/login.html'));
});
-
- test('should sign out successfully', async ({ page }) => {
- await page.getByTestId('sign_out').click();
-
- await page.waitForURL((url) => url.href.endsWith('/login.html'));
-
- await expect(page.getByTestId('sign_in')).toBeVisible();
- });
-
- test('should change theme to dark and then light', async ({ page }) => {
- await page.getByTestId('theme_dark').click();
-
- await expect(page.locator('body[data-theme="dark"]')).toBeVisible();
-
-
- await page.getByTestId('theme_light').click();
-
- await expect(page.locator('body:not([data-theme="dark"])')).toBeVisible();
- });
});
diff --git a/client_v2/tests/e2e/dhcp.spec.ts b/client_v2/tests/e2e/dhcp.spec.ts
deleted file mode 100644
index 1c5465e2..00000000
--- a/client_v2/tests/e2e/dhcp.spec.ts
+++ /dev/null
@@ -1,64 +0,0 @@
-import { test, expect } from '@playwright/test';
-import { ADMIN_PASSWORD, ADMIN_USERNAME } from '../constants';
-import { getDHCPConfig } from '../helpers/network';
-
-const dhcpConfig = getDHCPConfig();
-const INTERFACE_NAME = dhcpConfig.interfaceName;
-const RANGE_START = dhcpConfig.rangeStart;
-const RANGE_END = dhcpConfig.rangeEnd;
-const SUBNET_MASK = dhcpConfig.subnetMask;
-const LEASE_TIME = '86400';
-
-test.describe('DHCP Configuration', () => {
- test.beforeEach(async ({ page }) => {
- await page.goto('/login.html');
- await page.getByTestId('username').click();
- await page.getByTestId('username').fill(ADMIN_USERNAME);
- await page.getByTestId('password').click();
- await page.getByTestId('password').fill(ADMIN_PASSWORD);
- await page.keyboard.press('Tab');
- await page.getByTestId('sign_in').click();
- await page.waitForURL((url) => !url.href.endsWith('/login.html'));
- await page.goto(`/#dhcp`);
- });
-
- test('should select the correct DHCP interface', async ({ page }) => {
- await page.getByTestId('interface_name').selectOption(INTERFACE_NAME);
- expect(await page.locator('select[name="interface_name"]').inputValue()).toBe(INTERFACE_NAME);
- });
-
- test('should configure DHCP IPv4 settings correctly', async ({ page }) => {
- await page.getByTestId('interface_name').selectOption(INTERFACE_NAME);
- await page.getByTestId('v4_gateway_ip').click();
- await page.getByTestId('v4_gateway_ip').fill('192.168.1.99');
- await page.getByTestId('v4_subnet_mask').click();
- await page.getByTestId('v4_subnet_mask').fill(SUBNET_MASK);
- await page.getByTestId('v4_range_start').click();
- await page.getByTestId('v4_range_start').fill(RANGE_START);
- await page.getByTestId('v4_range_end').click();
- await page.getByTestId('v4_range_end').fill(RANGE_END);
- await page.getByTestId('v4_lease_duration').click();
- await page.getByTestId('v4_lease_duration').fill(LEASE_TIME);
- await page.getByTestId('v4_save').click();
- });
-
- test('should show error for invalid DHCP IPv4 range', async ({ page }) => {
- await page.getByTestId('interface_name').selectOption(INTERFACE_NAME);
- await page.getByTestId('v4_range_start').click();
- await page.getByTestId('v4_range_start').fill(RANGE_END);
- await page.getByTestId('v4_range_end').click();
- await page.getByTestId('v4_range_end').fill(RANGE_START);
- await page.keyboard.press('Tab');
-
- expect(await page.getByText('Must be greater than range').isVisible()).toBe(true);
- });
-
- test('should show error for invalid DHCP IPv4 address', async ({ page }) => {
- await page.getByTestId('interface_name').selectOption(INTERFACE_NAME);
- await page.getByTestId('v4_gateway_ip').click();
- await page.getByTestId('v4_gateway_ip').fill('192.168.1.200s');
- await page.keyboard.press('Tab');
-
- expect(await page.getByText('Invalid IPv4 address').isVisible()).toBe(true);
- });
-});
diff --git a/client_v2/tests/e2e/dns-settings.spec.ts b/client_v2/tests/e2e/dns-settings.spec.ts
deleted file mode 100644
index e1094bd2..00000000
--- a/client_v2/tests/e2e/dns-settings.spec.ts
+++ /dev/null
@@ -1,52 +0,0 @@
-import { test, expect, type Page } from '@playwright/test';
-import { ADMIN_USERNAME, ADMIN_PASSWORD } from '../constants';
-
-test.describe('DNS Settings', () => {
- test.beforeEach(async ({ page }) => {
- // Login before each test
- await page.goto('/login.html');
- await page.getByTestId('username').click();
- await page.getByTestId('username').fill(ADMIN_USERNAME);
- await page.getByTestId('password').click();
- await page.getByTestId('password').fill(ADMIN_PASSWORD);
- await page.keyboard.press('Tab');
- await page.getByTestId('sign_in').click();
- await page.waitForURL((url) => !url.href.endsWith('/login.html'));
- });
-
- const runDNSSettingsTest = async (page: Page, address: string) => {
- await page.goto('/#dns');
-
- const currentDns = await page.getByTestId('upstream_dns').inputValue();
-
- await page.getByTestId('upstream_dns').fill(address);
- await page.getByTestId('dns_upstream_test').click();
-
- await page.waitForTimeout(2000);
-
- await expect(page.getByTestId('upstream_dns')).toHaveValue(address);
-
- await page.getByTestId('upstream_dns').fill(currentDns);
- await page.getByTestId('dns_upstream_save').click({ force: true });
- };
-
- test('test for Default DNS', async ({ page }) => {
- await runDNSSettingsTest(page, 'https://dns10.quad9.net/dns-query');
- });
-
- test('test for Plain DNS', async ({ page }) => {
- await runDNSSettingsTest(page, '94.140.14.140');
- });
-
- test('test for DNS-over-HTTPS', async ({ page }) => {
- await runDNSSettingsTest(page, 'https://unfiltered.adguard-dns.com/dns-query');
- });
-
- test('test for DNS-over-TLS', async ({ page }) => {
- await runDNSSettingsTest(page, 'tls://unfiltered.adguard-dns.com');
- });
-
- test('test for DNS-over-QUIC', async ({ page }) => {
- await runDNSSettingsTest(page, 'quic://unfiltered.adguard-dns.com');
- });
-});
diff --git a/client_v2/tests/e2e/filtering.spec.ts b/client_v2/tests/e2e/filtering.spec.ts
deleted file mode 100644
index 49bec97a..00000000
--- a/client_v2/tests/e2e/filtering.spec.ts
+++ /dev/null
@@ -1,73 +0,0 @@
-import { test, expect, type Page } from '@playwright/test';
-import { execSync } from 'child_process';
-import { ADMIN_USERNAME, ADMIN_PASSWORD } from '../constants';
-
-test.describe('Filtering', () => {
- test.beforeEach(async ({ page }) => {
- // Login before each test
- await page.goto('/login.html');
- await page.getByTestId('username').click();
- await page.getByTestId('username').fill(ADMIN_USERNAME);
- await page.getByTestId('password').click();
- await page.getByTestId('password').fill(ADMIN_PASSWORD);
- await page.keyboard.press('Tab');
- await page.getByTestId('sign_in').click();
- await page.waitForURL((url) => !url.href.endsWith('/login.html'));
- });
-
- const runTerminalCommand = (command: string) => {
- try {
- console.info(`Executing command: ${command}`);
-
- const output = execSync(command, { encoding: 'utf-8', stdio: 'pipe' }).trim();
-
- console.info('Command executed successfully.');
- console.debug(`Command output:\n${output}`);
-
- return output;
- } catch (error: any) {
- console.error(`Command execution failed with error:\n${error.message}`);
- throw new Error(`Failed to execute command: ${command}\nError: ${error.message}`);
- }
- }
-
- const runCustomRuleTest = async (page: Page, domain_to_block: string) => {
- await page.goto('/#custom_rules');
-
- await page.getByTestId('custom_rule_textarea').fill(domain_to_block);
- await page.getByTestId('apply_custom_rule').click();
-
- const nslookupBlockedResult = await runTerminalCommand(`nslookup ${domain_to_block} 127.0.0.1`).toString();
-
- console.info(`nslookup blocked CNAME result: '${nslookupBlockedResult}'`);
-
- const currentRules = await page.getByTestId('custom_rule_textarea').inputValue();
- console.debug(`Current rules before removal:\n${currentRules}`);
-
- if (currentRules.includes(domain_to_block)) {
- const updatedRules = currentRules
- .split('\n')
- .filter((line) => line.trim() !== domain_to_block.trim())
- .join('\n');
-
- await page.getByTestId('custom_rule_textarea').fill(updatedRules);
- console.info(`Rule '${domain_to_block}' removed successfully.`);
-
- console.info('Applying the updated filtering rules after removal.');
- await page.getByTestId('apply_custom_rule').click();
-
- await page.waitForLoadState('domcontentloaded');
-
- console.info(`Filtering rules successfully updated after removing '${domain_to_block}'.`);
- } else {
- console.warn(`Rule '${domain_to_block}' not found. No changes were made.`);
- }
-
- const nslookupUnblockedResult = await runTerminalCommand(`nslookup ${domain_to_block} 127.0.0.1`).toString();
- console.info(`nslookup unblocked CNAME result: '${nslookupUnblockedResult}'`);
- };
-
- test('Test blocking rule for apple.com', async ({ page }) => {
- await runCustomRuleTest(page, 'apple.com');
- });
-});
diff --git a/client_v2/tests/e2e/general-settings.spec.ts b/client_v2/tests/e2e/general-settings.spec.ts
index ff23a687..348fb8d8 100644
--- a/client_v2/tests/e2e/general-settings.spec.ts
+++ b/client_v2/tests/e2e/general-settings.spec.ts
@@ -5,19 +5,19 @@ import { ADMIN_USERNAME, ADMIN_PASSWORD } from '../constants';
test.describe('General Settings', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/login.html');
- await page.getByTestId('username').click();
- await page.getByTestId('username').fill(ADMIN_USERNAME);
- await page.getByTestId('password').click();
- await page.getByTestId('password').fill(ADMIN_PASSWORD);
+ await page.locator('#username').click();
+ await page.locator('#username').fill(ADMIN_USERNAME);
+ await page.locator('#password').click();
+ await page.locator('#password').fill(ADMIN_PASSWORD);
await page.keyboard.press('Tab');
- await page.getByTestId('sign_in').click();
+ await page.locator('#sign_in').click();
await page.waitForURL((url) => !url.href.endsWith('/login.html'));
});
test('should toggle browsing security feature and verify DNS changes', async ({ page }) => {
await page.goto('/#settings');
- const browsingSecurity = await page.getByTestId('safebrowsing');
+ const browsingSecurity = await page.locator('#safebrowsing');
const browsingSecurityLabel = await browsingSecurity.locator('xpath=following-sibling::*[1]');
const initialState = await browsingSecurity.isChecked();
@@ -45,7 +45,7 @@ test.describe('General Settings', () => {
test('should toggle parental control feature and verify DNS changes', async ({ page }) => {
await page.goto('/#settings');
- const parentalControl = page.getByTestId('parental');
+ const parentalControl = page.locator('#parental');
const parentalControlLabel = await parentalControl.locator('xpath=following-sibling::*[1]');
const initialState = await parentalControl.isChecked();
@@ -73,7 +73,7 @@ test.describe('General Settings', () => {
test('should toggle safe search feature', async ({ page }) => {
await page.goto('/#settings');
- const safeSearch = page.getByTestId('safesearch');
+ const safeSearch = page.locator('#safesearch');
const safeSearchLabel = await safeSearch.locator('xpath=following-sibling::*[1]');
const initialState = await safeSearch.isChecked();
diff --git a/client_v2/tests/e2e/globalSetup.ts b/client_v2/tests/e2e/globalSetup.ts
index 3d56b53e..14e55e0c 100644
--- a/client_v2/tests/e2e/globalSetup.ts
+++ b/client_v2/tests/e2e/globalSetup.ts
@@ -10,16 +10,16 @@ async function globalSetup(config: FullConfig) {
try {
await page.goto('/');
- await page.getByTestId('install_get_started').click();
- await page.getByTestId('install_web_port').fill(PORT.toString());
- await page.getByTestId('install_next').click();
- await page.getByTestId('install_username').fill(ADMIN_USERNAME);
- await page.getByTestId('install_password').fill(ADMIN_PASSWORD);
- await page.getByTestId('install_confirm_password').click();
- await page.getByTestId('install_confirm_password').fill(ADMIN_PASSWORD);
- await page.getByTestId('install_next').click();
- await page.getByTestId('install_next').click();
- await page.getByTestId('install_open_dashboard').click();
+ await page.locator('#install_get_started').click();
+ await page.locator('#install_web_port').fill(PORT.toString());
+ await page.locator('#install_next').click();
+ await page.locator('#install_username').fill(ADMIN_USERNAME);
+ await page.locator('#install_password').fill(ADMIN_PASSWORD);
+ await page.locator('#install_confirm_password').click();
+ await page.locator('#install_confirm_password').fill(ADMIN_PASSWORD);
+ await page.locator('#install_next').click();
+ await page.locator('#install_next').click();
+ await page.locator('#install_open_dashboard').click();
await page.waitForURL((url) => !url.href.endsWith('/install.html'));
} catch (error) {
console.error('Error during global setup:', error);
diff --git a/client_v2/tests/e2e/login.spec.ts b/client_v2/tests/e2e/login.spec.ts
index 776e9145..e17db69f 100644
--- a/client_v2/tests/e2e/login.spec.ts
+++ b/client_v2/tests/e2e/login.spec.ts
@@ -5,12 +5,12 @@ import { ADMIN_PASSWORD, ADMIN_USERNAME } from '../constants';
test.describe('Login', () => {
test('should successfully log in with valid credentials', async ({ page }) => {
await page.goto('/login.html');
- await page.getByTestId('username').click();
- await page.getByTestId('username').fill(ADMIN_USERNAME);
- await page.getByTestId('password').click();
- await page.getByTestId('password').fill(ADMIN_PASSWORD);
+ await page.locator('#username').click();
+ await page.locator('#username').fill(ADMIN_USERNAME);
+ await page.locator('#password').click();
+ await page.locator('#password').fill(ADMIN_PASSWORD);
await page.keyboard.press('Tab');
- await page.getByTestId('sign_in').click();
+ await page.locator('#sign_in').click();
await page.waitForURL((url) => !url.href.endsWith('/login.html'));
});
});
diff --git a/client_v2/tests/e2e/querylog.spec.ts b/client_v2/tests/e2e/querylog.spec.ts
deleted file mode 100644
index 37b2591d..00000000
--- a/client_v2/tests/e2e/querylog.spec.ts
+++ /dev/null
@@ -1,124 +0,0 @@
-import { test, expect } from '@playwright/test';
-import { ADMIN_USERNAME, ADMIN_PASSWORD } from '../constants';
-
-test.describe('QueryLog', () => {
- test.beforeEach(async ({ page }) => {
- await page.goto('/login.html');
- await page.getByTestId('username').click();
- await page.getByTestId('username').fill(ADMIN_USERNAME);
- await page.getByTestId('password').click();
- await page.getByTestId('password').fill(ADMIN_PASSWORD);
- await page.keyboard.press('Tab');
- await page.getByTestId('sign_in').click();
- await page.waitForURL((url) => !url.href.endsWith('/login.html'));
- });
-
- test('Search of queryLog should work correctly', async ({ page }) => {
- await page.route('/control/querylog', async (route) => {
- await route.fulfill({
- status: 200,
- contentType: 'application/json',
- body: JSON.stringify(
- {
- "data": [
- {
- "answer": [
- {
- "type": "A",
- "value": "77.88.44.242",
- "ttl": 294
- },
- {
- "type": "A",
- "value": "5.255.255.242",
- "ttl": 294
- },
- {
- "type": "A",
- "value": "77.88.55.242",
- "ttl": 294
- }
- ],
- "answer_dnssec": false,
- "cached": false,
- "client": "127.0.0.1",
- "client_info": {
- "whois": {},
- "name": "localhost",
- "disallowed_rule": "127.0.0.1",
- "disallowed": false
- },
- "client_proto": "",
- "elapsedMs": "78.163167",
- "question": {
- "class": "IN",
- "name": "ya.ru",
- "type": "A"
- },
- "reason": "NotFilteredNotFound",
- "rules": [],
- "status": "NOERROR",
- "time": "2024-07-17T16:02:37.500662+02:00",
- "upstream": "https://dns10.quad9.net:443/dns-query"
- },
- {
- "answer": [
- {
- "type": "A",
- "value": "77.88.55.242",
- "ttl": 351
- },
- {
- "type": "A",
- "value": "77.88.44.242",
- "ttl": 351
- },
- {
- "type": "A",
- "value": "5.255.255.242",
- "ttl": 351
- }
- ],
- "answer_dnssec": false,
- "cached": false,
- "client": "127.0.0.1",
- "client_info": {
- "whois": {},
- "name": "localhost",
- "disallowed_rule": "127.0.0.1",
- "disallowed": false
- },
- "client_proto": "",
- "elapsedMs": "5051.070708",
- "question": {
- "class": "IN",
- "name": "ya.ru",
- "type": "A"
- },
- "reason": "NotFilteredNotFound",
- "rules": [],
- "status": "NOERROR",
- "time": "2024-07-17T16:02:37.4983+02:00",
- "upstream": "https://dns10.quad9.net:443/dns-query"
- }
- ],
- "oldest": "2024-07-17T16:02:37.4983+02:00"
- }
- ),
- });
- });
-
- await page.goto('/#logs');
-
- await page.getByTestId('querylog_search').fill('127.0.0.1');
-
- const [request] = await Promise.all([
- page.waitForRequest((req) => req.url().includes('/control/querylog')),
- ]);
-
- if (request) {
- expect(request.url()).toContain('search=127.0.0.1');
- expect(await page.getByTestId('querylog_cell').first().isVisible()).toBe(true);
- }
- });
-});
diff --git a/client_v2/tsconfig.json b/client_v2/tsconfig.json
index 99d28817..64fafbd9 100644
--- a/client_v2/tsconfig.json
+++ b/client_v2/tsconfig.json
@@ -4,6 +4,7 @@
"jsx": "react",
"module": "ESNext",
"moduleResolution": "bundler",
+ "baseUrl": ".",
"allowArbitraryExtensions": true,
"resolveJsonModule": true,
"allowJs": true,
diff --git a/client_v2/webpack.common.js b/client_v2/webpack.common.js
index c5857175..0e3b7a7a 100644
--- a/client_v2/webpack.common.js
+++ b/client_v2/webpack.common.js
@@ -33,7 +33,7 @@ function getAliasesFromTsconfig(tsconfigPath, resourcesPath) {
Object.entries(tsConfig.compilerOptions.paths).forEach(([alias, targetArr]) => {
if (alias.endsWith('/*')) {
const aliasName = alias.replace('/*', '');
- let target = targetArr[0].replace('/*', '');
+ const target = targetArr[0].replace('/*', '');
aliases[aliasName] = path.resolve(resourcesPath, target);
}
});
diff --git a/client_v2/webpack.dev.js b/client_v2/webpack.dev.js
index aa9b4c5c..912f3b60 100644
--- a/client_v2/webpack.dev.js
+++ b/client_v2/webpack.dev.js
@@ -1,6 +1,7 @@
import { merge } from 'webpack-merge';
import yaml from 'js-yaml';
import fs from 'fs';
+
import { BASE_URL } from './constants.js';
import common from './webpack.common.js';
diff --git a/client_v2/webpack.prod.js b/client_v2/webpack.prod.js
index 0d678616..49b93f09 100644
--- a/client_v2/webpack.prod.js
+++ b/client_v2/webpack.prod.js
@@ -1,4 +1,5 @@
import { merge } from 'webpack-merge';
+/* eslint-disable-next-line import/extensions */
import common from './webpack.common.js';
export default merge(common, {