diff --git a/.github/workflows/auto-test.yml b/.github/workflows/auto-test.yml index b49a253c3..273b1dba2 100644 --- a/.github/workflows/auto-test.yml +++ b/.github/workflows/auto-test.yml @@ -18,7 +18,7 @@ jobs: strategy: matrix: os: [macos-latest, ubuntu-latest, windows-latest] - node: [ 14, 16, 17, 18 ] + node: [ 14, 16, 18, 19 ] # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ steps: @@ -66,3 +66,19 @@ jobs: - run: npm install - run: npm run build - run: npm run cy:test + + frontend-unit-tests: + needs: [ check-linters ] + runs-on: ubuntu-latest + steps: + - run: git config --global core.autocrlf false # Mainly for Windows + - uses: actions/checkout@v3 + + - name: Use Node.js 14 + uses: actions/setup-node@v3 + with: + node-version: 14 + cache: 'npm' + - run: npm install + - run: npm run build + - run: npm run cy:run:unit diff --git a/README.md b/README.md index 1f26da8bf..b1a0928c8 100644 --- a/README.md +++ b/README.md @@ -7,9 +7,9 @@ -It is a self-hosted monitoring tool like "Uptime Robot". +Uptime Kuma is an easy-to-use self-hosted monitoring tool. - + ## 🥔 Live Demo diff --git a/config/cypress.frontend.config.js b/config/cypress.frontend.config.js new file mode 100644 index 000000000..eecdcb8dd --- /dev/null +++ b/config/cypress.frontend.config.js @@ -0,0 +1,10 @@ +const { defineConfig } = require("cypress"); + +module.exports = defineConfig({ + e2e: { + supportFile: false, + specPattern: [ + "test/cypress/unit/**/*.js" + ], + } +}); diff --git a/docker/builder-go.dockerfile b/docker/builder-go.dockerfile index 5d38cf4ec..79c1a95ba 100644 --- a/docker/builder-go.dockerfile +++ b/docker/builder-go.dockerfile @@ -8,9 +8,9 @@ ARG TARGETPLATFORM COPY ./extra/ ./extra/ # Compile healthcheck.go -RUN apt update -RUN apt --yes --no-install-recommends install curl -RUN curl -sL https://deb.nodesource.com/setup_18.x | bash -RUN apt --yes --no-install-recommends install nodejs -RUN node -v -RUN node ./extra/build-healthcheck.js $TARGETPLATFORM +RUN apt update && \ + apt --yes --no-install-recommends install curl && \ + curl -sL https://deb.nodesource.com/setup_18.x | bash && \ + apt --yes --no-install-recommends install nodejs && \ + node ./extra/build-healthcheck.js $TARGETPLATFORM && \ + apt --yes remove nodejs diff --git a/docker/dockerfile b/docker/dockerfile index 26f70dd3b..775cec595 100644 --- a/docker/dockerfile +++ b/docker/dockerfile @@ -12,10 +12,13 @@ FROM louislam/uptime-kuma:base-debian AS build WORKDIR /app ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=1 +COPY .npmrc .npmrc +COPY package.json package.json +COPY package-lock.json package-lock.json +RUN npm ci --omit=dev COPY . . COPY --from=build_healthcheck /app/extra/healthcheck /app/extra/healthcheck -RUN npm ci --production && \ - chmod +x /app/extra/entrypoint.sh +RUN chmod +x /app/extra/entrypoint.sh ############################################ # ⭐ Main Image diff --git a/docker/dockerfile-alpine b/docker/dockerfile-alpine index ab9255f95..43f26b8bb 100644 --- a/docker/dockerfile-alpine +++ b/docker/dockerfile-alpine @@ -3,10 +3,12 @@ WORKDIR /app ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=1 +COPY .npmrc .npmrc +COPY package.json package.json +COPY package-lock.json package-lock.json +RUN npm ci --omit=dev COPY . . -RUN npm ci --production && \ - chmod +x /app/extra/entrypoint.sh - +RUN chmod +x /app/extra/entrypoint.sh FROM louislam/uptime-kuma:base-alpine AS release WORKDIR /app diff --git a/package-lock.json b/package-lock.json index 7e88d1269..3fed4f5b6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,15 +1,16 @@ { "name": "uptime-kuma", - "version": "1.19.3", + "version": "1.19.4", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "uptime-kuma", - "version": "1.19.3", + "version": "1.19.4", "license": "MIT", "dependencies": { "@grpc/grpc-js": "~1.7.3", + "@louislam/ping": "~0.4.2-mod.0", "@louislam/sqlite3": "15.1.2", "args-parser": "~1.3.0", "axios": "~0.27.0", @@ -48,7 +49,6 @@ "password-hash": "~1.2.2", "pg": "~8.8.0", "pg-connection-string": "~2.5.0", - "ping": "~0.4.2", "prom-client": "~13.2.0", "prometheus-api-metrics": "~3.2.1", "protobufjs": "~7.1.1", @@ -3161,6 +3161,19 @@ "resolved": "https://registry.npmjs.org/@js-joda/core/-/core-5.5.1.tgz", "integrity": "sha512-oTFmkyv5MhgkHdZhoe5lwRoKW0t4njPvK3g7ODvK/prkoC5bwylKcyQJMsmjvgHBXoy4u5iLnB5yQ7AljouHAA==" }, + "node_modules/@louislam/ping": { + "version": "0.4.2-mod.0", + "resolved": "https://registry.npmjs.org/@louislam/ping/-/ping-0.4.2-mod.0.tgz", + "integrity": "sha512-cyHnJHsMkC+sFU32GBzX5SlwdTb+BIBlwsdwsDm+AS9jcS1sz7JPBrdCStqpNkVn5lUUQZ7Ak5DRwlWuwJOYAg==", + "dependencies": { + "command-exists": "~1.2.9", + "q": "1.x", + "underscore": "^1.12.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/@louislam/sqlite3": { "version": "15.1.2", "resolved": "https://registry.npmjs.org/@louislam/sqlite3/-/sqlite3-15.1.2.tgz", @@ -13502,18 +13515,6 @@ "node": ">=0.10.0" } }, - "node_modules/ping": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/ping/-/ping-0.4.2.tgz", - "integrity": "sha512-1uAw0bzHtrPbPo2s6no06oZAzY6KqKclEJR1JRZKIHKXKlPdrz9N0/1MPPB+BbrvMjN3Mk0pcod3bfLNZFRo9w==", - "dependencies": { - "q": "1.x", - "underscore": "^1.12.0" - }, - "engines": { - "node": ">=4.0.0" - } - }, "node_modules/pirates": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.5.tgz", @@ -19462,6 +19463,16 @@ "resolved": "https://registry.npmjs.org/@js-joda/core/-/core-5.5.1.tgz", "integrity": "sha512-oTFmkyv5MhgkHdZhoe5lwRoKW0t4njPvK3g7ODvK/prkoC5bwylKcyQJMsmjvgHBXoy4u5iLnB5yQ7AljouHAA==" }, + "@louislam/ping": { + "version": "0.4.2-mod.0", + "resolved": "https://registry.npmjs.org/@louislam/ping/-/ping-0.4.2-mod.0.tgz", + "integrity": "sha512-cyHnJHsMkC+sFU32GBzX5SlwdTb+BIBlwsdwsDm+AS9jcS1sz7JPBrdCStqpNkVn5lUUQZ7Ak5DRwlWuwJOYAg==", + "requires": { + "command-exists": "~1.2.9", + "q": "1.x", + "underscore": "^1.12.0" + } + }, "@louislam/sqlite3": { "version": "15.1.2", "resolved": "https://registry.npmjs.org/@louislam/sqlite3/-/sqlite3-15.1.2.tgz", @@ -27316,15 +27327,6 @@ "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", "dev": true }, - "ping": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/ping/-/ping-0.4.2.tgz", - "integrity": "sha512-1uAw0bzHtrPbPo2s6no06oZAzY6KqKclEJR1JRZKIHKXKlPdrz9N0/1MPPB+BbrvMjN3Mk0pcod3bfLNZFRo9w==", - "requires": { - "q": "1.x", - "underscore": "^1.12.0" - } - }, "pirates": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.5.tgz", diff --git a/package.json b/package.json index 0175e4b1d..8093bec59 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "uptime-kuma", - "version": "1.19.3", + "version": "1.19.4", "license": "MIT", "repository": { "type": "git", @@ -39,7 +39,7 @@ "build-docker-nightly-amd64": "docker buildx build -f docker/dockerfile --platform linux/amd64 -t louislam/uptime-kuma:nightly-amd64 --target nightly . --push --progress plain", "build-docker-pr-test": "docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64 -t louislam/uptime-kuma:pr-test --target pr-test . --push", "upload-artifacts": "docker buildx build -f docker/dockerfile --platform linux/amd64 -t louislam/uptime-kuma:upload-artifact --build-arg VERSION --build-arg GITHUB_TOKEN --target upload-artifact . --progress plain", - "setup": "git checkout 1.19.3 && npm ci --production && npm run download-dist", + "setup": "git checkout 1.19.4 && npm ci --production && npm run download-dist", "download-dist": "node extra/download-dist.js", "mark-as-nightly": "node extra/mark-as-nightly.js", "reset-password": "node extra/reset-password.js", @@ -61,11 +61,13 @@ "start-pr-test": "node extra/checkout-pr.js && npm install && npm run dev", "cy:test": "node test/prepare-test-server.js && node server/server.js --port=3002 --data-dir=./data/test/ --e2e", "cy:run": "npx cypress run --browser chrome --headless --config-file ./config/cypress.config.js", + "cy:run:unit": "npx cypress run --browser chrome --headless --config-file ./config/cypress.frontend.config.js", "cypress-open": "concurrently -k -r \"node test/prepare-test-server.js && node server/server.js --port=3002 --data-dir=./data/test/\" \"cypress open --config-file ./config/cypress.config.js\"", "build-healthcheck-armv7": "cross-env GOOS=linux GOARCH=arm GOARM=7 go build -x -o ./extra/healthcheck-armv7 ./extra/healthcheck.go" }, "dependencies": { "@grpc/grpc-js": "~1.7.3", + "@louislam/ping": "~0.4.2-mod.0", "@louislam/sqlite3": "15.1.2", "args-parser": "~1.3.0", "axios": "~0.27.0", @@ -104,7 +106,6 @@ "password-hash": "~1.2.2", "pg": "~8.8.0", "pg-connection-string": "~2.5.0", - "ping": "~0.4.2", "prom-client": "~13.2.0", "prometheus-api-metrics": "~3.2.1", "protobufjs": "~7.1.1", diff --git a/server/server.js b/server/server.js index 5276ba92c..45cf47c35 100644 --- a/server/server.js +++ b/server/server.js @@ -942,13 +942,21 @@ let needSetup = false; try { checkLogin(socket); - let bean = await R.findOne("monitor", " id = ? ", [ tag.id ]); + let bean = await R.findOne("tag", " id = ? ", [ tag.id ]); + if (bean == null) { + callback({ + ok: false, + msg: "Tag not found", + }); + return; + } bean.name = tag.name; bean.color = tag.color; await R.store(bean); callback({ ok: true, + msg: "Saved", tag: await bean.toJSON(), }); diff --git a/server/util-server.js b/server/util-server.js index 4a30017a4..60d8baac7 100644 --- a/server/util-server.js +++ b/server/util-server.js @@ -1,5 +1,5 @@ const tcpp = require("tcp-ping"); -const ping = require("ping"); +const ping = require("@louislam/ping"); const { R } = require("redbean-node"); const { log, genSecret } = require("../src/util"); const passwordHash = require("./password-hash"); diff --git a/src/assets/localization.scss b/src/assets/localization.scss index f9a28d8a4..97be37785 100644 --- a/src/assets/localization.scss +++ b/src/assets/localization.scss @@ -2,4 +2,8 @@ html[lang='fa'] { #app { font-family: 'IRANSans', 'Iranian Sans','B Nazanin', 'Tahoma', ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, segoe ui, Roboto, helvetica neue, Arial, noto sans, sans-serif, apple color emoji, segoe ui emoji, segoe ui symbol, noto color emoji; } -} \ No newline at end of file +} + +ul.multiselect__content { + padding-left: 0 !important; +} diff --git a/src/components/DockerHostDialog.vue b/src/components/DockerHostDialog.vue index 50ffa49c1..193b30128 100644 --- a/src/components/DockerHostDialog.vue +++ b/src/components/DockerHostDialog.vue @@ -73,7 +73,7 @@ export default { emits: [ "added" ], data() { return { - model: null, + modal: null, processing: false, id: null, connectionTypes: [ "socket", "tcp" ], diff --git a/src/components/Tag.vue b/src/components/Tag.vue index 0325d9900..cbbe0b796 100644 --- a/src/components/Tag.vue +++ b/src/components/Tag.vue @@ -41,7 +41,7 @@ export default { }, computed: { displayText() { - if (this.item.value === "") { + if (this.item.value === "" || this.item.value === undefined) { return this.item.name; } else { return `${this.item.name}: ${this.item.value}`; diff --git a/src/components/TagEditDialog.vue b/src/components/TagEditDialog.vue new file mode 100644 index 000000000..2f6afbed4 --- /dev/null +++ b/src/components/TagEditDialog.vue @@ -0,0 +1,376 @@ + + + + + + + + {{ $t("Edit Tag") }} + + + + + + {{ $t("Name") }} + + + + + {{ $t("Color") }} + + + + + + {{ option.name }} + + + + + {{ option.name }} + + + + + + + + + + + + {{ $tc("Monitor", selectedMonitors.length) }} + + + {{ monitor.name }} + + + + + + + {{ $t("Add a monitor") }}: + + {{ monitor.name }} + + + + + + + + + + + + + {{ $t("confirmDeleteTagMsg") }} + + + + + + diff --git a/src/components/TagsManager.vue b/src/components/TagsManager.vue index 8481fdfeb..b302cd20f 100644 --- a/src/components/TagsManager.vue +++ b/src/components/TagsManager.vue @@ -130,6 +130,7 @@ import { Modal } from "bootstrap"; import VueMultiselect from "vue-multiselect"; import { useToast } from "vue-toastification"; +import { colorOptions } from "../util-frontend"; import Tag from "../components/Tag.vue"; const toast = useToast(); @@ -176,24 +177,7 @@ export default { return this.preSelectedTags.concat(this.newTags).filter(tag => !this.deleteTags.find(monitorTag => monitorTag.id === tag.id)); }, colorOptions() { - return [ - { name: this.$t("Gray"), - color: "#4B5563" }, - { name: this.$t("Red"), - color: "#DC2626" }, - { name: this.$t("Orange"), - color: "#D97706" }, - { name: this.$t("Green"), - color: "#059669" }, - { name: this.$t("Blue"), - color: "#2563EB" }, - { name: this.$t("Indigo"), - color: "#4F46E5" }, - { name: this.$t("Purple"), - color: "#7C3AED" }, - { name: this.$t("Pink"), - color: "#DB2777" }, - ]; + return colorOptions(this); }, validateDraftTag() { let nameInvalid = false; diff --git a/src/components/settings/Tags.vue b/src/components/settings/Tags.vue new file mode 100644 index 000000000..347a6ef29 --- /dev/null +++ b/src/components/settings/Tags.vue @@ -0,0 +1,171 @@ + + + + + + + + + {{ monitorsByTag(tag.id).length }} {{ $tc("Monitor", monitorsByTag(tag.id).length) }} + + + + + + + + + + + + + + + {{ $t("confirmDeleteTagMsg") }} + + + + + + + diff --git a/src/languages/cs-CZ.js b/src/languages/cs-CZ.js index 99ae9ef0a..3396e97b9 100644 --- a/src/languages/cs-CZ.js +++ b/src/languages/cs-CZ.js @@ -591,7 +591,7 @@ export default { "You can divide numbers with": "Čísla můžete dělit pomocí", "or": "nebo", recurringInterval: "Interval", - "Recurring": "Recurring", + "Recurring": "Opakující se", strategyManual: "Aktivní/Neaktivní Ručně", warningTimezone: "Používá se časové pásmo serveru", weekdayShortMon: "Po", diff --git a/src/languages/en.js b/src/languages/en.js index 259a34a64..f75f7a5dc 100644 --- a/src/languages/en.js +++ b/src/languages/en.js @@ -74,6 +74,7 @@ export default { Current: "Current", Uptime: "Uptime", "Cert Exp.": "Cert Exp.", + Monitor: "Monitor | Monitors", day: "day | days", "-day": "-day", hour: "hour", @@ -190,6 +191,7 @@ export default { Indigo: "Indigo", Purple: "Purple", Pink: "Pink", + Custom: "Custom", "Search...": "Search...", "Avg. Ping": "Avg. Ping", "Avg. Response": "Avg. Response", @@ -677,5 +679,6 @@ export default { "Specific Monitor Type": "Specific Monitor Type", dataRetentionTimeError: "Retention period must be 0 or greater", infiniteRetention: "Set to 0 for infinite retention.", + confirmDeleteTagMsg: "Are you sure you want to delete this tag? Monitors associated with this tag will not be deleted.", "Body Encoding": "Body Encoding", }; diff --git a/src/languages/fr-FR.js b/src/languages/fr-FR.js index 6a6323a96..ca61bbdc3 100644 --- a/src/languages/fr-FR.js +++ b/src/languages/fr-FR.js @@ -675,4 +675,6 @@ export default { "General Monitor Type": "Type de sonde générale", "Passive Monitor Type": "Type de sonde passive", "Specific Monitor Type": "Type de sonde spécifique", + dataRetentionTimeError: "La durée de conservation doit être supérieure ou égale à 0", + infiniteRetention: "Définissez la valeur à 0 pour une durée de conservation infinie.", }; diff --git a/src/pages/Settings.vue b/src/pages/Settings.vue index f2dd3475c..f5d32e0eb 100644 --- a/src/pages/Settings.vue +++ b/src/pages/Settings.vue @@ -95,6 +95,9 @@ export default { "reverse-proxy": { title: this.$t("Reverse Proxy"), }, + tags: { + title: this.$t("Tags"), + }, "monitor-history": { title: this.$t("Monitor History"), }, diff --git a/src/router.js b/src/router.js index 380488264..b5b46c307 100644 --- a/src/router.js +++ b/src/router.js @@ -24,6 +24,7 @@ import Appearance from "./components/settings/Appearance.vue"; import General from "./components/settings/General.vue"; const Notifications = () => import("./components/settings/Notifications.vue"); import ReverseProxy from "./components/settings/ReverseProxy.vue"; +import Tags from "./components/settings/Tags.vue"; import MonitorHistory from "./components/settings/MonitorHistory.vue"; const Security = () => import("./components/settings/Security.vue"); import Proxies from "./components/settings/Proxies.vue"; @@ -95,6 +96,10 @@ const routes = [ path: "reverse-proxy", component: ReverseProxy, }, + { + path: "tags", + component: Tags, + }, { path: "monitor-history", component: MonitorHistory, diff --git a/src/util-frontend.js b/src/util-frontend.js index 3323f3279..3ff751fff 100644 --- a/src/util-frontend.js +++ b/src/util-frontend.js @@ -78,3 +78,29 @@ export function getResBaseURL() { return ""; } } + +/** + * Get the tag color options + * Shared between components + * @returns {Object[]} + */ +export function colorOptions(self) { + return [ + { name: self.$t("Gray"), + color: "#4B5563" }, + { name: self.$t("Red"), + color: "#DC2626" }, + { name: self.$t("Orange"), + color: "#D97706" }, + { name: self.$t("Green"), + color: "#059669" }, + { name: self.$t("Blue"), + color: "#2563EB" }, + { name: self.$t("Indigo"), + color: "#4F46E5" }, + { name: self.$t("Purple"), + color: "#7C3AED" }, + { name: self.$t("Pink"), + color: "#DB2777" }, + ]; +} diff --git a/test/cypress/unit/i18n.spec.js b/test/cypress/unit/i18n.spec.js new file mode 100644 index 000000000..da63d95a5 --- /dev/null +++ b/test/cypress/unit/i18n.spec.js @@ -0,0 +1,44 @@ +import { currentLocale } from "../../../src/i18n"; + +describe("Test i18n.js", () => { + + it("currentLocale()", () => { + const setLanguage = (language) => { + Object.defineProperty(window.navigator, 'language', { + value: language, + writable: true + }); + } + setLanguage('en-EN'); + + expect(currentLocale()).equal("en"); + + setLanguage('zh-HK'); + expect(currentLocale()).equal("zh-HK"); + + // Note that in Safari on iOS prior to 10.2, the country code returned is lowercase: "en-us", "fr-fr" etc. + // https://developer.mozilla.org/en-US/docs/Web/API/Navigator/language + setLanguage('zh-hk'); + expect(currentLocale()).equal("en"); + + setLanguage('en-US'); + expect(currentLocale()).equal("en"); + + setLanguage('ja-ZZ'); + expect(currentLocale()).equal("ja"); + + setLanguage('zz-ZZ'); + expect(currentLocale()).equal("en"); + + setLanguage('zz-ZZ'); + expect(currentLocale()).equal("en"); + + setLanguage('en'); + localStorage.locale = "en"; + expect(currentLocale()).equal("en"); + + localStorage.locale = "zh-HK"; + expect(currentLocale()).equal("zh-HK"); + }); + +}); \ No newline at end of file