Skip to content

Commit 817f014

Browse files
marcus-svajggoebel
andauthored
add cost tracking properties for vmtemplate (hobbyfarm#239)
* add cost tracking properties for vmtemplate * Add Dashboard to display costs per event * Display cost per month * Global Cost Dashboard * Display more digits for the base price * Add settings for currency symbol * sortable table * Add linechart to show accumulated costs --------- Co-authored-by: Jan-Gerrit Göbel <[email protected]>
1 parent b6cd7ad commit 817f014

30 files changed

+1013
-55
lines changed

package-lock.json

Lines changed: 20 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
"angular-split": "^17.2.0",
4242
"brace": "^0.11.1",
4343
"chart.js": "^4.3.0",
44+
"chartjs-adapter-date-fns": "^3.0.0",
4445
"chartjs-plugin-datalabels": "^2.2.0",
4546
"dompurify": "^3.1.7",
4647
"marked": "^12.0.2",

src/app/app-routing.module.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { RolesComponent } from './configuration/roles/roles/roles.component';
1919
import { SessionStatisticsComponent } from './session-statistics/session-statistics.component';
2020
import { SettingsComponent } from './configuration/settings/settings.component';
2121
import { DashboardDetailsComponent } from './dashboards/dashboard-details/dashboard-details.component';
22+
import { CostStatisticsComponent } from './cost-statistics/cost-dashboard.component';
2223

2324
const routes: Routes = [
2425
{ path: '', redirectTo: '/home', pathMatch: 'full' },
@@ -32,6 +33,10 @@ const routes: Routes = [
3233
path: 'statistics/sessions',
3334
component: SessionStatisticsComponent,
3435
},
36+
{
37+
path: 'statistics/costs',
38+
component: CostStatisticsComponent,
39+
},
3540
],
3641
},
3742
{

src/app/app.module.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,10 @@ import { TooltipComponent } from './tooltip/tooltip.component';
172172
import { ScrollingModule } from '@angular/cdk/scrolling';
173173
import { AuthnService } from './data/authn.service';
174174
import { SessionProgressService } from './progress/session-progress.service';
175+
import { CostService } from './data/cost.service';
176+
import { CostDashboardComponent } from './dashboards/cost-dashboard/cost-dashboard.component';
177+
import { CostStatisticsComponent } from './cost-statistics/cost-dashboard.component';
178+
import { MonthlyCostChartComponent } from './dashboards/cost-dashboard/monthly-cost-chart/monthly-cost-chart.component';
175179

176180
ClarityIcons.addIcons(
177181
plusIcon,
@@ -244,6 +248,7 @@ export function jwtOptionsFactory(): JwtConfig {
244248
AppComponent,
245249
HomeComponent,
246250
SessionStatisticsComponent,
251+
CostStatisticsComponent,
247252
SessionTimeStatisticsComponent,
248253
HeaderComponent,
249254
EventComponent,
@@ -279,6 +284,8 @@ export function jwtOptionsFactory(): JwtConfig {
279284
DashboardsComponent,
280285
VmDashboardComponent,
281286
UsersDashboardComponent,
287+
CostDashboardComponent,
288+
MonthlyCostChartComponent,
282289
StepComponent,
283290
HfMarkdownComponent,
284291
TerminalComponent,
@@ -369,6 +376,7 @@ export function jwtOptionsFactory(): JwtConfig {
369376
PredefinedServiceService,
370377
ThemeService,
371378
TypedSettingsService,
379+
CostService,
372380
{
373381
provide: APP_INITIALIZER,
374382
useFactory: appInitializerFn,

src/app/configuration/vmtemplates/edit-vmtemplate/edit-vmtemplate.component.html

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,39 @@
6262
</clr-input-container>
6363
</form>
6464
</clr-wizard-page>
65+
66+
<clr-wizard-page [clrWizardPageNextDisabled]="!costDetails.valid">
67+
<ng-template clrPageTitle>Cost</ng-template>
68+
69+
<form clrForm [formGroup]="costDetails">
70+
<clr-input-container>
71+
<label>Base Price</label>
72+
<input
73+
clrInput
74+
type="number"
75+
placeholder="base price"
76+
name="cost_base_price"
77+
formControlName="cost_base_price"
78+
/>
79+
<clr-control-error *clrIfError="'invalidFloat'"
80+
>Optional base price needs to be a floating-point number greater or equal zero</clr-control-error
81+
>
82+
</clr-input-container>
83+
<clr-select-container>
84+
<label>Time Unit</label>
85+
<select title="cost_time_unit" clrSelect formControlName="cost_time_unit">
86+
<option value=""></option>
87+
<option value="seconds">seconds</option>
88+
<option value="minutes">minutes</option>
89+
<option value="hours">hours</option>
90+
</select>
91+
</clr-select-container>
92+
<clr-control-error *ngIf="costDetails.errors?.invalidAllOrNone">
93+
Either both or none of the fields must be set.
94+
</clr-control-error>
95+
</form>
96+
</clr-wizard-page>
97+
6598
<clr-wizard-page [clrWizardPageNextDisabled]="!configMap.valid">
6699
<ng-template clrPageTitle>Config Map</ng-template>
67100

@@ -197,6 +230,59 @@ <h4>Basic Information</h4>
197230
</tbody>
198231
</table>
199232

233+
<h4>Cost</h4>
234+
<table class="table table-compact">
235+
<thead>
236+
<tr>
237+
<th>Option</th>
238+
<th>Value</th>
239+
</tr>
240+
</thead>
241+
<tbody>
242+
@if (!!this.editTemplate) {
243+
<tr>
244+
<td>Base Price</td>
245+
@if (template.cost_base_price == uneditedTemplate.cost_base_price) {
246+
<td>
247+
{{ template.cost_base_price ?? 'None' }}
248+
</td>
249+
} @else {
250+
<td>
251+
<span class="del-elem arrow-after">{{
252+
uneditedTemplate.cost_base_price ?? 'None'
253+
}}</span>
254+
<span class="add-elem">{{ template.cost_base_price ?? 'None' }}</span>
255+
</td>
256+
}
257+
</tr>
258+
<tr>
259+
<td>Time Unit</td>
260+
@if (template.cost_time_unit == uneditedTemplate.cost_time_unit) {
261+
<td>
262+
{{ template.cost_time_unit ?? 'None' }}
263+
</td>
264+
} @else {
265+
<td>
266+
<span class="del-elem arrow-after">{{
267+
uneditedTemplate.cost_time_unit ?? 'None'
268+
}}</span>
269+
<span class="add-elem">{{ template.cost_time_unit ?? 'None' }}</span>
270+
</td>
271+
}
272+
</tr>
273+
} @else {
274+
<tr>
275+
<td>Base Price</td>
276+
<td>{{ template.cost_base_price ?? 'None' }}</td>
277+
</tr>
278+
<tr>
279+
<td>Time Unit</td>
280+
<td>{{ template.cost_time_unit ?? 'None' }}</td>
281+
</tr>
282+
}
283+
</tbody>
284+
</table>
285+
200286
<h4>Config Map</h4>
201287
<table class="table table-compact">
202288
<thead>

src/app/configuration/vmtemplates/edit-vmtemplate/edit-vmtemplate.component.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ import { ClrWizard } from '@clr/angular';
1919
import { DEFAULT_ALERT_ERROR_DURATION } from 'src/app/alert/alert';
2020
import { AlertComponent } from 'src/app/alert/alert.component';
2121
import { CloudInitConfig } from 'src/app/data/cloud-init-config';
22+
import { EitherAllOrNoneValidator } from '../../../validators/eitherallornone.validator';
23+
import { FloatValidator } from '../../../validators/float.validator';
2224
import { GenericKeyValueGroup } from 'src/app/data/forms';
2325
import { ServerResponse } from 'src/app/data/serverresponse';
2426
import { VMTemplateServiceConfiguration } from 'src/app/data/vm-template-service-configuration';
@@ -36,6 +38,10 @@ export class EditVmtemplateComponent implements OnInit, OnChanges {
3638
name: FormControl<string>;
3739
image: FormControl<string>;
3840
}>;
41+
public costDetails: FormGroup<{
42+
cost_base_price: FormControl<string>;
43+
cost_time_unit: FormControl<string>;
44+
}>;
3945
public configMap: FormGroup<{
4046
mappings: FormArray<GenericKeyValueGroup<string>>;
4147
}>;
@@ -81,6 +87,7 @@ export class EditVmtemplateComponent implements OnInit, OnChanges {
8187

8288
private _build() {
8389
this.buildConfigMap();
90+
this.buildCostDetails()
8491
this.buildTemplateDetails();
8592
}
8693

@@ -97,6 +104,23 @@ export class EditVmtemplateComponent implements OnInit, OnChanges {
97104
});
98105
}
99106

107+
public buildCostDetails(vmTemplate: VMTemplate | null = null) {
108+
this.costDetails = this._fb.group(
109+
{
110+
cost_base_price: this._fb.control<string>(
111+
vmTemplate?.cost_base_price ?? '',
112+
FloatValidator(0, Number.MAX_VALUE),
113+
),
114+
cost_time_unit: this._fb.control<string>(
115+
vmTemplate?.cost_time_unit ?? '',
116+
),
117+
},
118+
{
119+
validators: [EitherAllOrNoneValidator(['cost_base_price', 'cost_time_unit'])],
120+
}
121+
);
122+
}
123+
100124
public buildConfigMap() {
101125
this.configMap = this._fb.group({
102126
mappings: this._fb.array<GenericKeyValueGroup<string>>([
@@ -166,6 +190,16 @@ export class EditVmtemplateComponent implements OnInit, OnChanges {
166190
this.template.image = this.templateDetails.controls.image.value;
167191
}
168192

193+
public copyCostDetails() {
194+
this.template.cost_base_price = this.costDetails.controls.cost_base_price.value
195+
? this.costDetails.controls.cost_base_price.value
196+
: undefined;
197+
this.template.cost_time_unit = this.costDetails.controls.cost_time_unit.value
198+
? this.costDetails.controls.cost_time_unit.value
199+
: undefined;
200+
}
201+
202+
169203
public copyConfigMap() {
170204
this.template.config_map = {};
171205
for (let i = 0; i < this.configMap.controls.mappings.length; i++) {
@@ -186,6 +220,7 @@ export class EditVmtemplateComponent implements OnInit, OnChanges {
186220
}
187221
public copyTemplate() {
188222
this.copyConfigMap();
223+
this.copyCostDetails()
189224
this.copyTemplateDetails();
190225
}
191226

@@ -231,6 +266,7 @@ export class EditVmtemplateComponent implements OnInit, OnChanges {
231266

232267
private _prepare(vmTemplate: VMTemplate) {
233268
this.buildTemplateDetails(vmTemplate);
269+
this.buildCostDetails(vmTemplate);
234270
this.prepareConfigMap(vmTemplate);
235271
}
236272

@@ -242,6 +278,7 @@ export class EditVmtemplateComponent implements OnInit, OnChanges {
242278
this.wizard.pages.first.makeCurrent();
243279
} else {
244280
this.buildTemplateDetails();
281+
this.buildCostDetails();
245282
this.buildConfigMap();
246283
}
247284
this.uneditedTemplate = structuredClone(this.template);

src/app/configuration/vmtemplates/vmtemplate-detail/vmtemplate-detail.component.html

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,23 @@
1616
<clr-stack-content>{{ currentVmTemplate.image }}</clr-stack-content>
1717
</clr-stack-block>
1818
</clr-stack-block>
19+
<clr-stack-block>
20+
<clr-stack-label>Cost</clr-stack-label>
21+
<clr-stack-block>
22+
<clr-stack-label class="stackbox-header">Option</clr-stack-label>
23+
<clr-stack-content class="stackbox-header">Value</clr-stack-content>
24+
</clr-stack-block>
25+
<clr-stack-block>
26+
<clr-stack-label>Base Price</clr-stack-label>
27+
<clr-stack-content>
28+
{{ currentVmTemplate.cost_base_price ? currentVmTemplate.cost_base_price : 'None' }}
29+
</clr-stack-content>
30+
</clr-stack-block>
31+
<clr-stack-block>
32+
<clr-stack-label>Time Unit</clr-stack-label>
33+
{{ currentVmTemplate.cost_time_unit ? currentVmTemplate.cost_time_unit : 'None' }}
34+
</clr-stack-block>
35+
</clr-stack-block>
1936
@if (
2037
currentVmTemplate.config_map && !isEmpty(currentVmTemplate.config_map)
2138
) {
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<div class="clr-row">
2+
<!-- Top Row -->
3+
<div class="clr-col">
4+
<div class="card">
5+
<div class="card-block">
6+
<h4 class="card-title">
7+
{{ allCost.total | currency: currencySymbol }}
8+
</h4>
9+
<p class="card-text">All resources</p>
10+
</div>
11+
</div>
12+
</div>
13+
</div>
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
.card {
2+
display: flex;
3+
align-items: center;
4+
justify-content: center;
5+
text-align: center;
6+
}
7+
8+
.card-block {
9+
display: flex;
10+
flex-direction: column;
11+
align-items: center;
12+
justify-content: center;
13+
}

0 commit comments

Comments
 (0)