Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
d889210
introduce `Licensed Endpoints` tab and migrate controls from internal…
PhilBastian Jul 9, 2026
32f3cf3
migrate endpoint throughput graph logic
PhilBastian Jul 10, 2026
d342ae0
make throughput graph points larger to make hovering easier
PhilBastian Jul 13, 2026
048d4ee
change hidden feature toggle combo to something that isn't likely to …
PhilBastian Jul 13, 2026
a38e05e
include endpoint size calculations
PhilBastian Jul 13, 2026
28518ee
introduce second level of navigation for non-endpoint queues
PhilBastian Jul 13, 2026
cd68a5a
extract data fetch to a store and implement infrastructure/excluded q…
PhilBastian Jul 13, 2026
8576f68
- rename monthly throughput
PhilBastian Jul 13, 2026
f4b9fcb
adjust layout to flex
PhilBastian Jul 13, 2026
18c4f3c
rearrange layout
PhilBastian Jul 14, 2026
98d6e7f
include licensed size comparisons
PhilBastian Jul 14, 2026
291e52a
extra collapsed header details
PhilBastian Jul 14, 2026
9e16d67
default to collapsed
PhilBastian Jul 14, 2026
f9544eb
make main endpoint paging sticky so that it's visible when all endpoi…
PhilBastian Jul 14, 2026
85aa1e0
change detail order for better association
PhilBastian Jul 14, 2026
caf930a
display warning on navigation controls when license is potentially in…
PhilBastian Jul 15, 2026
dcbe4b7
read details from licensedetails API
PhilBastian Jul 16, 2026
70f6fc8
handle upload of metadata file
PhilBastian Jul 16, 2026
169f041
validate endpoint metadata id and licenseid match
PhilBastian Jul 17, 2026
f11a7d2
unify detail row (label: text) display
PhilBastian Jul 17, 2026
f1dcabd
onyl show license details tab when it's possible to be supported
PhilBastian Jul 20, 2026
6494b8b
fix license edition check
PhilBastian Jul 20, 2026
d085215
fix tests
PhilBastian Jul 20, 2026
5b59c2f
update local anchor to button for visibility
PhilBastian Jul 20, 2026
31df3a0
update button styles to fit designspace better
PhilBastian Jul 20, 2026
42206bc
set actual page sizes
PhilBastian Jul 20, 2026
2b6b2ec
Merge branch 'master' into licensed_endpoints
PhilBastian Jul 20, 2026
cc1993d
potential link off to pricing estimate
PhilBastian Jul 21, 2026
f4660f5
make linter happy
PhilBastian Jul 22, 2026
9c3334e
more lint issues
PhilBastian Jul 22, 2026
ebf577e
fix test after change from link to button
PhilBastian Jul 22, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/Frontend/src/assets/pagination.css
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
}

.pagination {
margin: 1.5em;
margin: 1.5rem;
}

.pagination > li > button:hover,
Expand Down
29 changes: 26 additions & 3 deletions src/Frontend/src/components/DataView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const props = withDefaults(
itemsPerPageOptions?: number[];
itemsPerPage?: number;
showPagination?: boolean;
stickyPagination?: boolean;
showItemsPerPage?: boolean;
}>(),
{ itemsPerPageOptions: () => [20, 35, 50, 75], itemsPerPage: 50, showPagination: true, showItemsPerPage: false }
Expand All @@ -18,15 +19,37 @@ const pageNumber = ref(1);
const itemsPerPage = ref(props.itemsPerPage);
const pageData = computed(() => props.data.slice((pageNumber.value - 1) * itemsPerPage.value, Math.min(pageNumber.value * itemsPerPage.value, props.data.length)));

const emit = defineEmits<{ itemsPerPageChanged: [value: number] }>();
const emit = defineEmits<{ itemsPerPageChanged: [value: number]; pageChanged: [] }>();

watch(itemsPerPage, () => emit("itemsPerPageChanged", itemsPerPage.value));
watch(pageNumber, () => emit("pageChanged"));
</script>

<template>
<slot name="data" :pageData="pageData" />
<div class="row">
<slot name="data" :pageData="pageData" :pageNumber="pageNumber" />
<div class="row" :class="{ 'sticky-pagination': stickyPagination }">
<ItemsPerPage v-if="showItemsPerPage" v-model="itemsPerPage" :options="itemsPerPageOptions" />
<PaginationStrip v-if="showPagination" v-model="pageNumber" :totalCount="data.length" :itemsPerPage="itemsPerPage" />
</div>
</template>

<style scoped>
.sticky-pagination {
position: sticky;
bottom: 0;
margin-bottom: 0;
margin-top: 3em;
}

