]>
Commit | Line | Data |
---|---|---|
1 | <template> | |
2 | <div | |
3 | id="app" | |
4 | v-if="config" | |
5 | :class="[ | |
6 | `theme-${config.theme}`, | |
7 | isDark ? 'is-dark' : 'is-light', | |
8 | !config.footer ? 'no-footer' : '', | |
9 | ]" | |
10 | > | |
11 | <DynamicTheme :themes="config.colors" /> | |
12 | <div id="bighead"> | |
13 | <section v-if="config.header" class="first-line"> | |
14 | <div v-cloak class="container"> | |
15 | <div class="logo"> | |
16 | <a href="#"> | |
17 | <img v-if="config.logo" :src="config.logo" alt="dashboard logo" /> | |
18 | </a> | |
19 | <i v-if="config.icon" :class="config.icon"></i> | |
20 | </div> | |
21 | <div | |
22 | class="dashboard-title" | |
23 | :class="{ 'no-logo': !config.icon || !config.logo }" | |
24 | > | |
25 | <span class="headline">{{ config.subtitle }}</span> | |
26 | <h1>{{ config.title }}</h1> | |
27 | </div> | |
28 | </div> | |
29 | </section> | |
30 | ||
31 | <Navbar | |
32 | :open="showMenu" | |
33 | :links="config.links" | |
34 | @navbar-toggle="showMenu = !showMenu" | |
35 | > | |
36 | <DarkMode | |
37 | @updated="isDark = $event" | |
38 | :defaultValue="this.config.defaults.colorTheme" | |
39 | /> | |
40 | ||
41 | <SettingToggle | |
42 | @updated="vlayout = $event" | |
43 | name="vlayout" | |
44 | icon="fa-list" | |
45 | iconAlt="fa-columns" | |
46 | :defaultValue="this.config.defaults.layout == 'columns'" | |
47 | /> | |
48 | ||
49 | <SearchInput | |
50 | class="navbar-item is-inline-block-mobile" | |
51 | :hotkey="searchHotkey()" | |
52 | @input="filterServices" | |
53 | @search-focus="showMenu = true" | |
54 | @search-open="navigateToFirstService" | |
55 | @search-cancel="filterServices" | |
56 | /> | |
57 | </Navbar> | |
58 | </div> | |
59 | ||
60 | <section id="main-section" class="section"> | |
61 | <div v-cloak class="container"> | |
62 | <ConnectivityChecker | |
63 | v-if="config.connectivityCheck" | |
64 | @network-status-update="offline = $event" | |
65 | /> | |
66 | ||
67 | <GetStarted v-if="configurationNeeded" /> | |
68 | ||
69 | <div v-if="!offline"> | |
70 | <!-- Optional messages --> | |
71 | <Message :item="config.message" /> | |
72 | ||
73 | <!-- Horizontal layout --> | |
74 | <div v-if="!vlayout || filter" class="columns is-multiline"> | |
75 | <template v-for="(group, groupIndex) in services"> | |
76 | <h2 | |
77 | v-if="group.name" | |
78 | class="column is-full group-title" | |
79 | :key="`header-${groupIndex}`" | |
80 | > | |
81 | <i v-if="group.icon" :class="['fa-fw', group.icon]"></i> | |
82 | <div v-else-if="group.logo" class="group-logo media-left"> | |
83 | <figure class="image is-48x48"> | |
84 | <img :src="group.logo" :alt="`${group.name} logo`" /> | |
85 | </figure> | |
86 | </div> | |
87 | {{ group.name }} | |
88 | </h2> | |
89 | <Service | |
90 | v-for="(item, index) in group.items" | |
91 | :key="`service-${groupIndex}-${index}`" | |
92 | :item="item" | |
93 | :proxy="config.proxy" | |
94 | :class="['column', `is-${12 / config.columns}`]" | |
95 | /> | |
96 | </template> | |
97 | </div> | |
98 | ||
99 | <!-- Vertical layout --> | |
100 | <div | |
101 | v-if="!filter && vlayout" | |
102 | class="columns is-multiline layout-vertical" | |
103 | > | |
104 | <div | |
105 | :class="['column', `is-${12 / config.columns}`]" | |
106 | v-for="(group, groupIndex) in services" | |
107 | :key="groupIndex" | |
108 | > | |
109 | <h2 v-if="group.name" class="group-title"> | |
110 | <i v-if="group.icon" :class="['fa-fw', group.icon]"></i> | |
111 | <div v-else-if="group.logo" class="group-logo media-left"> | |
112 | <figure class="image is-48x48"> | |
113 | <img :src="group.logo" :alt="`${group.name} logo`" /> | |
114 | </figure> | |
115 | </div> | |
116 | {{ group.name }} | |
117 | </h2> | |
118 | <Service | |
119 | v-for="(item, index) in group.items" | |
120 | :key="index" | |
121 | :item="item" | |
122 | :proxy="config.proxy" | |
123 | /> | |
124 | </div> | |
125 | </div> | |
126 | </div> | |
127 | </div> | |
128 | </section> | |
129 | ||
130 | <footer class="footer"> | |
131 | <div class="container"> | |
132 | <div | |
133 | class="content has-text-centered" | |
134 | v-if="config.footer" | |
135 | v-html="config.footer" | |
136 | ></div> | |
137 | </div> | |
138 | </footer> | |
139 | </div> | |
140 | </template> | |
141 | ||
142 | <script> | |
143 | const jsyaml = require("js-yaml"); | |
144 | const merge = require("lodash.merge"); | |
145 | ||
146 | import Navbar from "./components/Navbar.vue"; | |
147 | import GetStarted from "./components/GetStarted.vue"; | |
148 | import ConnectivityChecker from "./components/ConnectivityChecker.vue"; | |
149 | import Service from "./components/Service.vue"; | |
150 | import Message from "./components/Message.vue"; | |
151 | import SearchInput from "./components/SearchInput.vue"; | |
152 | import SettingToggle from "./components/SettingToggle.vue"; | |
153 | import DarkMode from "./components/DarkMode.vue"; | |
154 | import DynamicTheme from "./components/DynamicTheme.vue"; | |
155 | ||
156 | import defaultConfig from "./assets/defaults.yml"; | |
157 | ||
158 | export default { | |
159 | name: "App", | |
160 | components: { | |
161 | Navbar, | |
162 | GetStarted, | |
163 | ConnectivityChecker, | |
164 | Service, | |
165 | Message, | |
166 | SearchInput, | |
167 | SettingToggle, | |
168 | DarkMode, | |
169 | DynamicTheme, | |
170 | }, | |
171 | data: function () { | |
172 | return { | |
173 | loaded: false, | |
174 | configNotFound: false, | |
175 | config: null, | |
176 | services: null, | |
177 | offline: false, | |
178 | filter: "", | |
179 | vlayout: true, | |
180 | isDark: null, | |
181 | showMenu: false, | |
182 | }; | |
183 | }, | |
184 | computed: { | |
185 | configurationNeeded: function () { | |
186 | return (this.loaded && !this.services) || this.configNotFound; | |
187 | }, | |
188 | }, | |
189 | created: async function () { | |
190 | this.buildDashboard(); | |
191 | window.onhashchange = this.buildDashboard; | |
192 | this.loaded = true; | |
193 | }, | |
194 | methods: { | |
195 | searchHotkey() { | |
196 | if (this.config.hotkey && this.config.hotkey.search) { | |
197 | return this.config.hotkey.search; | |
198 | } | |
199 | }, | |
200 | buildDashboard: async function () { | |
201 | const defaults = jsyaml.load(defaultConfig); | |
202 | let config; | |
203 | try { | |
204 | config = await this.getConfig(); | |
205 | const path = | |
206 | window.location.hash.substring(1) != "" | |
207 | ? window.location.hash.substring(1) | |
208 | : null; | |
209 | ||
210 | if (path) { | |
211 | let pathConfig = await this.getConfig(`assets/${path}.yml`); // the slash (/) is included in the pathname | |
212 | config = Object.assign(config, pathConfig); | |
213 | } | |
214 | } catch (error) { | |
215 | console.log(error); | |
216 | config = this.handleErrors("⚠️ Error loading configuration", error); | |
217 | } | |
218 | this.config = merge(defaults, config); | |
219 | this.services = this.config.services; | |
220 | ||
221 | document.title = | |
222 | this.config.documentTitle || | |
223 | `${this.config.title} | ${this.config.subtitle}`; | |
224 | if (this.config.stylesheet) { | |
225 | let stylesheet = ""; | |
226 | for (const file of this.config.stylesheet) { | |
227 | stylesheet += `@import "${file}";`; | |
228 | } | |
229 | this.createStylesheet(stylesheet); | |
230 | } | |
231 | }, | |
232 | getConfig: function (path = "assets/config.yml") { | |
233 | return fetch(path).then((response) => { | |
234 | if (response.status == 404 || response.redirected) { | |
235 | this.configNotFound = true; | |
236 | return {}; | |
237 | } | |
238 | ||
239 | if (!response.ok) { | |
240 | throw Error(`${response.statusText}: ${response.body}`); | |
241 | } | |
242 | ||
243 | const that = this; | |
244 | return response | |
245 | .text() | |
246 | .then((body) => { | |
247 | return jsyaml.load(body); | |
248 | }) | |
249 | .then(function (config) { | |
250 | if (config.externalConfig) { | |
251 | return that.getConfig(config.externalConfig); | |
252 | } | |
253 | return config; | |
254 | }); | |
255 | }); | |
256 | }, | |
257 | matchesFilter: function (item) { | |
258 | return ( | |
259 | item.name.toLowerCase().includes(this.filter) || | |
260 | (item.subtitle && item.subtitle.toLowerCase().includes(this.filter)) || | |
261 | (item.tag && item.tag.toLowerCase().includes(this.filter)) || | |
262 | (item.keywords && item.keywords.toLowerCase().includes(this.filter)) | |
263 | ); | |
264 | }, | |
265 | navigateToFirstService: function (target) { | |
266 | try { | |
267 | const service = this.services[0].items[0]; | |
268 | window.open(service.url, target || service.target || "_self"); | |
269 | } catch (error) { | |
270 | console.warning("fail to open service"); | |
271 | } | |
272 | }, | |
273 | filterServices: function (filter) { | |
274 | this.filter = filter; | |
275 | ||
276 | if (!filter) { | |
277 | this.services = this.config.services; | |
278 | return; | |
279 | } | |
280 | ||
281 | const searchResultItems = []; | |
282 | for (const group of this.config.services) { | |
283 | for (const item of group.items) { | |
284 | if (this.matchesFilter(item)) { | |
285 | searchResultItems.push(item); | |
286 | } | |
287 | } | |
288 | } | |
289 | ||
290 | this.services = [ | |
291 | { | |
292 | name: filter, | |
293 | icon: "fas fa-search", | |
294 | items: searchResultItems, | |
295 | }, | |
296 | ]; | |
297 | }, | |
298 | handleErrors: function (title, content) { | |
299 | return { | |
300 | message: { | |
301 | title: title, | |
302 | style: "is-danger", | |
303 | content: content, | |
304 | }, | |
305 | }; | |
306 | }, | |
307 | createStylesheet: function (css) { | |
308 | let style = document.createElement("style"); | |
309 | style.appendChild(document.createTextNode(css)); | |
310 | document.head.appendChild(style); | |
311 | }, | |
312 | }, | |
313 | }; | |
314 | </script> |