Files
openobserve/web/src/components/EnterpriseUpgradeDialog.vue
T

1500 lines
47 KiB
Vue

<!-- Copyright 2023 OpenObserve Inc.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<template>
<q-dialog v-model="showDialog" @hide="onDialogHide">
<q-card class="enterprise-dialog-v3" style="min-width: 1200px; max-width: 1400px">
<!-- Close Button -->
<q-btn
icon="cancel"
flat
round
dense
v-close-popup
class="close-btn-top-right"
/>
<div class="dialog-split-layout" :class="{ 'cloud-layout': dialogConfig.isCloudLayout }">
<!-- Left Panel - Hero Section (hidden for Cloud) -->
<div v-if="!dialogConfig.isCloudLayout" class="hero-panel">
<div class="hero-content">
<div class="hero-icon">
<q-icon name="workspace_premium" size="48px" />
</div>
<h2 class="hero-title">{{ dialogConfig.heroTitle }}</h2>
<p class="offer-text">
{{ dialogConfig.offerText }}
</p>
<div class="hero-offer">
<!-- Loading State: Show skeleton -->
<template v-if="isLoadingLicense && dialogConfig.showUsageIndicator">
<q-skeleton
type="circle"
size="40px"
class="usage-indicator"
animation="pulse"
style="background: rgba(255, 255, 255, 0.2);"
/>
<q-skeleton
type="rect"
width="200px"
height="44px"
class="offer-badge-skeleton"
animation="pulse"
style="background: rgba(255, 255, 255, 0.2); border-radius: 24px;"
/>
</template>
<!-- Loaded State: Show actual data -->
<template v-else>
<div class="offer-badge" :class="{ 'licensed-badge': dialogConfig.isLicensed }">
<q-icon v-if="!dialogConfig.showUsageIndicator" :name="dialogConfig.badgeIcon" size="20px" class="q-mr-xs" />
<span>{{ dialogConfig.badgeText }}</span>
</div>
</template>
</div>
<!-- Usage Chart (only for Enterprise with license) -->
<div v-if="dialogConfig.isLicensed && chartData" class="usage-chart-section">
<div class="chart-wrapper">
<div class="usage-chart-container" style="height: 150px; width: 100%;">
<ChartRenderer
:key="dashboardRenderKey"
:data="chartData"
/>
</div>
<div v-if="isIngestionUnlimited" class="text-caption" style="color: rgba(255, 255, 255, 0.7); font-size: 10px; text-align: center; margin-top: 4px;">
{{ t('about.usage_shows_zero_unlimited') }}
</div>
</div>
</div>
<!-- License Limit Note (only for Enterprise without license) -->
<div v-if="dialogConfig.showLicenseNote" class="license-note">
<q-icon name="info" size="14px" class="q-mr-xs" />
<span>{{ dialogConfig.licenseNoteText }}</span>
</div>
<div class="hero-actions">
<q-btn
v-if="dialogConfig.showPrimaryButton"
unelevated
:label="dialogConfig.primaryButtonText"
@click="handlePrimaryButtonClick"
:icon-right="dialogConfig.primaryButtonIcon"
no-caps
class="download-btn"
/>
<q-btn
v-if="dialogConfig.showContactSales"
flat
:label="t('about.enterprise_offer.buttons.contact_sales')"
@click="contactSales"
no-caps
class="learn-more-btn"
color="white"
/>
<q-btn
v-else
flat
:label="t('about.enterprise_offer.buttons.learn_more')"
@click="openDocsLink"
no-caps
class="learn-more-btn"
color="white"
/>
</div>
</div>
</div>
<!-- Right Panel - Features List -->
<div class="features-panel">
<div class="features-header">
<h4>{{ dialogConfig.featuresTitle }}</h4>
<p class="header-subtitle">{{ dialogConfig.featuresSubtitle }}</p>
</div>
<!-- Cloud 3-column layout -->
<div v-if="dialogConfig.isCloudLayout" class="features-list cloud-three-column">
<!-- Column 1: Core Features -->
<div
v-for="feature in coreFeatures"
:key="feature.name"
class="feature-list-item"
:class="{ 'has-link': feature.link }"
@click="feature.link && openFeatureLink(feature.link)"
>
<div class="feature-icon-badge">
<q-icon :name="feature.icon" size="15px" />
</div>
<div class="feature-content">
<div class="feature-name">
{{ feature.name }}
<q-icon v-if="feature.link" name="open_in_new" size="12px" class="external-link-icon" />
</div>
<div class="feature-desc">{{ feature.note }}</div>
</div>
</div>
<!-- Columns 2 & 3: Enterprise Features -->
<div
v-for="feature in enterpriseFeatures"
v-show="!feature.cloudHidden"
:key="feature.name"
class="feature-list-item"
:class="{ 'has-link': feature.link }"
@click="feature.link && openFeatureLink(feature.link)"
>
<div class="feature-icon-badge">
<q-icon :name="feature.icon" size="15px" />
</div>
<div class="feature-content">
<div class="feature-name">
{{ feature.name }}
<q-icon v-if="feature.link" name="open_in_new" size="12px" class="external-link-icon" />
<span v-if="feature.beta" class="beta-badge">BETA</span>
</div>
<div class="feature-desc">{{ feature.note }}</div>
</div>
</div>
</div>
<!-- Standard 2-column layout for non-Cloud -->
<div v-else class="features-list">
<div
v-for="feature in enterpriseFeatures"
:key="feature.name"
class="feature-list-item"
:class="{ 'has-link': feature.link }"
@click="feature.link && openFeatureLink(feature.link)"
>
<div class="feature-icon-badge">
<q-icon :name="feature.icon" size="15px" />
</div>
<div class="feature-content">
<div class="feature-name">
{{ feature.name }}
<q-icon v-if="feature.link" name="open_in_new" size="12px" class="external-link-icon" />
<span v-if="feature.beta" class="beta-badge">BETA</span>
<span v-if="feature.requiresHA" class="ha-badge">
HA
<q-tooltip anchor="top middle" self="bottom middle" :offset="[0, 8]">
{{ t('about.enterprise_offer.tooltip.high_availability_mode_only') }}
</q-tooltip>
</span>
</div>
<div class="feature-desc">{{ feature.note }}</div>
</div>
</div>
</div>
</div>
</div>
</q-card>
</q-dialog>
</template>
<script lang="ts">
import { defineComponent, ref, computed, PropType, watch, defineAsyncComponent } from "vue";
import { useStore } from "vuex";
import { useRouter } from "vue-router";
import { useQuasar } from "quasar";
import { useI18n } from "vue-i18n";
import config from "@/aws-exports";
import licenseServer from "@/services/license_server";
const ChartRenderer = defineAsyncComponent(
() => import("@/components/dashboards/panels/ChartRenderer.vue")
);
// Feature documentation links configuration
const FEATURE_DOCS_BASE_URL = "https://o2.ws/";
const FEATURE_LINKS = {
// Core Features
logs_metrics_traces: "logs_metrics_traces",
rum: "rum",
alerts: "alerts",
dashboards: "dashboards",
reports: "reports",
vrl_functions: "vrl_func",
pipelines: "pipelines",
high_availability: "high_avail",
multitenancy: "multitenancy",
dynamic_schema: "dynamic_schema",
multilingual_gui: "multilang_gui",
// Enterprise Features
single_sign_on: "sso",
rbac: "rbac",
federated_search: "fed_search",
query_management: "query_mgmt",
workload_management: "workload_mgmt",
audit_trail: "audit_trail",
action_scripts: "action_scripts",
sensitive_data_redaction: "data_redact",
pipeline_remote_destinations: "pipeline_remote",
query_optimizer: "query_opt",
incident_management: "incident_mgmt",
sre_agent: "sre_agent",
ai_assistant: "ai_assistant",
anomaly_detection: "anomaly_detect",
metrics_auto_downsampling: "metrics_downsample",
log_patterns: "log_patterns",
mcp_server: "mcp_server",
rate_limiting: "rate_limit",
broadcast_join: "broadcast_join",
logs_metrics_traces_correlation: "telemetry_corr",
service_maps: "service_maps",
};
export default defineComponent({
name: "EnterpriseUpgradeDialog",
components: {
ChartRenderer,
},
props: {
modelValue: {
type: Boolean,
default: false,
},
},
emits: ["update:modelValue"],
setup(props, { emit }) {
const showDialog = ref(props.modelValue);
const store = useStore();
const router = useRouter();
const $q = useQuasar();
const { t } = useI18n();
const licenseData = ref<any>(null);
const isLoadingLicense = ref(false);
const chartData = ref<any>(null);
const dashboardRenderKey = ref(0);
// Fetch license data when dialog opens for Enterprise with license
const fetchLicenseData = async () => {
const isEnterprise = config.isEnterprise === 'true';
const isCloud = config.isCloud === 'true';
const hasLicense = store.state.zoConfig?.license_expiry && store.state.zoConfig.license_expiry !== 0;
// Only fetch for Enterprise with license (not Cloud)
if (isEnterprise && hasLicense && !isCloud) {
isLoadingLicense.value = true;
try {
const response = await licenseServer.get_license();
licenseData.value = response.data;
// Generate dashboard after license data is fetched
generateUsageDashboard();
} catch (error) {
console.error("Failed to fetch license data:", error);
// On error, set default values (0% usage, unlimited)
licenseData.value = {
license: {
limits: {
Ingestion: {
value: 0
}
}
},
ingestion_used: 0
};
} finally {
isLoadingLicense.value = false;
}
}
};
// Dialog configuration based on deployment type
const dialogConfig = computed(() => {
const isEnterprise = config.isEnterprise === 'true';
const isCloud = config.isCloud === 'true';
const hasLicense = store.state.zoConfig?.license_expiry && store.state.zoConfig.license_expiry !== 0;
// Calculate ingestion quota limit for non-licensed enterprise
// Use ingestion_quota (the limit), not ingestion_quota_used (the usage percentage)
const ingestionQuota = store.state.zoConfig?.ingestion_quota ?? 200; // Use nullish coalescing to allow 0
// Get usage percentage for circular indicator (this is already a percentage)
const usagePercentage = store.state.zoConfig?.ingestion_quota_used ?? 0;
// Cloud (check this first because Cloud has both isCloud=true and isEnterprise=true)
if (isCloud) {
return {
heroTitle: t("about.enterprise_offer.cloud.hero_title"),
offerText: t("about.enterprise_offer.cloud.offer_text"),
badgeText: t("about.enterprise_offer.cloud.badge_text"),
badgeIcon: "bolt",
featuresTitle: t("about.enterprise_offer.cloud.features_title"),
featuresSubtitle: t("about.enterprise_offer.cloud.features_subtitle"),
primaryButtonText: t("about.enterprise_offer.cloud.primary_button_text"),
primaryButtonIcon: "download",
showPrimaryButton: true,
isCloudLayout: true,
};
}
// Enterprise without license
if (isEnterprise && !hasLicense) {
return {
heroTitle: t("about.enterprise_offer.enterprise_without_license.hero_title"),
offerText: t("about.enterprise_offer.enterprise_without_license.offer_text"),
badgeText: t("about.enterprise_offer.enterprise_without_license.badge_text", { quota: ingestionQuota }),
badgeIcon: "data_usage",
showUsageIndicator: true,
usagePercentage: usagePercentage,
featuresTitle: t("about.enterprise_offer.enterprise_without_license.features_title"),
featuresSubtitle: t("about.enterprise_offer.enterprise_without_license.features_subtitle"),
primaryButtonText: t("about.enterprise_offer.enterprise_without_license.primary_button_text"),
primaryButtonIcon: "key",
showPrimaryButton: true,
showLicenseNote: true, // Show note about license limits
licenseNoteText: t("about.enterprise_offer.enterprise_without_license.license_note"),
};
}
// Enterprise with license
if (isEnterprise && hasLicense) {
// Calculate ingestion usage from license data
const ingestionLimit = licenseData.value?.license?.limits?.Ingestion?.value || 0;
const ingestionUsedPercentage = licenseData.value?.ingestion_used || 0;
const badgeText = ingestionLimit > 0
? t("about.enterprise_offer.enterprise_with_license.badge_text_limited", { limit: ingestionLimit })
: t("about.enterprise_offer.enterprise_with_license.badge_text_unlimited");
return {
heroTitle: t("about.enterprise_offer.enterprise_with_license.hero_title"),
offerText: t("about.enterprise_offer.enterprise_with_license.offer_text"),
badgeText: badgeText,
badgeIcon: "verified",
showUsageIndicator: true,
usagePercentage: ingestionUsedPercentage,
featuresTitle: t("about.enterprise_offer.enterprise_with_license.features_title"),
featuresSubtitle: t("about.enterprise_offer.enterprise_with_license.features_subtitle"),
primaryButtonText: t("about.enterprise_offer.enterprise_with_license.primary_button_text"),
primaryButtonIcon: "key",
showPrimaryButton: true,
showContactSales: true,
isLicensed: true, // Flag to apply different badge styling
};
}
// Open Source (both false) - Default fallback
return {
heroTitle: t("about.enterprise_offer.open_source.hero_title"),
offerText: t("about.enterprise_offer.open_source.offer_text"),
badgeText: t("about.enterprise_offer.open_source.badge_text"),
badgeIcon: "card_giftcard",
featuresTitle: t("about.enterprise_offer.open_source.features_title"),
featuresSubtitle: t("about.enterprise_offer.open_source.features_subtitle"),
primaryButtonText: t("about.enterprise_offer.open_source.primary_button_text"),
primaryButtonIcon: "download",
showPrimaryButton: true,
};
});
// Core features list - available in all versions (for Cloud 3-column layout)
const coreFeatures = [
{
name: t("about.enterprise_offer.core_features.logs_metrics_traces.name"),
note: t("about.enterprise_offer.core_features.logs_metrics_traces.note"),
icon: "storage",
link: FEATURE_DOCS_BASE_URL + FEATURE_LINKS.logs_metrics_traces,
},
{
name: t("about.enterprise_offer.core_features.rum.name"),
note: t("about.enterprise_offer.core_features.rum.note"),
icon: "visibility",
link: FEATURE_DOCS_BASE_URL + FEATURE_LINKS.rum,
},
{
name: t("about.enterprise_offer.core_features.alerts.name"),
note: t("about.enterprise_offer.core_features.alerts.note"),
icon: "notifications_active",
link: FEATURE_DOCS_BASE_URL + FEATURE_LINKS.alerts,
},
{
name: t("about.enterprise_offer.core_features.dashboards.name"),
note: t("about.enterprise_offer.core_features.dashboards.note"),
icon: "dashboard",
link: FEATURE_DOCS_BASE_URL + FEATURE_LINKS.dashboards,
},
{
name: t("about.enterprise_offer.core_features.reports.name"),
note: t("about.enterprise_offer.core_features.reports.note"),
icon: "description",
link: FEATURE_DOCS_BASE_URL + FEATURE_LINKS.reports,
},
{
name: t("about.enterprise_offer.core_features.vrl_functions.name"),
note: t("about.enterprise_offer.core_features.vrl_functions.note"),
icon: "functions",
link: FEATURE_DOCS_BASE_URL + FEATURE_LINKS.vrl_functions,
},
{
name: t("about.enterprise_offer.core_features.pipelines.name"),
note: t("about.enterprise_offer.core_features.pipelines.note"),
icon: "account_tree",
link: FEATURE_DOCS_BASE_URL + FEATURE_LINKS.pipelines,
},
{
name: t("about.enterprise_offer.core_features.high_availability.name"),
note: t("about.enterprise_offer.core_features.high_availability.note"),
icon: "cloud_done",
link: FEATURE_DOCS_BASE_URL + FEATURE_LINKS.high_availability,
},
{
name: t("about.enterprise_offer.core_features.multitenancy.name"),
note: t("about.enterprise_offer.core_features.multitenancy.note"),
icon: "corporate_fare",
link: FEATURE_DOCS_BASE_URL + FEATURE_LINKS.multitenancy,
},
{
name: t("about.enterprise_offer.core_features.dynamic_schema.name"),
note: t("about.enterprise_offer.core_features.dynamic_schema.note"),
icon: "schema",
link: FEATURE_DOCS_BASE_URL + FEATURE_LINKS.dynamic_schema,
},
{
name: t("about.enterprise_offer.core_features.multilingual_gui.name"),
note: t("about.enterprise_offer.core_features.multilingual_gui.note"),
icon: "language",
link: FEATURE_DOCS_BASE_URL + FEATURE_LINKS.multilingual_gui,
},
];
// Enterprise features list - all 21 features
const enterpriseFeatures = [
{
name: t("about.enterprise_offer.enterprise_features.single_sign_on.name"),
note: t("about.enterprise_offer.enterprise_features.single_sign_on.note"),
icon: "key",
requiresHA: true,
link: FEATURE_DOCS_BASE_URL + FEATURE_LINKS.single_sign_on,
},
{
name: t("about.enterprise_offer.enterprise_features.rbac.name"),
note: t("about.enterprise_offer.enterprise_features.rbac.note"),
icon: "admin_panel_settings",
requiresHA: true,
link: FEATURE_DOCS_BASE_URL + FEATURE_LINKS.rbac,
},
{
name: t("about.enterprise_offer.enterprise_features.federated_search.name"),
note: t("about.enterprise_offer.enterprise_features.federated_search.note"),
icon: "hub",
requiresHA: true,
link: FEATURE_DOCS_BASE_URL + FEATURE_LINKS.federated_search,
},
{
name: t("about.enterprise_offer.enterprise_features.query_management.name"),
note: t("about.enterprise_offer.enterprise_features.query_management.note"),
icon: "insights",
requiresHA: false,
link: FEATURE_DOCS_BASE_URL + FEATURE_LINKS.query_management,
},
{
name: t("about.enterprise_offer.enterprise_features.workload_management.name"),
note: t("about.enterprise_offer.enterprise_features.workload_management.note"),
icon: "speed",
requiresHA: false,
link: FEATURE_DOCS_BASE_URL + FEATURE_LINKS.workload_management,
},
{
name: t("about.enterprise_offer.enterprise_features.audit_trail.name"),
note: t("about.enterprise_offer.enterprise_features.audit_trail.note"),
icon: "fact_check",
requiresHA: false,
link: FEATURE_DOCS_BASE_URL + FEATURE_LINKS.audit_trail,
},
{
name: t("about.enterprise_offer.enterprise_features.action_scripts.name"),
note: t("about.enterprise_offer.enterprise_features.action_scripts.note"),
icon: "code",
requiresHA: true,
cloudHidden: true, // Hide from Cloud layout
link: FEATURE_DOCS_BASE_URL + FEATURE_LINKS.action_scripts,
},
{
name: t("about.enterprise_offer.enterprise_features.sensitive_data_redaction.name"),
note: t("about.enterprise_offer.enterprise_features.sensitive_data_redaction.note"),
icon: "shield",
requiresHA: false,
link: FEATURE_DOCS_BASE_URL + FEATURE_LINKS.sensitive_data_redaction,
},
{
name: t("about.enterprise_offer.enterprise_features.pipeline_remote_destinations.name"),
note: t("about.enterprise_offer.enterprise_features.pipeline_remote_destinations.note"),
icon: "alt_route",
requiresHA: false,
link: FEATURE_DOCS_BASE_URL + FEATURE_LINKS.pipeline_remote_destinations,
},
{
name: t("about.enterprise_offer.enterprise_features.query_optimizer.name"),
note: t("about.enterprise_offer.enterprise_features.query_optimizer.note"),
icon: "memory",
requiresHA: false,
link: FEATURE_DOCS_BASE_URL + FEATURE_LINKS.query_optimizer,
},
{
name: t("about.enterprise_offer.enterprise_features.incident_management.name"),
note: t("about.enterprise_offer.enterprise_features.incident_management.note"),
icon: "emergency",
requiresHA: true,
beta: true,
link: FEATURE_DOCS_BASE_URL + FEATURE_LINKS.incident_management,
},
{
name: t("about.enterprise_offer.enterprise_features.sre_agent.name"),
note: t("about.enterprise_offer.enterprise_features.sre_agent.note"),
icon: "smart_toy",
requiresHA: true,
beta: true,
link: FEATURE_DOCS_BASE_URL + FEATURE_LINKS.sre_agent,
},
{
name: t("about.enterprise_offer.enterprise_features.ai_assistant.name"),
note: t("about.enterprise_offer.enterprise_features.ai_assistant.note"),
icon: "psychology",
requiresHA: true,
beta: true,
link: FEATURE_DOCS_BASE_URL + FEATURE_LINKS.ai_assistant,
},
{
name: t("about.enterprise_offer.enterprise_features.anomaly_detection.name"),
note: t("about.enterprise_offer.enterprise_features.anomaly_detection.note"),
icon: "query_stats",
requiresHA: false,
beta: true,
link: FEATURE_DOCS_BASE_URL + FEATURE_LINKS.anomaly_detection,
},
{
name: t("about.enterprise_offer.enterprise_features.metrics_auto_downsampling.name"),
note: t("about.enterprise_offer.enterprise_features.metrics_auto_downsampling.note"),
icon: "compress",
requiresHA: false,
link: FEATURE_DOCS_BASE_URL + FEATURE_LINKS.metrics_auto_downsampling,
},
{
name: t("about.enterprise_offer.enterprise_features.log_patterns.name"),
note: t("about.enterprise_offer.enterprise_features.log_patterns.note"),
icon: "pattern",
requiresHA: false,
link: FEATURE_DOCS_BASE_URL + FEATURE_LINKS.log_patterns,
},
{
name: t("about.enterprise_offer.enterprise_features.mcp_server.name"),
note: t("about.enterprise_offer.enterprise_features.mcp_server.note"),
icon: "dns",
requiresHA: true,
link: FEATURE_DOCS_BASE_URL + FEATURE_LINKS.mcp_server,
},
{
name: t("about.enterprise_offer.enterprise_features.rate_limiting.name"),
note: t("about.enterprise_offer.enterprise_features.rate_limiting.note"),
icon: "speed",
requiresHA: false,
link: FEATURE_DOCS_BASE_URL + FEATURE_LINKS.rate_limiting,
},
{
name: t("about.enterprise_offer.enterprise_features.broadcast_join.name"),
note: t("about.enterprise_offer.enterprise_features.broadcast_join.note"),
icon: "call_merge",
requiresHA: true,
link: FEATURE_DOCS_BASE_URL + FEATURE_LINKS.broadcast_join,
},
{
name: t("about.enterprise_offer.enterprise_features.logs_metrics_traces_correlation.name"),
note: t("about.enterprise_offer.enterprise_features.logs_metrics_traces_correlation.note"),
icon: "auto_graph",
requiresHA: false,
link: FEATURE_DOCS_BASE_URL + FEATURE_LINKS.logs_metrics_traces_correlation,
},
{
name: t("about.enterprise_offer.enterprise_features.service_maps.name"),
note: t("about.enterprise_offer.enterprise_features.service_maps.note"),
icon: "account_tree",
requiresHA: false,
link: FEATURE_DOCS_BASE_URL + FEATURE_LINKS.service_maps,
},
];
const onDialogHide = () => {
emit("update:modelValue", false);
};
const openDownloadPage = () => {
window.open("https://o2.ws/download_resources", "_blank");
};
const openDocsLink = () => {
const isEnterprise = config.isEnterprise === 'true';
const isCloud = config.isCloud === 'true';
const hasLicense = store.state.zoConfig?.license_expiry && store.state.zoConfig.license_expiry !== 0;
let docsUrl = "https://openobserve.ai/docs/";
// Enterprise without license - redirect to license docs
if (isEnterprise && !hasLicense) {
docsUrl = "https://o2.ws/license_guide";
}
// Open Source or Cloud - redirect to enterprise edition installation guide
else if (!isEnterprise || isCloud) {
docsUrl = "https://o2.ws/ent_install_guide";
}
window.open(docsUrl, "_blank");
};
const contactSales = () => {
window.open("https://o2.ws/contact_us", "_blank");
};
const openFeatureLink = (url: string) => {
window.open(url, "_blank");
};
const navigateToLicense = () => {
// Get meta org identifier
const metaOrgIdentifier = store.state.zoConfig.meta_org;
// Find the meta org from the organizations list
const metaOrg = store.state.organizations?.find(
(org: any) => org.identifier === metaOrgIdentifier
);
if (metaOrg) {
// Create the org option object so that it will be used to switch to meta org
const metaOrgOption = {
label: metaOrg.name,
id: metaOrg.id,
identifier: metaOrg.identifier,
user_email: store.state.userInfo.email,
ingest_threshold: metaOrg.ingest_threshold,
search_threshold: metaOrg.search_threshold,
};
// Set the selected organization using dispatch
store.dispatch("setSelectedOrganization", metaOrgOption);
// Close the dialog
emit("update:modelValue", false);
// Navigate to license page with the meta org identifier
router.push({
name: 'license',
query: { org_identifier: metaOrgIdentifier }
});
} else {
// Show error notification when user doesn't have access to meta org
$q.notify({
message: t("about.enterprise_offer.error_messages.not_authorized_manage_license"),
color: 'negative',
timeout: 5000,
});
}
};
const handlePrimaryButtonClick = () => {
const isEnterprise = config.isEnterprise === 'true';
const isCloud = config.isCloud === 'true';
const hasLicense = store.state.zoConfig?.license_expiry && store.state.zoConfig.license_expiry !== 0;
// Cloud - open download page
if (isCloud) {
openDownloadPage();
}
// Enterprise with license - navigate to license page
else if (isEnterprise && hasLicense) {
navigateToLicense();
}
// Enterprise without license - navigate to license page
else if (isEnterprise && !hasLicense) {
navigateToLicense();
}
// Open Source - open download page
else {
openDownloadPage();
}
};
// Function to get progress color based on percentage
const getProgressColor = (percentage: number): string => {
if (percentage >= 80) {
return "red";
} else if (percentage >= 60) {
return "orange";
} else {
return "green";
}
};
// Check if ingestion is unlimited
const isIngestionUnlimited = computed(() => {
return licenseData.value?.license?.limits?.Ingestion?.typ === "Unlimited";
});
// Get ingestion limit value for threshold
const ingestionLimitGB = computed(() => {
if (isIngestionUnlimited.value) {
return null; // No limit for unlimited plans
}
return licenseData.value?.license?.limits?.Ingestion?.value || 100;
});
// Generate usage chart data for ChartRenderer
const generateUsageDashboard = async () => {
try {
// Get ingestion history from store
const ingestionHistory = store.state.zoConfig?.ingestion_history || [];
// Don't generate chart if there's no ingestion history data
if (!ingestionHistory || ingestionHistory.length === 0) {
chartData.value = null;
return;
}
const dates = [];
let values = [];
let dataUnit = 'GB'; // Default unit
let unitDivisor = 1024; // Default: MB to GB
// Use actual ingestion history data
// Data format: [{ ts: "2026-02-27T00:00:00", value: 202.90125079791258 }]
// Values are in MB, determine best unit based on data range
if (ingestionHistory.length > 0) {
// Sort by timestamp to ensure chronological order
const sortedHistory = [...ingestionHistory].sort((a, b) =>
new Date(a.ts).getTime() - new Date(b.ts).getTime()
);
// Find max value to determine appropriate unit
const maxValueMB = Math.max(...sortedHistory.map((item: any) => item.value));
// Determine best unit based on max value
if (maxValueMB >= 1024 * 1024) {
// Use TB if max value is >= 1 TB
dataUnit = 'TB';
unitDivisor = 1024 * 1024;
} else if (maxValueMB >= 1024) {
// Use GB if max value is >= 1 GB
dataUnit = 'GB';
unitDivisor = 1024;
} else {
// Use MB for smaller values
dataUnit = 'MB';
unitDivisor = 1;
}
sortedHistory.forEach((item: any) => {
const date = new Date(item.ts);
const day = date.getDate();
dates.push(`${day}`);
// Convert MB to appropriate unit
values.push(item.value / unitDivisor);
});
}
// Calculate Y-axis max and interval based on ingestion limit
const thresholdGB = ingestionLimitGB.value;
const maxDataValue = values.length > 0 ? Math.max(...values) : 0;
// Convert threshold to the same unit as data
let thresholdInDataUnit = 0;
if (thresholdGB && thresholdGB > 0) {
if (dataUnit === 'TB') {
thresholdInDataUnit = thresholdGB / 1024; // GB to TB
} else if (dataUnit === 'GB') {
thresholdInDataUnit = thresholdGB; // Already in GB
} else {
thresholdInDataUnit = thresholdGB * 1024; // GB to MB
}
}
// Determine Y-axis max: use the greater of (threshold * 1.2) or (maxData * 1.2)
let yAxisMax;
if (thresholdInDataUnit > 0) {
yAxisMax = Math.max(thresholdInDataUnit * 1.2, maxDataValue * 1.2);
} else {
yAxisMax = maxDataValue * 1.2;
}
// Calculate nice interval for Y-axis (aim for 4-5 grid lines)
const calculateInterval = (max: number) => {
const targetIntervals = 4;
const rawInterval = max / targetIntervals;
// Round to nice numbers (1, 2, 5, 10, 20, 50, 100, etc.)
const magnitude = Math.pow(10, Math.floor(Math.log10(rawInterval)));
const normalized = rawInterval / magnitude;
let niceInterval;
if (normalized <= 1) niceInterval = 1 * magnitude;
else if (normalized <= 2) niceInterval = 2 * magnitude;
else if (normalized <= 5) niceInterval = 5 * magnitude;
else niceInterval = 10 * magnitude;
return niceInterval;
};
const yAxisInterval = yAxisMax > 0 ? calculateInterval(yAxisMax) : undefined;
// Create mark line for threshold if limit exists
const markLine: any = thresholdGB && thresholdGB > 0 ? {
silent: true,
symbol: 'none',
label: {
show: false
},
lineStyle: {
color: '#FF0000',
width: 2,
type: 'solid'
},
data: [{
yAxis: thresholdInDataUnit
}]
} : undefined;
// Simple echarts configuration for bar chart
// ChartRenderer expects data in format: { options: { ... } }
chartData.value = {
options: {
backgroundColor: 'transparent',
grid: {
left: '10%',
right: '5%',
top: '10%',
bottom: '15%',
containLabel: true
},
xAxis: {
type: 'category',
data: dates,
axisLine: {
show: true,
lineStyle: { color: 'rgba(255, 255, 255, 0.3)' }
},
axisTick: {
show: true,
lineStyle: { color: 'rgba(255, 255, 255, 0.3)' }
},
axisLabel: {
show: true,
color: 'rgba(255, 255, 255, 0.8)',
fontSize: 10,
interval: Math.floor(dates.length / 6) // Show ~6 labels based on actual data length
}
},
yAxis: {
type: 'value',
min: 0,
max: yAxisMax,
interval: yAxisInterval,
axisLine: {
show: true,
lineStyle: { color: 'rgba(255, 255, 255, 0.3)' }
},
axisTick: {
show: true,
lineStyle: { color: 'rgba(255, 255, 255, 0.3)' }
},
axisLabel: {
show: true,
color: 'rgba(255, 255, 255, 0.8)',
fontSize: 10,
formatter: (value: number) => {
return value.toFixed(0) + dataUnit;
}
},
splitLine: {
show: true,
lineStyle: {
color: 'rgba(255, 255, 255, 0.1)',
type: 'dashed'
}
}
},
series: [{
type: 'bar',
data: values.map((value, index) => {
// Color bars red if they exceed threshold, otherwise green
const exceeds = thresholdInDataUnit > 0 && value > thresholdInDataUnit;
return {
value: value,
itemStyle: {
color: exceeds ? '#FF6B6B' : '#22c55e' // Red if exceeds, green if within limit
}
};
}),
barWidth: '60%',
markLine: markLine
}],
tooltip: {
show: true,
trigger: 'axis',
backgroundColor: 'rgba(0, 0, 0, 0.8)',
borderColor: 'rgba(255, 255, 255, 0.2)',
borderWidth: 1,
textStyle: {
color: '#fff',
fontSize: 12
},
formatter: (params: any) => {
const dayNum = params[0].name;
const value = params[0].value;
const formattedValue = value.toFixed(2) + ' ' + dataUnit;
// Get the actual date from ingestion history for this day
const ingestionHistory = store.state.zoConfig?.ingestion_history || [];
const matchingEntry = ingestionHistory.find((item: any) => {
const date = new Date(item.ts);
return date.getDate() === parseInt(dayNum);
});
if (matchingEntry) {
const fullDate = new Date(matchingEntry.ts);
const monthName = fullDate.toLocaleString('default', { month: 'short' });
return `${monthName} ${dayNum}<br/>Usage: ${formattedValue}`;
}
return `Day ${dayNum}<br/>Usage: ${formattedValue}`;
}
},
animation: true
}
};
dashboardRenderKey.value++;
} catch (error) {
console.error("Failed to generate chart data:", error);
}
};
// Watch for dialog opening to fetch license data
watch(showDialog, (newVal) => {
if (newVal) {
fetchLicenseData();
}
});
return {
showDialog,
dialogConfig,
coreFeatures,
enterpriseFeatures,
onDialogHide,
openDownloadPage,
openDocsLink,
contactSales,
openFeatureLink,
navigateToLicense,
handlePrimaryButtonClick,
getProgressColor,
isLoadingLicense,
licenseData,
chartData,
dashboardRenderKey,
isIngestionUnlimited,
t,
Math,
};
},
watch: {
modelValue(newVal) {
this.showDialog = newVal;
},
showDialog(newVal) {
if (!newVal) {
this.$emit("update:modelValue", false);
}
},
},
});
</script>
<style scoped lang="scss">
.enterprise-dialog-v3 {
overflow: hidden;
position: relative;
}
.close-btn-top-right {
position: absolute;
top: 16px;
right: 16px;
z-index: 100;
color: rgba(0, 0, 0, 0.6);
&:hover {
color: rgba(0, 0, 0, 0.87);
}
}
.dialog-split-layout {
display: flex;
height: 780px;
max-height: 92vh;
&.cloud-layout {
.features-panel {
flex: 1;
max-width: 100%;
}
}
}
// Left Panel - Hero Section
.hero-panel {
flex: 0 0 35%;
background: linear-gradient(135deg, var(--q-primary) 0%, color-mix(in srgb, var(--q-primary) 85%, black 15%) 100%);
padding: 40px;
display: flex;
flex-direction: column;
position: relative;
color: white;
overflow: hidden;
.hero-content {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
max-width: 400px;
width: 100%;
}
.hero-icon {
width: 80px;
height: 80px;
background: rgba(255, 255, 255, 0.15);
border-radius: 16px;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 24px;
backdrop-filter: blur(10px);
.q-icon {
color: white;
}
}
.hero-title {
font-size: 32px;
font-weight: 700;
margin: 0 0 16px 0;
line-height: 1.2;
text-align: center;
}
.offer-text {
margin: 0 0 24px 0;
font-size: 14px;
line-height: 1.6;
opacity: 0.95;
text-align: center;
}
.hero-offer {
margin-bottom: 32px;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
gap: 16px;
.usage-indicator {
flex-shrink: 0;
}
.offer-badge-skeleton {
flex-shrink: 0;
}
.offer-badge {
display: inline-flex;
align-items: center;
background: linear-gradient(135deg, #22c55e 0%, #4ade80 100%);
padding: 10px 20px;
border-radius: 24px;
font-weight: 700;
font-size: 15px;
backdrop-filter: blur(10px);
color: white;
box-shadow: 0 4px 16px rgba(34, 197, 94, 0.4);
.q-icon {
color: white;
font-size: 20px;
}
// Licensed badge styling - neutral white/transparent for users who already have license
&.licensed-badge {
background: rgba(255, 255, 255, 0.2);
color: white;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
.q-icon {
color: white;
}
}
}
}
.usage-chart-section {
width: 100%;
margin-bottom: 24px;
background: rgba(255, 255, 255, 0.1);
border-radius: 12px;
padding: 16px;
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
.chart-wrapper {
position: relative;
width: 100%;
}
.usage-chart-container {
width: 100%;
overflow: visible;
padding: 0;
margin: 0 auto;
min-height: 150px;
max-height: 150px;
.grid-stack-item-content {
border: 0px !important;
}
}
}
.license-note {
display: flex;
align-items: center;
justify-content: center;
margin-top: -8px;
margin-bottom: 16px;
padding: 8px 16px;
background: rgba(255, 255, 255, 0.15);
border-radius: 8px;
color: rgba(255, 255, 255, 0.9);
font-size: 12px;
font-weight: 500;
backdrop-filter: blur(10px);
white-space: nowrap;
.q-icon {
color: rgba(255, 255, 255, 0.85);
}
}
.hero-actions {
display: flex;
flex-direction: column;
gap: 12px;
width: 100%;
.download-btn {
background: white !important;
color: var(--q-primary) !important;
font-weight: 700;
padding: 10px 32px;
font-size: 15px;
border-radius: 8px !important;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
letter-spacing: 0.3px;
&:hover {
transform: translateY(-3px) scale(1.02);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.25);
}
&:active {
transform: translateY(-1px) scale(0.98);
}
}
.learn-more-btn {
font-weight: 600;
padding: 10px 24px;
font-size: 15px;
border-radius: 8px !important;
border: 2px solid rgba(255, 255, 255, 0.3);
transition: all 0.3s ease;
letter-spacing: 0.2px;
background: transparent;
&:hover {
background: rgba(255, 255, 255, 0.15);
border-color: rgba(255, 255, 255, 0.5);
transform: translateX(4px);
}
&:active {
transform: scale(0.96);
}
}
}
}
// Right Panel - Features List
.features-panel {
flex: 1;
background: white;
display: flex;
flex-direction: column;
overflow: hidden;
.features-header {
padding: 16px 32px 12px 32px;
background: white;
position: sticky;
top: 0;
z-index: 10;
border-bottom: 1px solid rgba(0, 0, 0, 0.08);
text-align: center;
.header-icon-wrapper {
display: inline-flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
background: linear-gradient(135deg, var(--q-primary), color-mix(in srgb, var(--q-primary) 85%, purple 15%));
border-radius: 12px;
margin-bottom: 8px;
.header-icon {
color: white;
font-size: 18px;
}
}
h4 {
font-size: 18px;
font-weight: 800;
margin: 0 0 4px 0;
color: rgba(0, 0, 0, 0.9);
letter-spacing: -0.3px;
}
.header-subtitle {
font-size: 12px;
color: rgba(0, 0, 0, 0.6);
margin: 0;
font-weight: 500;
}
}
.features-list {
flex: 1;
overflow-y: auto;
padding: 8px 32px 16px 32px;
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 7px 14px;
align-content: start;
&.cloud-three-column {
grid-template-columns: repeat(3, 1fr);
}
}
.feature-list-item {
display: flex;
gap: 10px;
padding: 8px 12px;
border-radius: 6px;
border: 1px solid rgba(0, 0, 0, 0.08);
transition: all 0.2s ease;
&:hover {
background: rgba(0, 0, 0, 0.03);
border-color: rgba(0, 0, 0, 0.12);
}
&.has-link {
cursor: pointer;
&:hover {
background: rgba(var(--q-primary-rgb), 0.05);
border-color: rgba(var(--q-primary-rgb), 0.3);
transform: translateX(2px);
}
&:active {
transform: translateX(0);
}
}
.feature-icon-badge {
flex-shrink: 0;
width: 30px;
height: 30px;
border-radius: 8px;
background: rgba(var(--q-primary-rgb), 0.1);
display: flex;
align-items: center;
justify-content: center;
color: var(--q-primary);
.q-icon {
font-size: 15px;
}
}
.feature-content {
flex: 1;
min-width: 0;
}
.feature-name {
font-size: 13px;
font-weight: 600;
color: rgba(0, 0, 0, 0.87);
margin-bottom: 2px;
line-height: 1.25;
display: flex;
align-items: center;
gap: 6px;
}
.external-link-icon {
opacity: 0.6;
margin-left: 4px;
vertical-align: middle;
}
.ha-badge {
display: inline-flex;
align-items: center;
padding: 2px 7px;
background: rgba(var(--q-primary-rgb), 0.15);
color: var(--q-primary);
border-radius: 4px;
font-size: 9px;
font-weight: 700;
letter-spacing: 0.5px;
line-height: 1;
cursor: pointer;
margin-left: 4px;
}
.beta-badge {
display: inline-flex;
align-items: center;
padding: 1px 4px;
color: var(--q-primary);
border: 1px solid var(--q-primary);
border-radius: 10px;
font-size: 8px;
font-weight: 700;
letter-spacing: 0.5px;
line-height: 1.4;
text-transform: uppercase;
}
.feature-desc {
font-size: 11px;
color: rgba(0, 0, 0, 0.55);
line-height: 1.25;
}
}
}
// Dark mode
body.body--dark {
.close-btn-top-right {
color: rgba(255, 255, 255, 0.7);
&:hover {
color: rgba(255, 255, 255, 0.95);
}
}
.features-panel {
background: #1e1e1e;
.features-header {
background: #1e1e1e;
border-bottom-color: rgba(255, 255, 255, 0.1);
h4 {
color: rgba(255, 255, 255, 0.95);
}
.header-subtitle {
color: rgba(255, 255, 255, 0.6);
}
}
.feature-list-item {
border: 1px solid rgba(255, 255, 255, 0.12);
&:hover {
background: rgba(255, 255, 255, 0.05);
}
&.has-link {
&:hover {
background: rgba(var(--q-primary-rgb), 0.1);
border-color: rgba(var(--q-primary-rgb), 0.4);
}
}
.feature-icon-badge {
background: rgba(var(--q-primary-rgb), 0.15);
}
.feature-name {
color: rgba(255, 255, 255, 0.95);
}
.ha-badge {
background: rgba(var(--q-primary-rgb), 0.2);
color: var(--q-primary);
}
.beta-badge {
color: var(--q-primary);
border-color: var(--q-primary);
}
.feature-desc {
color: rgba(255, 255, 255, 0.55);
}
}
}
}
@media (max-width: 900px) {
.dialog-split-layout {
flex-direction: column;
}
.hero-panel {
flex: 0 0 auto;
min-height: 400px;
}
}
</style>