.sticky-pagination {
--shadow-color: hsl(215deg 50% 30%);
filter: drop-shadow(2px 4px 8px hsl(from var(--shadow-color) h s l / 0.4));
}
</style>

<style>
/* use smaller margins for sticky version, since we want it close to the bottom of the screen when scrolling */
.sticky-pagination .pagination {
margin: 0 1.5rem;
}
</style>
33 changes: 33 additions & 0 deletions src/Frontend/src/components/DetailsItem.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<script setup lang="ts">
defineProps<{ label: string; tooltip?: string; ariaLabel?: string }>();
</script>

<template>
<div class="details-item">
<label>{{ label }}</label>
<!-- role and aria-label are used for tests -->
<span role="note" :title="tooltip" :aria-label="ariaLabel"><slot></slot></span>
</div>
</template>

<style scoped>
.details-item {
display: grid;
gap: 1rem;
grid-template-columns: 1fr 1fr;
align-items: center;
}

.details-item label {
font-weight: bold;
margin: 0;
}

.details-item label::after {
content: ":";
}

.details-item span {
padding: 0.375rem 0;
}
</style>
5 changes: 3 additions & 2 deletions src/Frontend/src/components/ExclamationMark.vue
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,12 @@ const props = withDefaults(
}

.danger {
color: #ce4844 !important;
/* NOTE: this is currently being overwritten from main.css. https://github.com/Particular/ServicePulse/issues/3072 should address this */
color: #ce4844;
background: linear-gradient(white, white) center/20% 72% no-repeat;
}

