|
110 | 110 | {{ isFiltered ? $t("toolbar.download-filtered") : $t("toolbar.download") }} |
111 | 111 | </a> |
112 | 112 | </li> |
| 113 | + <li v-if="isSupported"> |
| 114 | + <a @click="copyLogs()"> |
| 115 | + <mdi:content-copy /> |
| 116 | + {{ isFiltered ? $t("toolbar.copy-filtered-logs") : $t("toolbar.copy-logs") }} |
| 117 | + </a> |
| 118 | + </li> |
113 | 119 | <li> |
114 | 120 | <a @click="copyPermalink()"> |
115 | 121 | <material-symbols:link /> |
@@ -189,7 +195,7 @@ const { actionStates, start, stop, restart } = useContainerActions(toRef(() => c |
189 | 195 | const router = useRouter(); |
190 | 196 | const { copy, copied, isSupported } = useClipboard(); |
191 | 197 | const { t } = useI18n(); |
192 | | -const { showToast } = useToast(); |
| 198 | +const { showToast, removeToast } = useToast(); |
193 | 199 |
|
194 | 200 | async function copyPermalink() { |
195 | 201 | const url = router.resolve({ |
@@ -225,6 +231,68 @@ async function copyPermalink() { |
225 | 231 | } |
226 | 232 | } |
227 | 233 |
|
| 234 | +async function copyLogs() { |
| 235 | + const params = new URLSearchParams(); |
| 236 | + if (streamConfig.value.stdout) params.append("stdout", "1"); |
| 237 | + if (streamConfig.value.stderr) params.append("stderr", "1"); |
| 238 | + params.append("everything", "1"); |
| 239 | +
|
| 240 | + const { debouncedSearchFilter } = useSearchFilter(); |
| 241 | + if (debouncedSearchFilter.value) { |
| 242 | + params.append("filter", debouncedSearchFilter.value); |
| 243 | + } |
| 244 | +
|
| 245 | + const selectedLevels = Array.from(levels.value); |
| 246 | + if (selectedLevels.length > 0 && selectedLevels.length < allLevels.length) { |
| 247 | + selectedLevels.forEach((level) => params.append("levels", level)); |
| 248 | + } |
| 249 | +
|
| 250 | + const url = withBase(`/api/hosts/${container.host}/containers/${container.id}/logs?${params.toString()}`); |
| 251 | +
|
| 252 | + const toastId = "copy-logs"; |
| 253 | + showToast( |
| 254 | + { |
| 255 | + id: toastId, |
| 256 | + title: t("toolbar.copying-logs"), |
| 257 | + message: "", |
| 258 | + type: "info", |
| 259 | + }, |
| 260 | + { once: true }, |
| 261 | + ); |
| 262 | +
|
| 263 | + const blobPromise = fetch(url, { headers: { Accept: "text/plain" } }) |
| 264 | + .then((response) => { |
| 265 | + if (!response.ok) throw new Error(response.statusText); |
| 266 | + return response.blob(); |
| 267 | + }) |
| 268 | + .then((blob) => { |
| 269 | + removeToast(toastId); |
| 270 | + showToast( |
| 271 | + { |
| 272 | + title: t("toasts.copied.title"), |
| 273 | + message: t("toasts.copied.message"), |
| 274 | + type: "info", |
| 275 | + }, |
| 276 | + { expire: 2000 }, |
| 277 | + ); |
| 278 | + return blob; |
| 279 | + }) |
| 280 | + .catch((err) => { |
| 281 | + removeToast(toastId); |
| 282 | + showToast( |
| 283 | + { |
| 284 | + title: "Error", |
| 285 | + message: err.message, |
| 286 | + type: "error", |
| 287 | + }, |
| 288 | + { expire: 5000 }, |
| 289 | + ); |
| 290 | + throw err; |
| 291 | + }); |
| 292 | +
|
| 293 | + await navigator.clipboard.write([new ClipboardItem({ "text/plain": blobPromise })]); |
| 294 | +} |
| 295 | +
|
228 | 296 | onKeyStroke("f", (e) => { |
229 | 297 | if (hasComplexLogs.value) { |
230 | 298 | if ((e.ctrlKey || e.metaKey) && e.shiftKey) { |
|
0 commit comments