mirror of https://github.com/rancher/dashboard.git
502 lines
16 KiB
Vue
502 lines
16 KiB
Vue
<script>
|
|
import { mapGetters } from 'vuex';
|
|
import ResourceTable from '@shell/components/ResourceTable';
|
|
import { STATE, AGE, NAME } from '@shell/config/table-headers';
|
|
import { uniq } from '@shell/utils/array';
|
|
import { MANAGEMENT, NAMESPACE, VIRTUAL_TYPES } from '@shell/config/types';
|
|
import { PROJECT_ID } from '@shell/config/query-params';
|
|
import Masthead from '@shell/components/ResourceList/Masthead';
|
|
import { mapPref, GROUP_RESOURCES, ALL_NAMESPACES } from '@shell/store/prefs';
|
|
import MoveModal from '@shell/components/MoveModal';
|
|
import { defaultTableSortGenerationFn } from '@shell/components/ResourceTable.vue';
|
|
import { NAMESPACE_FILTER_ALL_ORPHANS } from '@shell/utils/namespace-filter';
|
|
import ResourceFetch from '@shell/mixins/resource-fetch';
|
|
|
|
export default {
|
|
name: 'ListProjectNamespace',
|
|
components: {
|
|
Masthead, MoveModal, ResourceTable
|
|
},
|
|
mixins: [ResourceFetch],
|
|
|
|
props: {
|
|
createProjectLocationOverride: {
|
|
type: Object,
|
|
default: () => null
|
|
},
|
|
|
|
createNamespaceLocationOverride: {
|
|
type: Object,
|
|
default: () => null
|
|
}
|
|
},
|
|
|
|
async fetch() {
|
|
const inStore = this.$store.getters['currentStore'](NAMESPACE);
|
|
|
|
this.schema = this.$store.getters[`${ inStore }/schemaFor`](NAMESPACE);
|
|
this.projectSchema = this.$store.getters[`management/schemaFor`](MANAGEMENT.PROJECT);
|
|
|
|
if ( !this.schema ) {
|
|
// clusterReady: When switching routes, it will cause clusterReady to change, causing itself to repeat rendering。
|
|
// this.$store.dispatch('loadingError', `Type ${ NAMESPACE } not found`);
|
|
|
|
return;
|
|
}
|
|
|
|
await this.$fetchType(NAMESPACE);
|
|
this.projects = await this.$store.dispatch('management/findAll', { type: MANAGEMENT.PROJECT, opt: { force: true } });
|
|
},
|
|
|
|
data() {
|
|
return {
|
|
loadResources: [NAMESPACE],
|
|
loadIndeterminate: true,
|
|
schema: null,
|
|
projects: [],
|
|
projectSchema: null,
|
|
MANAGEMENT,
|
|
VIRTUAL_TYPES,
|
|
defaultCreateProjectLocation: {
|
|
name: 'c-cluster-product-resource-create',
|
|
params: {
|
|
product: this.$store.getters['currentProduct'].name,
|
|
resource: MANAGEMENT.PROJECT
|
|
},
|
|
}
|
|
};
|
|
},
|
|
|
|
computed: {
|
|
...mapGetters(['currentCluster', 'currentProduct']),
|
|
namespaces() {
|
|
const inStore = this.$store.getters['currentStore'](NAMESPACE);
|
|
|
|
return this.$store.getters[`${ inStore }/all`](NAMESPACE);
|
|
},
|
|
loading() {
|
|
return !this.currentCluster || this.namespaces.length ? false : this.$fetchState.pending;
|
|
},
|
|
showIncrementalLoadingIndicator() {
|
|
return this.perfConfig?.incrementalLoading?.enabled;
|
|
},
|
|
isNamespaceCreatable() {
|
|
return (this.schema?.collectionMethods || []).includes('POST');
|
|
},
|
|
headers() {
|
|
const project = {
|
|
name: 'project',
|
|
label: this.t('tableHeaders.project'),
|
|
value: 'project.nameDisplay',
|
|
sort: ['projectNameSort', 'nameSort'],
|
|
};
|
|
|
|
return [
|
|
STATE,
|
|
NAME,
|
|
this.groupPreference === 'none' ? project : null,
|
|
AGE
|
|
].filter(h => h);
|
|
},
|
|
projectIdsWithNamespaces() {
|
|
const ids = this.rows
|
|
.map(row => row.projectId)
|
|
.filter(id => id);
|
|
|
|
return uniq(ids);
|
|
},
|
|
clusterProjects() {
|
|
const clusterId = this.currentCluster.id;
|
|
|
|
// Get the list of projects from the store so that the list
|
|
// is updated if a new project is created or removed.
|
|
const projectsInAllClusters = this.$store.getters['management/all'](MANAGEMENT.PROJECT);
|
|
|
|
if (this.currentProduct?.customNamespaceFilter && this.currentProduct?.inStore && this.$store.getters[`${ this.currentProduct.inStore }/filterProject`]) {
|
|
return this.$store.getters[`${ this.currentProduct.inStore }/filterProject`];
|
|
}
|
|
|
|
const clustersInProjects = projectsInAllClusters.filter(project => project.spec.clusterName === clusterId);
|
|
|
|
return clustersInProjects;
|
|
},
|
|
projectsWithoutNamespaces() {
|
|
return this.activeProjects.filter((project) => {
|
|
return !this.projectIdsWithNamespaces.find(item => project?.id?.endsWith(`/${ item }`));
|
|
});
|
|
},
|
|
// We're using this because we need to show projects as groups even if the project doesn't have any namespaces.
|
|
rowsWithFakeNamespaces() {
|
|
const fakeRows = this.projectsWithoutNamespaces.map((project) => {
|
|
return {
|
|
groupByLabel: `${ ('resourceTable.groupLabel.notInAProject') }-${ project.id }`,
|
|
isFake: true,
|
|
mainRowKey: project.id,
|
|
nameDisplay: project.spec?.displayName, // Enable filtering by the project name
|
|
project,
|
|
availableActions: []
|
|
};
|
|
});
|
|
|
|
if (this.showMockNotInProjectGroup) {
|
|
fakeRows.push( {
|
|
groupByLabel: this.t('resourceTable.groupLabel.notInAProject'), // Same as the groupByLabel for the namespace model
|
|
mainRowKey: 'fake-empty',
|
|
});
|
|
}
|
|
|
|
return [...this.rows, ...fakeRows];
|
|
},
|
|
createProjectLocation() {
|
|
return this.createProjectLocationOverride || this.defaultCreateProjectLocation;
|
|
},
|
|
groupPreference: mapPref(GROUP_RESOURCES),
|
|
activeNamespaceFilters() {
|
|
return this.$store.getters['activeNamespaceFilters'];
|
|
},
|
|
activeProjectFilters() {
|
|
const activeProjects = {};
|
|
|
|
for (const filter of this.activeNamespaceFilters) {
|
|
const [type, id] = filter.split('://', 2);
|
|
|
|
if (type === 'project') {
|
|
activeProjects[id] = true;
|
|
}
|
|
}
|
|
|
|
return activeProjects;
|
|
},
|
|
activeProjects() {
|
|
const namespaceFilters = this.$store.getters['activeNamespaceFilters'];
|
|
|
|
if (namespaceFilters.includes(NAMESPACE_FILTER_ALL_ORPHANS) && Object.keys(this.activeProjectFilters).length === 0) {
|
|
// If the user wants to only see namespaces that are not
|
|
// in a project, don't show any projects.
|
|
return [];
|
|
}
|
|
|
|
// If the user is not filtering by any projects or namespaces, return
|
|
// all projects in the cluster.
|
|
if (!this.userIsFilteringForSpecificNamespaceOrProject()) {
|
|
return this.clusterProjects;
|
|
}
|
|
|
|
// Filter out projects that are not selected in the top nav.
|
|
return this.clusterProjects.filter((projectData) => {
|
|
const projectId = projectData.id.split('/')[1];
|
|
|
|
return !!this.activeProjectFilters[projectId];
|
|
});
|
|
},
|
|
activeNamespaces() {
|
|
// Apply namespace filters from the top nav.
|
|
const activeNamespaces = this.$store.getters['namespaces']();
|
|
|
|
return this.namespaces.filter((namespaceData) => {
|
|
return !!activeNamespaces[namespaceData.metadata.name];
|
|
});
|
|
},
|
|
filteredRows() {
|
|
return this.groupPreference === 'none' ? this.rows : this.rowsWithFakeNamespaces;
|
|
},
|
|
rows() {
|
|
if (this.$store.getters['prefs/get'](ALL_NAMESPACES)) {
|
|
// If all namespaces options are turned on in the user preferences,
|
|
// return all namespaces including system namespaces and RBAC
|
|
// management namespaces.
|
|
return this.activeNamespaces;
|
|
}
|
|
|
|
return this.activeNamespaces.filter((namespace) => {
|
|
const isSettingSystemNamespace = this.$store.getters['systemNamespaces'].includes(namespace.metadata.name);
|
|
const systemNS = namespace.isSystem || namespace.isFleetManaged || isSettingSystemNamespace;
|
|
|
|
return this.currentProduct?.hideSystemResources ? !systemNS : true;
|
|
});
|
|
},
|
|
|
|
canSeeProjectlessNamespaces() {
|
|
return this.currentCluster.canUpdate;
|
|
},
|
|
|
|
showMockNotInProjectGroup() {
|
|
if (!this.canSeeProjectlessNamespaces) {
|
|
return false;
|
|
}
|
|
|
|
const someNamespacesAreNotInProject = !this.rows.some(row => !row.project);
|
|
|
|
// Hide the "Not in a Project" group if the user is filtering
|
|
// for specific namespaces or projects.
|
|
const usingSpecificFilter = this.userIsFilteringForSpecificNamespaceOrProject();
|
|
|
|
return !usingSpecificFilter && someNamespacesAreNotInProject;
|
|
},
|
|
|
|
notInProjectKey() {
|
|
return this.$store.getters['i18n/t']('resourceTable.groupLabel.notInAProject');
|
|
}
|
|
},
|
|
methods: {
|
|
/**
|
|
* Get PSA HTML to be displayed in the tooltips
|
|
*/
|
|
getPsaTooltip(row) {
|
|
const dictionary = row.psaTooltipsDescription;
|
|
const list = Object.values(dictionary)
|
|
.sort()
|
|
.map(text => `<li>${ text }</li>`).join('');
|
|
const title = `<p>${ this.t('podSecurityAdmission.name') }: </p>`;
|
|
|
|
return `${ title }<ul class="psa-tooltip">${ list }</ul>`;
|
|
},
|
|
|
|
userIsFilteringForSpecificNamespaceOrProject() {
|
|
const activeFilters = this.$store.getters['namespaceFilters'];
|
|
|
|
for (let i = 0; i < activeFilters.length; i++) {
|
|
const filter = activeFilters[i];
|
|
const filterType = filter.split('://')[0];
|
|
|
|
if (filterType === 'ns' || filterType === 'project') {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
},
|
|
slotName(project) {
|
|
return `main-row:${ project.id }`;
|
|
},
|
|
createNamespaceLocation(group) {
|
|
const project = group.rows[0].project;
|
|
|
|
const location = this.createNamespaceLocationOverride ? { ...this.createNamespaceLocationOverride } : {
|
|
name: 'c-cluster-product-resource-create',
|
|
params: {
|
|
product: this.$store.getters['currentProduct'].name,
|
|
resource: NAMESPACE
|
|
},
|
|
};
|
|
|
|
location.query = { [PROJECT_ID]: project?.metadata.name };
|
|
|
|
return location;
|
|
},
|
|
showProjectAction(event, group) {
|
|
const project = group.rows[0].project;
|
|
|
|
this.$store.commit(`action-menu/show`, {
|
|
resources: [project],
|
|
elem: event.target
|
|
});
|
|
},
|
|
showProjectActionButton(group) {
|
|
const project = group.rows[0].project;
|
|
|
|
return !!project;
|
|
},
|
|
projectLabel(group) {
|
|
const row = group.rows[0];
|
|
|
|
if (row.isFake) {
|
|
return this.t('resourceTable.groupLabel.project', { name: row.project?.nameDisplay }, true);
|
|
}
|
|
|
|
return row.groupByLabel;
|
|
},
|
|
|
|
projectDescription(group) {
|
|
const project = group.rows[0].project;
|
|
|
|
return project?.description;
|
|
},
|
|
|
|
clearSelection() {
|
|
this.$refs.table.clearSelection();
|
|
},
|
|
|
|
sortGenerationFn() {
|
|
// The sort generation function creates a unique value and is used to create a key including sort details.
|
|
// The unique key determines if the list is redrawn or a cached version is shown.
|
|
// Because we ensure the 'not in a project' group is there via a row, and timing issues, the unqiue key doesn't change
|
|
// after a namespace is removed... so the list won't update... so we need to inject a string to ensure the key is fresh
|
|
const base = defaultTableSortGenerationFn(this.schema, this.$store);
|
|
|
|
return base + (this.showMockNotInProjectGroup ? '-mock' : '');
|
|
},
|
|
|
|
}
|
|
};
|
|
</script>
|
|
|
|
<template>
|
|
<div class="project-namespaces">
|
|
<Masthead
|
|
:schema="projectSchema"
|
|
:type-display="t('projectNamespaces.label')"
|
|
:resource="MANAGEMENT.PROJECT"
|
|
:favorite-resource="VIRTUAL_TYPES.PROJECT_NAMESPACES"
|
|
:create-location="createProjectLocation"
|
|
:create-button-label="t('projectNamespaces.createProject')"
|
|
:show-incremental-loading-indicator="showIncrementalLoadingIndicator"
|
|
:load-resources="loadResources"
|
|
:load-indeterminate="loadIndeterminate"
|
|
/>
|
|
<ResourceTable
|
|
ref="table"
|
|
class="table"
|
|
v-bind="$attrs"
|
|
:schema="schema"
|
|
:headers="headers"
|
|
:rows="filteredRows"
|
|
:groupable="true"
|
|
:sort-generation-fn="sortGenerationFn"
|
|
:loading="loading"
|
|
group-tooltip="resourceTable.groupBy.project"
|
|
key-field="_key"
|
|
v-on="$listeners"
|
|
>
|
|
<template #group-by="group">
|
|
<div
|
|
class="project-bar"
|
|
:class="{'has-description': projectDescription(group.group)}"
|
|
>
|
|
<div
|
|
v-trim-whitespace
|
|
class="group-tab"
|
|
>
|
|
<div
|
|
class="project-name"
|
|
v-html="projectLabel(group.group)"
|
|
/>
|
|
<div
|
|
v-if="projectDescription(group.group)"
|
|
class="description text-muted text-small"
|
|
>
|
|
{{ projectDescription(group.group) }}
|
|
</div>
|
|
</div>
|
|
<div class="right">
|
|
<n-link
|
|
v-if="isNamespaceCreatable && (canSeeProjectlessNamespaces || group.group.key !== notInProjectKey)"
|
|
class="create-namespace btn btn-sm role-secondary mr-5"
|
|
:to="createNamespaceLocation(group.group)"
|
|
>
|
|
{{ t('projectNamespaces.createNamespace') }}
|
|
</n-link>
|
|
<button
|
|
type="button"
|
|
class="project-action btn btn-sm role-multi-action actions mr-10"
|
|
:class="{invisible: !showProjectActionButton(group.group)}"
|
|
@click="showProjectAction($event, group.group)"
|
|
>
|
|
<i class="icon icon-actions" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
<template #cell:project="{row}">
|
|
<span v-if="row.project">{{ row.project.nameDisplay }}</span>
|
|
<span
|
|
v-else
|
|
class="text-muted"
|
|
>–</span>
|
|
</template>
|
|
<template #cell:name="{row}">
|
|
<div class="namespace-name">
|
|
<n-link
|
|
v-if="row.detailLocation && !row.hideDetailLocation"
|
|
:to="row.detailLocation"
|
|
>
|
|
{{ row.name }}
|
|
</n-link>
|
|
<span v-else>
|
|
{{ row.name }}
|
|
</span>
|
|
<i
|
|
v-if="row.hasSystemLabels"
|
|
v-tooltip="getPsaTooltip(row)"
|
|
class="icon icon-lock ml-5"
|
|
/>
|
|
</div>
|
|
</template>
|
|
<template
|
|
v-for="project in projectsWithoutNamespaces"
|
|
v-slot:[slotName(project)]
|
|
>
|
|
<tr
|
|
:key="project.id"
|
|
class="main-row"
|
|
>
|
|
<td
|
|
class="empty text-center"
|
|
colspan="5"
|
|
>
|
|
{{ t('projectNamespaces.noNamespaces') }}
|
|
</td>
|
|
</tr>
|
|
</template>
|
|
<template #main-row:fake-empty>
|
|
<tr class="main-row">
|
|
<td
|
|
class="empty text-center"
|
|
colspan="5"
|
|
>
|
|
{{ t('projectNamespaces.noProjectNoNamespaces') }}
|
|
</td>
|
|
</tr>
|
|
</template>
|
|
</ResourceTable>
|
|
<MoveModal @moving="clearSelection" />
|
|
</div>
|
|
</template>
|
|
<style lang="scss" scoped>
|
|
.project-namespaces {
|
|
& ::v-deep {
|
|
.project-name {
|
|
line-height: 30px;
|
|
}
|
|
|
|
.project-bar {
|
|
display: flex;
|
|
flex-direction: row;
|
|
justify-content: space-between;
|
|
|
|
&.has-description {
|
|
.right {
|
|
margin-top: 5px;
|
|
}
|
|
.group-tab {
|
|
&, &::after {
|
|
height: 50px;
|
|
}
|
|
|
|
&::after {
|
|
right: -20px;
|
|
}
|
|
|
|
.description {
|
|
margin-top: -20px;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
.namespace-name {
|
|
display: flex;
|
|
align-items: center;
|
|
}
|
|
}
|
|
}
|
|
</style>
|
|
<style lang="scss">
|
|
.psa-tooltip {
|
|
// These could pop up a lot as the mouse moves around, keep them as small and unintrusive as possible
|
|
// (easier to test with v-tooltip="{ content: getPSA(row), autoHide: false, show: true }")
|
|
margin: 3px 0;
|
|
padding: 0 8px 0 22px;
|
|
}
|
|
</style>
|