]> git.proxmox.com Git - ceph.git/blob - ceph/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-list/pool-list.component.ts
49449f5badd963869a518dbed5d5c678546abb69
[ceph.git] / ceph / src / pybind / mgr / dashboard / frontend / src / app / ceph / pool / pool-list / pool-list.component.ts
1 import { Component, OnInit, TemplateRef, ViewChild } from '@angular/core';
2
3 import { I18n } from '@ngx-translate/i18n-polyfill';
4 import * as _ from 'lodash';
5 import { BsModalRef, BsModalService } from 'ngx-bootstrap/modal';
6
7 import { ConfigurationService } from '../../../shared/api/configuration.service';
8 import { PoolService } from '../../../shared/api/pool.service';
9 import { CriticalConfirmationModalComponent } from '../../../shared/components/critical-confirmation-modal/critical-confirmation-modal.component';
10 import { ActionLabelsI18n, URLVerbs } from '../../../shared/constants/app.constants';
11 import { TableComponent } from '../../../shared/datatable/table/table.component';
12 import { CellTemplate } from '../../../shared/enum/cell-template.enum';
13 import { Icons } from '../../../shared/enum/icons.enum';
14 import { ViewCacheStatus } from '../../../shared/enum/view-cache-status.enum';
15 import { CdTableAction } from '../../../shared/models/cd-table-action';
16 import { CdTableColumn } from '../../../shared/models/cd-table-column';
17 import { CdTableSelection } from '../../../shared/models/cd-table-selection';
18 import { ExecutingTask } from '../../../shared/models/executing-task';
19 import { FinishedTask } from '../../../shared/models/finished-task';
20 import { Permissions } from '../../../shared/models/permissions';
21 import { DimlessPipe } from '../../../shared/pipes/dimless.pipe';
22 import { AuthStorageService } from '../../../shared/services/auth-storage.service';
23 import { TaskListService } from '../../../shared/services/task-list.service';
24 import { TaskWrapperService } from '../../../shared/services/task-wrapper.service';
25 import { URLBuilderService } from '../../../shared/services/url-builder.service';
26 import { PgCategoryService } from '../../shared/pg-category.service';
27 import { Pool } from '../pool';
28 import { PoolStat, PoolStats } from '../pool-stat';
29
30 const BASE_URL = 'pool';
31
32 @Component({
33 selector: 'cd-pool-list',
34 templateUrl: './pool-list.component.html',
35 providers: [
36 TaskListService,
37 { provide: URLBuilderService, useValue: new URLBuilderService(BASE_URL) }
38 ],
39 styleUrls: ['./pool-list.component.scss']
40 })
41 export class PoolListComponent implements OnInit {
42 @ViewChild(TableComponent, { static: true })
43 table: TableComponent;
44 @ViewChild('poolUsageTpl', { static: true })
45 poolUsageTpl: TemplateRef<any>;
46
47 @ViewChild('poolConfigurationSourceTpl', { static: false })
48 poolConfigurationSourceTpl: TemplateRef<any>;
49
50 pools: Pool[] = [];
51 columns: CdTableColumn[];
52 selection = new CdTableSelection();
53 modalRef: BsModalRef;
54 executingTasks: ExecutingTask[] = [];
55 permissions: Permissions;
56 tableActions: CdTableAction[];
57 viewCacheStatusList: any[];
58 selectionCacheTiers: any[] = [];
59 monAllowPoolDelete = false;
60
61 constructor(
62 private poolService: PoolService,
63 private taskWrapper: TaskWrapperService,
64 private authStorageService: AuthStorageService,
65 private taskListService: TaskListService,
66 private modalService: BsModalService,
67 private i18n: I18n,
68 private pgCategoryService: PgCategoryService,
69 private dimlessPipe: DimlessPipe,
70 private urlBuilder: URLBuilderService,
71 private configurationService: ConfigurationService,
72 public actionLabels: ActionLabelsI18n
73 ) {
74 this.permissions = this.authStorageService.getPermissions();
75 this.tableActions = [
76 {
77 permission: 'create',
78 icon: Icons.add,
79 routerLink: () => this.urlBuilder.getCreate(),
80 name: this.actionLabels.CREATE
81 },
82 {
83 permission: 'update',
84 icon: Icons.edit,
85 routerLink: () =>
86 this.urlBuilder.getEdit(encodeURIComponent(this.selection.first().pool_name)),
87 name: this.actionLabels.EDIT
88 },
89 {
90 permission: 'delete',
91 icon: Icons.destroy,
92 click: () => this.deletePoolModal(),
93 name: this.actionLabels.DELETE,
94 disable: () => !this.selection.first() || !this.monAllowPoolDelete,
95 disableDesc: () => this.getDisableDesc()
96 }
97 ];
98
99 // Note, we need read permissions to get the 'mon_allow_pool_delete'
100 // configuration option.
101 if (this.permissions.configOpt.read) {
102 this.configurationService.get('mon_allow_pool_delete').subscribe((data: any) => {
103 if (_.has(data, 'value')) {
104 const monSection = _.find(data.value, (v) => {
105 return v.section === 'mon';
106 }) || { value: false };
107 this.monAllowPoolDelete = monSection.value === 'true' ? true : false;
108 }
109 });
110 }
111 }
112
113 ngOnInit() {
114 const compare = (prop: string, pool1: Pool, pool2: Pool) =>
115 _.get(pool1, prop) > _.get(pool2, prop) ? 1 : -1;
116 this.columns = [
117 {
118 prop: 'pool_name',
119 name: this.i18n('Name'),
120 flexGrow: 4,
121 cellTransformation: CellTemplate.executing
122 },
123 {
124 prop: 'type',
125 name: this.i18n('Type'),
126 flexGrow: 2
127 },
128 {
129 prop: 'application_metadata',
130 name: this.i18n('Applications'),
131 flexGrow: 3
132 },
133 {
134 prop: 'pg_status',
135 name: this.i18n('PG Status'),
136 flexGrow: 3,
137 cellClass: ({ row, column, value }): any => {
138 return this.getPgStatusCellClass(row, column, value);
139 }
140 },
141 {
142 prop: 'size',
143 name: this.i18n('Replica Size'),
144 flexGrow: 2,
145 cellClass: 'text-right'
146 },
147 {
148 prop: 'last_change',
149 name: this.i18n('Last Change'),
150 flexGrow: 2,
151 cellClass: 'text-right'
152 },
153 {
154 prop: 'erasure_code_profile',
155 name: this.i18n('Erasure Coded Profile'),
156 flexGrow: 2
157 },
158 {
159 prop: 'crush_rule',
160 name: this.i18n('Crush Ruleset'),
161 flexGrow: 3
162 },
163 {
164 name: this.i18n('Usage'),
165 prop: 'usage',
166 cellTemplate: this.poolUsageTpl,
167 flexGrow: 3
168 },
169 {
170 prop: 'stats.rd_bytes.rates',
171 name: this.i18n('Read bytes'),
172 comparator: (_valueA: any, _valueB: any, rowA: Pool, rowB: Pool) =>
173 compare('stats.rd_bytes.latest', rowA, rowB),
174 cellTransformation: CellTemplate.sparkline,
175 flexGrow: 3
176 },
177 {
178 prop: 'stats.wr_bytes.rates',
179 name: this.i18n('Write bytes'),
180 comparator: (_valueA: any, _valueB: any, rowA: Pool, rowB: Pool) =>
181 compare('stats.wr_bytes.latest', rowA, rowB),
182 cellTransformation: CellTemplate.sparkline,
183 flexGrow: 3
184 },
185 {
186 prop: 'stats.rd.rate',
187 name: this.i18n('Read ops'),
188 flexGrow: 1,
189 pipe: this.dimlessPipe,
190 cellTransformation: CellTemplate.perSecond
191 },
192 {
193 prop: 'stats.wr.rate',
194 name: this.i18n('Write ops'),
195 flexGrow: 1,
196 pipe: this.dimlessPipe,
197 cellTransformation: CellTemplate.perSecond
198 }
199 ];
200
201 this.taskListService.init(
202 () => this.poolService.getList(),
203 undefined,
204 (pools) => (this.pools = this.transformPoolsData(pools)),
205 () => {
206 this.table.reset(); // Disable loading indicator.
207 this.viewCacheStatusList = [{ status: ViewCacheStatus.ValueException }];
208 },
209 (task) => task.name.startsWith(`${BASE_URL}/`),
210 (pool, task) => task.metadata['pool_name'] === pool.pool_name,
211 { default: (metadata: any) => new Pool(metadata['pool_name']) }
212 );
213 }
214
215 updateSelection(selection: CdTableSelection) {
216 this.selection = selection;
217 this.getSelectionTiers();
218 }
219
220 deletePoolModal() {
221 const name = this.selection.first().pool_name;
222 this.modalRef = this.modalService.show(CriticalConfirmationModalComponent, {
223 initialState: {
224 itemDescription: 'Pool',
225 itemNames: [name],
226 submitActionObservable: () =>
227 this.taskWrapper.wrapTaskAroundCall({
228 task: new FinishedTask(`${BASE_URL}/${URLVerbs.DELETE}`, { pool_name: name }),
229 call: this.poolService.delete(name)
230 })
231 }
232 });
233 }
234
235 getPgStatusCellClass(_row: any, _column: any, value: string): object {
236 return {
237 'text-right': true,
238 [`pg-${this.pgCategoryService.getTypeByStates(value)}`]: true
239 };
240 }
241
242 transformPoolsData(pools: any) {
243 const requiredStats = ['bytes_used', 'max_avail', 'rd_bytes', 'wr_bytes', 'rd', 'wr'];
244 const emptyStat: PoolStat = { latest: 0, rate: 0, rates: [] };
245
246 _.forEach(pools, (pool: Pool) => {
247 pool['pg_status'] = this.transformPgStatus(pool['pg_status']);
248 const stats: PoolStats = {};
249 _.forEach(requiredStats, (stat) => {
250 stats[stat] = pool.stats && pool.stats[stat] ? pool.stats[stat] : emptyStat;
251 });
252 pool['stats'] = stats;
253 const avail = stats.bytes_used.latest + stats.max_avail.latest;
254 pool['usage'] = avail > 0 ? stats.bytes_used.latest / avail : avail;
255
256 if (
257 !pool.cdExecuting &&
258 pool.pg_num + pool.pg_placement_num !== pool.pg_num_target + pool.pg_placement_num_target
259 ) {
260 pool['cdExecuting'] = 'Updating';
261 }
262
263 ['rd_bytes', 'wr_bytes'].forEach((stat) => {
264 pool.stats[stat].rates = pool.stats[stat].rates.map((point: any) => point[1]);
265 });
266 pool.cdIsBinary = true;
267 });
268
269 return pools;
270 }
271
272 transformPgStatus(pgStatus: any): string {
273 const strings: string[] = [];
274 _.forEach(pgStatus, (count, state) => {
275 strings.push(`${count} ${state}`);
276 });
277
278 return strings.join(', ');
279 }
280
281 getSelectionTiers() {
282 const cacheTierIds = this.selection.hasSingleSelection
283 ? this.selection.first()['tiers'] || []
284 : [];
285 this.selectionCacheTiers = this.pools.filter((pool) => cacheTierIds.includes(pool.pool));
286 }
287
288 getDisableDesc(): string | undefined {
289 if (!this.monAllowPoolDelete) {
290 return this.i18n(
291 'Pool deletion is disabled by the mon_allow_pool_delete configuration setting.'
292 );
293 }
294
295 return undefined;
296 }
297 }