336 lines
9.1 KiB
Vue
336 lines
9.1 KiB
Vue
<script setup>
|
|
import { computed } from "vue";
|
|
import ReleaseEntityLink from "@/components/release/ReleaseEntityLink.vue";
|
|
|
|
const props = defineProps({
|
|
channels: {
|
|
type: Array,
|
|
default: () => [],
|
|
},
|
|
selectedChannelSlug: {
|
|
type: String,
|
|
default: "",
|
|
},
|
|
selectedApp: {
|
|
type: String,
|
|
default: "all",
|
|
},
|
|
selectedBranch: {
|
|
type: String,
|
|
default: "master",
|
|
},
|
|
branchOptions: {
|
|
type: Array,
|
|
default: () => [],
|
|
},
|
|
branchStatus: {
|
|
type: Object,
|
|
default: () => ({}),
|
|
},
|
|
runtimeSource: {
|
|
type: String,
|
|
default: "",
|
|
},
|
|
runtimeApiBaseUrl: {
|
|
type: String,
|
|
default: "",
|
|
},
|
|
runtimeFrontendBaseUrl: {
|
|
type: String,
|
|
default: "",
|
|
},
|
|
busy: {
|
|
type: String,
|
|
default: "",
|
|
},
|
|
loading: {
|
|
type: Boolean,
|
|
default: false,
|
|
},
|
|
refreshing: {
|
|
type: Boolean,
|
|
default: false,
|
|
},
|
|
loadingText: {
|
|
type: String,
|
|
default: "Loading release context...",
|
|
},
|
|
canDeploy: {
|
|
type: Boolean,
|
|
default: false,
|
|
},
|
|
});
|
|
|
|
const emit = defineEmits(["update:channel", "update:app", "update:branch", "test", "sync", "refresh", "open-entity"]);
|
|
|
|
const routeSlugForChannel = (channel) => {
|
|
const slug = String(channel?.slug || channel?.channel_slug || "").trim().toLowerCase();
|
|
return slug === "stable" ? "master" : slug;
|
|
};
|
|
|
|
const selectedChannel = computed(
|
|
() =>
|
|
props.channels.find((channel) => String(channel.slug || channel.channel_slug) === props.selectedChannelSlug) ||
|
|
props.channels.find((channel) => channel.default_channel) ||
|
|
props.channels[0] ||
|
|
null
|
|
);
|
|
|
|
const selectedRouteSlug = computed(() => routeSlugForChannel(selectedChannel.value) || props.selectedBranch || "master");
|
|
|
|
const selectedEndpointApps = computed(() => {
|
|
if (props.selectedApp === "frontend") return ["frontend"];
|
|
if (props.selectedApp === "api") return ["api"];
|
|
return ["frontend", "api"];
|
|
});
|
|
|
|
const isLocalRuntime = computed(() => String(props.runtimeSource || "").toLowerCase() === "local");
|
|
const releaseSourceLabel = computed(() => (isLocalRuntime.value ? "Local source" : "Deployment source"));
|
|
const runtimeFrontendBaseUrl = computed(() => {
|
|
const value = String(props.runtimeFrontendBaseUrl || "").replace(/\/+$/, "");
|
|
if (value) return value;
|
|
if (typeof window !== "undefined" && window.location?.origin) return window.location.origin;
|
|
return "/";
|
|
});
|
|
const runtimeApiBaseUrl = computed(() => String(props.runtimeApiBaseUrl || "/api").replace(/\/+$/, "") || "/api");
|
|
const endpointForApp = (app) =>
|
|
isLocalRuntime.value
|
|
? app === "api"
|
|
? runtimeApiBaseUrl.value
|
|
: runtimeFrontendBaseUrl.value
|
|
: `https://api-v2.truckwash.io/${selectedRouteSlug.value}/${app}`;
|
|
const endpointLabelForApp = (app) =>
|
|
isLocalRuntime.value
|
|
? app === "api"
|
|
? runtimeApiBaseUrl.value
|
|
: "local frontend"
|
|
: `/${selectedRouteSlug.value}/${app}`;
|
|
|
|
const appBranchStatus = computed(() => {
|
|
if (props.selectedApp === "frontend" || props.selectedApp === "api") {
|
|
return props.branchStatus?.[props.selectedApp] || {};
|
|
}
|
|
const values = [props.branchStatus?.frontend, props.branchStatus?.api].filter(Boolean);
|
|
const missing = values.find((status) => ["missing", "not_configured", "unknown"].includes(String(status.state || "").toLowerCase()));
|
|
return missing || values[0] || {};
|
|
});
|
|
|
|
const branchState = computed(() => String(appBranchStatus.value?.state || "unknown").toLowerCase());
|
|
const branchWarning = computed(() =>
|
|
["missing", "not_configured", "unknown", "stale", "failed"].includes(branchState.value)
|
|
? `${props.selectedBranch || "unknown"} is ${branchState.value || "unknown"} for the selected context.`
|
|
: ""
|
|
);
|
|
</script>
|
|
|
|
<template>
|
|
<section
|
|
class="release-context-bar"
|
|
data-testid="release-context-bar"
|
|
aria-label="Release context"
|
|
:aria-busy="loading || refreshing"
|
|
>
|
|
<div v-if="loading" class="release-context-bar__loading" data-testid="release-context-loading" role="status" aria-live="polite">
|
|
<i class="fas fa-spinner fa-spin" aria-hidden="true"></i>
|
|
<span>{{ loadingText }}</span>
|
|
</div>
|
|
<div class="release-context-bar__controls">
|
|
<label>
|
|
<span>Channel</span>
|
|
<select
|
|
class="select is-small"
|
|
:value="selectedChannelSlug"
|
|
data-testid="release-context-channel"
|
|
:disabled="loading"
|
|
@change="emit('update:channel', $event.target.value)"
|
|
>
|
|
<option v-for="channel in channels" :key="channel.id || channel.slug" :value="channel.slug">
|
|
{{ channel.name || channel.slug }} / {{ routeSlugForChannel(channel) || "unknown" }}
|
|
</option>
|
|
</select>
|
|
</label>
|
|
|
|
<label>
|
|
<span>App</span>
|
|
<select
|
|
class="select is-small"
|
|
:value="selectedApp"
|
|
data-testid="release-context-app"
|
|
:disabled="loading"
|
|
@change="emit('update:app', $event.target.value)"
|
|
>
|
|
<option value="all">All</option>
|
|
<option value="frontend">Frontend</option>
|
|
<option value="api">API</option>
|
|
</select>
|
|
</label>
|
|
|
|
<label class="release-context-bar__branch">
|
|
<span>Branch</span>
|
|
<input
|
|
class="input is-small"
|
|
list="release-context-branches"
|
|
:value="selectedBranch"
|
|
data-testid="release-context-branch"
|
|
:disabled="loading"
|
|
@change="emit('update:branch', $event.target.value)"
|
|
@keydown.enter.prevent="emit('update:branch', $event.target.value)"
|
|
/>
|
|
<datalist id="release-context-branches">
|
|
<option v-for="branch in branchOptions" :key="branch" :value="branch" />
|
|
</datalist>
|
|
</label>
|
|
</div>
|
|
|
|
<div class="release-context-bar__status">
|
|
<b-tag
|
|
:class="isLocalRuntime ? 'is-info' : 'is-success'"
|
|
data-testid="release-context-source"
|
|
>
|
|
{{ releaseSourceLabel }}
|
|
</b-tag>
|
|
<b-tag :class="branchState === 'available' || branchState === 'ready' ? 'is-success' : 'is-warning'">
|
|
{{ branchState }}
|
|
</b-tag>
|
|
<span v-if="branchWarning" class="release-context-bar__warning" data-testid="release-context-branch-warning">
|
|
{{ branchWarning }}
|
|
</span>
|
|
</div>
|
|
|
|
<div class="release-context-bar__endpoints" data-testid="release-context-endpoints">
|
|
<ReleaseEntityLink
|
|
v-for="app in selectedEndpointApps"
|
|
:key="app"
|
|
type="endpoint"
|
|
:label="endpointLabelForApp(app)"
|
|
:entity="{ url: endpointForApp(app), route_prefix: endpointLabelForApp(app), app, channel: selectedChannelSlug }"
|
|
@open="(payload) => emit('open-entity', payload)"
|
|
/>
|
|
</div>
|
|
|
|
<div class="release-context-bar__actions">
|
|
<b-button
|
|
size="is-small"
|
|
icon-left="vial"
|
|
icon-pack="fas"
|
|
:disabled="loading || !canDeploy"
|
|
:loading="busy === 'release:test'"
|
|
data-testid="release-context-test"
|
|
@click="emit('test')"
|
|
>
|
|
Test
|
|
</b-button>
|
|
<b-button
|
|
size="is-small"
|
|
icon-left="rotate"
|
|
icon-pack="fas"
|
|
:disabled="loading || !canDeploy || !selectedChannel"
|
|
:loading="busy === `channel:${selectedChannel?.id}:sync`"
|
|
data-testid="release-context-sync"
|
|
@click="emit('sync', selectedChannel)"
|
|
>
|
|
Sync
|
|
</b-button>
|
|
<b-button
|
|
size="is-small"
|
|
icon-left="refresh"
|
|
icon-pack="fas"
|
|
data-testid="release-context-refresh"
|
|
:disabled="loading || refreshing"
|
|
:loading="refreshing"
|
|
@click="emit('refresh')"
|
|
>
|
|
Refresh
|
|
</b-button>
|
|
</div>
|
|
</section>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.release-context-bar {
|
|
align-items: center;
|
|
background: #f8fafc;
|
|
border-bottom: 1px solid #d8dee8;
|
|
display: grid;
|
|
gap: 0.75rem;
|
|
grid-template-columns: minmax(0, 1.4fr) minmax(11rem, 0.6fr) minmax(0, 1fr) auto;
|
|
min-height: 4rem;
|
|
padding: 0.6rem 0.75rem;
|
|
}
|
|
|
|
.release-context-bar__loading {
|
|
align-items: center;
|
|
color: #52627a;
|
|
display: inline-flex;
|
|
font-size: 0.82rem;
|
|
font-weight: 700;
|
|
gap: 0.45rem;
|
|
grid-column: 1 / -1;
|
|
}
|
|
|
|
.release-context-bar__controls,
|
|
.release-context-bar__status,
|
|
.release-context-bar__endpoints,
|
|
.release-context-bar__actions {
|
|
align-items: center;
|
|
display: flex;
|
|
gap: 0.5rem;
|
|
min-width: 0;
|
|
}
|
|
|
|
.release-context-bar label {
|
|
display: grid;
|
|
gap: 0.2rem;
|
|
min-width: 8rem;
|
|
}
|
|
|
|
.release-context-bar label > span {
|
|
color: #667085;
|
|
font-size: 0.72rem;
|
|
font-weight: 700;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.release-context-bar__branch {
|
|
min-width: 12rem;
|
|
}
|
|
|
|
.release-context-bar__status {
|
|
align-items: flex-start;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.release-context-bar__status .tag {
|
|
flex: 0 0 auto;
|
|
}
|
|
|
|
.release-context-bar__warning {
|
|
color: #9f1f17;
|
|
flex: 1 1 12rem;
|
|
font-size: 0.82rem;
|
|
line-height: 1.25;
|
|
min-width: min(12rem, 100%);
|
|
overflow-wrap: break-word;
|
|
word-break: normal;
|
|
}
|
|
|
|
.release-context-bar__endpoints {
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.release-context-bar__actions {
|
|
justify-content: flex-end;
|
|
}
|
|
|
|
@media (max-width: 1180px) {
|
|
.release-context-bar {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.release-context-bar__controls,
|
|
.release-context-bar__actions {
|
|
flex-wrap: wrap;
|
|
}
|
|
}
|
|
</style>
|