diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b2efe3e09..46e9a8990 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,6 +1,6 @@ # Project Info -First of all, thank you everyone who made pull requests for Uptime Kuma, I never thought GitHub Community can be that nice! And also because of this, I also never thought other people actually read my code and edit my code. It is not structed and commented so well, lol. Sorry about that. +First of all, thank you everyone who made pull requests for Uptime Kuma, I never thought GitHub Community can be that nice! And also because of this, I also never thought other people actually read my code and edit my code. It is not structured and commented so well, lol. Sorry about that. The project was created with vite.js (vue3). Then I created a sub-directory called "server" for server part. Both frontend and backend share the same package.json. @@ -27,7 +27,7 @@ The frontend code build into "dist" directory. The server (express.js) exposes t ## Can I create a pull request for Uptime Kuma? -Generally, if the pull request is working fine and it do not affect any existing logic, workflow and perfomance, I will merge into the master branch once it is tested. +Generally, if the pull request is working fine and it do not affect any existing logic, workflow and performance, I will merge into the master branch once it is tested. If you are not sure whether I will accept your pull request, feel free to create an empty pull request draft first. @@ -66,13 +66,13 @@ I do not have such knowledge to test it. #### ⚠ Low Priority - Harsh Mode -Some pull requests are required to modifiy the core. To be honest, I do not want anyone to try to do that, because it would spend a lot of your time. I will review your pull request harshly. Also you may need to write a lot of unit tests to ensure that there is no breaking change. +Some pull requests are required to modify the core. To be honest, I do not want anyone to try to do that, because it would spend a lot of your time. I will review your pull request harshly. Also you may need to write a lot of unit tests to ensure that there is no breaking change. - Touch large parts of code of any very important features - Touch monitoring logic - Drop a table or drop a column for any reason - Touch the entry point of Docker or Node.js -- Modifiy auth +- Modify auth #### *️⃣ Low Priority @@ -114,7 +114,7 @@ I personally do not like something need to learn so much and need to config so m - Node.js >= 14 - Git -- IDE that supports ESLint and EditorConfig (I am using Intellji Idea) +- IDE that supports ESLint and EditorConfig (I am using IntelliJ IDEA) - A SQLite tool (SQLite Expert Personal is suggested) ## Install dependencies @@ -141,7 +141,7 @@ express.js is just used for serving the frontend built files (index.html, .js an - model/ (Object model, auto mapping to the database table name) - modules/ (Modified 3rd-party modules) -- notification-providers/ (indivdual notification logic) +- notification-providers/ (individual notification logic) - routers/ (Express Routers) - scoket-handler (Socket.io Handlers) - server.js (Server main logic) diff --git a/config/jest-debug-env.js b/config/jest-debug-env.js new file mode 100644 index 000000000..74f6d7835 --- /dev/null +++ b/config/jest-debug-env.js @@ -0,0 +1,33 @@ +const PuppeteerEnvironment = require("jest-environment-puppeteer"); +const util = require("util"); + +class DebugEnv extends PuppeteerEnvironment { + async handleTestEvent(event, state) { + const ignoredEvents = [ + "setup", + "add_hook", + "start_describe_definition", + "add_test", + "finish_describe_definition", + "run_start", + "run_describe_start", + "test_start", + "hook_start", + "hook_success", + "test_fn_start", + "test_fn_success", + "test_done", + "run_describe_finish", + "run_finish", + "teardown", + "test_fn_failure", + ]; + if (!ignoredEvents.includes(event.name)) { + console.log( + new Date().toString() + ` Unhandled event [${event.name}] ` + util.inspect(event) + ); + } + } +} + +module.exports = DebugEnv; diff --git a/config/jest-puppeteer.config.js b/config/jest-puppeteer.config.js index 07830ca3c..dc4f7b344 100644 --- a/config/jest-puppeteer.config.js +++ b/config/jest-puppeteer.config.js @@ -1,6 +1,20 @@ module.exports = { "launch": { + "dumpio": true, + "slowMo": 500, "headless": process.env.HEADLESS_TEST || false, "userDataDir": "./data/test-chrome-profile", + args: [ + "--disable-setuid-sandbox", + "--disable-gpu", + "--disable-dev-shm-usage", + "--no-default-browser-check", + "--no-experiments", + "--no-first-run", + "--no-pings", + "--no-sandbox", + "--no-zygote", + "--single-process", + ], } }; diff --git a/config/jest.config.js b/config/jest.config.js index 4baaa0fb6..2d3f585ef 100644 --- a/config/jest.config.js +++ b/config/jest.config.js @@ -5,6 +5,7 @@ module.exports = { "__DEV__": true }, "testRegex": "./test/e2e.spec.js", + "testEnvironment": "./config/jest-debug-env.js", "rootDir": "..", "testTimeout": 30000, }; diff --git a/package.json b/package.json index 79f01b558..f516f4f6b 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "build": "vite build --config ./config/vite.config.js", "test": "node test/prepare-test-server.js && node server/server.js --port=3002 --data-dir=./data/test/ --test", "test-with-build": "npm run build && npm test", - "jest": "node test/prepare-jest.js && npm run jest-frontend && npm run jest-backend && jest --config=./config/jest.config.js", + "jest": "node test/prepare-jest.js && npm run jest-frontend && npm run jest-backend && jest --runInBand --config=./config/jest.config.js", "jest-frontend": "cross-env TEST_FRONTEND=1 jest --config=./config/jest-frontend.config.js", "jest-backend": "cross-env TEST_BACKEND=1 jest --config=./config/jest-backend.config.js", "tsc": "tsc", diff --git a/server/database.js b/server/database.js index 41d91e858..fbef40bb1 100644 --- a/server/database.js +++ b/server/database.js @@ -79,7 +79,7 @@ class Database { console.log(`Data Dir: ${Database.dataDir}`); } - static async connect() { + static async connect(testMode = false) { const acquireConnectionTimeout = 120 * 1000; const Dialect = require("knex/lib/dialects/sqlite3/index.js"); @@ -112,8 +112,13 @@ class Database { await R.autoloadModels("./server/model"); await R.exec("PRAGMA foreign_keys = ON"); - // Change to WAL - await R.exec("PRAGMA journal_mode = WAL"); + if (testMode) { + // Change to MEMORY + await R.exec("PRAGMA journal_mode = MEMORY"); + } else { + // Change to WAL + await R.exec("PRAGMA journal_mode = WAL"); + } await R.exec("PRAGMA cache_size = -12000"); await R.exec("PRAGMA auto_vacuum = FULL"); diff --git a/server/server.js b/server/server.js index 78106515a..07249fb44 100644 --- a/server/server.js +++ b/server/server.js @@ -177,7 +177,7 @@ exports.entryPage = "dashboard"; (async () => { Database.init(args); - await initDatabase(); + await initDatabase(testMode); exports.entryPage = await setting("entryPage"); @@ -539,8 +539,8 @@ exports.entryPage = "dashboard"; await updateMonitorNotification(bean.id, notificationIDList); - await startMonitor(socket.userID, bean.id); await sendMonitorList(socket); + await startMonitor(socket.userID, bean.id); callback({ ok: true, @@ -1415,14 +1415,14 @@ async function getMonitorJSONList(userID) { return result; } -async function initDatabase() { +async function initDatabase(testMode = false) { if (! fs.existsSync(Database.path)) { console.log("Copying Database"); fs.copyFileSync(Database.templatePath, Database.path); } console.log("Connecting to the Database"); - await Database.connect(); + await Database.connect(testMode); console.log("Connected"); // Patch the database diff --git a/src/assets/vars.scss b/src/assets/vars.scss index 2f4369832..91ab917e5 100644 --- a/src/assets/vars.scss +++ b/src/assets/vars.scss @@ -12,6 +12,7 @@ $dark-font-color2: #020b05; $dark-bg: #0d1117; $dark-bg2: #070a10; $dark-border-color: #1d2634; +$dark-header-bg: #161b22; $easing-in: cubic-bezier(0.54, 0.78, 0.55, 0.97); $easing-out: cubic-bezier(0.25, 0.46, 0.45, 0.94); diff --git a/src/components/MonitorList.vue b/src/components/MonitorList.vue index bd771f8f0..ef51e89cd 100644 --- a/src/components/MonitorList.vue +++ b/src/components/MonitorList.vue @@ -137,7 +137,7 @@ export default { justify-content: space-between; .dark & { - background-color: #161b22; + background-color: $dark-header-bg; border-bottom: 0; } } diff --git a/src/components/settings/About.vue b/src/components/settings/About.vue new file mode 100644 index 000000000..baa72f39a --- /dev/null +++ b/src/components/settings/About.vue @@ -0,0 +1,25 @@ + + + + + diff --git a/src/components/settings/Appearance.vue b/src/components/settings/Appearance.vue new file mode 100644 index 000000000..e0a3d6430 --- /dev/null +++ b/src/components/settings/Appearance.vue @@ -0,0 +1,149 @@ + + + + + diff --git a/src/components/settings/Backup.vue b/src/components/settings/Backup.vue new file mode 100644 index 000000000..6ac28d468 --- /dev/null +++ b/src/components/settings/Backup.vue @@ -0,0 +1,213 @@ + + + + + diff --git a/src/components/settings/General.vue b/src/components/settings/General.vue new file mode 100644 index 000000000..a1b42d85f --- /dev/null +++ b/src/components/settings/General.vue @@ -0,0 +1,191 @@ + + + + + diff --git a/src/components/settings/MonitorHistory.vue b/src/components/settings/MonitorHistory.vue new file mode 100644 index 000000000..9b5b8bd78 --- /dev/null +++ b/src/components/settings/MonitorHistory.vue @@ -0,0 +1,133 @@ + + + + + diff --git a/src/components/settings/Notifications.vue b/src/components/settings/Notifications.vue new file mode 100644 index 000000000..b2cbcf48a --- /dev/null +++ b/src/components/settings/Notifications.vue @@ -0,0 +1,46 @@ + + + + + diff --git a/src/components/settings/Security.vue b/src/components/settings/Security.vue new file mode 100644 index 000000000..4ef6b3d9e --- /dev/null +++ b/src/components/settings/Security.vue @@ -0,0 +1,323 @@ + + + + + diff --git a/src/languages/README.md b/src/languages/README.md index 6ba7d95eb..52b70fa84 100644 --- a/src/languages/README.md +++ b/src/languages/README.md @@ -4,7 +4,7 @@ 2. Create a language file (e.g. `zh-TW.js`). The filename must be ISO language code: http://www.lingoes.net/en/translator/langcode.htm 3. Run `npm run update-language-files`. You can also use this command to check if there are new strings to translate for your language. 4. Your language file should be filled in. You can translate now. -5. Translate `src/pages/Settings.vue` (search for a `Confirm` component with `rel="confirmDisableAuth"`). +5. Translate `src/components/settings/Security.vue` (search for a `Confirm` component with `rel="confirmDisableAuth"`). 6. Import your language file in `src/i18n.js` and add it to `languageList` constant. 7. Make a [pull request](https://github.com/louislam/uptime-kuma/pulls) when you have done. diff --git a/src/languages/en.js b/src/languages/en.js index 15c3cd0f3..a503b5235 100644 --- a/src/languages/en.js +++ b/src/languages/en.js @@ -307,4 +307,5 @@ export default { steamApiKeyDescription: "For monitoring a Steam Game Server you need a Steam Web-API key. You can register your API key here: ", "Current User": "Current User", recent: "Recent", + shrinkDatabaseDescription: "Trigger database VACCUM for SQLite. If your database is created after 1.10.0, AUTO_VACCUM is already enabled and this action is not needed.", }; diff --git a/src/layouts/Layout.vue b/src/layouts/Layout.vue index 7228a460d..75173e1fc 100644 --- a/src/layouts/Layout.vue +++ b/src/layouts/Layout.vue @@ -29,7 +29,7 @@ @@ -188,8 +188,8 @@ main { .dark { header { - background-color: #161b22; - border-bottom-color: #161b22 !important; + background-color: $dark-header-bg; + border-bottom-color: $dark-header-bg !important; span { color: #f0f6fc; diff --git a/src/pages/EditMonitor.vue b/src/pages/EditMonitor.vue index 65c3dad6e..11be3ed5b 100644 --- a/src/pages/EditMonitor.vue +++ b/src/pages/EditMonitor.vue @@ -194,7 +194,7 @@
- +
diff --git a/src/pages/Settings.vue b/src/pages/Settings.vue index 9d501407d..bacda3a38 100644 --- a/src/pages/Settings.vue +++ b/src/pages/Settings.vue @@ -1,527 +1,91 @@ @@ -667,37 +121,7 @@ export default { .shadow-box { padding: 20px; -} - -.btn-check:active + .btn-outline-primary, -.btn-check:checked + .btn-outline-primary, -.btn-check:hover + .btn-outline-primary { - color: #fff; -} - -.dark { - .list-group-item { - background-color: $dark-bg2; - color: $dark-font-color; - } - - .btn-check:active + .btn-outline-primary, - .btn-check:checked + .btn-outline-primary, - .btn-check:hover + .btn-outline-primary { - color: #000; - } - - #importBackup { - &::file-selector-button { - color: $primary; - background-color: $dark-bg; - } - - &:hover:not(:disabled):not([readonly])::file-selector-button { - color: $dark-font-color2; - background-color: $primary; - } - } + min-height: calc(100vh - 155px); } footer { @@ -707,4 +131,59 @@ footer { padding-bottom: 30px; text-align: center; } + +.settings-menu { + flex: 0 0 auto; + width: 300px; + + a { + text-decoration: none !important; + } + + .menu-item { + border-radius: 10px; + margin: 0.5em; + padding: 0.7em 1em; + cursor: pointer; + } + + .menu-item:hover { + background: $highlight-white; + + .dark & { + background: $dark-header-bg; + } + } + + .active .menu-item { + background: $highlight-white; + border-left: 4px solid $primary; + border-top-left-radius: 0; + border-bottom-left-radius: 0; + + .dark & { + background: $dark-header-bg; + } + } +} + +.settings-content { + flex: 0 0 auto; + width: calc(100% - 300px); + + .settings-content-header { + width: calc(100% + 20px); + border-bottom: 1px solid #dee2e6; + border-radius: 0 10px 0 0; + margin-top: -20px; + margin-right: -20px; + padding: 12.5px 1em; + font-size: 26px; + + .dark & { + background: $dark-header-bg; + border-bottom: 0; + } + } +} diff --git a/src/router.js b/src/router.js index 5c3fda941..a2414eb60 100644 --- a/src/router.js +++ b/src/router.js @@ -11,6 +11,14 @@ import Setup from "./pages/Setup.vue"; const StatusPage = () => import("./pages/StatusPage.vue"); import Entry from "./pages/Entry.vue"; +import Appearance from "./components/settings/Appearance.vue"; +import General from "./components/settings/General.vue"; +import Notifications from "./components/settings/Notifications.vue"; +import MonitorHistory from "./components/settings/MonitorHistory.vue"; +import Security from "./components/settings/Security.vue"; +import Backup from "./components/settings/Backup.vue"; +import About from "./components/settings/About.vue"; + const routes = [ { path: "/", @@ -59,6 +67,37 @@ const routes = [ { path: "/settings", component: Settings, + children: [ + { + path: "general", + alias: "", + component: General, + }, + { + path: "appearance", + component: Appearance, + }, + { + path: "notifications", + component: Notifications, + }, + { + path: "monitor-history", + component: MonitorHistory, + }, + { + path: "security", + component: Security, + }, + { + path: "backup", + component: Backup, + }, + { + path: "about", + component: About, + }, + ] }, ], }, diff --git a/test/e2e.spec.js b/test/e2e.spec.js index 03920b306..66bbb794b 100644 --- a/test/e2e.spec.js +++ b/test/e2e.spec.js @@ -59,18 +59,37 @@ describe("Init", () => { // Go to / await page.goto(baseURL); - await sleep(3000); + await page.waitForSelector("h1.mb-3"); pathname = await page.evaluate(() => location.pathname); expect(pathname).toEqual("/dashboard"); }); + it("should create monitor", async () => { + // Create monitor + await page.goto(baseURL + "/add"); + await page.waitForSelector("#name"); + + await page.type("#name", "Myself"); + await page.waitForSelector("#url"); + await page.click("#url", { clickCount: 3 }); + await page.keyboard.type(baseURL); + await page.keyboard.press("Enter"); + + await page.waitForFunction(() => { + const badge = document.querySelector("span.badge"); + return badge && badge.innerText == "100%"; + }, { timeout: 5000 }); + + }); + // Settings Page describe("Settings", () => { - beforeAll(async () => { + beforeEach(async () => { await page.goto(baseURL + "/settings"); }); it("Change Language", async () => { + await page.goto(baseURL + "/settings/appearance"); await page.waitForSelector("#language"); await page.select("#language", "zh-HK"); @@ -83,20 +102,33 @@ describe("Init", () => { }); it("Change Theme", async () => { - await sleep(1000); + await page.goto(baseURL + "/settings/appearance"); // Dark await click(page, ".btn[for=btncheck2]"); await page.waitForSelector("div.dark"); - await sleep(1000); + await page.waitForSelector(".btn[for=btncheck1]"); // Light await click(page, ".btn[for=btncheck1]"); await page.waitForSelector("div.light"); }); - // TODO: Heartbeat Bar Style + it("Change Heartbeat Bar Style", async () => { + await page.goto(baseURL + "/settings/appearance"); + + // Bottom + await click(page, ".btn[for=btncheck5]"); + await page.waitForSelector("div.hp-bar-big"); + + // None + await click(page, ".btn[for=btncheck6]"); + await page.waitForSelector("div.hp-bar-big", { + hidden: true, + timeout: 1000 + }); + }); // TODO: Timezone @@ -108,14 +140,14 @@ describe("Init", () => { // Yes await click(page, "#searchEngineIndexYes"); await click(page, "form > div > .btn[type=submit]"); - await sleep(2000); + await sleep(1000); res = await axios.get(baseURL + "/robots.txt"); expect(res.data).not.toContain("Disallow: /"); // No await click(page, "#searchEngineIndexNo"); await click(page, "form > div > .btn[type=submit]"); - await sleep(2000); + await sleep(1000); res = await axios.get(baseURL + "/robots.txt"); expect(res.data).toContain("Disallow: /"); }); @@ -125,25 +157,25 @@ describe("Init", () => { // Default await newPage.goto(baseURL); - await sleep(3000); + await newPage.waitForSelector("h1.mb-3", { timeout: 3000 }); let pathname = await newPage.evaluate(() => location.pathname); expect(pathname).toEqual("/dashboard"); // Status Page await click(page, "#entryPageNo"); await click(page, "form > div > .btn[type=submit]"); - await sleep(4000); + await sleep(1000); await newPage.goto(baseURL); - await sleep(4000); + await newPage.waitForSelector("img.logo", { timeout: 3000 }); pathname = await newPage.evaluate(() => location.pathname); expect(pathname).toEqual("/status"); // Back to Dashboard await click(page, "#entryPageYes"); await click(page, "form > div > .btn[type=submit]"); - await sleep(4000); + await sleep(1000); await newPage.goto(baseURL); - await sleep(4000); + await newPage.waitForSelector("h1.mb-3", { timeout: 3000 }); pathname = await newPage.evaluate(() => location.pathname); expect(pathname).toEqual("/dashboard"); @@ -151,7 +183,7 @@ describe("Init", () => { }); it("Change Password (wrong current password)", async () => { - await page.goto(baseURL + "/settings"); + await page.goto(baseURL + "/settings/security"); await page.waitForSelector("#current-password"); await page.type("#current-password", "wrong_passw$$d"); @@ -159,10 +191,10 @@ describe("Init", () => { await page.type("#repeat-new-password", "new_password123"); // Save - await click(page, "form > div > .btn[type=submit]", 1); - await sleep(4000); + await click(page, "form > div > .btn[type=submit]", 0); + await sleep(1000); - await click(page, ".btn-danger.btn.me-2"); + await click(page, "#logout-btn"); await login("admin", "new_password123"); let elementCount = await page.evaluate(() => document.querySelectorAll("#floatingPassword").length); expect(elementCount).toEqual(1); @@ -171,24 +203,26 @@ describe("Init", () => { }); it("Change Password (wrong repeat)", async () => { - await page.goto(baseURL + "/settings"); + await page.goto(baseURL + "/settings/security"); await page.waitForSelector("#current-password"); await page.type("#current-password", "admin123"); await page.type("#new-password", "new_password123"); await page.type("#repeat-new-password", "new_password1234567898797898"); - await click(page, "form > div > .btn[type=submit]", 1); - await sleep(4000); + await click(page, "form > div > .btn[type=submit]", 0); + await sleep(1000); - await click(page, ".btn-danger.btn.me-2"); + await click(page, "#logout-btn"); await login("admin", "new_password123"); let elementCount = await page.evaluate(() => document.querySelectorAll("#floatingPassword").length); expect(elementCount).toEqual(1); await login("admin", "admin123"); - await sleep(3000); + await page.waitForSelector("#current-password"); + let pathname = await page.evaluate(() => location.pathname); + expect(pathname).toEqual("/settings/security"); }); // TODO: 2FA @@ -197,9 +231,35 @@ describe("Init", () => { // TODO: Import Backup - // TODO: Disable Auth + it("Should disable & enable auth", async () => { + await page.goto(baseURL + "/settings/security"); + await click(page, "#disableAuth-btn"); + await click(page, ".btn.btn-danger[data-bs-dismiss='modal']", 2); // Not a good way to do it + await page.waitForSelector("#enableAuth-btn", { timeout: 3000 }); + await page.waitForSelector("#logout-btn", { + hidden: true, + timeout: 3000 + }); - // TODO: Clear Stats + const newPage = await browser.newPage(); + await newPage.goto(baseURL); + await newPage.waitForSelector("span.badge", { timeout: 3000 }); + newPage.close(); + + await click(page, "#enableAuth-btn"); + await login("admin", "admin123"); + await page.waitForSelector("#disableAuth-btn", { timeout: 3000 }); + }); + + // it("Should clear all statistics", async () => { + // await page.goto(baseURL + "/settings/monitor-history"); + // await click(page, "#clearAllStats-btn"); + // await click(page, ".btn.btn-danger"); + // await page.waitForFunction(() => { + // const badge = document.querySelector("span.badge"); + // return badge && badge.innerText == "0%"; + // }, { timeout: 3000 }); + // }); }); /*