Merge pull request #13306 from torchiaf/13189-fleet-created-by-label

Fleet: Created By label
This commit is contained in:
Francesco Torchia 2025-02-25 11:08:04 +01:00 committed by GitHub
commit 9e3b7b1b4e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 278 additions and 25 deletions

View File

@ -2,7 +2,8 @@ export const gitRepoCreateRequest = {
type: 'fleet.cattle.io.gitrepo', type: 'fleet.cattle.io.gitrepo',
metadata: { metadata: {
namespace: 'fleet-default', namespace: 'fleet-default',
name: 'fleet-e2e-test-gitrepo' name: 'fleet-e2e-test-gitrepo',
labels: {},
}, },
spec: { spec: {
repo: 'https://github.com/rancher/fleet-test-data.git', repo: 'https://github.com/rancher/fleet-test-data.git',

View File

@ -11,6 +11,9 @@ const fakeProvClusterId = 'some-fake-cluster-id';
const fakeMgmtClusterId = 'some-fake-mgmt-id'; const fakeMgmtClusterId = 'some-fake-mgmt-id';
describe('Git Repo', { testIsolation: 'off', tags: ['@fleet', '@adminUser'] }, () => { describe('Git Repo', { testIsolation: 'off', tags: ['@fleet', '@adminUser'] }, () => {
let adminUsername = '';
let adminUserId = '';
describe('Create', () => { describe('Create', () => {
const listPage = new FleetGitRepoListPagePo(); const listPage = new FleetGitRepoListPagePo();
const gitRepoCreatePage = new GitRepoCreatePo('_'); const gitRepoCreatePage = new GitRepoCreatePo('_');
@ -36,6 +39,11 @@ describe('Git Repo', { testIsolation: 'off', tags: ['@fleet', '@adminUser'] }, (
}); });
beforeEach(() => { beforeEach(() => {
cy.getRancherResource('v3', 'users?me=true').then((resp: Cypress.Response<any>) => {
adminUserId = resp.body.data[0].id.trim();
adminUsername = resp.body.data[0].username.trim();
});
cy.createE2EResourceName('git-repo').as('gitRepo'); cy.createE2EResourceName('git-repo').as('gitRepo');
}); });
@ -91,7 +99,10 @@ describe('Git Repo', { testIsolation: 'off', tags: ['@fleet', '@adminUser'] }, (
return cy.wait('@interceptGitRepo'); return cy.wait('@interceptGitRepo');
}) })
.then(({ request, response }) => { .then(({ request, response }) => {
gitRepoCreateRequest.metadata.labels['fleet.cattle.io/created-by-display-name'] = adminUsername;
gitRepoCreateRequest.metadata.labels['fleet.cattle.io/created-by-user-id'] = adminUserId;
gitRepoCreateRequest.spec.helmSecretName = secretName; gitRepoCreateRequest.spec.helmSecretName = secretName;
expect(response.statusCode).to.eq(201); expect(response.statusCode).to.eq(201);
expect(request.body).to.deep.eq(gitRepoCreateRequest); expect(request.body).to.deep.eq(gitRepoCreateRequest);

View File

@ -5068,6 +5068,7 @@ resourceDetail:
view: "{subtype} {name}" view: "{subtype} {name}"
masthead: masthead:
age: Age age: Age
createdBy: Created by
restartCount: Pod Restarts restartCount: Pod Restarts
defaultBannerMessage: defaultBannerMessage:
error: This resource is currently in an error state, but there isn't a detailed message available. error: This resource is currently in an error state, but there isn't a detailed message available.

View File

@ -504,10 +504,32 @@ export default {
{{ namespace }} {{ namespace }}
</span> </span>
</span> </span>
<span v-if="parent.showAge">{{ t("resourceDetail.masthead.age") }}: <LiveDate <span v-if="parent.showAge">
class="live-date" {{ t("resourceDetail.masthead.age") }}:
:value="value.creationTimestamp" <LiveDate
/></span> class="live-date"
:value="value.creationTimestamp"
/>
</span>
<span
v-if="value.showCreatedBy"
data-testid="masthead-subheader-createdBy"
>
{{ t("resourceDetail.masthead.createdBy") }}:
<router-link
v-if="value.createdBy.location"
:to="value.createdBy.location"
data-testid="masthead-subheader-createdBy-link"
>
{{ value.createdBy.displayName }}
</router-link>
<span
v-else
data-testid="masthead-subheader-createdBy_plain-text"
>
{{ value.createdBy.displayName }}
</span>
</span>
<span v-if="value.showPodRestarts">{{ t("resourceDetail.masthead.restartCount") }}:<span class="live-data"> {{ value.restartCount }}</span></span> <span v-if="value.showPodRestarts">{{ t("resourceDetail.masthead.restartCount") }}:<span class="live-data"> {{ value.restartCount }}</span></span>
</div> </div>
</div> </div>

View File

@ -0,0 +1,61 @@
import { mount, RouterLinkStub } from '@vue/test-utils';
import { _VIEW } from '@shell/config/query-params';
import Masthead from '@shell/components/ResourceDetail/Masthead.vue';
const mockedStore = () => {
return {
getters: {
currentStore: () => 'current_store',
currentProduct: { inStore: 'cluster' },
isExplorer: false,
currentCluster: {},
'type-map/labelFor': jest.fn(),
'type-map/optionsFor': jest.fn(),
'current_store/schemaFor': jest.fn(),
},
};
};
const requiredSetup = () => {
return {
stubs: {
'router-link': RouterLinkStub,
LiveDate: true
},
mocks: { $store: mockedStore() }
};
};
describe('component: Masthead', () => {
it.each([
['hidden', '', false, { displayName: 'admin', location: { id: 'resource-id' } }, false, false],
['plain-text', 'admin', true, { displayName: 'admin', location: null }, false, true],
['link', 'foo', true, { displayName: 'foo', location: { id: 'resource-id' } }, true, false],
])('"Created By" should be %p, with text: %p', (
_,
text,
showCreatedBy,
createdBy,
showLink,
showPlainText,
) => {
const wrapper = mount(Masthead, {
props: {
mode: _VIEW,
value: {
showCreatedBy,
createdBy,
},
},
global: { ...requiredSetup() }
});
const container = wrapper.find('[data-testid="masthead-subheader-createdBy"]');
const link = wrapper.find('[data-testid="masthead-subheader-createdBy-link"]');
const plainText = wrapper.find('[data-testid="masthead-subheader-createdBy_plain-text"]');
expect(link.exists()).toBe(showLink);
expect(plainText.exists()).toBe(showPlainText);
expect(showLink || showPlainText ? container.element.textContent : '').toContain(text);
});
});

View File

@ -73,8 +73,12 @@ export default {
return this.value.metadata.name; return this.value.metadata.name;
}, },
repoNamespace() {
return this.value.metadata.namespace;
},
bundleCounts() { bundleCounts() {
const resources = this.bundles.filter((item) => item.repoName === this.repoName); const resources = this.bundles.filter((item) => item.namespace === this.repoNamespace && item.repoName === this.repoName);
if (!resources.length) { if (!resources.length) {
return []; return [];
@ -89,7 +93,7 @@ export default {
return; return;
} }
const k = status?.summary.ready > 0 && status?.summary.desiredReady === status.summary.ready; const k = status?.summary?.ready > 0 && status?.summary.desiredReady === status?.summary?.ready;
if (k) { if (k) {
out.ready.count += 1; out.ready.count += 1;

View File

@ -14,12 +14,12 @@ const mockedBundlesInRepo = [{
apiVersion: 'fleet.cattle.io/v1alpha1', apiVersion: 'fleet.cattle.io/v1alpha1',
kind: 'Bundle', kind: 'Bundle',
repoName: REPO_NAME, repoName: REPO_NAME,
namespace: 'fleet-default',
metadata: { metadata: {
labels: { labels: {
'fleet.cattle.io/commit': '3640888439d1b7b6a53dbeee291a533eea2632ab', 'fleet.cattle.io/commit': '3640888439d1b7b6a53dbeee291a533eea2632ab',
'fleet.cattle.io/repo-name': REPO_NAME 'fleet.cattle.io/repo-name': REPO_NAME
}, },
name: `${ REPO_NAME }-${ CLUSTER_NAME }-1234`, name: `${ REPO_NAME }-${ CLUSTER_NAME }-1234`,
namespace: 'fleet-default', namespace: 'fleet-default',
state: { state: {
@ -54,12 +54,12 @@ const mockedBundlesInRepo = [{
apiVersion: 'fleet.cattle.io/v1alpha1', apiVersion: 'fleet.cattle.io/v1alpha1',
kind: 'Bundle', kind: 'Bundle',
repoName: REPO_NAME, repoName: REPO_NAME,
namespace: 'fleet-default',
metadata: { metadata: {
labels: { labels: {
'fleet.cattle.io/commit': '3640888439d1b7b6a53dbeee291a533eea2632ab', 'fleet.cattle.io/commit': '3640888439d1b7b6a53dbeee291a533eea2632ab',
'fleet.cattle.io/repo-name': REPO_NAME 'fleet.cattle.io/repo-name': REPO_NAME
}, },
name: `${ REPO_NAME }-${ CLUSTER_NAME }-5678`, name: `${ REPO_NAME }-${ CLUSTER_NAME }-5678`,
namespace: 'fleet-default', namespace: 'fleet-default',
state: { state: {
@ -95,14 +95,54 @@ const mockedBundlesOutOfRepo = [{
apiVersion: 'fleet.cattle.io/v1alpha1', apiVersion: 'fleet.cattle.io/v1alpha1',
kind: 'Bundle', kind: 'Bundle',
repoName: REPO_NAME_VARIANT, repoName: REPO_NAME_VARIANT,
namespace: 'custom-namespace',
metadata: { metadata: {
labels: { labels: {
'fleet.cattle.io/commit': '3640888439d1b7b6a53dbeee291a533eea2632ab', 'fleet.cattle.io/commit': '3640888439d1b7b6a53dbeee291a533eea2632ab',
'fleet.cattle.io/repo-name': REPO_NAME_VARIANT 'fleet.cattle.io/repo-name': REPO_NAME_VARIANT
}, },
name: `${ REPO_NAME_VARIANT }-${ CLUSTER_NAME }-1234`, name: `${ REPO_NAME_VARIANT }-${ CLUSTER_NAME }-1234`,
namespace: 'fleet-default', namespace: 'custom-namespace',
state: {
error: false,
message: 'Resource is Ready',
name: 'active',
transitioning: false
},
},
spec: {
correctDrift: { },
forceSyncGeneration: 2,
ignore: { },
namespace: 'custom-namespace-name',
resources: [
{
content: 'apiVersion: v1\nkind: ConfigMap\nmetadata:\n name: configmap-test\n annotations:\n {}\n# key: string\n labels:\n {}\n# key: string\ndata:\n key1: val1\n key2: val2\n key3: val3',
name: 'test-configmap.yaml'
}
],
targets: [
{
clusterName: 'nb-cluster0-28',
namespace: 'custom-namespace-name'
}
]
}
},
{
id: `fleet-default/${ REPO_NAME }-${ CLUSTER_NAME }-1234`,
type: 'fleet.cattle.io.bundle',
apiVersion: 'fleet.cattle.io/v1alpha1',
kind: 'Bundle',
repoName: REPO_NAME,
namespace: 'custom-namespace',
metadata: {
labels: {
'fleet.cattle.io/commit': '3640888439d1b7b6a53dbeee291a533eea2632ab',
'fleet.cattle.io/repo-name': REPO_NAME
},
name: `${ REPO_NAME }-${ CLUSTER_NAME }-1234`,
namespace: 'custom-namespace',
state: { state: {
error: false, error: false,
message: 'Resource is Ready', message: 'Resource is Ready',
@ -135,14 +175,14 @@ const mockedBundlesOutOfRepo = [{
apiVersion: 'fleet.cattle.io/v1alpha1', apiVersion: 'fleet.cattle.io/v1alpha1',
kind: 'Bundle', kind: 'Bundle',
repoName: REPO_NAME_VARIANT, repoName: REPO_NAME_VARIANT,
namespace: 'custom-namespace',
metadata: { metadata: {
labels: { labels: {
'fleet.cattle.io/commit': '3640888439d1b7b6a53dbeee291a533eea2632ab', 'fleet.cattle.io/commit': '3640888439d1b7b6a53dbeee291a533eea2632ab',
'fleet.cattle.io/repo-name': REPO_NAME_VARIANT 'fleet.cattle.io/repo-name': REPO_NAME_VARIANT
}, },
name: `${ REPO_NAME_VARIANT }-${ CLUSTER_NAME }-5678`, name: `${ REPO_NAME_VARIANT }-${ CLUSTER_NAME }-5678`,
namespace: 'fleet-default', namespace: 'custom-namespace',
state: { state: {
error: false, error: false,
message: 'Resource is Ready', message: 'Resource is Ready',
@ -176,7 +216,6 @@ const mockRepo = {
apiVersion: 'fleet.cattle.io/v1alpha1', apiVersion: 'fleet.cattle.io/v1alpha1',
kind: 'GitRepo', kind: 'GitRepo',
metadata: { metadata: {
name: `${ REPO_NAME }`, name: `${ REPO_NAME }`,
namespace: 'fleet-default', namespace: 'fleet-default',
state: { state: {

View File

@ -109,6 +109,7 @@ export const CATALOG = {
}; };
export const FLEET = { export const FLEET = {
REPO_NAME: 'fleet.cattle.io/repo-name',
CLUSTER_DISPLAY_NAME: 'management.cattle.io/cluster-display-name', CLUSTER_DISPLAY_NAME: 'management.cattle.io/cluster-display-name',
CLUSTER_NAME: 'management.cattle.io/cluster-name', CLUSTER_NAME: 'management.cattle.io/cluster-name',
BUNDLE_ID: 'fleet.cattle.io/bundle-id', BUNDLE_ID: 'fleet.cattle.io/bundle-id',
@ -116,7 +117,9 @@ export const FLEET = {
BUNDLE_NAMESPACE: 'fleet.cattle.io/bundle-namespace', BUNDLE_NAMESPACE: 'fleet.cattle.io/bundle-namespace',
MANAGED: 'fleet.cattle.io/managed', MANAGED: 'fleet.cattle.io/managed',
CLUSTER_NAMESPACE: 'fleet.cattle.io/cluster-namespace', CLUSTER_NAMESPACE: 'fleet.cattle.io/cluster-namespace',
CLUSTER: 'fleet.cattle.io/cluster' CLUSTER: 'fleet.cattle.io/cluster',
CREATED_BY_USER_ID: 'fleet.cattle.io/created-by-user-id',
CREATED_BY_USER_NAME: 'fleet.cattle.io/created-by-display-name',
}; };
export const RBAC = { PRODUCT: 'management.cattle.io/ui-product' }; export const RBAC = { PRODUCT: 'management.cattle.io/ui-product' };

View File

@ -1,4 +1,5 @@
<script> <script>
import { MANAGEMENT } from '@shell/config/types';
import FleetBundleResources from '@shell/components/fleet/FleetBundleResources.vue'; import FleetBundleResources from '@shell/components/fleet/FleetBundleResources.vue';
import FleetUtils from '@shell/utils/fleet'; import FleetUtils from '@shell/utils/fleet';
@ -13,6 +14,12 @@ export default {
} }
}, },
async fetch() {
if (this.value.authorId && this.$store.getters['management/schemaFor'](MANAGEMENT.USER)) {
await this.$store.dispatch(`management/findAll`, { type: MANAGEMENT.USER }, { root: true });
}
},
computed: { computed: {
bundleResources() { bundleResources() {
return FleetUtils.resourcesFromBundleStatus(this.value?.status); return FleetUtils.resourcesFromBundleStatus(this.value?.status);

View File

@ -5,7 +5,7 @@ import FleetSummary from '@shell/components/fleet/FleetSummary';
import { Banner } from '@components/Banner'; import { Banner } from '@components/Banner';
import FleetResources from '@shell/components/fleet/FleetResources'; import FleetResources from '@shell/components/fleet/FleetResources';
import Tab from '@shell/components/Tabbed/Tab'; import Tab from '@shell/components/Tabbed/Tab';
import { FLEET } from '@shell/config/types'; import { FLEET, MANAGEMENT } from '@shell/config/types';
import { isHarvesterCluster } from '@shell/utils/cluster'; import { isHarvesterCluster } from '@shell/utils/cluster';
import FleetBundles from '@shell/components/fleet/FleetBundles.vue'; import FleetBundles from '@shell/components/fleet/FleetBundles.vue';
import { checkSchemasForFindAllHash } from '@shell/utils/auth'; import { checkSchemasForFindAllHash } from '@shell/utils/auth';
@ -91,6 +91,10 @@ export default {
} }
}, this.$store); }, this.$store);
if (this.value.authorId && this.$store.getters['management/schemaFor'](MANAGEMENT.USER)) {
await this.$store.dispatch(`management/findAll`, { type: MANAGEMENT.USER }, { root: true });
}
this.allBundles = allDispatches.allBundles || []; this.allBundles = allDispatches.allBundles || [];
this.allFleetClusters = allDispatches.allFleetClusters || []; this.allFleetClusters = allDispatches.allFleetClusters || [];
}, },

View File

@ -319,6 +319,7 @@ export default {
created() { created() {
this.registerBeforeHook(this.cleanTLS, 'cleanTLS'); this.registerBeforeHook(this.cleanTLS, 'cleanTLS');
this.registerBeforeHook(this.doCreateSecrets, `registerAuthSecrets${ new Date().getTime() }`, 99); this.registerBeforeHook(this.doCreateSecrets, `registerAuthSecrets${ new Date().getTime() }`, 99);
this.registerBeforeHook(this.updateBeforeSave);
}, },
methods: { methods: {
@ -569,11 +570,14 @@ export default {
}); });
}, },
onSave() { updateBeforeSave() {
this.value.spec['correctDrift'] = { enabled: this.correctDriftEnabled }; this.value.spec['correctDrift'] = { enabled: this.correctDriftEnabled };
this.save(); if (this.mode === _CREATE) {
} this.value.metadata.labels[FLEET_LABELS.CREATED_BY_USER_ID] = this.value.currentUser.id;
this.value.metadata.labels[FLEET_LABELS.CREATED_BY_USER_NAME] = this.value.currentUser.username;
}
},
} }
}; };
</script> </script>
@ -594,7 +598,7 @@ export default {
class="wizard" class="wizard"
@cancel="done" @cancel="done"
@error="e=>errors = e" @error="e=>errors = e"
@finish="onSave" @finish="save"
> >
<template #noticeBanner> <template #noticeBanner>
<Banner <Banner

View File

@ -2,7 +2,8 @@ import { escapeHtml, ucFirst } from '@shell/utils/string';
import SteveModel from '@shell/plugins/steve/steve-class'; import SteveModel from '@shell/plugins/steve/steve-class';
import typeHelper from '@shell/utils/type-helpers'; import typeHelper from '@shell/utils/type-helpers';
import { addObject, addObjects, findBy } from '@shell/utils/array'; import { addObject, addObjects, findBy } from '@shell/utils/array';
import { FLEET } from '@shell/config/types'; import { FLEET, MANAGEMENT } from '@shell/config/types';
import { FLEET as FLEET_ANNOTATIONS } from '@shell/config/labels-annotations';
import { convertSelectorObj, matching } from '@shell/utils/selector'; import { convertSelectorObj, matching } from '@shell/utils/selector';
export default class FleetBundle extends SteveModel { export default class FleetBundle extends SteveModel {
@ -21,7 +22,7 @@ export default class FleetBundle extends SteveModel {
get repoName() { get repoName() {
const labels = this.metadata?.labels || {}; const labels = this.metadata?.labels || {};
return labels['fleet.cattle.io/repo-name']; return labels[FLEET_ANNOTATIONS.REPO_NAME];
} }
get targetClusters() { get targetClusters() {
@ -127,4 +128,41 @@ export default class FleetBundle extends SteveModel {
); );
} }
} }
get authorId() {
return this.metadata?.labels?.[FLEET_ANNOTATIONS.CREATED_BY_USER_ID];
}
get author() {
if (this.authorId) {
return this.$rootGetters['management/byId'](MANAGEMENT.USER, this.authorId);
}
return null;
}
get createdBy() {
const displayName = this.metadata?.labels?.[FLEET_ANNOTATIONS.CREATED_BY_USER_NAME];
if (!displayName) {
return null;
}
return {
displayName,
location: !this.author ? null : {
name: 'c-cluster-product-resource-id',
params: {
cluster: '_',
product: 'auth',
resource: MANAGEMENT.USER,
id: this.author.id,
}
}
};
}
get showCreatedBy() {
return !!this.createdBy;
}
} }

View File

@ -2,7 +2,7 @@ import { convert, matching, convertSelectorObj } from '@shell/utils/selector';
import jsyaml from 'js-yaml'; import jsyaml from 'js-yaml';
import isEmpty from 'lodash/isEmpty'; import isEmpty from 'lodash/isEmpty';
import { escapeHtml } from '@shell/utils/string'; import { escapeHtml } from '@shell/utils/string';
import { FLEET } from '@shell/config/types'; import { FLEET, MANAGEMENT } from '@shell/config/types';
import { FLEET as FLEET_ANNOTATIONS } from '@shell/config/labels-annotations'; import { FLEET as FLEET_ANNOTATIONS } from '@shell/config/labels-annotations';
import { addObject, addObjects, findBy, insertAt } from '@shell/utils/array'; import { addObject, addObjects, findBy, insertAt } from '@shell/utils/array';
import { set } from '@shell/utils/object'; import { set } from '@shell/utils/object';
@ -42,6 +42,10 @@ function normalizeStateCounts(data) {
} }
export default class GitRepo extends SteveModel { export default class GitRepo extends SteveModel {
get currentUser() {
return this.$rootGetters['auth/v3User'] || {};
}
applyDefaults() { applyDefaults() {
const spec = this.spec || {}; const spec = this.spec || {};
const meta = this.metadata || {}; const meta = this.metadata || {};
@ -131,6 +135,18 @@ export default class GitRepo extends SteveModel {
this.save(); this.save();
} }
goToClone() {
if (this.metadata?.labels?.[FLEET_ANNOTATIONS.CREATED_BY_USER_ID]) {
delete this.metadata.labels[FLEET_ANNOTATIONS.CREATED_BY_USER_ID];
}
if (this.metadata?.labels?.[FLEET_ANNOTATIONS.CREATED_BY_USER_NAME]) {
delete this.metadata.labels[FLEET_ANNOTATIONS.CREATED_BY_USER_NAME];
}
super.goToClone();
}
forceUpdate() { forceUpdate() {
const now = this.spec.forceSyncGeneration || 1; const now = this.spec.forceSyncGeneration || 1;
@ -352,16 +368,21 @@ export default class GitRepo extends SteveModel {
} }
get bundles() { get bundles() {
return this.$getters['matching'](FLEET.BUNDLE, { 'fleet.cattle.io/repo-name': this.name }, this.namespace); return this.$getters['matching'](FLEET.BUNDLE, { [FLEET_ANNOTATIONS.REPO_NAME]: this.name }, this.namespace);
} }
get bundleDeployments() { get bundleDeployments() {
return this.$getters['matching'](FLEET.BUNDLE_DEPLOYMENT, { 'fleet.cattle.io/repo-name': this.name }); return this.$getters['matching'](FLEET.BUNDLE_DEPLOYMENT, { [FLEET_ANNOTATIONS.REPO_NAME]: this.name });
} }
get allBundlesStatuses() { get allBundlesStatuses() {
return this.bundles.reduce((acc, bundle) => { return this.bundles.reduce((acc, bundle) => {
if (isEmpty(bundle.status?.summary)) {
return acc;
}
const { nonReadyResources, ...summary } = bundle.status?.summary; const { nonReadyResources, ...summary } = bundle.status?.summary;
const bdCounts = normalizeStateCounts(summary); const bdCounts = normalizeStateCounts(summary);
const state = primaryDisplayStatusFromCount(bdCounts.states); const state = primaryDisplayStatusFromCount(bdCounts.states);
@ -479,4 +500,41 @@ export default class GitRepo extends SteveModel {
get clustersList() { get clustersList() {
return this.$getters['all'](FLEET.CLUSTER); return this.$getters['all'](FLEET.CLUSTER);
} }
get authorId() {
return this.metadata?.labels?.[FLEET_ANNOTATIONS.CREATED_BY_USER_ID];
}
get author() {
if (this.authorId) {
return this.$rootGetters['management/byId'](MANAGEMENT.USER, this.authorId);
}
return null;
}
get createdBy() {
const displayName = this.metadata?.labels?.[FLEET_ANNOTATIONS.CREATED_BY_USER_NAME];
if (!displayName) {
return null;
}
return {
displayName,
location: !this.author ? null : {
name: 'c-cluster-product-resource-id',
params: {
cluster: '_',
product: 'auth',
resource: MANAGEMENT.USER,
id: this.author.id,
}
}
};
}
get showCreatedBy() {
return !!this.createdBy;
}
} }