@rancher/shell 0.3.24 → 0.3.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/styles/themes/_light.scss +1 -1
- package/assets/translations/en-us.yaml +36 -7
- package/assets/translations/zh-hans.yaml +1 -1
- package/components/ClusterIconMenu.vue +143 -0
- package/components/CruResource.vue +10 -1
- package/components/ExplorerProjectsNamespaces.vue +11 -1
- package/components/FixedBanner.vue +17 -1
- package/components/Markdown.vue +1 -1
- package/components/Questions/__tests__/Yaml.test.ts +3 -2
- package/components/SortableTable/index.vue +3 -2
- package/components/__tests__/ProjectRow.test.ts +63 -0
- package/components/auth/RoleDetailEdit.vue +19 -2
- package/components/auth/__tests__/RoleDetailEdit.test.ts +41 -0
- package/components/auth/login/saml.vue +12 -1
- package/components/form/LabeledSelect.vue +12 -5
- package/components/form/Members/ClusterPermissionsEditor.vue +1 -1
- package/components/form/Members/MembershipEditor.vue +6 -1
- package/components/form/ResourceQuota/ProjectRow.vue +6 -2
- package/components/form/__tests__/KeyValue.test.ts +6 -3
- package/components/form/__tests__/LabeledSelect.test.ts +18 -0
- package/components/formatter/PodsUsage.vue +11 -36
- package/components/formatter/PrincipalGroupBindings.vue +8 -5
- package/components/formatter/__tests__/PodsUsage.test.ts +36 -19
- package/components/nav/Group.vue +25 -27
- package/components/nav/Header.vue +12 -5
- package/components/nav/Pinned.vue +47 -0
- package/components/nav/TopLevelMenu.vue +233 -60
- package/components/nav/Type.vue +57 -3
- package/config/home-links.js +1 -1
- package/config/product/istio.js +15 -5
- package/config/router.js +3 -9
- package/config/table-headers.js +5 -6
- package/config/uiplugins.js +1 -0
- package/core/plugin-helpers.js +3 -0
- package/core/types.ts +6 -1
- package/creators/app/files/.vscode/settings.json +0 -1
- package/detail/__tests__/autoscaling.horizontalpodautoscaler.test.ts +118 -0
- package/detail/autoscaling.horizontalpodautoscaler/index.vue +4 -4
- package/detail/provisioning.cattle.io.cluster.vue +7 -5
- package/edit/__tests__/management.cattle.io.clusterroletemplatebinding.test.ts +58 -0
- package/edit/__tests__/namespace.test.ts +5 -3
- package/edit/management.cattle.io.clusterroletemplatebinding.vue +3 -11
- package/edit/namespace.vue +8 -4
- package/edit/provisioning.cattle.io.cluster/Basics.vue +662 -0
- package/edit/provisioning.cattle.io.cluster/CustomCommand.vue +6 -0
- package/edit/provisioning.cattle.io.cluster/DrainOptions.vue +13 -8
- package/edit/provisioning.cattle.io.cluster/MachinePool.vue +11 -2
- package/edit/provisioning.cattle.io.cluster/MemberRoles.vue +40 -0
- package/edit/provisioning.cattle.io.cluster/__tests__/Basics.tests.ts +237 -0
- package/edit/provisioning.cattle.io.cluster/__tests__/CustomCommand.tests.ts +71 -23
- package/edit/provisioning.cattle.io.cluster/__tests__/DrainOptions.test.ts +52 -0
- package/edit/provisioning.cattle.io.cluster/__tests__/rke2.test.ts +65 -142
- package/edit/provisioning.cattle.io.cluster/rke2.vue +211 -599
- package/edit/workload/storage/__tests__/Storage.test.ts +2 -2
- package/edit/workload/storage/persistentVolumeClaim/__tests__/persistentvolumeclaim.test.ts +36 -0
- package/edit/workload/storage/persistentVolumeClaim/persistentvolumeclaim.vue +15 -7
- package/initialize/index.js +5 -5
- package/layouts/default.vue +6 -6
- package/layouts/home.vue +6 -2
- package/layouts/plain.vue +9 -2
- package/list/fleet.cattle.io.cluster.vue +2 -2
- package/list/management.cattle.io.feature.vue +1 -1
- package/machine-config/vmwarevsphere.vue +48 -7
- package/mixins/brand.js +0 -8
- package/mixins/child-hook.js +2 -2
- package/mixins/create-edit-view/impl.js +3 -3
- package/models/__tests__/management.cattle.io.node.ts +96 -0
- package/models/__tests__/node.ts +74 -0
- package/models/cluster/node.js +6 -5
- package/models/cluster.x-k8s.io.machinedeployment.js +2 -2
- package/models/management.cattle.io.cluster.js +22 -1
- package/models/management.cattle.io.clusterroletemplatebinding.js +3 -3
- package/models/management.cattle.io.globalrole.js +17 -2
- package/models/management.cattle.io.node.js +6 -4
- package/models/management.cattle.io.projectroletemplatebinding.js +3 -3
- package/models/management.cattle.io.roletemplate.js +17 -2
- package/package.json +2 -6
- package/pages/about.vue +2 -0
- package/pages/auth/setup.vue +5 -4
- package/pages/c/_cluster/monitoring/index.vue +8 -3
- package/pages/c/_cluster/uiplugins/CatalogList/CatalogLoadDialog.vue +9 -66
- package/pages/c/_cluster/uiplugins/CatalogList/CatalogUninstallDialog.vue +182 -0
- package/pages/c/_cluster/uiplugins/CatalogList/index.vue +15 -32
- package/pages/c/_cluster/uiplugins/UninstallDialog.vue +8 -46
- package/pages/c/_cluster/uiplugins/index.vue +64 -64
- package/pages/diagnostic.vue +0 -39
- package/pages/home.vue +1 -1
- package/plugins/dashboard-store/normalize.js +4 -4
- package/plugins/int-number.js +5 -2
- package/plugins/positive-int-number.js +19 -0
- package/plugins/steve/__tests__/getters.spec.ts +15 -0
- package/plugins/steve/getters.js +22 -10
- package/rancher-components/Form/LabeledInput/LabeledInput.vue +0 -8
- package/rancher-components/Form/Radio/RadioButton.test.ts +3 -7
- package/scripts/extension/helm/charts/ui-plugin-server/Chart.yaml +2 -2
- package/store/index.js +4 -0
- package/store/prefs.js +1 -0
- package/types/shell/index.d.ts +13 -4
- package/utils/__tests__/cluster.test.ts +55 -0
- package/utils/__tests__/object.test.ts +21 -2
- package/utils/cluster.js +47 -1
- package/utils/object.js +12 -5
- package/utils/validators/formRules/__tests__/index.test.ts +13 -1
- package/utils/validators/formRules/index.ts +4 -0
- package/utils/validators/role-template.js +9 -1
- package/utils/version.js +1 -1
- package/yarn-error.log +16 -16
- package/components/ClusterProviderIconMenu.vue +0 -161
- package/content/docs/en-us/getting-started.md +0 -224
- package/content/docs/en-us/whats-new.md +0 -29
- package/content/docs/zh-hans/getting-started.md +0 -224
- package/content/docs/zh-hans/whats-new.md +0 -28
- package/pages/docs/_doc.vue +0 -345
- package/pages/docs/toc.js +0 -27
- package/plugins/console.js +0 -34
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { shallowMount } from '@vue/test-utils';
|
|
2
|
+
import HorizontalPodAutoScaler from '@shell/detail/autoscaling.horizontalpodautoscaler/index.vue';
|
|
3
|
+
|
|
4
|
+
describe('view: autoscaling.horizontalpodautoscaler', () => {
|
|
5
|
+
const mockStore = {
|
|
6
|
+
getters: {
|
|
7
|
+
'i18n/t': (text: string) => text,
|
|
8
|
+
t: (text: string) => text,
|
|
9
|
+
currentStore: () => 'current_store',
|
|
10
|
+
'current_store/schemaFor': jest.fn(),
|
|
11
|
+
'current_store/all': jest.fn(),
|
|
12
|
+
workspace: jest.fn(),
|
|
13
|
+
},
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const mocks = {
|
|
17
|
+
$store: mockStore,
|
|
18
|
+
$fetchState: { pending: false },
|
|
19
|
+
$route: {
|
|
20
|
+
query: { AS: '' },
|
|
21
|
+
name: {
|
|
22
|
+
endsWith: () => {
|
|
23
|
+
return false;
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const value = {
|
|
30
|
+
status: {
|
|
31
|
+
currentMetrics: [
|
|
32
|
+
{
|
|
33
|
+
resource: {
|
|
34
|
+
current: {
|
|
35
|
+
averageUtilization: 8,
|
|
36
|
+
averageValue: '11481088'
|
|
37
|
+
},
|
|
38
|
+
name: 'memory'
|
|
39
|
+
},
|
|
40
|
+
type: 'Resource'
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
resource: {
|
|
44
|
+
current: {
|
|
45
|
+
averageUtilization: 0,
|
|
46
|
+
averageValue: '1m'
|
|
47
|
+
},
|
|
48
|
+
name: 'cpu'
|
|
49
|
+
},
|
|
50
|
+
type: 'Resource'
|
|
51
|
+
}
|
|
52
|
+
],
|
|
53
|
+
},
|
|
54
|
+
spec: {
|
|
55
|
+
maxReplicas: 10,
|
|
56
|
+
metrics: [
|
|
57
|
+
{
|
|
58
|
+
resource: {
|
|
59
|
+
name: 'memory',
|
|
60
|
+
target: {
|
|
61
|
+
averageUtilization: 80,
|
|
62
|
+
type: 'Utilization'
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
type: 'Resource'
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
resource: {
|
|
69
|
+
name: 'cpu',
|
|
70
|
+
target: {
|
|
71
|
+
averageUtilization: 50,
|
|
72
|
+
type: 'Utilization'
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
type: 'Resource'
|
|
76
|
+
}
|
|
77
|
+
],
|
|
78
|
+
minReplicas: 1,
|
|
79
|
+
scaleTargetRef: {
|
|
80
|
+
apiVersion: 'apps/v1',
|
|
81
|
+
kind: 'Deployment',
|
|
82
|
+
name: 'php-apache'
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const metricsValue = Object.values(value.spec.metrics);
|
|
88
|
+
const currentMetrics = Object.values(value.status.currentMetrics);
|
|
89
|
+
|
|
90
|
+
const wrapper = shallowMount(HorizontalPodAutoScaler, {
|
|
91
|
+
mocks,
|
|
92
|
+
propsData: { value },
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe.each(value.spec.metrics)('should display metrics for each resource:', (metric) => {
|
|
96
|
+
const name = metric.resource.name;
|
|
97
|
+
|
|
98
|
+
it(`${ name }:`, () => {
|
|
99
|
+
// Resource metrics
|
|
100
|
+
const resourceValue = wrapper.find(`[data-testid="resource-metrics-value-${ name }"]`);
|
|
101
|
+
const resourceName = wrapper.find(`[data-testid="resource-metrics-name-${ name }"]`);
|
|
102
|
+
const metricValue = metricsValue.find((f) => f.resource.name === name)?.resource;
|
|
103
|
+
|
|
104
|
+
// Current Metrics
|
|
105
|
+
const averageUtilization = wrapper.find(`[data-testid="current-metrics-Average Utilization-${ name }"]`);
|
|
106
|
+
const averageValue = wrapper.find(`[data-testid="current-metrics-Average Value-${ name }"]`);
|
|
107
|
+
const currentResource = currentMetrics.find((f) => f.resource.name === name)?.resource.current;
|
|
108
|
+
|
|
109
|
+
// Resource metrics
|
|
110
|
+
expect(resourceValue.element.textContent).toBe(`${ metricValue?.target?.averageUtilization }`);
|
|
111
|
+
expect(resourceName.element.textContent).toBe(`${ metricValue?.name }`);
|
|
112
|
+
|
|
113
|
+
// Current Metrics
|
|
114
|
+
expect(averageUtilization.element.textContent).toBe(`${ currentResource?.averageUtilization }`);
|
|
115
|
+
expect(averageValue.element.textContent).toBe(`${ currentResource?.averageValue }`);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
});
|
|
@@ -43,7 +43,7 @@ export default {
|
|
|
43
43
|
return metrics.map((metric) => {
|
|
44
44
|
const metricValue = get(metric, camelCase(metric.type));
|
|
45
45
|
const targetType = metricValue?.target?.type;
|
|
46
|
-
const currentMatch = findBy(currentMetrics, '
|
|
46
|
+
const currentMatch = findBy(currentMetrics, 'resource.name', metric.resource.name);
|
|
47
47
|
const current = currentMatch ? get(currentMatch, `${ camelCase(metric.type) }.current`) : null;
|
|
48
48
|
const currentMetricsKVs = [];
|
|
49
49
|
|
|
@@ -128,7 +128,7 @@ export default {
|
|
|
128
128
|
<label class="text-label">
|
|
129
129
|
<t k="hpa.metrics.headers.value" />:
|
|
130
130
|
</label>
|
|
131
|
-
<span>{{ metric.targetValue }}</span>
|
|
131
|
+
<span :data-testid="`resource-metrics-value-${metric.subRowContent.resourceName}`">{{ metric.targetValue }}</span>
|
|
132
132
|
</div>
|
|
133
133
|
<div v-if="metric.metricSource === 'Object'">
|
|
134
134
|
<div class="mb-5">
|
|
@@ -155,7 +155,7 @@ export default {
|
|
|
155
155
|
<label class="text-label">
|
|
156
156
|
<t k="hpa.metrics.headers.resource" />:
|
|
157
157
|
</label>
|
|
158
|
-
<span>{{ metric.subRowContent.resourceName }}</span>
|
|
158
|
+
<span :data-testid="`resource-metrics-name-${metric.subRowContent.resourceName}`">{{ metric.subRowContent.resourceName }}</span>
|
|
159
159
|
</div>
|
|
160
160
|
</div>
|
|
161
161
|
</div>
|
|
@@ -173,7 +173,7 @@ export default {
|
|
|
173
173
|
<label class="text-label">
|
|
174
174
|
{{ current.targetName }}:
|
|
175
175
|
</label>
|
|
176
|
-
<span>{{ current.targetValue }}</span>
|
|
176
|
+
<span :data-testid="`current-metrics-${current.targetName}-${metric.subRowContent.resourceName}`">{{ current.targetValue }}</span>
|
|
177
177
|
</div>
|
|
178
178
|
</div>
|
|
179
179
|
</div>
|
|
@@ -339,9 +339,10 @@ export default {
|
|
|
339
339
|
pool._clusterSpec = mp;
|
|
340
340
|
|
|
341
341
|
return {
|
|
342
|
-
poolId:
|
|
343
|
-
mainRowKey:
|
|
342
|
+
poolId: pool.id,
|
|
343
|
+
mainRowKey: 'isFake',
|
|
344
344
|
pool,
|
|
345
|
+
availableActions: []
|
|
345
346
|
};
|
|
346
347
|
});
|
|
347
348
|
},
|
|
@@ -361,9 +362,10 @@ export default {
|
|
|
361
362
|
const emptyNodePools = this.allNodePools.filter((x) => x.spec.clusterName === this.value.mgmtClusterId && x.spec.quantity === 0);
|
|
362
363
|
|
|
363
364
|
return emptyNodePools.map((np) => ({
|
|
364
|
-
spec:
|
|
365
|
-
mainRowKey:
|
|
366
|
-
pool:
|
|
365
|
+
spec: { nodePoolName: np.id.replace('/', ':') },
|
|
366
|
+
mainRowKey: 'isFake',
|
|
367
|
+
pool: np,
|
|
368
|
+
availableActions: []
|
|
367
369
|
}));
|
|
368
370
|
},
|
|
369
371
|
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/* eslint-disable jest/no-hooks */
|
|
2
|
+
import { mount } from '@vue/test-utils';
|
|
3
|
+
import ClusterRoleTemplateBinding from '@shell/edit/management.cattle.io.clusterroletemplatebinding.vue';
|
|
4
|
+
import Banner from '@components/Banner/Banner.vue';
|
|
5
|
+
import CruResource from '@shell/components/CruResource';
|
|
6
|
+
|
|
7
|
+
describe('view: management.cattle.io.clusterroletemplatebinding should', () => {
|
|
8
|
+
let wrapper: any;
|
|
9
|
+
|
|
10
|
+
const stubs = {
|
|
11
|
+
ClusterPermissionsEditor: true,
|
|
12
|
+
Loading: true
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const requiredSetup = () => ({
|
|
16
|
+
// Remove all these mocks after migration to Vue 2.7/3 due mixin logic
|
|
17
|
+
mocks: {
|
|
18
|
+
$store: {
|
|
19
|
+
getters: {
|
|
20
|
+
currentStore: () => 'current_store',
|
|
21
|
+
'current_store/schemaFor': jest.fn(),
|
|
22
|
+
'current_store/all': jest.fn(),
|
|
23
|
+
currentCluster: { id: 'my-cluster' },
|
|
24
|
+
currentProduct: { inStore: 'whatever' },
|
|
25
|
+
'i18n/t': (val) => val,
|
|
26
|
+
'i18n/exists': jest.fn(),
|
|
27
|
+
},
|
|
28
|
+
dispatch: { 'management/findAll': () => ([]) }
|
|
29
|
+
},
|
|
30
|
+
$fetchState: { pending: false },
|
|
31
|
+
$route: { query: { AS: '' } },
|
|
32
|
+
$router: {
|
|
33
|
+
applyQuery: jest.fn(),
|
|
34
|
+
replace: jest.fn()
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
propsData: { value: {} },
|
|
38
|
+
stubs,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
afterEach(() => {
|
|
42
|
+
wrapper.destroy();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('should only show one error banner', async() => {
|
|
46
|
+
const errors = ['mistake!'];
|
|
47
|
+
|
|
48
|
+
wrapper = mount(ClusterRoleTemplateBinding, { ...requiredSetup() });
|
|
49
|
+
|
|
50
|
+
const cruResourceElem = wrapper.findComponent(CruResource);
|
|
51
|
+
|
|
52
|
+
await cruResourceElem.vm.$emit('error', errors);
|
|
53
|
+
|
|
54
|
+
const bannerElems = wrapper.findAllComponents(Banner);
|
|
55
|
+
|
|
56
|
+
expect(bannerElems).toHaveLength(1);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
@@ -25,9 +25,10 @@ describe('view Namespace should', () => {
|
|
|
25
25
|
$router: { applyQuery: {} },
|
|
26
26
|
$store: {
|
|
27
27
|
getters: {
|
|
28
|
-
'i18n/t':
|
|
29
|
-
'management/all':
|
|
30
|
-
currentProduct:
|
|
28
|
+
'i18n/t': jest.fn(),
|
|
29
|
+
'management/all': () => ([project]),
|
|
30
|
+
currentProduct: jest.fn(),
|
|
31
|
+
isStandaloneHarvester: false
|
|
31
32
|
}
|
|
32
33
|
}
|
|
33
34
|
},
|
|
@@ -36,6 +37,7 @@ describe('view Namespace should', () => {
|
|
|
36
37
|
ContainerResourceLimit: { template: '<div data-testid="limits"></div>' }, // Ensure value to be added to component
|
|
37
38
|
NameNsDescription: true,
|
|
38
39
|
Tab: true,
|
|
40
|
+
ResourceTabs: true
|
|
39
41
|
}
|
|
40
42
|
});
|
|
41
43
|
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
<script>
|
|
2
2
|
import CreateEditView from '@shell/mixins/create-edit-view';
|
|
3
3
|
import CruResource from '@shell/components/CruResource';
|
|
4
|
-
import Banner from '@components/Banner/Banner.vue';
|
|
5
4
|
import { MANAGEMENT } from '@shell/config/types';
|
|
6
5
|
import Loading from '@shell/components/Loading';
|
|
7
6
|
import ClusterPermissionsEditor from '@shell/components/form/Members/ClusterPermissionsEditor';
|
|
@@ -9,7 +8,6 @@ import { exceptionToErrorsArray } from '@shell/utils/error';
|
|
|
9
8
|
|
|
10
9
|
export default {
|
|
11
10
|
components: {
|
|
12
|
-
Banner,
|
|
13
11
|
ClusterPermissionsEditor,
|
|
14
12
|
CruResource,
|
|
15
13
|
Loading,
|
|
@@ -64,15 +62,9 @@ export default {
|
|
|
64
62
|
@finish="saveOverride"
|
|
65
63
|
@cancel="done"
|
|
66
64
|
>
|
|
67
|
-
<ClusterPermissionsEditor
|
|
68
|
-
v-model="bindings"
|
|
69
|
-
:cluster-name="$store.getters['currentCluster'].id"
|
|
70
|
-
/>
|
|
71
|
-
<Banner
|
|
72
|
-
v-for="(err, i) in errors"
|
|
73
|
-
:key="i"
|
|
74
|
-
color="error"
|
|
75
|
-
:label="err"
|
|
65
|
+
<ClusterPermissionsEditor
|
|
66
|
+
v-model="bindings"
|
|
67
|
+
:cluster-name="$store.getters['currentCluster'].id"
|
|
76
68
|
/>
|
|
77
69
|
</CruResource>
|
|
78
70
|
</template>
|
package/edit/namespace.vue
CHANGED
|
@@ -7,8 +7,8 @@ import { MANAGEMENT } from '@shell/config/types';
|
|
|
7
7
|
import { CONTAINER_DEFAULT_RESOURCE_LIMIT, PROJECT } from '@shell/config/labels-annotations';
|
|
8
8
|
import ContainerResourceLimit from '@shell/components/ContainerResourceLimit';
|
|
9
9
|
import PodSecurityAdmission from '@shell/components/PodSecurityAdmission';
|
|
10
|
-
import Tabbed from '@shell/components/Tabbed';
|
|
11
10
|
import Tab from '@shell/components/Tabbed/Tab';
|
|
11
|
+
import ResourceTabs from '@shell/components/form/ResourceTabs/index.vue';
|
|
12
12
|
import CruResource from '@shell/components/CruResource';
|
|
13
13
|
import { PROJECT_ID, _VIEW, FLAT_VIEW, _CREATE } from '@shell/config/query-params';
|
|
14
14
|
import MoveModal from '@shell/components/MoveModal';
|
|
@@ -30,7 +30,7 @@ export default {
|
|
|
30
30
|
PodSecurityAdmission,
|
|
31
31
|
ResourceQuota,
|
|
32
32
|
Tab,
|
|
33
|
-
|
|
33
|
+
ResourceTabs,
|
|
34
34
|
MoveModal
|
|
35
35
|
},
|
|
36
36
|
|
|
@@ -198,7 +198,11 @@ export default {
|
|
|
198
198
|
/>
|
|
199
199
|
</template>
|
|
200
200
|
</NameNsDescription>
|
|
201
|
-
<
|
|
201
|
+
<ResourceTabs
|
|
202
|
+
v-model="value"
|
|
203
|
+
:mode="mode"
|
|
204
|
+
:side-tabs="true"
|
|
205
|
+
>
|
|
202
206
|
<Tab
|
|
203
207
|
v-if="showResourceQuota"
|
|
204
208
|
:weight="1"
|
|
@@ -269,7 +273,7 @@ export default {
|
|
|
269
273
|
@updateLabels="PSAChanged"
|
|
270
274
|
/>
|
|
271
275
|
</Tab>
|
|
272
|
-
</
|
|
276
|
+
</ResourceTabs>
|
|
273
277
|
<MoveModal v-if="projects" />
|
|
274
278
|
</CruResource>
|
|
275
279
|
</template>
|