diff --git a/.github/actions/build-docker-image/action.yaml b/.github/actions/build-docker-image/action.yaml
index 8b55d29b..3424255b 100644
--- a/.github/actions/build-docker-image/action.yaml
+++ b/.github/actions/build-docker-image/action.yaml
@@ -1,5 +1,8 @@
-name: Build docker Image
-description: Build and tag specific Docker images
+name: Build docker image
+description: Build, tag and optionally push a Docker image to GHCR
+
+# The caller is responsible for checking out the repository first and for
+# deciding which tags apply - this action does not inspect the triggering event.
inputs:
platforms:
@@ -12,36 +15,24 @@ inputs:
required: false
image_tags:
default: ""
- description: "The tags to apply to the image"
+ description: "Semver version to tag the image with (e.g. 1.2.3). Skipped when empty."
+ required: false
+ extra_tag:
+ default: ""
+ description: "An extra raw tag to apply to the image (e.g. edge, canary, pr-42)"
required: false
push:
default: "false"
- description: "Push the image to the registries"
+ description: "Push the image to the registry"
required: false
token:
description: "Github token"
required: true
- tag_suffix:
- default: ""
- description: "Suffix to append to the image tags"
- required: false
- extra_tag:
- default: ""
- description: "An extra raw tag to apply to the image (e.g. edge, canary)"
- required: false
runs:
using: composite
steps:
- - name: Checkout
- uses: actions/checkout@v6
- with:
- persist-credentials: false
-
- - name: Set up QEMU
- uses: docker/setup-qemu-action@v4
-
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
@@ -61,38 +52,17 @@ runs:
images: ghcr.io/${{ github.repository }}
tags: |
type=sha
- type=raw,value=canary,enable=${{ github.event_name == 'workflow_dispatch' }}
- type=raw,event=workflow_dispatch,value=${{ github.event.inputs.dispatch-tag }}
- type=semver,pattern={{version}},value=${{ inputs.image_tags }},branch=develop
+ type=semver,pattern={{version}},value=${{ inputs.image_tags }}
type=raw,value=${{ inputs.extra_tag }},enable=${{ inputs.extra_tag != '' }}
- - name: Clean variables
- id: clean
- shell: bash
- run: |
- # Replace illegal characters in tag suffix
- echo suffix=$(echo "${{ inputs.tag_suffix }}" | sed -e 's/[^a-zA-Z0-9._-]/_/g') >> $GITHUB_OUTPUT
-
- - name: Add suffix to image tags
- id: tag_suffix
- shell: bash
- run: |
- delimiter="$(openssl rand -hex 8)"
- echo "tags<<${delimiter}" >> $GITHUB_OUTPUT
- # if tag_suffix is set, append it to each tag
- if [[ -n "${{ inputs.tag_suffix }}" ]]; then
- echo "${{ steps.meta.outputs.tags }}" | sed -e "s/$/-${{ steps.clean.outputs.suffix }}/" >> $GITHUB_OUTPUT
- else
- echo "${{ steps.meta.outputs.tags }}" >> $GITHUB_OUTPUT
- fi
- echo "${delimiter}" >> $GITHUB_OUTPUT
-
- - name: Build image for push
+ - name: Build image
uses: docker/build-push-action@v7.2.0
with:
+ context: .
file: ${{ inputs.docker_file }}
- tags: ${{ steps.tag_suffix.outputs.tags }}
+ tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
platforms: ${{ inputs.platforms }}
push: ${{ inputs.push == 'true' }}
-
+ cache-from: type=gha
+ cache-to: type=gha,mode=max
diff --git a/.github/actions/setup-and-cache/action.yaml b/.github/actions/setup-and-cache/action.yaml
index bdbcf07a..4a44c361 100644
--- a/.github/actions/setup-and-cache/action.yaml
+++ b/.github/actions/setup-and-cache/action.yaml
@@ -1,44 +1,21 @@
name: Setup and cache
-description: Setup for node, pnpm and cache for browser testing binaries
+description: Setup node and pnpm with a cached pnpm store
inputs:
node-version:
required: false
description: Node version for setup-node
- default: 22.x
+ default: 24.x
runs:
using: composite
steps:
- name: Install pnpm
- uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
+ uses: pnpm/action-setup@v6
- name: Set node version to ${{ inputs.node-version }}
- uses: actions/setup-node@v4
+ uses: actions/setup-node@v6
with:
node-version: ${{ inputs.node-version }}
-
- - name: Resolve package versions
- id: resolve-package-versions
- shell: bash
- run: >
- echo "$(
- node -e "
- const fs = require('fs');
- const lockfile = fs.readFileSync('./pnpm-lock.yaml', 'utf8');
- const pattern = (name) => new RegExp(name + ':\\\s+specifier: [\\\s\\\w\\\.^]+version: (\\\d+\\\.\\\d+\\\.\\\d+)');
- const nuxtVersion = lockfile.match(pattern('nuxt'))[1];
- console.log('NUXT_VERSION=' + nuxtVersion);
- "
- )" >> $GITHUB_OUTPUT
-
- - name: Print versions
- shell: bash
- run: echo "${{ toJson(steps.resolve-package-versions.outputs) }}"
-
-# - name: Check resolved package versions
-# shell: bash
-# if: contains(fromJSON('[null, "", "null"]'), steps.resolve-package-version)
-# run: echo "Failed to resolve package versions. See log above." && exit 1
-
+ cache: pnpm
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index 2a891e76..a542c242 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -12,41 +12,12 @@ concurrency:
cancel-in-progress: true
jobs:
- changed:
- runs-on: ubuntu-latest
- name: 'Check differences'
- outputs:
- should_skip: ${{ steps.changed-files.outputs.only_changed == 'true' }}
-
- steps:
- - uses: actions/checkout@v6
- with:
- persist-credentials: false
-
- - name: Get changed files
- id: changed-files
- uses: tj-actions/changed-files@a284dc1814e3fd07f2e34267fc8f81227ed29fb8 # v45.0.9
- with:
- files: |
- docs/**
- .github/**
- !.github/workflows/ci.yml
- **.md
-
run-tests:
- needs: changed
name: 'Build & Test'
- # if: needs.changed.outputs.should_skip != 'true'
- runs-on: ${{ matrix.os }}
+ runs-on: ubuntu-latest
timeout-minutes: 30
- strategy:
- matrix:
- os: [ ubuntu-latest ]
- node_version: [ 24 ]
- fail-fast: false
-
steps:
- name: Checkout
uses: actions/checkout@v6
@@ -61,12 +32,12 @@ jobs:
- uses: ./.github/actions/setup-and-cache
with:
- node-version: ${{ matrix.node_version }}
+ node-version: 24
- - uses: browser-actions/setup-chrome@c785b87e244131f27c9f19c1a33e2ead956ab7ce # v1.7.3
+ - uses: browser-actions/setup-chrome@v2
- - name: Install pnpm
- run: pnpm i
+ - name: Install dependencies
+ run: pnpm install --frozen-lockfile
- name: Build
run: pnpm run build
@@ -74,18 +45,26 @@ jobs:
- name: Test
run: pnpm run test:ci
- test-image-compilation:
- name: "Test production image compilation"
+ preview-image:
+ name: 'Build preview image'
+ if: ${{ github.event_name == 'pull_request' }}
runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ packages: write
steps:
- name: Checkout # required for finding action
uses: actions/checkout@v6
with:
persist-credentials: false
- - uses: ./.github/actions/build-docker-image
+ # Pull requests from forks get a read-only token, so they can only verify
+ # that the image still compiles - they cannot publish a preview image.
+ - name: Build preview image
+ uses: ./.github/actions/build-docker-image
with:
platforms: linux/amd64
docker_file: prod.Dockerfile
- push: 'false'
+ push: ${{ github.event.pull_request.head.repo.full_name == github.repository }}
token: ${{ secrets.GITHUB_TOKEN }}
+ extra_tag: pr-${{ github.event.number }}
diff --git a/.github/workflows/docker-preview.yaml b/.github/workflows/docker-preview.yaml
deleted file mode 100644
index 60db4ca7..00000000
--- a/.github/workflows/docker-preview.yaml
+++ /dev/null
@@ -1,59 +0,0 @@
-name: Docker Preview Images
-
-on:
- push:
- branches-ignore:
- - develop
- pull_request:
- branches:
- - develop
- types: [opened, synchronize, reopened]
-
-concurrency:
- group: ${{ github.workflow }}-${{ github.ref }}
- cancel-in-progress: true
-
-jobs:
- build-edge-image:
- name: Build and push edge Docker image
- if: ${{ github.event_name == 'push' }}
- runs-on: ubuntu-latest
- permissions:
- contents: read
- packages: write
- steps:
- - name: Checkout
- uses: actions/checkout@v6
- with:
- persist-credentials: false
-
- - name: Build edge image
- uses: ./.github/actions/build-docker-image
- with:
- platforms: linux/amd64
- docker_file: prod.Dockerfile
- push: 'true'
- token: ${{ secrets.GITHUB_TOKEN }}
- extra_tag: edge
-
- build-canary-image:
- name: Build and push canary Docker image
- if: ${{ github.event_name == 'pull_request' }}
- runs-on: ubuntu-latest
- permissions:
- contents: read
- packages: write
- steps:
- - name: Checkout
- uses: actions/checkout@v6
- with:
- persist-credentials: false
-
- - name: Build canary image
- uses: ./.github/actions/build-docker-image
- with:
- platforms: linux/amd64
- docker_file: prod.Dockerfile
- push: 'true'
- token: ${{ secrets.GITHUB_TOKEN }}
- extra_tag: canary
diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml
index 5c677e4b..84168c64 100644
--- a/.github/workflows/release.yaml
+++ b/.github/workflows/release.yaml
@@ -13,6 +13,7 @@ concurrency:
jobs:
release-please:
name: Release
+ if: ${{ github.event_name == 'push' }}
runs-on: ubuntu-latest
outputs:
released: ${{ steps.release.outputs.release_created }}
@@ -33,10 +34,13 @@ jobs:
echo "Was a release created: ${{ steps.release.outputs.release_created }}"
echo "New version: ${{ steps.release.outputs.version }}"
- build-prod-image:
- name: Build and push production Docker image
- if: ${{ needs.release-please.outputs.released == 'true' }}
+ build-image:
+ # A single build per commit: develop HEAD is always tagged `edge` and picks up
+ # the semver tag as well when release-please cut a release for that same commit.
+ # Manual runs skip release-please entirely and publish a `canary` tag instead.
+ name: Build and push Docker image
needs: release-please # Wait until pkg bumped
+ if: ${{ !cancelled() && needs.release-please.result != 'failure' }}
runs-on: ubuntu-latest
permissions:
contents: read
@@ -49,9 +53,10 @@ jobs:
- name: check-version
run: |
- echo "Version from release please: ${{ needs.release-please.outputs.version }}"
+ echo "Release created: ${{ needs.release-please.outputs.released || 'false' }}"
+ echo "Version from release please: ${{ needs.release-please.outputs.version || '(none)' }}"
- - name: Build production image
+ - name: Build image
uses: ./.github/actions/build-docker-image
with:
platforms: linux/amd64
@@ -59,24 +64,4 @@ jobs:
image_tags: ${{ needs.release-please.outputs.version }}
push: 'true'
token: ${{ secrets.GITHUB_TOKEN }}
-
- build-canary-image:
- name: Build and push canary image
- if: ${{ github.event_name == 'workflow_dispatch' }}
- runs-on: ubuntu-latest
- permissions:
- contents: read
- packages: write
- steps:
- - name: Checkout # required for finding action
- uses: actions/checkout@v6
- with:
- persist-credentials: false
-
- - name: Build canary image
- uses: ./.github/actions/build-docker-image
- with:
- platforms: linux/amd64
- docker_file: prod.Dockerfile
- push: 'true'
- token: ${{ secrets.GITHUB_TOKEN }}
+ extra_tag: ${{ github.event_name == 'workflow_dispatch' && 'canary' || 'edge' }}
diff --git a/Dockerfile b/Dockerfile
index a98848b6..f718e5bc 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -9,18 +9,17 @@ RUN corepack enable
WORKDIR /app
-# pnpm-workspace.yaml carries patchedDependencies and allowBuilds (pnpm v11+);
-# without it, patches are silently skipped and build scripts are not run.
+# for patchedDependencies
COPY pnpm-lock.yaml package.json pnpm-workspace.yaml ./
COPY patches /app/patches
-# Remove once corepack bug fixed https://github.com/nodejs/corepack/issues/612#issuecomment-2629613697
-ENV COREPACK_INTEGRITY_KEYS=0
-
RUN pnpm install --frozen-lockfile
COPY . .
+# always from scratch
+RUN rm -rf .nuxt
+
ENV NUXT_HOST=0.0.0.0
ENV NUXT_PORT=3000
diff --git a/app/components/TableRowMetadata.vue b/app/components/TableRowMetadata.vue
deleted file mode 100644
index 7cf1983c..00000000
--- a/app/components/TableRowMetadata.vue
+++ /dev/null
@@ -1,26 +0,0 @@
-
-
-
-
- Additional Metadata
-
-
- {{ prettifyKey(key as unknown as string) }}: {{ value }}
-
-
-
-
-
-
-
diff --git a/app/components/analysis/AnalysesTable.vue b/app/components/analysis/AnalysesTable.vue
index 216c1c7b..af6741a2 100644
--- a/app/components/analysis/AnalysesTable.vue
+++ b/app/components/analysis/AnalysesTable.vue
@@ -551,7 +551,7 @@ const onCloseNavToast = () => {
-
+
@@ -136,7 +141,9 @@ const links = computed(() =>
.menu-bar-header {
border-radius: 0;
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08), 0 1px 2px rgba(0, 0, 0, 0.06);
+ box-shadow:
+ 0 2px 8px rgba(0, 0, 0, 0.08),
+ 0 1px 2px rgba(0, 0, 0, 0.06);
}
.menu-bar-header .menu-bar-item {
diff --git a/app/components/table/ExpandRowButtons.vue b/app/components/table/ExpandRowButtons.vue
deleted file mode 100644
index a46d3c8c..00000000
--- a/app/components/table/ExpandRowButtons.vue
+++ /dev/null
@@ -1,54 +0,0 @@
-
-
-
-
-
-
-
-
-
-
diff --git a/app/components/table/SearchBar.vue b/app/components/table/SearchBar.vue
index a925496d..42474a5d 100644
--- a/app/components/table/SearchBar.vue
+++ b/app/components/table/SearchBar.vue
@@ -2,9 +2,9 @@
import IconField from "primevue/iconfield";
import InputIcon from "primevue/inputicon";
-const props = defineProps({
- searchTerm: [String, undefined],
-});
+const props = defineProps<{
+ searchTerm?: string;
+}>();
const emit = defineEmits(["clearFilters", "updateSearch"]);
diff --git a/app/components/uptime/BucketDrilldownDialog.vue b/app/components/uptime/BucketDrilldownDialog.vue
new file mode 100644
index 00000000..5dcb9bd3
--- /dev/null
+++ b/app/components/uptime/BucketDrilldownDialog.vue
@@ -0,0 +1,210 @@
+
+
+
+
+
+
+
diff --git a/app/components/uptime/ServiceUptimeCard.vue b/app/components/uptime/ServiceUptimeCard.vue
new file mode 100644
index 00000000..86d1ed43
--- /dev/null
+++ b/app/components/uptime/ServiceUptimeCard.vue
@@ -0,0 +1,125 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ summary.detail }}
+
+
+
+ {{ rangeStart }}
+ {{ rangeEnd }}
+
+
+
+
+
+
diff --git a/app/components/uptime/UptimeToolbar.vue b/app/components/uptime/UptimeToolbar.vue
new file mode 100644
index 00000000..d3a8a562
--- /dev/null
+++ b/app/components/uptime/UptimeToolbar.vue
@@ -0,0 +1,249 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/components/uptime/UptimeTrack.vue b/app/components/uptime/UptimeTrack.vue
new file mode 100644
index 00000000..97700efc
--- /dev/null
+++ b/app/components/uptime/UptimeTrack.vue
@@ -0,0 +1,171 @@
+
+
+
+
+
+
+
diff --git a/app/composables/useAPIFetch.ts b/app/composables/useAPIFetch.ts
index d53f6e9a..e0c0aede 100644
--- a/app/composables/useAPIFetch.ts
+++ b/app/composables/useAPIFetch.ts
@@ -1,14 +1,9 @@
import type {
AnalysisNode,
- BodyKongProjectLinkKongProjectProjectIdDatastoreDatastoreIdPost,
- DetailedAnalysis,
- EventLogResponse,
- LinkDataStoreProject,
ListServices,
Project,
ProjectNode,
- UnlinkResponse,
- UserSettings,
+ ServiceHealthHistory,
} from "~/services/Api";
import { useFetch, type UseFetchOptions, useNuxtApp } from "nuxt/app";
@@ -23,33 +18,26 @@ export function useAPIFetch(
});
}
-// Event endpoints
-export function getEvents(
+// Health endpoints
+export function getServiceHealthHistory(
query: {
- limit?: number;
- offset?: number;
start_date?: string;
end_date?: string;
- service_tag?: string;
+ service?: string[];
+ include_checks?: boolean;
+ limit?: number;
+ resolution?: number;
} = {},
opts?,
) {
- return useAPIFetch("/events", {
- ...opts,
- method: "GET",
- query: {
- limit: 50,
- ...query,
+ return useNuxtApp().$hubApi(
+ "/health/services/history",
+ {
+ ...opts,
+ method: "GET",
+ query,
},
- });
-}
-
-// Node endpoints
-export function getNodeConfiguration(opts?) {
- return useAPIFetch("/node/settings", {
- ...opts,
- method: "GET",
- });
+ );
}
// Hub endpoints
@@ -74,16 +62,6 @@ export function getProjects(opts?) {
});
}
-export function getAnalyses(opts?) {
- return useAPIFetch("/analyses", {
- ...opts,
- method: "GET",
- query: {
- sort: "-updated_at",
- },
- });
-}
-
export function getAnalysisNodes(opts?) {
return useAPIFetch(
"/analysis-nodes?include=analysis,node",
@@ -113,7 +91,7 @@ export function deleteDataStore(
cascade: boolean = false,
opts?,
) {
- return useAPIFetch(`/kong/datastore/${dataStoreIdOrName}`, {
+ return useNuxtApp().$hubApi(`/kong/datastore/${dataStoreIdOrName}`, {
...opts,
method: "DELETE",
query: {
@@ -121,43 +99,3 @@ export function deleteDataStore(
},
});
}
-
-export function linkProjectToDataStore(
- projectId: string,
- datastoreId: string,
- linkProps: BodyKongProjectLinkKongProjectProjectIdDatastoreDatastoreIdPost = {},
- opts?,
-) {
- return useAPIFetch(
- `/kong/project/${projectId}/datastore/${datastoreId}`,
- {
- ...opts,
- method: "POST",
- body: linkProps,
- },
- );
-}
-
-export function deleteProjectFromKong(projectId: string, opts?) {
- return useAPIFetch(`/kong/project/${projectId}`, {
- ...opts,
- method: "DELETE",
- });
-}
-
-// Results endpoints
-export function downloadLocalObject(objectId: string, opts?) {
- return useAPIFetch(`/local/${objectId}`, {
- ...opts,
- method: "GET",
- headers: { "Content-Disposition": "application/octet-stream" },
- });
-}
-
-export function downloadIntermediateObject(objectId: string, opts?) {
- return useAPIFetch(`/intermediate/${objectId}`, {
- ...opts,
- method: "GET",
- headers: { "Content-Disposition": "application/octet-stream" },
- });
-}
diff --git a/app/composables/useDataStoreList.ts b/app/composables/useDataStoreList.ts
index e9573179..75787532 100644
--- a/app/composables/useDataStoreList.ts
+++ b/app/composables/useDataStoreList.ts
@@ -19,7 +19,7 @@ export function buildProjectNameMap(
const DATA_ROW_UNIX_COLS = ["created_at", "updated_at"];
-export async function useDataStoreList() {
+export function useDataStoreList() {
const dataStores = ref([]);
const projectNameMap = ref