<commit_before>import * as ko from 'knockout'
import { Image, Container, ObservableContainer, State, ApplicationRemoteDTO } from './types'
import updateContainer from './update'
import socket from './socket'
import Monitor from './monitor'

export { Image, Container, ObservableContainer, State }

class StateManager {
  images = ko.observableArray<Image>([])
  containers = ko.observableArray<ObservableContainer>([])
  hosts = ko.observableArray<Concierge.Host>([])
  registries = ko.observableArray<Concierge.Registry>([])
  applications = ko.observableArray<Concierge.ApplicationDTO>([])
  applicationRemotes = ko.observableArray<ApplicationRemoteDTO>([])
  configuration = ko.observable<Partial<Concierge.Configuration>>({})
  credentials = ko.observableArray<Concierge.Credentials>([])
  monitors = ko.observableArray<Monitor<string>>([])

  toasts = ko.observableArray<{ msg: string; cls: string; remove: () => void }>([])
  toast = {
    primary: (msg: string) => this.showToast('toast-primary', msg),
    error: (msg: string) => this.showToast('toast-error', msg),
    warn: (msg: string) => this.showToast('toast-warning', msg),
    success: (msg: string) => this.showToast('toast-success', msg)
  }

  constructor() {
    this.getContainers()
    this.getHosts()
    this.getImages()
    this.getApplications()
    this.getApplicationRemotes()
    this.getConfiguration()
    this.getCredentials()

    setInterval(() => {
      this.getContainers()
      this.getImages()
    }, 5000)

    socket.on('stats', (event: ConciergeEvent<ContainerEvent>) => {
      const container = this.containers().find(container => event.name.startsWith(container.id()))
      if (!container) {
        return
      }

      updateContainer(container, event)
    })

    socket.on('build-status', (event: ConciergeEvent<BuildStatusEvent>) => {
      const remotes = this.applicationRemotes()
      const branch = event.event
      const existing = remotes.find(
        remote => remote.applicationId === branch.applicationId && remote.remote === branch.remote
      )

      if (existing) {
        const imageId = branch.imageId || existing.imageId
        this.applicationRemotes.replace(existing, {
          ...existing,
          image: this.getRemoteImage(imageId),
          imageId,
          sha: branch.sha,
          state: branch.state
        })
        return
      }

      this.applicationRemotes.push({
        id: 0,
        ...branch,
        imageId: branch.imageId || '',
        image: this.getRemoteImage(branch.imageId)
      })
    })
  }

  showToast = (cls: string, msg: string, duration: number = 5000) => {
    const toast = { msg, cls, remove: () => this.toasts.remove(toast) }
    this.toasts.push(toast)
    setTimeout(toast.remove, duration)
  }

  monitor = (event: string, name: string) => {
    const monitors = this.monitors()
    const match = monitors.find(mon => mon.event === event && mon.name === name)
    if (match) {
      return
    }
    this.monitors.push(new Monitor<string>(event, name))
  }

  getContainers = async () => {
    const containers: Container[] = await fetch('/api/hosts/containers').then(res => res.json())

    const seenContainers: string[] = []
    const stateContainers = this.containers()
    containers.forEach(container => {
      seenContainers.push(container.Id)
      const existing = stateContainers.find(c => container.Id.startsWith(c.id()))
      if (existing) {
        existing.state(container.State)
        existing.status(container.Status)

        const ports = portsToUrls(container)

        for (const port of ports) {
          const existingPort = existing.ports().find(ep => ep.private === port.private)
          if (!existingPort) {
            existing.ports.push(port)
            continue
          }

          if (existingPort.url !== port.url) {
            existing.ports.remove(existingPort)
            existing.ports.push(port)
          }
        }
        return
      }

      const newContainer = toObservableContainer(container)
      this.containers.push(newContainer)
    })

    // Remove every unseen container as it's probably been deleted
    this.containers.remove(c => {
      const wasSeen = seenContainers.some(seenId => seenId.startsWith(c.id()))
      return !wasSeen
    })
  }

  getHosts = () => {
    fetch('/api/hosts')
      .then(res => res.json())
      .then(hosts => {
        this.hosts.removeAll()
        this.hosts.push(...hosts)
      })
  }

  getImages = () => {
    fetch('/api/images')
      .then(res => res.json())
      .then(images => {
        for (const image of images) {
          // Remove deleted images
          this.images.remove(item => images.every((existing: Image) => existing.Id !== item.Id))

          const name = getTag(image.RepoTags || [])
          if (!name) {
            continue
          }

          image.name = name
          const existing = this.images().find(existing => existing.name === name)
          if (existing) {
            // Take no action if we already have the image and the Id has not changed
            if (existing.Id === image.Id) {
              continue
            }

            // Remove an existing image if it has a new Id
            // I.e. The image has been replaced with a new build
            this.images.replace(existing, image)
            continue
          }

          this.images.push(image)
        }
      })
  }