.warning {
color: var(--reduced-emphasis);
color: var(--bs-warning);
}
</style>
11 changes: 10 additions & 1 deletion src/Frontend/src/components/PaginationStrip.vue
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ const pages = computed(() => {
</script>

<template>
<div v-if="showPagination" class="col align-self-center">
<div v-if="showPagination" class="pagination-controls col align-self-center">
<ul aria-label="pagination" class="pagination justify-content-center">
<li v-for="page of pages" class="page-item" :key="page.key">
<button :aria-pressed="page.class?.active" :disabled="page.class?.disabled" :aria-label="page.key" class="page-link" @click="pageNumber = page.page" :class="page.class">{{ page.label }}</button>
Expand All @@ -136,4 +136,13 @@ const pages = computed(() => {
cursor: not-allowed;
pointer-events: all !important;
}

.pagination-controls {
display: flex;
justify-content: center;
}

.pagination-controls .page-link.active {
z-index: 0; /* pagination.scss has this set higher, for no obvious reason, which messes up with sticky-pagination over the top of other pagination */
}
</style>
79 changes: 40 additions & 39 deletions src/Frontend/src/components/configuration/PlatformLicense.vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { useLicenseStore } from "@/stores/LicenseStore";
import LoadingSpinner from "../LoadingSpinner.vue";
import ColumnHeader from "../ColumnHeader.vue";
import DataView from "../DataView.vue";
import DetailsItem from "../DetailsItem.vue";

const configurationStore = useConfigurationStore();
const { configuration } = storeToRefs(configurationStore);
Expand All @@ -29,13 +30,9 @@ const { licenseStatus, license } = licenseStore;
<div class="box">
<div class="row">
<div class="license-info">
<div>
<b>Platform license type:</b> <span role="note" aria-label="license-type">{{ typeText(license, configuration) }}{{ licenseEdition }}</span>
</div>

<DetailsItem label="Platform License Type" aria-label="license-type"> {{ typeText(license, configuration) }}{{ licenseEdition }} </DetailsItem>
<template v-if="licenseStatus.isSubscriptionLicense">
<div>
<b>License expiry date: </b>
<DetailsItem label="License Expiry Date">
<span
role="note"
aria-label="license-expiry-date"
Expand All @@ -47,12 +44,11 @@ const { licenseStatus, license } = licenseStore;
<span role="note" aria-label="license-days-left">{{ licenseStatus.subscriptionDaysLeft }}</span>
<exclamation-mark :type="convertToWarningLevel(licenseStatus.warningLevel)" />
</span>
<div class="license-expired-text" role="note" aria-label="license-expired" v-if="licenseStatus.isPlatformExpired">Your license expired. Please update the license to continue using the Particular Service Platform.</div>
</div>
</DetailsItem>
<div class="license-expired-text" role="note" aria-label="license-expired" v-if="licenseStatus.isPlatformExpired">Your license expired. Please update the license to continue using the Particular Service Platform.</div>
</template>
<template v-if="licenseStatus.isTrialLicense">
<div>
<b>License expiry date: </b>
<DetailsItem label="License Expiry Date">
<span
role="note"
aria-label="license-expiry-date"
Expand All @@ -64,41 +60,35 @@ const { licenseStatus, license } = licenseStore;
<span role="note" aria-label="license-days-left"> {{ licenseStatus.trialDaysLeft }}</span>
<exclamation-mark :type="convertToWarningLevel(licenseStatus.warningLevel)" />
</span>
<div class="license-expired-text" role="note" aria-label="license-expired" v-if="licenseStatus.isPlatformTrialExpired">
Your license expired. To continue using the Particular Service Platform you'll need to extend your license.
</div>
<div class="license-page-extend-trial" v-if="licenseStatus.isPlatformTrialExpiring && licenseStatus.isPlatformTrialExpired">
<a class="btn btn-default btn-primary" :href="license.license_extension_url" target="_blank">Extend your license <FAIcon :icon="faExternalLink" /></a>
</div>
</DetailsItem>
<div class="license-expired-text" role="note" aria-label="license-expired" v-if="licenseStatus.isPlatformTrialExpired">Your license expired. To continue using the Particular Service Platform you'll need to extend your license.</div>
<div class="license-page-extend-trial" v-if="licenseStatus.isPlatformTrialExpiring && licenseStatus.isPlatformTrialExpired">
<a class="btn btn-default btn-primary" :href="license.license_extension_url" target="_blank">Extend your license <FAIcon :icon="faExternalLink" /></a>
</div>
</template>
<template v-if="licenseStatus.isUpgradeProtectionLicense">
<div>
<span>
<b>Upgrade protection expiry date:</b>
<span
role="note"
aria-label="license-expiry-date"
:class="{
'license-expired': licenseStatus.isInvalidDueToUpgradeProtectionExpired,
}"
>
{{ formattedUpgradeProtectionExpiration }}
<span role="note" aria-label="license-days-left">{{ licenseStatus.upgradeDaysLeft }}</span>
<exclamation-mark :type="convertToWarningLevel(licenseStatus.warningLevel)" />
</span>
<DetailsItem label="Upgrade Protection Expiry Date">
<span
role="note"
aria-label="license-expiry-date"
:class="{
'license-expired': licenseStatus.isInvalidDueToUpgradeProtectionExpired,
}"
>
{{ formattedUpgradeProtectionExpiration }}
<span role="note" aria-label="license-days-left">{{ licenseStatus.upgradeDaysLeft }}</span>
<exclamation-mark :type="convertToWarningLevel(licenseStatus.warningLevel)" />
</span>
<div class="license-expired-text" role="note" aria-label="license-expired" v-if="licenseStatus.isValidWithExpiredUpgradeProtection || licenseStatus.isValidWithExpiringUpgradeProtection">
<b>Warning:</b> Once upgrade protection expires, you'll no longer have access to support or new product versions.
</div>
<div class="license-expired-text" v-if="licenseStatus.isInvalidDueToUpgradeProtectionExpired">Your license upgrade protection expired before this version of ServicePulse was released.</div>
</DetailsItem>
<div class="license-expired-text" role="note" aria-label="license-expired" v-if="licenseStatus.isValidWithExpiredUpgradeProtection || licenseStatus.isValidWithExpiringUpgradeProtection">
<b>Warning:</b> Once upgrade protection expires, you'll no longer have access to support or new product versions.
</div>
<div class="license-expired-text" v-if="licenseStatus.isInvalidDueToUpgradeProtectionExpired">Your license upgrade protection expired before this version of ServicePulse was released.</div>
</template>
<div>
<b>ServiceControl instance:</b>
<DetailsItem label="ServiceControl Instance">
{{ formattedInstanceName }}
</div>
<ul class="license-install-info">
</DetailsItem>
<ul class="license-install-info mt-2">
<li>
<a href="https://docs.particular.net/servicecontrol/license" target="_blank">Install or update a ServiceControl license</a>
</li>
Expand Down Expand Up @@ -143,7 +133,7 @@ const { licenseStatus, license } = licenseStore;
<style scoped>
.license-info {
font-size: 16px;
line-height: 3em;
max-width: 35em;
}

.license-install-info li {
Expand Down Expand Up @@ -171,4 +161,15 @@ const { licenseStatus, license } = licenseStore;
.licensed-endpoints span {
padding: 10px;
}

.license-expired {
display: flex;
gap: 0.5rem;
align-items: center;
}

.license-expired-text {
overflow: visible;
white-space: nowrap;
}
</style>
40 changes: 20 additions & 20 deletions src/Frontend/src/components/platformcapabilities/CapabilityCard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import { ref, computed } from "vue";
import { useRouter } from "vue-router";
import FAIcon from "@/components/FAIcon.vue";
import { faCircle, faTimes } from "@fortawesome/free-solid-svg-icons";
import { faCircle, faExternalLink, faTimes } from "@fortawesome/free-solid-svg-icons";
import type { StatusIndicator, WizardPage } from "@/components/platformcapabilities/types";
import { Capability, CapabilityStatus } from "@/components/platformcapabilities/constants";
import WizardDialog from "./WizardDialog.vue";
Expand All @@ -13,17 +13,21 @@ const emit = defineEmits<{
hide: [];
}>();

const props = defineProps<{
status: CapabilityStatus;
title: Capability;
subtitle: string;
helpButtonText: string;
helpButtonUrl: string;
description: string;
indicators?: StatusIndicator[];
isLoading?: boolean;
wizardPages?: WizardPage[];
}>();
const props = withDefaults(
defineProps<{
status: CapabilityStatus;
title: Capability;
subtitle: string;
helpButtonText: string;
helpButtonUrl: string;
description: string;
indicators?: StatusIndicator[];
isLoading?: boolean;
wizardPages?: WizardPage[];
allowDismiss?: boolean;
}>(),
{ allowDismiss: true }
);

const showWizard = ref(false);

Expand All @@ -33,14 +37,12 @@ const shouldShowWizard = computed(() => {

const dataStatus = computed(() => (props.isLoading ? "loading" : props.status.toLowerCase().replace(/ /g, "-")));

function isExternalUrl(url: string): boolean {
return url.startsWith("http://") || url.startsWith("https://");
}
const isExternalUrl = computed(() => props.helpButtonUrl.startsWith("http://") || props.helpButtonUrl.startsWith("https://"));

function handleButtonClick() {
if (shouldShowWizard.value) {
showWizard.value = true;
} else if (isExternalUrl(props.helpButtonUrl)) {
} else if (isExternalUrl.value) {
window.open(props.helpButtonUrl, "_blank");
} else {
router.push(props.helpButtonUrl);
Expand Down Expand Up @@ -83,7 +85,7 @@ function handleButtonClick() {
>
{{ props.status }}
</span>
<button class="hide-card-btn" @click="emit('hide')" v-tippy="'Hide this card'">
<button v-if="allowDismiss" class="hide-card-btn" @click="emit('hide')" v-tippy="'Hide this card'">
<FAIcon :icon="faTimes" />
</button>
</div>
Expand All @@ -94,9 +96,7 @@ function handleButtonClick() {
<div class="capability-description">
{{ props.description }}
</div>
<button class="btn btn-primary" @click="handleButtonClick">
{{ props.helpButtonText }}
</button>
<button class="btn btn-primary" @click="handleButtonClick">{{ props.helpButtonText }} <FAIcon v-if="!shouldShowWizard && isExternalUrl" :icon="faExternalLink" /></button>
</div>

<WizardDialog v-if="showWizard && wizardPages" :title="`Getting Started with ${props.title}`" :pages="wizardPages" @close="showWizard = false" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ export enum Capability {
Monitoring = "Monitoring",
Auditing = "Auditing",
Error = "Recoverability",
LicensedEndpointTracking = "Licensed Endpoint Tracking",
}
25 changes: 24 additions & 1 deletion src/Frontend/src/components/serviceControlClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,18 @@ class ServiceControlClient {
public async fetchTypedFromUrl<T>(url: string, signal?: AbortSignal): Promise<[Response, T]> {
const response = await authFetch(url, { signal });
if (!response.ok) throw new HttpError(response.status, response.statusText ?? "No response");
const data = await response.json();
if (!response.ok) {
let error: Error = new HttpError(response.status, response.statusText ?? "No response");
if (response.status === 400) {
try {
const errorResponse = await response.json();
error = new Error(errorResponse.detail ?? errorResponse.title);
// eslint-disable-next-line no-empty
} catch {}
}
throw error;
}
const data = response.status !== 204 && (await response.json());

return [response, data];
}
Expand All @@ -53,6 +64,18 @@ class ServiceControlClient {
return await authFetch(`${this.url}${suffix}`, requestOptions);
}

public async postFileToServiceControl(suffix: string, file: File, paramName = "file") {
const formData = new FormData();
formData.append(paramName, file);

const requestOptions: RequestInit = {
method: "POST",
body: formData,
};

return await authFetch(`${this.url}${suffix}`, requestOptions);
}

public async putToServiceControl(suffix: string, payload: object | null) {
const requestOptions: RequestInit = {
method: "PUT",
Expand Down
15 changes: 0 additions & 15 deletions src/Frontend/src/resources/EndpointThroughputSummary.ts

This file was deleted.

Loading