M src/components/host/host-bulk-download-modal.vue => src/components/host/host-bulk-download-modal.vue +21 -241
@@ 41,17 41,8 @@
</template>
<script setup>
-import { computed, inject, nextTick, ref, unref, watch } from 'vue'
-import { ZipWriter } from '@zip.js/zip.js'
-import { compareDates } from '@/utils/date'
-import {
- download,
- generateAgentConf,
- generateAgentSetupConf,
- generateWgQuickConf,
-} from '@/utils/download'
-import { env } from '@/utils/env'
-import { toast } from '@/utils/message'
+import { inject, nextTick, ref, unref, watch } from 'vue'
+import { useHostBulkDownload } from '@/mixins/host/use-host-bulk-download'
import { normalizeModel, normalizeModels, simulateModel } from '@/utils/model'
import { getPageantryParams, emptyPageantryState } from '@/utils/pageantry'
@@ 76,57 67,34 @@ const emit = defineEmits([
const $axiosApi = inject('$axiosApi')
-/** Promise to generate zip file content. */
-let zipPromise = null
-/** Active ZipWriter object. */
-let zipWriter = null
+const {
+ data,
+ endpointsTotal,
+ loading,
+ active,
+ cancelOrCloseLabel,
+ loadingProgress,
+ done,
+ total,
+ cancelOrCloseModal,
+ abortHost,
+ modelHost,
+ log,
+ findHosts,
+ makeSafeName,
+ initZip,
+ addToZip,
+ downloadZip,
+} = useHostBulkDownload(props, emit)
-/**
- * List of processed host models.
- * Each host model has an added `log` meta attr for list of log messages.
- */
-const data = ref([])
/** List of endpoints paged thus far. */
const endpoints = ref([])
-/** Total count of endpoints. */
-const endpointsTotal = ref(0)
/** Paging for endpoints. */
const endpointsPageantry = ref(emptyPageantryState())
-/** True if processing. */
-const loading = ref(false)
-
-/** True if dialog opened. */
-const active = computed({
- get: () => props.modelValue,
- set: (v, old) => (v !== old ? emit('update:modelValue', v) : undefined),
-})
-/** Label of close/cancel button. */
-const cancelOrCloseLabel = computed(() => (unref(loading) ? 'Cancel' : 'Close'))
-/** Progress percentage (0.0-1.0). */
-const loadingProgress = computed(() => {
- const t = unref(total)
- return t ? (unref(done) * 100) / t : 0
-})
-/** Count of processed host names. */
-const done = computed(() => data.value.length)
-/** Total host names to process. */
-const total = computed(() => props.hosts.length || unref(endpointsTotal))
watch(active, (v) => (v ? load() : undefined))
/**
- * Cancels the processing; or if finished processing, closes the dialog.
- */
-function cancelOrCloseModal() {
- if (loading.value) {
- loading.value = false
- emit('cancel')
- } else {
- active.value = false
- }
-}
-
-/**
* Starts the processing.
*/
async function load() {
@@ 172,18 140,6 @@ async function loadNext() {
}
/**
- * Returns true if processing has been canceled or a warning message logged.
- * @param host Host model to check for warning message.
- * @return {boolean} True if aborting.
- */
-function abortHost(host) {
- return (
- !unref(loading) ||
- host.meta.log.some((x) => ['danger', 'warning'].includes(x.variant))
- )
-}
-
-/**
* Adds the specified host model to the list of processed models,
* and queues the next set of work.
* @param host Host model.
@@ 195,28 151,6 @@ function nextHost(host) {
}
/**
- * Ensures the specified host has an appropriate model.
- * @param host Host model.
- * @param {string} name Host name if host is null.
- * @return Annotated host model.
- */
-function modelHost(host, name) {
- host = host || simulateModel({ attr: { name } })
- if (!host.meta.log) host.meta.log = []
- return host
-}
-
-/**
- * Addes a log message to the specified host model.
- * @param host Host model.
- * @param {string} message Message to log.
- * @param {string} variant Log level (eg 'warning').
- */
-function log(host, message, variant) {
- host.meta.log.push({ message, variant })
-}
-
-/**
* Finds the specified host by name.
* @param {string} name Host name.
* @return Host model.
@@ 243,18 177,6 @@ async function findHost(name) {
}
/**
- * Finds the list of hosts with the specified name.
- * @param {string} name Host name.
- * @return List of host models.
- */
-async function findHosts(name) {
- const { data } = await $axiosApi.get('/hosts', {
- params: { q: `"${name}"`, field: 'name' },
- })
- return normalizeModels(data)
-}
-
-/**
* Finds all interfaces for the specified host.
* @param host Host model.
* @return List of interface models.
@@ 306,148 228,6 @@ async function loadMoreEndpoints() {
endpointsTotal.value = data.meta.total
endpoints.value.push(...normalizeModels(data))
}
-
-/**
- * Replaces filename-unsafe characters with dashes.
- * @param {string} name Filename.
- * @return {string} Safe filename.
- */
-function makeSafeName(name) {
- // eslint-disable-next-line no-control-regex
- return name ? name.replace(/[\x00–\x1f\x7f"*/:<>?\\|]+/g, '') : ''
-}
-
-/**
- * Initializes the zip file structures.
- */
-function initZip() {
- if (zipWriter) zipWriter.close()
- if (props.downloadFiles === 'nothing') return
-
- const { readable, writable } = new TransformStream()
- zipPromise = new Response(readable).blob()
- zipWriter = new ZipWriter(writable)
-}
-
-/**
- * Adds the configuration files for specified host and interface to the zip.
- * @param host Host model.
- * @param iface Interface model.
- */
-async function addToZip(host, iface) {
- if (props.downloadFiles === 'nothing') return
-
- const directory = `hosts/${makeSafeName(host.attr.name)}`
- let result = null
-
- if (props.downloadFiles === 'wireguard') {
- iface = await loadInterfacePrivateKey(iface.id)
- result = await addFileToZip(
- `${directory}/${iface.attr.name}.conf`,
- generateWgQuickConf(iface),
- )
- } else {
- const agent = await loadAgent(host.id)
- result = await addFileToZip(
- `${directory}/${env.confName}.conf`,
- generateAgentConf(agent, host),
- )
- if (result.variant === 'success') {
- const code = await loadCode(agent.id)
- result = await addFileToZip(
- `${directory}/${env.confName}-setup.conf`,
- generateAgentSetupConf(agent, code),
- )
- }
- }
-
- log(host, result.message, result.variant)
-}
-
-/**
- * Adds the specified content to the zip under the specified path.
- * Returns a log entry indicating success or error.
- * @param {string} path Path in zip (eg 'hosts/Foo Bar/agent.conf').
- * @param {string} content File content.
- * @return Log entry with `message` and `variant` properties.
- */
-async function addFileToZip(path, content) {
- try {
- await zipWriter.add(path, new Blob([content]).stream())
- return { message: 'Zipped', variant: 'success' }
- } catch (e) {
- console.error(e) // eslint-disable-line no-console
- return { message: `Zip error: ${e}`, variant: 'danger' }
- }
-}
-
-/**
- * Completes the processing and downloads the zip file.
- */
-async function downloadZip() {
- if (zipWriter) {
- await zipWriter.close()
- zipWriter = null
- const zip = await zipPromise
- if (zip.size > 100) download(zip, `${env.confName}-hosts.zip`, 'blob')
- }
-
- const message = buildDoneMessage()
- toast(message, { variant: / 0 /.test(message) ? 'warning' : 'success' })
- loading.value = false
- emit('complete')
-}
-
-/**
- * Retreives the full key material for the specified interface, as well as its
- * enpoints (from which to generate the full wg-quick conf).
- * @param {string} interfaceId Interface pub ID.
- * @return Model for interface.
- */
-async function loadInterfacePrivateKey(interfaceId) {
- const url = `/interfaces/${interfaceId}/expected-conf-with-private-key`
- const { data } = await $axiosApi.get(url)
- return normalizeModel(data)
-}
-
-/**
- * Retrieves the active agent from the api.
- * @param {string} hostId Host pub ID.
- * @return Model for agent user.
- */
-async function loadAgent(hostId) {
- const { data } = await $axiosApi.get(`/hosts/${hostId}/members`)
- const agent = normalizeModels(data)
- .concat()
- .sort((a, b) => compareDates(a, b, (x) => x.attr.created))
- .find((x) => ['logger', 'supervisor'].includes(x.attr.membership))
- if (agent) return agent
-
- const rs = await $axiosApi.post(`/hosts/${hostId}/members/agent`)
- return normalizeModel(rs.data)
-}
-
-/**
- * Retrieves the active setup code from the api.
- * @param {string} agentId Agent pub ID.
- * @return Model for agent_setup user-code.
- */
-async function loadCode(agentId) {
- const url = `/users/${agentId}/credentials/signature/setup`
- const { data } = await $axiosApi.post(url)
- return normalizeModel(data)
-}
-
-/**
- * Builds the completion message.
- * @return {string} Completion message.
- */
-function buildDoneMessage() {
- const zipped = data.value.filter((host) => {
- return host.meta.log.some((x) => x.message === 'Zipped')
- })
- return `Zipped ${zipped.length} hosts`
-}
</script>
<style scoped lang="scss">
M src/components/host/host-bulk-upload-modal.vue => src/components/host/host-bulk-upload-modal.vue +21 -252
@@ 41,23 41,14 @@
</template>
<script setup>
-import { computed, inject, nextTick, ref, unref, watch } from 'vue'
-import { ZipWriter } from '@zip.js/zip.js'
-import { compareDates } from '@/utils/date'
-import {
- download,
- generateAgentConf,
- generateAgentSetupConf,
- generateWgQuickConf,
-} from '@/utils/download'
-import { env } from '@/utils/env'
+import { inject, nextTick, ref, unref, watch } from 'vue'
+import { useHostBulkDownload } from '@/mixins/host/use-host-bulk-download'
import {
generateNextAddress,
joinAddress,
parseAddress,
splitAddress,
} from '@/utils/ip'
-import { toast } from '@/utils/message'
import { normalizeModel, normalizeModels, simulateModel } from '@/utils/model'
import { randomBase64 } from '@/utils/text'
@@ 84,57 75,35 @@ const emit = defineEmits([
const $axiosApi = inject('$axiosApi')
-/** Promise to generate zip file content. */
-let zipPromise = null
-/** Active ZipWriter object. */
-let zipWriter = null
+const {
+ data,
+ loading,
+ active,
+ cancelOrCloseLabel,
+ loadingProgress,
+ done,
+ total,
+ cancelOrCloseModal,
+ abortHost,
+ modelHost,
+ log,
+ findHosts,
+ makeSafeName,
+ initZip,
+ addToZip,
+ downloadZip,
+} = useHostBulkDownload(props, emit)
-/**
- * List of processed host models.
- * Each host model has an added `log` meta attr for list of log messages.
- */
-const data = ref([])
/** Hub peer model. */
-const hubPeer = ref(null)
-/** True if processing. */
-const loading = ref(false)
+const hubPeer = ref()
/** Last interface address selected from each of the hub's subnets. */
const lastAddresses = ref([])
/** List of interface IP addresses already used by organization. */
const unavailableAddresses = ref([])
-/** True if dialog opened. */
-const active = computed({
- get: () => props.modelValue,
- set: (v, old) => (v !== old ? emit('update:modelValue', v) : undefined),
-})
-/** Label of close/cancel button. */
-const cancelOrCloseLabel = computed(() => (unref(loading) ? 'Cancel' : 'Close'))
-/** Progress percentage (0.0-1.0). */
-const loadingProgress = computed(() => {
- const t = unref(total)
- return t ? (unref(done) * 100) / t : 0
-})
-/** Count of processed host names. */
-const done = computed(() => data.value.length)
-/** Total host names to process. */
-const total = computed(() => props.hosts.length)
-
watch(active, (v) => (v ? load() : undefined))
/**
- * Cancels the processing; or if finished processing, closes the dialog.
- */
-function cancelOrCloseModal() {
- if (loading.value) {
- loading.value = false
- emit('cancel')
- } else {
- active.value = false
- }
-}
-
-/**
* Starts the processing.
*/
async function load() {
@@ 195,18 164,6 @@ async function loadNext() {
}
/**
- * Returns true if processing has been canceled or a warning message logged.
- * @param host Host model to check for warning message.
- * @return {boolean} True if aborting.
- */
-function abortHost(host) {
- return (
- !unref(loading) ||
- host.meta.log.some((x) => ['danger', 'warning'].includes(x.variant))
- )
-}
-
-/**
* Adds the specified host model to the list of processed models,
* and queues the next set of work.
* @param host Host model.
@@ 218,18 175,6 @@ function nextHost(host) {
}
/**
- * Ensures the specified host has an appropriate model.
- * @param host Host model.
- * @param {string} name Host name if host is null.
- * @return Annotated host model.
- */
-function modelHost(host, name) {
- host = host || simulateModel({ attr: { name } })
- host.meta = { log: [] }
- return host
-}
-
-/**
* Ensures the specified model has a "new" flag.
* @param o Model.
* @return Annotated host model.
@@ 240,16 185,6 @@ function modelNu(o) {
}
/**
- * Addes a log message to the specified host model.
- * @param host Host model.
- * @param {string} message Message to log.
- * @param {string} variant Log level (eg 'warning').
- */
-function log(host, message, variant) {
- host.meta.log.push({ message, variant })
-}
-
-/**
* Finds or creates the specified host by name.
* @param {string} name Host name.
* @return Host model.
@@ 451,18 386,6 @@ async function buildHubEndpointParams(peer, routing) {
}
/**
- * Finds the list of hosts with the specified name.
- * @param {string} name Host name.
- * @return List of host models.
- */
-async function findHosts(name) {
- const { data } = await $axiosApi.get('/hosts', {
- params: { q: `"${name}"`, field: 'name' },
- })
- return normalizeModels(data)
-}
-
-/**
* Finds the list of peers with the specified name.
* @param {string} name Peer name.
* @return List of peer models.
@@ 672,160 595,6 @@ async function generatePresharedKey() {
const { data } = await $axiosApi.post(url, { secret })
return normalizeModel(data).id
}
-
-/**
- * Replaces filename-unsafe characters with dashes.
- * @param {string} name Filename.
- * @return {string} Safe filename.
- */
-function makeSafeName(name) {
- // eslint-disable-next-line no-control-regex
- return name ? name.replace(/[\x00–\x1f\x7f"*/:<>?\\|]+/g, '') : ''
-}
-
-/**
- * Initializes the zip file structures.
- */
-function initZip() {
- if (zipWriter) zipWriter.close()
- if (props.downloadFiles === 'nothing') return
-
- const { readable, writable } = new TransformStream()
- zipPromise = new Response(readable).blob()
- zipWriter = new ZipWriter(writable)
-}
-
-/**
- * Adds the configuration files for specified host and interface to the zip.
- * @param host Host model.
- * @param iface Interface model.
- */
-async function addToZip(host, iface) {
- if (props.downloadFiles === 'nothing') return
-
- const directory = `hosts/${makeSafeName(host.attr.name)}`
- let result = null
-
- if (props.downloadFiles === 'wireguard') {
- iface = await loadInterfacePrivateKey(iface.id)
- result = await addFileToZip(
- `${directory}/${iface.attr.name}.conf`,
- generateWgQuickConf(iface),
- )
- } else {
- const agent = await loadAgent(host.id)
- result = await addFileToZip(
- `${directory}/${env.confName}.conf`,
- generateAgentConf(agent, host),
- )
- if (result.variant === 'success') {
- const code = await loadCode(agent.id)
- result = await addFileToZip(
- `${directory}/${env.confName}-setup.conf`,
- generateAgentSetupConf(agent, code),
- )
- }
- }
-
- log(host, result.message, result.variant)
-}
-
-/**
- * Adds the specified content to the zip under the specified path.
- * Returns a log entry indicating success or error.
- * @param {string} path Path in zip (eg 'hosts/Foo Bar/agent.conf').
- * @param {string} content File content.
- * @return Log entry with `message` and `variant` properties.
- */
-async function addFileToZip(path, content) {
- try {
- await zipWriter.add(path, new Blob([content]).stream())
- return { message: 'Zipped', variant: 'success' }
- } catch (e) {
- console.error(e) // eslint-disable-line no-console
- return { message: `Zip error: ${e}`, variant: 'danger' }
- }
-}
-
-/**
- * Completes the processing and downloads the zip file.
- */
-async function downloadZip() {
- if (zipWriter) {
- await zipWriter.close()
- zipWriter = null
- const zip = await zipPromise
- if (zip.size > 100) download(zip, `${env.confName}-hosts.zip`, 'blob')
- }
-
- const message = buildDoneMessage()
- toast(message, { variant: / 0 /.test(message) ? 'warning' : 'success' })
- loading.value = false
- emit('complete')
-}
-
-/**
- * Retreives the full key material for the specified interface, as well as its
- * enpoints (from which to generate the full wg-quick conf).
- * @param {string} interfaceId Interface pub ID.
- * @return Model for interface.
- */
-async function loadInterfacePrivateKey(interfaceId) {
- const url = `/interfaces/${interfaceId}/expected-conf-with-private-key`
- const { data } = await $axiosApi.get(url)
- return normalizeModel(data)
-}
-
-/**
- * Retrieves the active agent from the api.
- * @param {string} hostId Host pub ID.
- * @return Model for agent user.
- */
-async function loadAgent(hostId) {
- const { data } = await $axiosApi.get(`/hosts/${hostId}/members`)
- const agent = normalizeModels(data)
- .concat()
- .sort((a, b) => compareDates(a, b, (x) => x.attr.created))
- .find((x) => ['logger', 'supervisor'].includes(x.attr.membership))
- if (agent) return agent
-
- const rs = await $axiosApi.post(`/hosts/${hostId}/members/agent`)
- return normalizeModel(rs.data)
-}
-
-/**
- * Retrieves the active setup code from the api.
- * @param {string} agentId Agent pub ID.
- * @return Model for agent_setup user-code.
- */
-async function loadCode(agentId) {
- const url = `/users/${agentId}/credentials/signature/setup`
- const { data } = await $axiosApi.post(url)
- return normalizeModel(data)
-}
-
-/**
- * Builds the completion message.
- * @return {string} Completion message.
- */
-function buildDoneMessage() {
- if (props.downloadFiles === 'nothing') {
- const added = data.value.filter((host) => {
- return host.meta.log.some((x) => x.message === 'Created new host')
- })
- if (added.length) return `Added ${added.length} hosts`
-
- const updated = data.value.filter((host) => {
- return host.meta.log.some((x) => /^Created|^Updated/.test(x.message))
- })
- return `Updated ${updated.length} hosts`
- }
-
- const zipped = data.value.filter((host) => {
- return host.meta.log.some((x) => x.message === 'Zipped')
- })
- return `Zipped ${zipped.length} hosts`
-}
</script>
<style scoped lang="scss">
M src/mixins/host/use-host-bulk-download.js => src/mixins/host/use-host-bulk-download.js +339 -2
@@ 1,6 1,266 @@
-import { computed, inject, ref, unref } from 'vue'
+import { computed, inject, ref, toRef, unref } from 'vue'
+import { ZipWriter } from '@zip.js/zip.js'
+import { compareDates } from '@/utils/date'
+import {
+ download,
+ generateAgentConf,
+ generateAgentSetupConf,
+ generateWgQuickConf,
+} from '@/utils/download'
+import { env } from '@/utils/env'
import { splitAddress } from '@/utils/ip'
-import { normalizeModel } from '@/utils/model'
+import { toast } from '@/utils/message'
+import { normalizeModel, normalizeModels, simulateModel } from '@/utils/model'
+
+/**
+ * Cancels the processing; or if finished processing, closes the dialog.
+ * @param loading Ref to loading flag.
+ * @param active Ref to active flag.
+ * @param emit Emit function.
+ */
+function cancelOrCloseModal(loading, active, emit) {
+ if (loading.value) {
+ loading.value = false
+ emit('cancel')
+ } else {
+ active.value = false
+ }
+}
+
+/**
+ * Returns true if processing has been canceled or a warning message logged.
+ * @param host Host model to check for warning message.
+ * @param loading Ref to loading flag.
+ * @return {boolean} True if aborting.
+ */
+function abortHost(host, loading) {
+ return (
+ !unref(loading) ||
+ host.meta.log.some((x) => ['danger', 'warning'].includes(x.variant))
+ )
+}
+
+/**
+ * Ensures the specified host has an appropriate model.
+ * @param host Host model.
+ * @param {string} name Host name if host is null.
+ * @return Annotated host model.
+ */
+function modelHost(host, name) {
+ host = host || simulateModel({ attr: { name } })
+ if (!host.meta.log) host.meta.log = []
+ return host
+}
+
+/**
+ * Addes a log message to the specified host model.
+ * @param host Host model.
+ * @param {string} message Message to log.
+ * @param {string} variant Log level (eg 'warning').
+ */
+function log(host, message, variant) {
+ host.meta.log.push({ message, variant })
+}
+
+/**
+ * Finds the list of hosts with the specified name.
+ * @param {string} name Host name.
+ * @param $axiosApi Axios API instance.
+ * @return List of host models.
+ */
+async function findHosts(name, $axiosApi) {
+ const { data } = await $axiosApi.get('/hosts', {
+ params: { q: `"${name}"`, field: 'name' },
+ })
+ return normalizeModels(data)
+}
+
+/**
+ * Replaces filename-unsafe characters with dashes.
+ * @param {string} name Filename.
+ * @return {string} Safe filename.
+ */
+function makeSafeName(name) {
+ // eslint-disable-next-line no-control-regex
+ return name ? name.replace(/[\x00–\x1f\x7f"*/:<>?\\|]+/g, '') : ''
+}
+
+/**
+ * Initializes the zip file structures.
+ * @param downloadFiles Ref to download type.
+ * @param zipPromise Ref to zip promise.
+ * @param zipWriter Ref to zip writer.
+ */
+function initZip(downloadFiles, zipPromise, zipWriter) {
+ const oldWriter = unref(zipWriter)
+ if (oldWriter) oldWriter.close()
+ if (unref(downloadFiles) === 'nothing') return
+
+ const { readable, writable } = new TransformStream()
+ zipPromise.value = new Response(readable).blob()
+ zipWriter.value = new ZipWriter(writable)
+}
+
+/**
+ * Adds the configuration files for specified host and interface to the zip.
+ * @param host Host model.
+ * @param iface Interface model.
+ * @param downloadFiles Ref to download type.
+ * @param zipWriter Ref to zip writer.
+ * @param $axiosApi Axios API instance.
+ */
+async function addToZip(host, iface, downloadFiles, zipWriter, $axiosApi) {
+ const type = unref(downloadFiles)
+ if (type === 'nothing') return
+
+ const directory = `hosts/${makeSafeName(host.attr.name)}`
+ let result = null
+
+ if (type === 'wireguard') {
+ iface = await loadInterfacePrivateKey(iface.id, $axiosApi)
+ result = await addFileToZip(
+ `${directory}/${iface.attr.name}.conf`,
+ generateWgQuickConf(iface),
+ zipWriter,
+ )
+ } else {
+ const agent = await loadAgent(host.id, $axiosApi)
+ result = await addFileToZip(
+ `${directory}/${env.confName}.conf`,
+ generateAgentConf(agent, host),
+ zipWriter,
+ )
+ if (result.variant === 'success') {
+ const code = await loadCode(agent.id, $axiosApi)
+ result = await addFileToZip(
+ `${directory}/${env.confName}-setup.conf`,
+ generateAgentSetupConf(agent, code),
+ zipWriter,
+ )
+ }
+ }
+
+ log(host, result.message, result.variant)
+}
+
+/**
+ * Adds the specified content to the zip under the specified path.
+ * Returns a log entry indicating success or error.
+ * @param {string} path Path in zip (eg 'hosts/Foo Bar/agent.conf').
+ * @param {string} content File content.
+ * @param zipWriter Ref to zip writer.
+ * @return Log entry with `message` and `variant` properties.
+ */
+async function addFileToZip(path, content, zipWriter) {
+ try {
+ await zipWriter.value.add(path, new Blob([content]).stream())
+ return { message: 'Zipped', variant: 'success' }
+ } catch (e) {
+ console.error(e) // eslint-disable-line no-console
+ return { message: `Zip error: ${e}`, variant: 'danger' }
+ }
+}
+
+/**
+ * Completes the processing and downloads the zip file.
+ * @param downloadFiles Ref to download type.
+ * @param zipPromise Ref to zip promise.
+ * @param zipWriter Ref to zip writer.
+ * @param loading Ref to loading flag.
+ * @param data Ref to data list.
+ * @param emit Emit function.
+ */
+async function downloadZip(
+ downloadFiles,
+ zipPromise,
+ zipWriter,
+ loading,
+ data,
+ emit,
+) {
+ const writer = unref(zipWriter)
+ if (writer) {
+ await writer.close()
+ zipWriter.value = null
+ const zip = await unref(zipPromise)
+ if (zip.size > 100) download(zip, `${env.confName}-hosts.zip`, 'blob')
+ }
+
+ const message = buildDoneMessage(downloadFiles, data)
+ toast(message, { variant: / 0 /.test(message) ? 'warning' : 'success' })
+ loading.value = false
+ emit('complete')
+}
+
+/**
+ * Retreives the full key material for the specified interface, as well as its
+ * enpoints (from which to generate the full wg-quick conf).
+ * @param {string} interfaceId Interface pub ID.
+ * @param $axiosApi Axios API instance.
+ * @return Model for interface.
+ */
+async function loadInterfacePrivateKey(interfaceId, $axiosApi) {
+ const url = `/interfaces/${interfaceId}/expected-conf-with-private-key`
+ const { data } = await $axiosApi.get(url)
+ return normalizeModel(data)
+}
+
+/**
+ * Retrieves the active agent from the api.
+ * @param {string} hostId Host pub ID.
+ * @param $axiosApi Axios API instance.
+ * @return Model for agent user.
+ */
+async function loadAgent(hostId, $axiosApi) {
+ const { data } = await $axiosApi.get(`/hosts/${hostId}/members`)
+ const agent = normalizeModels(data)
+ .concat()
+ .sort((a, b) => compareDates(a, b, (x) => x.attr.created))
+ .find((x) => ['logger', 'supervisor'].includes(x.attr.membership))
+ if (agent) return agent
+
+ const rs = await $axiosApi.post(`/hosts/${hostId}/members/agent`)
+ return normalizeModel(rs.data)
+}
+
+/**
+ * Retrieves the active setup code from the api.
+ * @param {string} agentId Agent pub ID.
+ * @param $axiosApi Axios API instance.
+ * @return Model for agent_setup user-code.
+ */
+async function loadCode(agentId, $axiosApi) {
+ const url = `/users/${agentId}/credentials/signature/setup`
+ const { data } = await $axiosApi.post(url)
+ return normalizeModel(data)
+}
+
+/**
+ * Builds the completion message.
+ * @param downloadFiles Ref to download type.
+ * @param data Ref to data list.
+ * @return {string} Completion message.
+ */
+function buildDoneMessage(downloadFiles, data) {
+ const done = unref(data)
+
+ if (unref(downloadFiles) === 'nothing') {
+ const added = done.filter((host) => {
+ return host.meta.log.some((x) => x.message === 'Created new host')
+ })
+ if (added.length) return `Added ${added.length} hosts`
+
+ const updated = done.filter((host) => {
+ return host.meta.log.some((x) => /^Created|^Updated/.test(x.message))
+ })
+ return `Updated ${updated.length} hosts`
+ }
+
+ const zipped = done.filter((host) => {
+ return host.meta.log.some((x) => x.message === 'Zipped')
+ })
+ return `Zipped ${zipped.length} hosts`
+}
/**
* Selects the default hub host on load.
@@ 189,3 449,80 @@ Carla's Workstation
},
}
}
+
+/**
+ * Refs for host bulk download.
+ *
+ * @param props Props instance.
+ * @param emit Emit function.
+ * @return Refs.
+ */
+export function useHostBulkDownload(props, emit) {
+ const $axiosApi = inject('$axiosApi')
+ const downloadFiles = toRef(() => props.downloadFiles)
+
+ /** Promise to generate zip file content. */
+ const zipPromise = ref()
+ /** Active ZipWriter object. */
+ const zipWriter = ref()
+
+ /**
+ * List of processed host models.
+ * Each host model has an added `log` meta attr for list of log messages.
+ */
+ const data = ref([])
+ /** Expected total count of endpoints. */
+ const endpointsTotal = ref(0)
+ /** True if processing. */
+ const loading = ref(false)
+
+ /** True if dialog opened. */
+ const active = computed({
+ get: () => props.modelValue,
+ set: (v, old) => (v !== old ? emit('update:modelValue', v) : undefined),
+ })
+ /** Label of close/cancel button. */
+ const cancelOrCloseLabel = computed(() =>
+ unref(loading) ? 'Cancel' : 'Close',
+ )
+ /** Progress percentage (0.0-1.0). */
+ const loadingProgress = computed(() => {
+ const t = unref(total)
+ return t ? (unref(done) * 100) / t : 0
+ })
+ /** Count of processed host names. */
+ const done = computed(() => data.value.length)
+ /** Total host names to process. */
+ const total = computed(() => props.hosts.length || unref(endpointsTotal))
+
+ return {
+ data,
+ endpointsTotal,
+ loading,
+ active,
+ cancelOrCloseLabel,
+ loadingProgress,
+ done,
+ total,
+ cancelOrCloseModal: () => cancelOrCloseModal(loading, active, emit),
+ abortHost: (host) => abortHost(host, loading),
+ modelHost,
+ log,
+ findHosts: async (name) => findHosts(name, $axiosApi),
+ makeSafeName,
+ initZip: () => initZip(downloadFiles, zipPromise, zipWriter),
+ addToZip: async (host, iface) => {
+ return addToZip(host, iface, downloadFiles, zipWriter, $axiosApi)
+ },
+ downloadZip: async () => {
+ return downloadZip(
+ downloadFiles,
+ zipPromise,
+ zipWriter,
+ loading,
+ data,
+ emit,
+ )
+ },
+ }
+}