Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: for APPLAUNCHER-28 various data fixes #34

Merged
merged 11 commits into from
Apr 15, 2024
69 changes: 33 additions & 36 deletions pkg/app-launcher/components/AppLauncherCard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -10,79 +10,72 @@ export default {
ButtonDropDown,
},
props: {
clusterId: {
type: String,
required: true,
},
favoritedServices: {
favoritedApps: {
type: Array,
required: true,
},
service: {
app: {
type: Object,
default: null,
},
ingress: {
type: Object,
default: null,
isInGlobalView: {
type: Boolean,
default: false,
},
},
methods: {
openLink(link) {
window.open(link);
},
toggleFavorite() {
this.$emit('toggle-favorite', this.service);
this.$emit('toggle-favorite', this.app)
},
},
computed: {
app() {
return this.service || this.ingress;
},
endpoints() {
return (
this.service?.spec?.ports?.map((port) => {
this.app?.spec?.ports?.map((port) => {
const endpoint = `${
isMaybeSecure(port.port, port.protocol) ? 'https' : 'http'
}:${this.service?.metadata?.name}:${port.port}`;
}:${this.app.metadata?.name}:${port.port}`;

return {
label: `${endpoint}${port.protocol === 'UDP' ? ' (UDP)' : ''}`,
value: `/k8s/clusters/${this.clusterId}/api/v1/namespaces/${this.service.metadata.namespace}/services/${endpoint}/proxy`,
value: `/k8s/clusters/${this.clusterId}/api/v1/namespaces/${this.app.namespace}/services/${endpoint}/proxy`,
};
}) ?? []
);
},
computedServiceName() {
return (
this.service?.metadata?.labels?.['app.kubernetes.io/component'] ??
this.service?.metadata?.name
this.app?.metadata?.labels?.['app.kubernetes.io/component'] ??
this.app?.metadata?.name
);
},
helmChart() {
return this.service?.metadata?.labels?.['helm.sh/chart'];
return this.app?.metadata?.labels?.['helm.sh/chart'];
},
kubernetesVersion() {
return this.service?.metadata?.labels?.['app.kubernetes.io/version'];
return this.app?.metadata?.labels?.['app.kubernetes.io/version'];
},
name() {
return this.service?.metadata?.name;
return this.app?.metadata?.name;
},
namespace() {
return this.service?.metadata?.namespace;
return this.app?.metadata?.namespace;
},
isFavorited() {
return this.favoritedServices.some(
(favoritedService) =>
favoritedService?.clusterId === this.clusterId &&
favoritedService?.service?.id === this.service?.id
return this.favoritedApps.some(
(favoritedApp) =>
favoritedApp?.id === this.app.id &&
favoritedApp?.kind === this.app.kind
);
},
isGlobalApp() {
return this.service?.metadata?.annotations?.['extensions.applauncher/global-app'] === 'true';
return this.app?.metadata?.annotations?.['extensions.applauncher/global-app'] === 'true';
},
ingressPath() {
return ingressFullPath(this.ingress, this.ingress?.spec?.rules?.[0]);
return ingressFullPath(this.app, this.app?.spec?.rules?.[0]);
}
},
name: 'AppLauncherCard',
Expand All @@ -95,32 +88,36 @@ export default {
<template #title>
<div style="width: 100%">
<p style="font-size: 1.2rem">
{{ service ? service?.metadata?.name : ingress?.metadata?.name }}
{{ app.metadata?.name }}
</p>
<div style="color: var(--input-label); display: flex; justify-content: space-between; margin-top: 4px;">
<p v-if="app.type === 'service' && app.metadata?.labels?.['app.kubernetes.io/version'] !== undefined">
<p v-if="app.kind === 'Service' && app.metadata?.labels?.['app.kubernetes.io/version'] !== undefined">
{{ kubernetesVersion }}
</p>
<p v-if="app.type === 'service' && app.metadata?.labels?.['helm.sh/chart'] !== undefined">
<p v-if="app.kind === 'Service' && app.metadata?.labels?.['helm.sh/chart'] !== undefined">
{{ helmChart }}
</p>
<p v-if="app.type === 'ingress'">
<p v-if="app.kind === 'Ingress'">
Ingress
</p>
</div>
</div>
</template>
<template #body>
<p v-if="app.type === 'service'">{{ namespace }}/{{ name }}</p>
<p v-if="app.type === 'ingress'">{{ ingressPath }}</p>
<p v-if="app.kind === 'Service'">
{{ (isGlobalApp || isFavorited) && isInGlobalView ? `${app.clusterName}/` : '' }}{{ namespace }}/{{ name }}
</p>
<p v-if="app.kind === 'Ingress'">
{{ (isGlobalApp || isFavorited) && isInGlobalView ? `${app.clusterName}: ` : '' }}{{ ingressPath }}
</p>
</template>
<template #actions>
<button v-if="!isGlobalApp" class="icon-button" @click="toggleFavorite">
<i :class="['icon', isFavorited ? 'icon-star' : 'icon-star-open']" />
</button>
<i v-else class="icon icon-globe icon-only" />
<a
v-if="(endpoints?.length ?? 0) <= 1 && app.type === 'service'"
v-if="(endpoints?.length ?? 0) <= 1 && app.kind === 'Service'"
:disabled="!endpoints?.length"
:href="endpoints[0]?.value"
target="_blank"
Expand All @@ -133,7 +130,7 @@ export default {
{{ t('appLauncher.launch') }}
</a>
<a
v-else-if="app.type === 'ingress'"
v-else-if="app.kind === 'Ingress'"
:href="ingressPath"
target="_blank"
rel="noopener noreferrer nofollow"
Expand Down
98 changes: 98 additions & 0 deletions pkg/app-launcher/components/ClusterActions.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
<script>
export default {
name: 'ClusterActions',
props: {
searchQuery: {
type: String,
required: true,
},
isGridView: {
type: Boolean,
required: true,
},
selectedCluster: {
type: String,
required: true,
},
clusterOptions: {
type: Array,
required: true,
},
},
emits: ['update:search-query', 'toggle-sort', 'update:selected-cluster', 'set-view'],
};
</script>

<template>
<div class="cluster-actions">
<div class="search-input">
<input :value="searchQuery" :placeholder="$store.getters['i18n/t']('appLauncher.filter')" @input="$emit('update:search-query', $event.target.value)" />
</div>
<button class="icon-button" @click="$emit('toggle-sort')" v-if="isGridView">
<i class="icon icon-sort" />
</button>
<div class="select-wrapper">
<select :value="selectedCluster" class="cluster-select" @change="$emit('update:selected-cluster', $event.target.value)">
<option v-for="option in clusterOptions" :key="option.value" :value="option.value">
{{ option.label }}
</option>
</select>
</div>
<button class="icon-button" @click="$emit('set-view', 'grid')">
<i class="icon icon-apps" />
</button>
<button class="icon-button" @click="$emit('set-view', 'list')">
<i class="icon icon-list-flat" />
</button>
</div>
</template>


<style scoped>
.cluster-actions {
display: flex;
align-items: center;
gap: 1rem;
position: fixed;
right: 5.8%;
top: 4.3rem;
z-index: 2;
padding-bottom: 0.425rem;
padding-right: 4.4rem;
background: inherit;
border-bottom: var(--header-border-size) solid var(--header-border);
}

.icon-button {
background: none;
border: none;
cursor: pointer;
padding: 0;
color: var(--primary);
font-size: 1.8rem;
}

.icon-button:hover {
color: var(--primary-hover);
}

.search-input {
text-align: right;
justify-content: flex-end;
display: flex;

input {
width: 190px;
padding: 11px;
font-size: 1rem;
border: 1px solid var(--border);
border-radius: 4px;
}
}

.select-wrapper select {
padding: 0.8rem;
border: 1px solid #ccc;
border-radius: 4px;
}
</style>
64 changes: 64 additions & 0 deletions pkg/app-launcher/components/ClusterGridView.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<script>
import AppLauncherCard from './AppLauncherCard.vue';

export default {
name: 'ClusterGridView',
components: {
AppLauncherCard,
},
props: {
clusterData: {
type: Object,
required: true,
},
favoritedApps: {
type: Array,
required: true,
},
},
emits: ['toggle-favorite'],
};
</script>

<template>
<div class="cluster-grid-view">
<div class="cluster-header">
<h1>
{{ clusterData.name }}
</h1>
</div>
<div class="services-by-cluster-grid">
<AppLauncherCard
v-for="app in clusterData.filteredApps"
:key="`${app.clusterId}-${app.id}-${app.kind}`"
:app="app" :isInGlobalView="false" :favorited-apps="favoritedApps"
@toggle-favorite="$emit('toggle-favorite', $event)"
/>
</div>
</div>
</template>

<style scoped lang="scss">
.services-by-cluster-grid {
display: grid;
grid-gap: 1rem;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
}

.cluster-header {
display: flex;
align-items: center;
justify-content: space-between;
margin: 1rem 0;
background: var(--header-bg);
border-bottom: var(--header-border-size) solid var(--header-border);
height: var(--header-height);
position: sticky;
top: 0;
z-index: 1;
}

.favorite-icon {
margin-right: 1rem;
}
</style>
Loading
Loading