diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/App.vue | 15 | ||||
-rw-r--r-- | src/components/GetStarted.vue | 35 | ||||
-rw-r--r-- | src/components/SearchInput.vue | 12 | ||||
-rw-r--r-- | src/components/services/AdGuardHome.vue | 3 | ||||
-rw-r--r-- | src/components/services/Lidarr.vue | 110 | ||||
-rw-r--r-- | src/components/services/Mealie.vue | 106 | ||||
-rw-r--r-- | src/components/services/Medusa.vue | 142 | ||||
-rw-r--r-- | src/components/services/OpenWeather.vue | 3 | ||||
-rw-r--r-- | src/components/services/PaperlessNG.vue | 79 | ||||
-rw-r--r-- | src/components/services/PiHole.vue | 85 | ||||
-rw-r--r-- | src/components/services/Portainer.vue | 131 | ||||
-rw-r--r-- | src/components/services/Prowlarr.vue | 94 | ||||
-rw-r--r-- | src/components/services/Radarr.vue | 156 | ||||
-rw-r--r-- | src/components/services/Sonarr.vue | 158 | ||||
-rw-r--r-- | src/mixins/service.js | 8 |
15 files changed, 681 insertions, 456 deletions
diff --git a/src/App.vue b/src/App.vue index 1f1791c..fda13c3 100644 --- a/src/App.vue +++ b/src/App.vue | |||
@@ -45,6 +45,7 @@ | |||
45 | 45 | ||
46 | <SearchInput | 46 | <SearchInput |
47 | class="navbar-item is-inline-block-mobile" | 47 | class="navbar-item is-inline-block-mobile" |
48 | :hotkey="searchHotkey()" | ||
48 | @input="filterServices" | 49 | @input="filterServices" |
49 | @search-focus="showMenu = true" | 50 | @search-focus="showMenu = true" |
50 | @search-open="navigateToFirstService" | 51 | @search-open="navigateToFirstService" |
@@ -59,6 +60,9 @@ | |||
59 | v-if="config.connectivityCheck" | 60 | v-if="config.connectivityCheck" |
60 | @network-status-update="offline = $event" | 61 | @network-status-update="offline = $event" |
61 | /> | 62 | /> |
63 | |||
64 | <GetStarted v-if="loaded && !services" /> | ||
65 | |||
62 | <div v-if="!offline"> | 66 | <div v-if="!offline"> |
63 | <!-- Optional messages --> | 67 | <!-- Optional messages --> |
64 | <Message :item="config.message" /> | 68 | <Message :item="config.message" /> |
@@ -133,6 +137,7 @@ const jsyaml = require("js-yaml"); | |||
133 | const merge = require("lodash.merge"); | 137 | const merge = require("lodash.merge"); |
134 | 138 | ||
135 | import Navbar from "./components/Navbar.vue"; | 139 | import Navbar from "./components/Navbar.vue"; |
140 | import GetStarted from "./components/GetStarted.vue"; | ||
136 | import ConnectivityChecker from "./components/ConnectivityChecker.vue"; | 141 | import ConnectivityChecker from "./components/ConnectivityChecker.vue"; |
137 | import Service from "./components/Service.vue"; | 142 | import Service from "./components/Service.vue"; |
138 | import Message from "./components/Message.vue"; | 143 | import Message from "./components/Message.vue"; |
@@ -147,6 +152,7 @@ export default { | |||
147 | name: "App", | 152 | name: "App", |
148 | components: { | 153 | components: { |
149 | Navbar, | 154 | Navbar, |
155 | GetStarted, | ||
150 | ConnectivityChecker, | 156 | ConnectivityChecker, |
151 | Service, | 157 | Service, |
152 | Message, | 158 | Message, |
@@ -157,6 +163,7 @@ export default { | |||
157 | }, | 163 | }, |
158 | data: function () { | 164 | data: function () { |
159 | return { | 165 | return { |
166 | loaded: false, | ||
160 | config: null, | 167 | config: null, |
161 | services: null, | 168 | services: null, |
162 | offline: false, | 169 | offline: false, |
@@ -169,8 +176,14 @@ export default { | |||
169 | created: async function () { | 176 | created: async function () { |
170 | this.buildDashboard(); | 177 | this.buildDashboard(); |
171 | window.onhashchange = this.buildDashboard; | 178 | window.onhashchange = this.buildDashboard; |
179 | this.loaded = true; | ||
172 | }, | 180 | }, |
173 | methods: { | 181 | methods: { |
182 | searchHotkey() { | ||
183 | if (this.config.hotkey && this.config.hotkey.search) { | ||
184 | return this.config.hotkey.search; | ||
185 | } | ||
186 | }, | ||
174 | buildDashboard: async function () { | 187 | buildDashboard: async function () { |
175 | const defaults = jsyaml.load(defaultConfig); | 188 | const defaults = jsyaml.load(defaultConfig); |
176 | let config; | 189 | let config; |
@@ -191,6 +204,7 @@ export default { | |||
191 | } | 204 | } |
192 | this.config = merge(defaults, config); | 205 | this.config = merge(defaults, config); |
193 | this.services = this.config.services; | 206 | this.services = this.config.services; |
207 | |||
194 | document.title = | 208 | document.title = |
195 | this.config.documentTitle || | 209 | this.config.documentTitle || |
196 | `${this.config.title} | ${this.config.subtitle}`; | 210 | `${this.config.title} | ${this.config.subtitle}`; |
@@ -209,6 +223,7 @@ export default { | |||
209 | window.location.href = response.url; | 223 | window.location.href = response.url; |
210 | return; | 224 | return; |
211 | } | 225 | } |
226 | |||
212 | if (!response.ok) { | 227 | if (!response.ok) { |
213 | throw Error(`${response.statusText}: ${response.body}`); | 228 | throw Error(`${response.statusText}: ${response.body}`); |
214 | } | 229 | } |
diff --git a/src/components/GetStarted.vue b/src/components/GetStarted.vue new file mode 100644 index 0000000..dcabc02 --- /dev/null +++ b/src/components/GetStarted.vue | |||
@@ -0,0 +1,35 @@ | |||
1 | <template> | ||
2 | <article> | ||
3 | <div class="m-6 has-text-centered py-6"> | ||
4 | <p class="is-size-5 mb-0">No configured service found!</p> | ||
5 | <p>Check out the documentation to start building your Homer dashboard.</p> | ||
6 | <p> | ||
7 | <a | ||
8 | class="button is-primary mt-5 has-text-weight-bold" | ||
9 | href="https://github.com/bastienwirtz/homer/blob/main/README.md#getting-started" | ||
10 | target="_blank" | ||
11 | > | ||
12 | Get started | ||
13 | </a> | ||
14 | </p> | ||
15 | </div> | ||
16 | </article> | ||
17 | </template> | ||
18 | |||
19 | <script> | ||
20 | export default { | ||
21 | name: "GetStarted", | ||
22 | }; | ||
23 | </script> | ||
24 | |||
25 | <style lang="scss" scoped> | ||
26 | p { | ||
27 | color: #4a4a4a; | ||
28 | } | ||
29 | |||
30 | body #app a { | ||
31 | font-weight: 900; | ||
32 | color: #ffffff; | ||
33 | font-family: "Lato", sans-serif; | ||
34 | } | ||
35 | </style> | ||
diff --git a/src/components/SearchInput.vue b/src/components/SearchInput.vue index 40c5a1d..165c992 100644 --- a/src/components/SearchInput.vue +++ b/src/components/SearchInput.vue | |||
@@ -15,10 +15,16 @@ | |||
15 | <script> | 15 | <script> |
16 | export default { | 16 | export default { |
17 | name: "SearchInput", | 17 | name: "SearchInput", |
18 | props: ["value"], | 18 | props: { |
19 | value: String, | ||
20 | hotkey: { | ||
21 | type: String, | ||
22 | default: "/", | ||
23 | }, | ||
24 | }, | ||
19 | mounted() { | 25 | mounted() { |
20 | this._keyListener = function (event) { | 26 | this._keyListener = function (event) { |
21 | if (event.key === "/") { | 27 | if (event.key === this.hotkey) { |
22 | event.preventDefault(); | 28 | event.preventDefault(); |
23 | this.focus(); | 29 | this.focus(); |
24 | } | 30 | } |
@@ -28,7 +34,7 @@ export default { | |||
28 | }; | 34 | }; |
29 | document.addEventListener("keydown", this._keyListener.bind(this)); | 35 | document.addEventListener("keydown", this._keyListener.bind(this)); |
30 | 36 | ||
31 | // fill seach from get parameter. | 37 | // fill search from get parameter. |
32 | const search = new URLSearchParams(window.location.search).get("search"); | 38 | const search = new URLSearchParams(window.location.search).get("search"); |
33 | if (search) { | 39 | if (search) { |
34 | this.$refs.search.value = search; | 40 | this.$refs.search.value = search; |
diff --git a/src/components/services/AdGuardHome.vue b/src/components/services/AdGuardHome.vue index b01f0f4..4c53398 100644 --- a/src/components/services/AdGuardHome.vue +++ b/src/components/services/AdGuardHome.vue | |||
@@ -76,9 +76,6 @@ export default { | |||
76 | </script> | 76 | </script> |
77 | 77 | ||
78 | <style scoped lang="scss"> | 78 | <style scoped lang="scss"> |
79 | .media-left img { | ||
80 | max-height: 100%; | ||
81 | } | ||
82 | .status { | 79 | .status { |
83 | font-size: 0.8rem; | 80 | font-size: 0.8rem; |
84 | color: var(--text-title); | 81 | color: var(--text-title); |
diff --git a/src/components/services/Lidarr.vue b/src/components/services/Lidarr.vue new file mode 100644 index 0000000..cbe5516 --- /dev/null +++ b/src/components/services/Lidarr.vue | |||
@@ -0,0 +1,110 @@ | |||
1 | <template> | ||
2 | <Generic :item="item"> | ||
3 | <template #indicator> | ||
4 | <div class="notifs"> | ||
5 | <strong v-if="activity > 0" class="notif activity" title="Activity"> | ||
6 | {{ activity }} | ||
7 | </strong> | ||
8 | <strong v-if="warnings > 0" class="notif warnings" title="Warning"> | ||
9 | {{ warnings }} | ||
10 | </strong> | ||
11 | <strong v-if="errors > 0" class="notif errors" title="Error"> | ||
12 | {{ errors }} | ||
13 | </strong> | ||
14 | <strong | ||
15 | v-if="serverError" | ||
16 | class="notif errors" | ||
17 | title="Connection error to Lidarr API, check url and apikey in config.yml" | ||
18 | >?</strong | ||
19 | > | ||
20 | </div> | ||
21 | </template> | ||
22 | </Generic> | ||
23 | </template> | ||
24 | |||
25 | <script> | ||
26 | import service from "@/mixins/service.js"; | ||
27 | import Generic from "./Generic.vue"; | ||
28 | |||
29 | export default { | ||
30 | name: "Lidarr", | ||
31 | mixins: [service], | ||
32 | props: { | ||
33 | item: Object, | ||
34 | }, | ||
35 | components: { | ||
36 | Generic, | ||
37 | }, | ||
38 | data: () => { | ||
39 | return { | ||
40 | activity: null, | ||
41 | warnings: null, | ||
42 | errors: null, | ||
43 | serverError: false, | ||
44 | }; | ||
45 | }, | ||
46 | created: function () { | ||
47 | this.fetchConfig(); | ||
48 | }, | ||
49 | methods: { | ||
50 | fetchConfig: function () { | ||
51 | this.fetch(`/api/v1/health?apikey=${this.item.apikey}`) | ||
52 | .then((health) => { | ||
53 | this.warnings = 0; | ||
54 | this.errors = 0; | ||
55 | for (var i = 0; i < health.length; i++) { | ||
56 | if (health[i].type == "warning") { | ||
57 | this.warnings++; | ||
58 | } else if (health[i].type == "error") { | ||
59 | this.errors++; | ||
60 | } | ||
61 | } | ||
62 | }) | ||
63 | .catch((e) => { | ||
64 | console.error(e); | ||
65 | this.serverError = true; | ||
66 | }); | ||
67 | this.fetch(`/api/v1/queue/status?apikey=${this.item.apikey}`) | ||
68 | .then((queue) => { | ||
69 | this.activity = queue.totalCount; | ||
70 | }) | ||
71 | .catch((e) => { | ||
72 | console.error(e); | ||
73 | this.serverError = true; | ||
74 | }); | ||
75 | }, | ||
76 | }, | ||
77 | }; | ||
78 | </script> | ||
79 | |||
80 | <style scoped lang="scss"> | ||
81 | .notifs { | ||
82 | position: absolute; | ||
83 | color: white; | ||
84 | font-family: sans-serif; | ||
85 | top: 0.3em; | ||
86 | right: 0.5em; | ||
87 | .notif { | ||
88 | display: inline-block; | ||
89 | padding-right: 0.35em; | ||
90 | padding-left: 0.35em; | ||
91 | padding-top: 0.2em; | ||
92 | padding-bottom: 0.2em; | ||
93 | border-radius: 0.25em; | ||
94 | position: relative; | ||
95 | margin-left: 0.3em; | ||
96 | font-size: 0.8em; | ||
97 | &.activity { | ||
98 | background-color: #4fb5d6; | ||
99 | } | ||
100 | |||
101 | &.warnings { | ||
102 | background-color: #d08d2e; | ||
103 | } | ||
104 | |||
105 | &.errors { | ||
106 | background-color: #e51111; | ||
107 | } | ||
108 | } | ||
109 | } | ||
110 | </style> | ||
diff --git a/src/components/services/Mealie.vue b/src/components/services/Mealie.vue index 790a9b1..b5b2255 100644 --- a/src/components/services/Mealie.vue +++ b/src/components/services/Mealie.vue | |||
@@ -1,47 +1,33 @@ | |||
1 | <template> | 1 | <template> |
2 | <div> | 2 | <Generic :item="item"> |
3 | <div class="card" :class="item.class"> | 3 | <template #content> |
4 | <a :href="item.url" :target="item.target" rel="noreferrer"> | 4 | <p class="title is-4">{{ item.name }}</p> |
5 | <div class="card-content"> | 5 | <p class="subtitle is-6"> |
6 | <div class="media"> | 6 | <template v-if="item.subtitle"> |
7 | <div v-if="item.logo" class="media-left"> | 7 | {{ item.subtitle }} |
8 | <figure class="image is-48x48"> | 8 | </template> |
9 | <img :src="item.logo" :alt="`${item.name} logo`" /> | 9 | <template v-else-if="meal"> Today: {{ meal.name }} </template> |
10 | </figure> | 10 | <template v-else-if="stats"> |
11 | </div> | 11 | happily keeping {{ stats.totalRecipes }} recipes organized |
12 | <div v-if="item.icon" class="media-left"> | 12 | </template> |
13 | <figure class="image is-48x48"> | 13 | </p> |
14 | <i style="font-size: 35px" :class="['fa-fw', item.icon]"></i> | 14 | </template> |
15 | </figure> | 15 | </Generic> |
16 | </div> | ||
17 | <div class="media-content"> | ||
18 | <p class="title is-4">{{ item.name }}</p> | ||
19 | <p class="subtitle is-6"> | ||
20 | <template v-if="item.subtitle"> | ||
21 | {{ item.subtitle }} | ||
22 | </template> | ||
23 | <template v-else-if="meal"> Today: {{ meal.name }} </template> | ||
24 | <template v-else-if="stats"> | ||
25 | happily keeping {{ stats.totalRecipes }} recipes organized | ||
26 | </template> | ||
27 | </p> | ||
28 | </div> | ||
29 | </div> | ||
30 | <div class="tag" :class="item.tagstyle" v-if="item.tag"> | ||
31 | <strong class="tag-text">#{{ item.tag }}</strong> | ||
32 | </div> | ||
33 | </div> | ||
34 | </a> | ||
35 | </div> | ||
36 | </div> | ||
37 | </template> | 16 | </template> |
38 | 17 | ||
39 | <script> | 18 | <script> |
19 | import service from "@/mixins/service.js"; | ||
20 | import Generic from "./Generic.vue"; | ||
21 | |||
40 | export default { | 22 | export default { |
41 | name: "Mealie", | 23 | name: "Mealie", |
24 | mixins: [service], | ||
42 | props: { | 25 | props: { |
43 | item: Object, | 26 | item: Object, |
44 | }, | 27 | }, |
28 | components: { | ||
29 | Generic, | ||
30 | }, | ||
45 | data: () => ({ | 31 | data: () => ({ |
46 | stats: null, | 32 | stats: null, |
47 | meal: null, | 33 | meal: null, |
@@ -51,44 +37,20 @@ export default { | |||
51 | }, | 37 | }, |
52 | methods: { | 38 | methods: { |
53 | fetchStatus: async function () { | 39 | fetchStatus: async function () { |
54 | if (this.item.subtitle != null) return; // omitting unnecessary ajax call as the subtitle is showing | 40 | const headers = { |
55 | this.meal = await fetch(`${this.item.url}/api/meal-plans/today/`, { | 41 | Authorization: "Bearer " + this.item.apikey, |
56 | headers: { | 42 | Accept: "application/json", |
57 | Authorization: "Bearer " + this.item.apikey, | 43 | }; |
58 | Accept: "application/json", | 44 | |
59 | }, | 45 | if (this.item.subtitle != null) return; |
60 | }) | 46 | |
61 | .then(function (response) { | 47 | this.meal = await this.fetch("/api/meal-plans/today/", { headers }).catch( |
62 | if (!response.ok) { | 48 | (e) => console.log(e) |
63 | throw new Error("Not 2xx response"); | 49 | ); |
64 | } else { | 50 | this.stats = await this.fetch("/api/debug/statistics/", { |
65 | if (response != null) { | 51 | headers, |
66 | return response.json(); | 52 | }).catch((e) => console.log(e)); |
67 | } | ||
68 | } | ||
69 | }) | ||
70 | .catch((e) => console.log(e)); | ||
71 | this.stats = await fetch(`${this.item.url}/api/debug/statistics/`, { | ||
72 | headers: { | ||
73 | Authorization: "Bearer " + this.item.apikey, | ||
74 | Accept: "application/json", | ||
75 | }, | ||
76 | }) | ||
77 | .then(function (response) { | ||
78 | if (!response.ok) { | ||
79 | throw new Error("Not 2xx response"); | ||
80 | } else { | ||
81 | return response.json(); | ||
82 | } | ||
83 | }) | ||
84 | .catch((e) => console.log(e)); | ||
85 | }, | 53 | }, |
86 | }, | 54 | }, |
87 | }; | 55 | }; |
88 | </script> | 56 | </script> |
89 | |||
90 | <style scoped lang="scss"> | ||
91 | .media-left img { | ||
92 | max-height: 100%; | ||
93 | } | ||
94 | </style> | ||
diff --git a/src/components/services/Medusa.vue b/src/components/services/Medusa.vue index 5720649..43b5651 100644 --- a/src/components/services/Medusa.vue +++ b/src/components/services/Medusa.vue | |||
@@ -1,65 +1,49 @@ | |||
1 | <template> | 1 | <template> |
2 | <div> | 2 | <Generic :item="item"> |
3 | <div class="card" :class="item.class"> | 3 | <template #indicator> |
4 | <a :href="item.url" :target="item.target" rel="noreferrer"> | 4 | <div class="notifs"> |
5 | <div class="card-content"> | 5 | <strong |
6 | <div class="media"> | 6 | v-if="config !== null && config.system.news.unread > 0" |
7 | <div v-if="item.logo" class="media-left"> | 7 | class="notif news" |
8 | <figure class="image is-48x48"> | 8 | title="News" |
9 | <img :src="item.logo" :alt="`${item.name} logo`" /> | 9 | >{{ config.system.news.unread }}</strong |
10 | </figure> | 10 | > |
11 | </div> | 11 | <strong |
12 | <div v-if="item.icon" class="media-left"> | 12 | v-if="config !== null && config.main.logs.numWarnings > 0" |
13 | <figure class="image is-48x48"> | 13 | class="notif warnings" |
14 | <i style="font-size: 35px" :class="['fa-fw', item.icon]"></i> | 14 | title="Warning" |
15 | </figure> | 15 | >{{ config.main.logs.numWarnings }}</strong |
16 | </div> | 16 | > |
17 | <div class="media-content"> | 17 | <strong |
18 | <p class="title is-4">{{ item.name }}</p> | 18 | v-if="config !== null && config.main.logs.numErrors > 0" |
19 | <p class="subtitle is-6">{{ item.subtitle }}</p> | 19 | class="notif errors" |
20 | </div> | 20 | title="Error" |
21 | <div class="notifs"> | 21 | >{{ config.main.logs.numErrors }}</strong |
22 | <strong | 22 | > |
23 | v-if="config !== null && config.system.news.unread > 0" | 23 | <strong |
24 | class="notif news" | 24 | v-if="serverError" |
25 | title="News" | 25 | class="notif errors" |
26 | >{{ config.system.news.unread }}</strong | 26 | title="Connection error to Medusa API, check url and apikey in config.yml" |
27 | > | 27 | >?</strong |
28 | <strong | 28 | > |
29 | v-if="config !== null && config.main.logs.numWarnings > 0" | 29 | </div> |
30 | class="notif warnings" | 30 | </template> |
31 | title="Warning" | 31 | </Generic> |
32 | >{{ config.main.logs.numWarnings }}</strong | ||
33 | > | ||
34 | <strong | ||
35 | v-if="config !== null && config.main.logs.numErrors > 0" | ||
36 | class="notif errors" | ||
37 | title="Error" | ||
38 | >{{ config.main.logs.numErrors }}</strong | ||
39 | > | ||
40 | <strong | ||
41 | v-if="serverError" | ||
42 | class="notif errors" | ||
43 | title="Connection error to Medusa API, check url and apikey in config.yml" | ||
44 | >?</strong | ||
45 | > | ||
46 | </div> | ||
47 | </div> | ||
48 | <div class="tag" :class="item.tagstyle" v-if="item.tag"> | ||
49 | <strong class="tag-text">#{{ item.tag }}</strong> | ||
50 | </div> | ||
51 | </div> | ||
52 | </a> | ||
53 | </div> | ||
54 | </div> | ||
55 | </template> | 32 | </template> |
56 | 33 | ||
57 | <script> | 34 | <script> |
35 | import service from "@/mixins/service.js"; | ||
36 | import Generic from "./Generic.vue"; | ||
37 | |||
58 | export default { | 38 | export default { |
59 | name: "Medusa", | 39 | name: "Medusa", |
40 | mixins: [service], | ||
60 | props: { | 41 | props: { |
61 | item: Object, | 42 | item: Object, |
62 | }, | 43 | }, |
44 | components: { | ||
45 | Generic, | ||
46 | }, | ||
63 | data: () => { | 47 | data: () => { |
64 | return { | 48 | return { |
65 | config: null, | 49 | config: null, |
@@ -71,16 +55,9 @@ export default { | |||
71 | }, | 55 | }, |
72 | methods: { | 56 | methods: { |
73 | fetchConfig: function () { | 57 | fetchConfig: function () { |
74 | fetch(`${this.item.url}/api/v2/config`, { | 58 | this.fetch("/api/v2/config", { |
75 | credentials: "include", | 59 | headers: { "X-Api-Key": this.item.apikey }, |
76 | headers: { "X-Api-Key": `${this.item.apikey}` }, | ||
77 | }) | 60 | }) |
78 | .then((response) => { | ||
79 | if (response.status != 200) { | ||
80 | throw new Error(response.statusText); | ||
81 | } | ||
82 | return response.json(); | ||
83 | }) | ||
84 | .then((conf) => { | 61 | .then((conf) => { |
85 | this.config = conf; | 62 | this.config = conf; |
86 | }) | 63 | }) |
@@ -94,35 +71,32 @@ export default { | |||
94 | </script> | 71 | </script> |
95 | 72 | ||
96 | <style scoped lang="scss"> | 73 | <style scoped lang="scss"> |
97 | .media-left img { | ||
98 | max-height: 100%; | ||
99 | } | ||
100 | .notifs { | 74 | .notifs { |
101 | position: absolute; | 75 | position: absolute; |
102 | color: white; | 76 | color: white; |
103 | font-family: sans-serif; | 77 | font-family: sans-serif; |
104 | top: 0.3em; | 78 | top: 0.3em; |
105 | right: 0.5em; | 79 | right: 0.5em; |
106 | } | 80 | .notif { |
107 | .notif { | 81 | padding-right: 0.35em; |
108 | padding-right: 0.35em; | 82 | padding-left: 0.35em; |
109 | padding-left: 0.35em; | 83 | padding-top: 0.2em; |
110 | padding-top: 0.2em; | 84 | padding-bottom: 0.2em; |
111 | padding-bottom: 0.2em; | 85 | border-radius: 0.25em; |
112 | border-radius: 0.25em; | 86 | position: relative; |
113 | position: relative; | 87 | margin-left: 0.3em; |
114 | margin-left: 0.3em; | 88 | font-size: 0.8em; |
115 | font-size: 0.8em; | 89 | &.news { |
116 | } | 90 | background-color: #777777; |
117 | .news { | 91 | } |
118 | background-color: #777777; | ||
119 | } | ||
120 | 92 | ||
121 | .warnings { | 93 | &.warnings { |
122 | background-color: #d08d2e; | 94 | background-color: #d08d2e; |
123 | } | 95 | } |
124 | 96 | ||
125 | .errors { | 97 | &.errors { |
126 | background-color: #e51111; | 98 | background-color: #e51111; |
99 | } | ||
100 | } | ||
127 | } | 101 | } |
128 | </style> | 102 | </style> |
diff --git a/src/components/services/OpenWeather.vue b/src/components/services/OpenWeather.vue index 09ff76a..79d5e37 100644 --- a/src/components/services/OpenWeather.vue +++ b/src/components/services/OpenWeather.vue | |||
@@ -64,7 +64,8 @@ export default { | |||
64 | locationQuery = `q=${this.item.location}`; | 64 | locationQuery = `q=${this.item.location}`; |
65 | } | 65 | } |
66 | 66 | ||
67 | const url = `https://api.openweathermap.org/data/2.5/weather?${locationQuery}&appid=${this.item.apiKey}&units=${this.item.units}`; | 67 | const apiKey = this.item.apikey || this.item.apiKey; |
68 | const url = `https://api.openweathermap.org/data/2.5/weather?${locationQuery}&appid=${apiKey}&units=${this.item.units}`; | ||
68 | fetch(url) | 69 | fetch(url) |
69 | .then((response) => { | 70 | .then((response) => { |
70 | if (!response.ok) { | 71 | if (!response.ok) { |
diff --git a/src/components/services/PaperlessNG.vue b/src/components/services/PaperlessNG.vue index af13317..69f2437 100644 --- a/src/components/services/PaperlessNG.vue +++ b/src/components/services/PaperlessNG.vue | |||
@@ -1,46 +1,32 @@ | |||
1 | <template> | 1 | <template> |
2 | <div> | 2 | <Generic :item="item"> |
3 | <div class="card" :class="item.class"> | 3 | <template #content> |
4 | <a :href="item.url" :target="item.target" rel="noreferrer"> | 4 | <p class="title is-4">{{ item.name }}</p> |
5 | <div class="card-content"> | 5 | <p class="subtitle is-6"> |
6 | <div class="media"> | 6 | <template v-if="item.subtitle"> |
7 | <div v-if="item.logo" class="media-left"> | 7 | {{ item.subtitle }} |
8 | <figure class="image is-48x48"> | 8 | </template> |
9 | <img :src="item.logo" :alt="`${item.name} logo`" /> | 9 | <template v-else-if="api"> |
10 | </figure> | 10 | happily storing {{ api.count }} documents |
11 | </div> | 11 | </template> |
12 | <div v-if="item.icon" class="media-left"> | 12 | </p> |
13 | <figure class="image is-48x48"> | 13 | </template> |
14 | <i style="font-size: 35px" :class="['fa-fw', item.icon]"></i> | 14 | </Generic> |
15 | </figure> | ||
16 | </div> | ||
17 | <div class="media-content"> | ||
18 | <p class="title is-4">{{ item.name }}</p> | ||
19 | <p class="subtitle is-6"> | ||
20 | <template v-if="item.subtitle"> | ||
21 | {{ item.subtitle }} | ||
22 | </template> | ||
23 | <template v-else-if="api"> | ||
24 | happily storing {{ api.count }} documents | ||
25 | </template> | ||
26 | </p> | ||
27 | </div> | ||
28 | </div> | ||
29 | <div class="tag" :class="item.tagstyle" v-if="item.tag"> | ||
30 | <strong class="tag-text">#{{ item.tag }}</strong> | ||
31 | </div> | ||
32 | </div> | ||
33 | </a> | ||
34 | </div> | ||
35 | </div> | ||
36 | </template> | 15 | </template> |
37 | 16 | ||
38 | <script> | 17 | <script> |
18 | import service from "@/mixins/service.js"; | ||
19 | import Generic from "./Generic.vue"; | ||
20 | |||
39 | export default { | 21 | export default { |
40 | name: "Paperless", | 22 | name: "Paperless", |
23 | mixins: [service], | ||
41 | props: { | 24 | props: { |
42 | item: Object, | 25 | item: Object, |
43 | }, | 26 | }, |
27 | components: { | ||
28 | Generic, | ||
29 | }, | ||
44 | data: () => ({ | 30 | data: () => ({ |
45 | api: null, | 31 | api: null, |
46 | }), | 32 | }), |
@@ -49,36 +35,21 @@ export default { | |||
49 | }, | 35 | }, |
50 | methods: { | 36 | methods: { |
51 | fetchStatus: async function () { | 37 | fetchStatus: async function () { |
52 | if (this.item.subtitle != null) return; // omitting unnecessary ajax call as the subtitle is showing | 38 | if (this.item.subtitle != null) return; |
53 | var apikey = this.item.apikey; | 39 | |
40 | const apikey = this.item.apikey; | ||
54 | if (!apikey) { | 41 | if (!apikey) { |
55 | console.error( | 42 | console.error( |
56 | "apikey is not present in config.yml for the paperless entry!" | 43 | "apikey is not present in config.yml for the paperless entry!" |
57 | ); | 44 | ); |
58 | return; | 45 | return; |
59 | } | 46 | } |
60 | const url = `${this.item.url}/api/documents/`; | 47 | this.api = await this.fetch("/api/documents/", { |
61 | this.api = await fetch(url, { | ||
62 | credentials: "include", | ||
63 | headers: { | 48 | headers: { |
64 | Authorization: "Token " + this.item.apikey, | 49 | Authorization: "Token " + this.item.apikey, |
65 | }, | 50 | }, |
66 | }) | 51 | }).catch((e) => console.log(e)); |
67 | .then(function (response) { | ||
68 | if (!response.ok) { | ||
69 | throw new Error("Not 2xx response"); | ||
70 | } else { | ||
71 | return response.json(); | ||
72 | } | ||
73 | }) | ||
74 | .catch((e) => console.log(e)); | ||
75 | }, | 52 | }, |
76 | }, | 53 | }, |
77 | }; | 54 | }; |
78 | </script> | 55 | </script> |
79 | |||
80 | <style scoped lang="scss"> | ||
81 | .media-left img { | ||
82 | max-height: 100%; | ||
83 | } | ||
84 | </style> | ||
diff --git a/src/components/services/PiHole.vue b/src/components/services/PiHole.vue index 87f7090..9aac016 100644 --- a/src/components/services/PiHole.vue +++ b/src/components/services/PiHole.vue | |||
@@ -1,59 +1,45 @@ | |||
1 | <template> | 1 | <template> |
2 | <div> | 2 | <Generic :item="item"> |
3 | <div class="card" :class="item.class"> | 3 | <template #content> |
4 | <a :href="item.url" :target="item.target" rel="noreferrer"> | 4 | <p class="title is-4">{{ item.name }}</p> |
5 | <div class="card-content"> | 5 | <p class="subtitle is-6"> |
6 | <div class="media"> | 6 | <template v-if="item.subtitle"> |
7 | <div v-if="item.logo" class="media-left"> | 7 | {{ item.subtitle }} |
8 | <figure class="image is-48x48"> | 8 | </template> |
9 | <img :src="item.logo" :alt="`${item.name} logo`" /> | 9 | <template v-else-if="percentage"> |
10 | </figure> | 10 | {{ percentage }}% blocked |
11 | </div> | 11 | </template> |
12 | <div v-if="item.icon" class="media-left"> | 12 | </p> |
13 | <figure class="image is-48x48"> | 13 | </template> |
14 | <i style="font-size: 35px" :class="['fa-fw', item.icon]"></i> | 14 | <template #indicator> |
15 | </figure> | 15 | <div v-if="status" class="status" :class="status"> |
16 | </div> | 16 | {{ status }} |
17 | <div class="media-content"> | 17 | </div> |
18 | <p class="title is-4">{{ item.name }}</p> | 18 | </template> |
19 | <p class="subtitle is-6"> | 19 | </Generic> |
20 | <template v-if="item.subtitle"> | ||
21 | {{ item.subtitle }} | ||
22 | </template> | ||
23 | <template v-else-if="api"> | ||
24 | {{ percentage }}% blocked | ||
25 | </template> | ||
26 | </p> | ||
27 | </div> | ||
28 | <div v-if="api" class="status" :class="api.status"> | ||
29 | {{ api.status }} | ||
30 | </div> | ||
31 | </div> | ||
32 | <div class="tag" :class="item.tagstyle" v-if="item.tag"> | ||
33 | <strong class="tag-text">#{{ item.tag }}</strong> | ||
34 | </div> | ||
35 | </div> | ||
36 | </a> | ||
37 | </div> | ||
38 | </div> | ||
39 | </template> | 20 | </template> |
40 | 21 | ||
41 | <script> | 22 | <script> |
23 | import service from "@/mixins/service.js"; | ||
24 | import Generic from "./Generic.vue"; | ||
25 | |||
42 | export default { | 26 | export default { |
43 | name: "PiHole", | 27 | name: "PiHole", |
28 | mixins: [service], | ||
44 | props: { | 29 | props: { |
45 | item: Object, | 30 | item: Object, |
46 | }, | 31 | }, |
32 | components: { | ||
33 | Generic, | ||
34 | }, | ||
47 | data: () => ({ | 35 | data: () => ({ |
48 | api: { | 36 | status: "", |
49 | status: "", | 37 | ads_percentage_today: 0, |
50 | ads_percentage_today: 0, | ||
51 | }, | ||
52 | }), | 38 | }), |
53 | computed: { | 39 | computed: { |
54 | percentage: function () { | 40 | percentage: function () { |
55 | if (this.api) { | 41 | if (this.ads_percentage_today) { |
56 | return this.api.ads_percentage_today.toFixed(1); | 42 | return this.ads_percentage_today.toFixed(1); |
57 | } | 43 | } |
58 | return ""; | 44 | return ""; |
59 | }, | 45 | }, |
@@ -63,21 +49,16 @@ export default { | |||
63 | }, | 49 | }, |
64 | methods: { | 50 | methods: { |
65 | fetchStatus: async function () { | 51 | fetchStatus: async function () { |
66 | const url = `${this.item.url}/api.php`; | 52 | const result = await this.fetch("/api.php").catch((e) => console.log(e)); |
67 | this.api = await fetch(url, { | 53 | |
68 | credentials: "include", | 54 | this.status = result.status; |
69 | }) | 55 | this.ads_percentage_today = result.ads_percentage_today; |
70 | .then((response) => response.json()) | ||
71 | .catch((e) => console.log(e)); | ||
72 | }, | 56 | }, |
73 | }, | 57 | }, |
74 | }; | 58 | }; |
75 | </script> | 59 | </script> |
76 | 60 | ||
77 | <style scoped lang="scss"> | 61 | <style scoped lang="scss"> |
78 | .media-left img { | ||
79 | max-height: 100%; | ||
80 | } | ||
81 | .status { | 62 | .status { |
82 | font-size: 0.8rem; | 63 | font-size: 0.8rem; |
83 | color: var(--text-title); | 64 | color: var(--text-title); |
diff --git a/src/components/services/Portainer.vue b/src/components/services/Portainer.vue new file mode 100644 index 0000000..c7e9962 --- /dev/null +++ b/src/components/services/Portainer.vue | |||
@@ -0,0 +1,131 @@ | |||
1 | <template> | ||
2 | <Generic :item="item"> | ||
3 | <template #indicator> | ||
4 | <div class="notifs"> | ||
5 | <strong v-if="running > 0" class="notif running" title="Running"> | ||
6 | {{ running }} | ||
7 | </strong> | ||
8 | <strong v-if="dead > 0" class="notif dead" title="Dead"> | ||
9 | {{ dead }} | ||
10 | </strong> | ||
11 | <strong | ||
12 | v-if="misc > 0" | ||
13 | class="notif misc" | ||
14 | title="Other (creating, paused, exited, etc.)" | ||
15 | > | ||
16 | {{ misc }} | ||
17 | </strong> | ||
18 | </div> | ||
19 | </template> | ||
20 | </Generic> | ||
21 | </template> | ||
22 | |||
23 | <script> | ||
24 | import service from "@/mixins/service.js"; | ||
25 | import Generic from "./Generic.vue"; | ||
26 | |||
27 | export default { | ||
28 | name: "Portainer", | ||
29 | mixins: [service], | ||
30 | props: { | ||
31 | item: Object, | ||
32 | }, | ||
33 | components: { | ||
34 | Generic, | ||
35 | }, | ||
36 | data: () => ({ | ||
37 | endpoints: null, | ||
38 | containers: null, | ||
39 | }), | ||
40 | computed: { | ||
41 | running: function () { | ||
42 | if (!this.containers) { | ||
43 | return ""; | ||
44 | } | ||
45 | return this.containers.filter((container) => { | ||
46 | return container.State.toLowerCase() === "running"; | ||
47 | }).length; | ||
48 | }, | ||
49 | dead: function () { | ||
50 | if (!this.containers) { | ||
51 | return ""; | ||
52 | } | ||
53 | return this.containers.filter((container) => { | ||
54 | return container.State.toLowerCase() === "dead"; | ||
55 | }).length; | ||
56 | }, | ||
57 | misc: function () { | ||
58 | if (!this.containers) { | ||
59 | return ""; | ||
60 | } | ||
61 | return this.containers.filter((container) => { | ||
62 | return ( | ||
63 | container.State.toLowerCase() !== "running" && | ||
64 | container.State.toLowerCase() !== "dead" | ||
65 | ); | ||
66 | }).length; | ||
67 | }, | ||
68 | }, | ||
69 | created() { | ||
70 | this.fetchStatus(); | ||
71 | }, | ||
72 | methods: { | ||
73 | fetchStatus: async function () { | ||
74 | const headers = { | ||
75 | "X-Api-Key": this.item.apikey, | ||
76 | }; | ||
77 | |||
78 | this.endpoints = await this.fetch("/api/endpoints", { headers }).catch( | ||
79 | (e) => { | ||
80 | console.error(e); | ||
81 | } | ||
82 | ); | ||
83 | |||
84 | let containers = []; | ||
85 | for (let endpoint of this.endpoints) { | ||
86 | const uri = `/api/endpoints/${endpoint.Id}/docker/containers/json?all=1`; | ||
87 | const endpointContainers = await this.fetch(uri, { headers }).catch( | ||
88 | (e) => { | ||
89 | console.error(e); | ||
90 | } | ||
91 | ); | ||
92 | |||
93 | containers = containers.concat(endpointContainers); | ||
94 | } | ||
95 | |||
96 | this.containers = containers; | ||
97 | }, | ||
98 | }, | ||
99 | }; | ||
100 | </script> | ||
101 | |||
102 | <style scoped lang="scss"> | ||
103 | .notifs { | ||
104 | position: absolute; | ||
105 | color: white; | ||
106 | font-family: sans-serif; | ||
107 | top: 0.3em; | ||
108 | right: 0.5em; | ||
109 | |||
110 | .notif { | ||
111 | display: inline-block; | ||
112 | padding: 0.2em 0.35em; | ||
113 | border-radius: 0.25em; | ||
114 | position: relative; | ||
115 | margin-left: 0.3em; | ||
116 | font-size: 0.8em; | ||
117 | |||
118 | &.running { | ||
119 | background-color: #4fd671; | ||
120 | } | ||
121 | |||
122 | &.dead { | ||
123 | background-color: #e51111; | ||
124 | } | ||
125 | |||
126 | &.misc { | ||
127 | background-color: #2ed0c8; | ||
128 | } | ||
129 | } | ||
130 | } | ||
131 | </style> | ||
diff --git a/src/components/services/Prowlarr.vue b/src/components/services/Prowlarr.vue new file mode 100644 index 0000000..abaa0f0 --- /dev/null +++ b/src/components/services/Prowlarr.vue | |||
@@ -0,0 +1,94 @@ | |||
1 | <template> | ||
2 | <Generic :item="item"> | ||
3 | <template #indicator> | ||
4 | <div class="notifs"> | ||
5 | <strong v-if="warnings > 0" class="notif warnings" title="Warning"> | ||
6 | {{ warnings }} | ||
7 | </strong> | ||
8 | <strong v-if="errors > 0" class="notif errors" title="Error"> | ||
9 | {{ errors }} | ||
10 | </strong> | ||
11 | <strong | ||
12 | v-if="serverError" | ||
13 | class="notif errors" | ||
14 | title="Connection error to Prowlarr API, check url and apikey in config.yml" | ||
15 | > | ||
16 | ? | ||
17 | </strong> | ||
18 | </div> | ||
19 | </template> | ||
20 | </Generic> | ||
21 | </template> | ||
22 | |||
23 | <script> | ||
24 | import service from "@/mixins/service.js"; | ||
25 | import Generic from "./Generic.vue"; | ||
26 | |||
27 | export default { | ||
28 | name: "Prowlarr", | ||
29 | mixins: [service], | ||
30 | props: { | ||
31 | item: Object, | ||
32 | }, | ||
33 | components: { | ||
34 | Generic, | ||
35 | }, | ||
36 | data: () => { | ||
37 | return { | ||
38 | warnings: null, | ||
39 | errors: null, | ||
40 | serverError: false, | ||
41 | }; | ||
42 | }, | ||
43 | created: function () { | ||
44 | this.fetchConfig(); | ||
45 | }, | ||
46 | methods: { | ||
47 | fetchConfig: function () { | ||
48 | this.fetch(`/api/v1/health?apikey=${this.item.apikey}`) | ||
49 | .then((health) => { | ||
50 | this.warnings = 0; | ||
51 | this.errors = 0; | ||
52 | for (var i = 0; i < health.length; i++) { | ||
53 | if (health[i].type == "warning") { | ||
54 | this.warnings++; | ||
55 | } else if (health[i].type == "error") { | ||
56 | this.errors++; | ||
57 | } | ||
58 | } | ||
59 | }) | ||
60 | .catch((e) => { | ||
61 | console.error(e); | ||
62 | this.serverError = true; | ||
63 | }); | ||
64 | }, | ||
65 | }, | ||
66 | }; | ||
67 | </script> | ||
68 | |||
69 | <style scoped lang="scss"> | ||
70 | .notifs { | ||
71 | position: absolute; | ||
72 | color: white; | ||
73 | font-family: sans-serif; | ||
74 | top: 0.3em; | ||
75 | right: 0.5em; | ||
76 | |||
77 | .notif { | ||
78 | display: inline-block; | ||
79 | padding: 0.2em 0.35em; | ||
80 | border-radius: 0.25em; | ||
81 | position: relative; | ||
82 | margin-left: 0.3em; | ||
83 | font-size: 0.8em; | ||
84 | |||
85 | &.warnings { | ||
86 | background-color: #d08d2e; | ||
87 | } | ||
88 | |||
89 | &.errors { | ||
90 | background-color: #e51111; | ||
91 | } | ||
92 | } | ||
93 | } | ||
94 | </style> | ||
diff --git a/src/components/services/Radarr.vue b/src/components/services/Radarr.vue index a9cdedf..5c40aaa 100644 --- a/src/components/services/Radarr.vue +++ b/src/components/services/Radarr.vue | |||
@@ -1,62 +1,43 @@ | |||
1 | <template> | 1 | <template> |
2 | <div> | 2 | <Generic :item="item"> |
3 | <div class="card" :class="item.class"> | 3 | <template #indicator> |
4 | <a :href="item.url" :target="item.target" rel="noreferrer"> | 4 | <div class="notifs"> |
5 | <div class="card-content"> | 5 | <strong v-if="activity > 0" class="notif activity" title="Activity"> |
6 | <div class="media"> | 6 | {{ activity }} |
7 | <div v-if="item.logo" class="media-left"> | 7 | </strong> |
8 | <figure class="image is-48x48"> | 8 | <strong v-if="warnings > 0" class="notif warnings" title="Warning"> |
9 | <img :src="item.logo" :alt="`${item.name} logo`" /> | 9 | {{ warnings }} |
10 | </figure> | 10 | </strong> |
11 | </div> | 11 | <strong v-if="errors > 0" class="notif errors" title="Error"> |
12 | <div v-if="item.icon" class="media-left"> | 12 | {{ errors }} |
13 | <figure class="image is-48x48"> | 13 | </strong> |
14 | <i style="font-size: 35px" :class="['fa-fw', item.icon]"></i> | 14 | <strong |
15 | </figure> | 15 | v-if="serverError" |
16 | </div> | 16 | class="notif errors" |
17 | <div class="media-content"> | 17 | title="Connection error to Radarr API, check url and apikey in config.yml" |
18 | <p class="title is-4">{{ item.name }}</p> | 18 | >?</strong |
19 | <p class="subtitle is-6">{{ item.subtitle }}</p> | 19 | > |
20 | </div> | 20 | </div> |
21 | <div class="notifs"> | 21 | </template> |
22 | <strong | 22 | </Generic> |
23 | v-if="activity > 0" | ||
24 | class="notif activity" | ||
25 | title="Activity" | ||
26 | >{{ activity }}</strong | ||
27 | > | ||
28 | <strong | ||
29 | v-if="warnings > 0" | ||
30 | class="notif warnings" | ||
31 | title="Warning" | ||
32 | >{{ warnings }}</strong | ||
33 | > | ||
34 | <strong v-if="errors > 0" class="notif errors" title="Error">{{ | ||
35 | errors | ||
36 | }}</strong> | ||
37 | <strong | ||
38 | v-if="serverError" | ||
39 | class="notif errors" | ||
40 | title="Connection error to Radarr API, check url and apikey in config.yml" | ||
41 | >?</strong | ||
42 | > | ||
43 | </div> | ||
44 | </div> | ||
45 | <div class="tag" :class="item.tagstyle" v-if="item.tag"> | ||
46 | <strong class="tag-text">#{{ item.tag }}</strong> | ||
47 | </div> | ||
48 | </div> | ||
49 | </a> | ||
50 | </div> | ||
51 | </div> | ||
52 | </template> | 23 | </template> |
53 | 24 | ||
54 | <script> | 25 | <script> |
26 | import service from "@/mixins/service.js"; | ||
27 | import Generic from "./Generic.vue"; | ||
28 | |||
29 | const V3_API = "/api/v3"; | ||
30 | const LEGACY_API = "/api"; | ||
31 | |||
55 | export default { | 32 | export default { |
56 | name: "Radarr", | 33 | name: "Radarr", |
34 | mixins: [service], | ||
57 | props: { | 35 | props: { |
58 | item: Object, | 36 | item: Object, |
59 | }, | 37 | }, |
38 | components: { | ||
39 | Generic, | ||
40 | }, | ||
60 | data: () => { | 41 | data: () => { |
61 | return { | 42 | return { |
62 | activity: null, | 43 | activity: null, |
@@ -68,17 +49,14 @@ export default { | |||
68 | created: function () { | 49 | created: function () { |
69 | this.fetchConfig(); | 50 | this.fetchConfig(); |
70 | }, | 51 | }, |
52 | computed: { | ||
53 | apiPath() { | ||
54 | return this.item.legacyApi ? LEGACY_API : V3_API; | ||
55 | }, | ||
56 | }, | ||
71 | methods: { | 57 | methods: { |
72 | fetchConfig: function () { | 58 | fetchConfig: function () { |
73 | fetch(`${this.item.url}/api/health?apikey=${this.item.apikey}`, { | 59 | this.fetch(`${this.apiPath}/health?apikey=${this.item.apikey}`) |
74 | credentials: "include", | ||
75 | }) | ||
76 | .then((response) => { | ||
77 | if (response.status != 200) { | ||
78 | throw new Error(response.statusText); | ||
79 | } | ||
80 | return response.json(); | ||
81 | }) | ||
82 | .then((health) => { | 60 | .then((health) => { |
83 | this.warnings = 0; | 61 | this.warnings = 0; |
84 | this.errors = 0; | 62 | this.errors = 0; |
@@ -94,21 +72,18 @@ export default { | |||
94 | console.error(e); | 72 | console.error(e); |
95 | this.serverError = true; | 73 | this.serverError = true; |
96 | }); | 74 | }); |
97 | fetch(`${this.item.url}/api/queue?apikey=${this.item.apikey}`, { | 75 | this.fetch(`${this.apiPath}/queue?apikey=${this.item.apikey}`) |
98 | credentials: "include", | ||
99 | }) | ||
100 | .then((response) => { | ||
101 | if (response.status != 200) { | ||
102 | throw new Error(response.statusText); | ||
103 | } | ||
104 | return response.json(); | ||
105 | }) | ||
106 | .then((queue) => { | 76 | .then((queue) => { |
107 | this.activity = 0; | 77 | this.activity = 0; |
108 | for (var i = 0; i < queue.length; i++) { | 78 | |
109 | if (queue[i].movie) { | 79 | if (this.item.legacyApi) { |
110 | this.activity++; | 80 | for (var i = 0; i < queue.length; i++) { |
81 | if (queue[i].movie) { | ||
82 | this.activity++; | ||
83 | } | ||
111 | } | 84 | } |
85 | } else { | ||
86 | this.activity = queue.totalRecords; | ||
112 | } | 87 | } |
113 | }) | 88 | }) |
114 | .catch((e) => { | 89 | .catch((e) => { |
@@ -121,35 +96,30 @@ export default { | |||
121 | </script> | 96 | </script> |
122 | 97 | ||
123 | <style scoped lang="scss"> | 98 | <style scoped lang="scss"> |
124 | .media-left img { | ||
125 | max-height: 100%; | ||
126 | } | ||
127 | .notifs { | 99 | .notifs { |
128 | position: absolute; | 100 | position: absolute; |
129 | color: white; | 101 | color: white; |
130 | font-family: sans-serif; | 102 | font-family: sans-serif; |
131 | top: 0.3em; | 103 | top: 0.3em; |
132 | right: 0.5em; | 104 | right: 0.5em; |
133 | } | 105 | .notif { |
134 | .notif { | 106 | display: inline-block; |
135 | padding-right: 0.35em; | 107 | padding: 0.2em 0.35em; |
136 | padding-left: 0.35em; | 108 | border-radius: 0.25em; |
137 | padding-top: 0.2em; | 109 | position: relative; |
138 | padding-bottom: 0.2em; | 110 | margin-left: 0.3em; |
139 | border-radius: 0.25em; | 111 | font-size: 0.8em; |
140 | position: relative; | 112 | &.activity { |
141 | margin-left: 0.3em; | 113 | background-color: #4fb5d6; |
142 | font-size: 0.8em; | 114 | } |
143 | } | ||
144 | .activity { | ||
145 | background-color: #4fb5d6; | ||
146 | } | ||
147 | 115 | ||
148 | .warnings { | 116 | &.warnings { |
149 | background-color: #d08d2e; | 117 | background-color: #d08d2e; |
150 | } | 118 | } |
151 | 119 | ||
152 | .errors { | 120 | &.errors { |
153 | background-color: #e51111; | 121 | background-color: #e51111; |
122 | } | ||
123 | } | ||
154 | } | 124 | } |
155 | </style> | 125 | </style> |
diff --git a/src/components/services/Sonarr.vue b/src/components/services/Sonarr.vue index 0270255..bb83b6b 100644 --- a/src/components/services/Sonarr.vue +++ b/src/components/services/Sonarr.vue | |||
@@ -1,62 +1,49 @@ | |||
1 | <template> | 1 | <template> |
2 | <div> | 2 | <Generic :item="item"> |
3 | <div class="card" :class="item.class"> | 3 | <template #indicator> |
4 | <a :href="item.url" :target="item.target" rel="noreferrer"> | 4 | <div class="notifs"> |
5 | <div class="card-content"> | 5 | <strong v-if="activity > 0" class="notif activity" title="Activity"> |
6 | <div class="media"> | 6 | {{ activity }} |
7 | <div v-if="item.logo" class="media-left"> | 7 | </strong> |
8 | <figure class="image is-48x48"> | 8 | <strong v-if="warnings > 0" class="notif warnings" title="Warning"> |
9 | <img :src="item.logo" :alt="`${item.name} logo`" /> | 9 | {{ warnings }} |
10 | </figure> | 10 | </strong> |
11 | </div> | 11 | <strong v-if="errors > 0" class="notif errors" title="Error"> |
12 | <div v-if="item.icon" class="media-left"> | 12 | {{ errors }} |
13 | <figure class="image is-48x48"> | 13 | </strong> |
14 | <i style="font-size: 35px" :class="['fa-fw', item.icon]"></i> | 14 | <strong |
15 | </figure> | 15 | v-if="serverError" |
16 | </div> | 16 | class="notif errors" |
17 | <div class="media-content"> | 17 | title="Connection error to Sonarr API, check url and apikey in config.yml" |
18 | <p class="title is-4">{{ item.name }}</p> | 18 | > |
19 | <p class="subtitle is-6">{{ item.subtitle }}</p> | 19 | ? |
20 | </div> | 20 | </strong> |
21 | <div class="notifs"> | 21 | </div> |
22 | <strong | 22 | </template> |
23 | v-if="activity > 0" | 23 | </Generic> |
24 | class="notif activity" | ||
25 | title="Activity" | ||
26 | >{{ activity }}</strong | ||
27 | > | ||
28 | <strong | ||
29 | v-if="warnings > 0" | ||
30 | class="notif warnings" | ||
31 | title="Warning" | ||
32 | >{{ warnings }}</strong | ||
33 | > | ||
34 | <strong v-if="errors > 0" class="notif errors" title="Error">{{ | ||
35 | errors | ||
36 | }}</strong> | ||
37 | <strong | ||
38 | v-if="serverError" | ||
39 | class="notif errors" | ||
40 | title="Connection error to Sonarr API, check url and apikey in config.yml" | ||
41 | >?</strong | ||
42 | > | ||
43 | </div> | ||
44 | </div> | ||
45 | <div class="tag" :class="item.tagstyle" v-if="item.tag"> | ||
46 | <strong class="tag-text">#{{ item.tag }}</strong> | ||
47 | </div> | ||
48 | </div> | ||
49 | </a> | ||
50 | </div> | ||
51 | </div> | ||
52 | </template> | 24 | </template> |
53 | 25 | ||
54 | <script> | 26 | <script> |
27 | import service from "@/mixins/service.js"; | ||
28 | import Generic from "./Generic.vue"; | ||
29 | |||
30 | const V3_API = "/api/v3"; | ||
31 | const LEGACY_API = "/api"; | ||
32 | |||
55 | export default { | 33 | export default { |
56 | name: "Sonarr", | 34 | name: "Sonarr", |
35 | mixins: [service], | ||
57 | props: { | 36 | props: { |
58 | item: Object, | 37 | item: Object, |
59 | }, | 38 | }, |
39 | components: { | ||
40 | Generic, | ||
41 | }, | ||
42 | computed: { | ||
43 | apiPath() { | ||
44 | return this.item.legacyApi ? LEGACY_API : V3_API; | ||
45 | }, | ||
46 | }, | ||
60 | data: () => { | 47 | data: () => { |
61 | return { | 48 | return { |
62 | activity: null, | 49 | activity: null, |
@@ -70,15 +57,7 @@ export default { | |||
70 | }, | 57 | }, |
71 | methods: { | 58 | methods: { |
72 | fetchConfig: function () { | 59 | fetchConfig: function () { |
73 | fetch(`${this.item.url}/api/health?apikey=${this.item.apikey}`, { | 60 | this.fetch(`${this.apiPath}/health?apikey=${this.item.apikey}`) |
74 | credentials: "include", | ||
75 | }) | ||
76 | .then((response) => { | ||
77 | if (response.status != 200) { | ||
78 | throw new Error(response.statusText); | ||
79 | } | ||
80 | return response.json(); | ||
81 | }) | ||
82 | .then((health) => { | 61 | .then((health) => { |
83 | this.warnings = 0; | 62 | this.warnings = 0; |
84 | this.errors = 0; | 63 | this.errors = 0; |
@@ -94,21 +73,17 @@ export default { | |||
94 | console.error(e); | 73 | console.error(e); |
95 | this.serverError = true; | 74 | this.serverError = true; |
96 | }); | 75 | }); |
97 | fetch(`${this.item.url}/api/queue?apikey=${this.item.apikey}`, { | 76 | this.fetch(`${this.apiPath}/queue?apikey=${this.item.apikey}`) |
98 | credentials: "include", | ||
99 | }) | ||
100 | .then((response) => { | ||
101 | if (response.status != 200) { | ||
102 | throw new Error(response.statusText); | ||
103 | } | ||
104 | return response.json(); | ||
105 | }) | ||
106 | .then((queue) => { | 77 | .then((queue) => { |
107 | this.activity = 0; | 78 | this.activity = 0; |
108 | for (var i = 0; i < queue.length; i++) { | 79 | if (this.item.legacyApi) { |
109 | if (queue[i].series) { | 80 | for (var i = 0; i < queue.length; i++) { |
110 | this.activity++; | 81 | if (queue[i].series) { |
82 | this.activity++; | ||
83 | } | ||
111 | } | 84 | } |
85 | } else { | ||
86 | this.activity = queue.totalRecords; | ||
112 | } | 87 | } |
113 | }) | 88 | }) |
114 | .catch((e) => { | 89 | .catch((e) => { |
@@ -121,35 +96,32 @@ export default { | |||
121 | </script> | 96 | </script> |
122 | 97 | ||
123 | <style scoped lang="scss"> | 98 | <style scoped lang="scss"> |
124 | .media-left img { | ||
125 | max-height: 100%; | ||
126 | } | ||
127 | .notifs { | 99 | .notifs { |
128 | position: absolute; | 100 | position: absolute; |
129 | color: white; | 101 | color: white; |
130 | font-family: sans-serif; | 102 | font-family: sans-serif; |
131 | top: 0.3em; | 103 | top: 0.3em; |
132 | right: 0.5em; | 104 | right: 0.5em; |
133 | } | ||
134 | .notif { | ||
135 | padding-right: 0.35em; | ||
136 | padding-left: 0.35em; | ||
137 | padding-top: 0.2em; | ||
138 | padding-bottom: 0.2em; | ||
139 | border-radius: 0.25em; | ||
140 | position: relative; | ||
141 | margin-left: 0.3em; | ||
142 | font-size: 0.8em; | ||
143 | } | ||
144 | .activity { | ||
145 | background-color: #4fb5d6; | ||
146 | } | ||
147 | 105 | ||
148 | .warnings { | 106 | .notif { |
149 | background-color: #d08d2e; | 107 | display: inline-block; |
150 | } | 108 | padding: 0.2em 0.35em; |
109 | border-radius: 0.25em; | ||
110 | position: relative; | ||
111 | margin-left: 0.3em; | ||
112 | font-size: 0.8em; | ||
113 | |||
114 | &.activity { | ||
115 | background-color: #4fb5d6; | ||
116 | } | ||
117 | |||
118 | &.warnings { | ||
119 | background-color: #d08d2e; | ||
120 | } | ||
151 | 121 | ||
152 | .errors { | 122 | &.errors { |
153 | background-color: #e51111; | 123 | background-color: #e51111; |
124 | } | ||
125 | } | ||
154 | } | 126 | } |
155 | </style> | 127 | </style> |
diff --git a/src/mixins/service.js b/src/mixins/service.js index 17fa6fc..abc708c 100644 --- a/src/mixins/service.js +++ b/src/mixins/service.js | |||
@@ -31,7 +31,13 @@ export default { | |||
31 | path = path.slice(1); | 31 | path = path.slice(1); |
32 | } | 32 | } |
33 | 33 | ||
34 | return fetch(`${this.endpoint}/${path}`, options).then((response) => { | 34 | let url = this.endpoint; |
35 | |||
36 | if (path) { | ||
37 | url = `${this.endpoint}/${path}`; | ||
38 | } | ||
39 | |||
40 | return fetch(url, options).then((response) => { | ||
35 | if (!response.ok) { | 41 | if (!response.ok) { |
36 | throw new Error("Not 2xx response"); | 42 | throw new Error("Not 2xx response"); |
37 | } | 43 | } |