~arx10/procustodibus-app

229d03a2bc300b3d16c5a81ebbc3d2bac96719be — Justin Ludwig a month ago 7040334
share common host-bulk-upload-modal fns
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,
      )
    },
  }
}