  getApplications = () => {
    fetch('/api/applications')
      .then(res => res.json())
      .then(apps => {
        for (const app of apps) {
          const existing = this.applications().find(ex => app.id === ex.id)
          if (!existing) {
            this.applications.push(app)
            continue
          }

          this.applications.replace(existing, app)
        }
      })
  }<commit_msg>Bugfix: Remove applications from state that have been deleted<commit_after>import * as ko from 'knockout'
import { Image, Container, ObservableContainer, State, ApplicationRemoteDTO } from './types'
import updateContainer from './update'
import socket from './socket'
import Monitor from './monitor'

export { Image, Container, ObservableContainer, State }

class StateManager {
  images = ko.observableArray<Image>([])
  containers = ko.observableArray<ObservableContainer>([])
  hosts = ko.observableArray<Concierge.Host>([])
  registries = ko.observableArray<Concierge.Registry>([])
  applications = ko.observableArray<Concierge.ApplicationDTO>([])
  applicationRemotes = ko.observableArray<ApplicationRemoteDTO>([])
  configuration = ko.observable<Partial<Concierge.Configuration>>({})
  credentials = ko.observableArray<Concierge.Credentials>([])
  monitors = ko.observableArray<Monitor<string>>([])

  toasts = ko.observableArray<{ msg: string; cls: string; remove: () => void }>([])
  toast = {
    primary: (msg: string) => this.showToast('toast-primary', msg),
    error: (msg: string) => this.showToast('toast-error', msg),
    warn: (msg: string) => this.showToast('toast-warning', msg),
    success: (msg: string) => this.showToast('toast-success', msg)
  }

  constructor() {
    this.getContainers()
    this.getHosts()
    this.getImages()
    this.getApplications()
    this.getApplicationRemotes()
    this.getConfiguration()
    this.getCredentials()

    setInterval(() => {
      this.getContainers()
      this.getImages()
    }, 5000)

    socket.on('stats', (event: ConciergeEvent<ContainerEvent>) => {
      const container = this.containers().find(container => event.name.startsWith(container.id()))
      if (!container) {
        return
      }

      updateContainer(container, event)
    })

    socket.on('build-status', (event: ConciergeEvent<BuildStatusEvent>) => {
      const remotes = this.applicationRemotes()
      const branch = event.event
      const existing = remotes.find(
        remote => remote.applicationId === branch.applicationId && remote.remote === branch.remote
      )

      if (existing) {
        const imageId = branch.imageId || existing.imageId
        this.applicationRemotes.replace(existing, {
          ...existing,
          image: this.getRemoteImage(imageId),
          imageId,
          sha: branch.sha,
          state: branch.state
        })
        return
      }

      this.applicationRemotes.push({
        id: 0,
        ...branch,
        imageId: branch.imageId || '',
        image: this.getRemoteImage(branch.imageId)
      })
    })
  }

  showToast = (cls: string, msg: string, duration: number = 5000) => {
    const toast = { msg, cls, remove: () => this.toasts.remove(toast) }
    this.toasts.push(toast)
    setTimeout(toast.remove, duration)
  }

  monitor = (event: string, name: string) => {
    const monitors = this.monitors()
    const match = monitors.find(mon => mon.event === event && mon.name === name)
    if (match) {
      return
    }
    this.monitors.push(new Monitor<string>(event, name))
  }

  getContainers = async () => {
    const containers: Container[] = await fetch('/api/hosts/containers').then(res => res.json())

    const seenContainers: string[] = []
    const stateContainers = this.containers()
    containers.forEach(container => {
      seenContainers.push(container.Id)
      const existing = stateContainers.find(c => container.Id.startsWith(c.id()))
      if (existing) {
        existing.state(container.State)
        existing.status(container.Status)

        const ports = portsToUrls(container)

        for (const port of ports) {
          const existingPort = existing.ports().find(ep => ep.private === port.private)
          if (!existingPort) {
            existing.ports.push(port)
            continue
          }

          if (existingPort.url !== port.url) {
            existing.ports.remove(existingPort)
            existing.ports.push(port)
          }
        }
        return
      }

      const newContainer = toObservableContainer(container)
      this.containers.push(newContainer)
    })

    // Remove every unseen container as it's probably been deleted
    this.containers.remove(c => {
      const wasSeen = seenContainers.some(seenId => seenId.startsWith(c.id()))
      return !wasSeen
    })
  }

  getHosts = () => {
    fetch('/api/hosts')
      .then(res => res.json())
      .then(hosts => {
        this.hosts.removeAll()
        this.hosts.push(...hosts)
      })
  }

  getImages = () => {
    fetch('/api/images')
      .then(res => res.json())
      .then(images => {
        for (const image of images) {
          // Remove deleted images
          this.images.remove(item => images.every((existing: Image) => existing.Id !== item.Id))

          const name = getTag(image.RepoTags || [])
          if (!name) {
            continue
          }

          image.name = name
          const existing = this.images().find(existing => existing.name === name)
          if (existing) {
            // Take no action if we already have the image and the Id has not changed
            if (existing.Id === image.Id) {
              continue
            }

            // Remove an existing image if it has a new Id
            // I.e. The image has been replaced with a new build
            this.images.replace(existing, image)
            continue
          }

          this.images.push(image)
        }
      })
  }

  getApplications = () => {
    fetch('/api/applications')
      .then(res => res.json())
      .then((apps: Concierge.ApplicationDTO[]) => {
        // Remove applications that have been deleted
        this.applications.remove(ex => apps.every(app => app.id !== ex.id))

        for (const app of apps) {
          const existing = this.applications().find(ex => app.id === ex.id)
          if (!existing) {
            this.applications.push(app)
            continue
          }

          this.applications.replace(existing, app)
        }
      })
  }
{'fraction_non_alphanumeric': 0.13756212618987448, 'fraction_numerical': 0.001516300227445034, 'mean_word_length': 2.4124748490945676, 'pattern_counts': {'":': 0, '<': 33, '<?xml version=': 0, '>': 103, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '4606512', 'n_tokens_mistral': 3448, 'n_tokens_neox': 3385, 'n_words': 725}