diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b2efe3e0..46e9a899 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 00000000..74f6d783 --- /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 07830ca3..dc4f7b34 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 4baaa0fb..2d3f585e 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 79f01b55..f516f4f6 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 41d91e85..fbef40bb 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 78106515..07249fb4 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 2f436983..91ab917e 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 bd771f8f..ef51e89c 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 00000000..baa72f39 --- /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 00000000..e0a3d643 --- /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 00000000..6ac28d46 --- /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 00000000..a1b42d85 --- /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 00000000..9b5b8bd7 --- /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 00000000..b2cbcf48 --- /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 00000000..4ef6b3d9 --- /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 6ba7d95e..52b70fa8 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 15c3cd0f..a503b523 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 7228a460..75173e1f 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 65c3dad6..11be3ed5 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 9d501407..bacda3a3 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 5c3fda94..a2414eb6 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 03920b30..66bbb794 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 }); + // }); }); /*