qlik-embed

This commit is contained in:
Manuel Romero
2023-11-17 12:23:53 +01:00
parent 8f8c18490a
commit 835d162ede
27 changed files with 641 additions and 43 deletions

View File

@@ -8,8 +8,25 @@
<link rel="icon" href="assets/favicon.ico">
<!-- Load environment variables -->
<script src="env.js"></script>
<script
crossorigin="anonymous"
type="application/javascript"
src="https://cdn.jsdelivr.net/npm/@qlik/embed-web-components"
data-host="https://qmicloud-dev.qliktech.com/proxy"
></script>
<!--<script
crossorigin="anonymous"
type="application/javascript"
src="https://cdn.jsdelivr.net/npm/@qlik/embed-web-components"
data-host="https://gear-presales.eu.qlikcloud.com"
data-web-integration-id="n4kMLH62hvXXC84q2vdfW15WUvrUw-HU"
data-cross-site-cookies="true"
></script>-->
<link rel="stylesheet" href="styles.fc71de1623889098932b.css"></head>
<body>
<app-root></app-root>
<script src="runtime.c51bd5b1c616d9ffddc1.js" defer></script><script src="polyfills-es5.6fef7e679f78bcc42760.js" nomodule defer></script><script src="polyfills.51f5cc3d1309de3a873d.js" defer></script><script src="scripts.1af868998801499c8755.js" defer></script><script src="main.05dfdb69a237b856ebb4.js" defer></script></body>
<script src="runtime.c51bd5b1c616d9ffddc1.js" defer></script><script src="polyfills-es5.6fef7e679f78bcc42760.js" nomodule defer></script><script src="polyfills.51f5cc3d1309de3a873d.js" defer></script><script src="scripts.1af868998801499c8755.js" defer></script><script src="main.218f4cae1bfdda7b6e1a.js" defer></script></body>
</html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,6 +1,6 @@
{
"name": "qmi-cloud-app",
"version": "3.0.0",
"version": "4.0.0",
"scripts": {
"start": "node -r esm server/server.js",
"start:dev": "nodemon -r esm server/server.js",
@@ -33,6 +33,7 @@
"bull-arena": "^2.6.4",
"chart.js": "^2.9.3",
"connect-mongo": "^3.0.0",
"cookie": "^0.6.0",
"cookie-parser": "^1.4.4",
"core-js": "^2.5.4",
"esm": "^3.2.25",
@@ -56,6 +57,8 @@
"rxjs": "~6.5.4",
"swagger-jsdoc": "3.5.0",
"swagger-ui-express": "4.1.3",
"uuid": "^9.0.1",
"ws": "^8.14.2",
"zone.js": "~0.10.3"
},
"devDependencies": {

View File

@@ -0,0 +1,52 @@
-----BEGIN PRIVATE KEY-----
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
HQe3R2QQtfmWZ320cdhukaok4yj3
-----END PRIVATE KEY-----

View File

@@ -10,6 +10,8 @@ const axios = require('axios');
const path = require('path');
const fs = require('fs');
var sessionStore;
// Start QuickStart here
var OIDCStrategy = require('passport-azure-ad').OIDCStrategy;
@@ -167,21 +169,27 @@ module.exports.init = function(app){
// set up session middleware
if (config.useMongoDBSessionStore) {
sessionStore = new MongoStore({
mongooseConnection: mongoose.connection,
//url: process.env.MONGO_URI,
autoRemove: 'interval',
autoRemoveInterval: 10
//clear_interval: config.mongoDBSessionMaxAge
});
//mongoose.connect(config.databaseUri);
app.use(expressSession({
secret: 'secret',
cookie: {maxAge: config.mongoDBSessionMaxAge * 1000},
store: new MongoStore({
mongooseConnection: mongoose.connection,
autoRemove: 'interval',
autoRemoveInterval: 10
//clear_interval: config.mongoDBSessionMaxAge
}),
store: sessionStore,
resave: true,
saveUninitialized: false
}));
} else {
app.use(expressSession({ secret: 'keyboard cat', resave: true, saveUninitialized: false }));
app.use(expressSession({
secret: 'keyboard cat',
resave: true,
saveUninitialized: false
}));
}
@@ -301,4 +309,6 @@ module.exports.ensureAuthenticatedAndIsMe = async function (req, res, next) {
}
}
return res.status(401).send("Error: Unauthorized");
};
};
module.exports.sessionStore = sessionStore;

248
server/routes/qsProxy.js Normal file
View File

@@ -0,0 +1,248 @@
const express = require('express')
const router = express.Router()
const jsonwebtoken = require("jsonwebtoken");
const fs = require("fs");
const path = require("path");
const cookie = require("cookie");
const cookieParser = require("cookie-parser");
const axios = require("axios");
const { v4: uuidv4 } = require("uuid");
const ws = require("ws");
const WebSocketServer = ws.WebSocketServer;
const WebSocket = ws.WebSocket;
const passport = require('../passport');
// This is the frontend application uri used for responding to requests.
const frontendUri = "https://outstanding-desert-gatsby.glitch.me";
const privKey = fs.readFileSync(path.resolve(__dirname, '..', 'certs', 'privateKey.pem'));
const TENANT_DOMAIN = process.env["TENANT_DOMAIN"] || "gear-presales.eu.qlikcloud.com";
const JWT_KEYID = process.env["JWT_KEYID"] || "8889be0d-6cf0-44eb-9fef-24bbc83712c5";
const JWT_ISSUER = process.env["JWT_ISSUER"] || TENANT_DOMAIN;
const JWT_USER_GROUPS = ["AnonJWTGroup"];
console.log("TENANT_DOMAIN", TENANT_DOMAIN);
const auth = {
generateToken: function (user) {
// kid and issuer have to match with the IDP config and the audience has to be qlik.api/jwt-login-session
const signingOptions = {
keyid: JWT_KEYID,
algorithm: "RS256",
issuer: JWT_ISSUER,
expiresIn: "60m",
notBefore: "0s",
audience: "qlik.api/login/jwt-session",
};
// These are the claims that will be accepted and mapped anything else will be ignored. sub, subtype and name are mandatory.
const uuid = uuidv4();
userEmail = user.mail;
console.log("JWT for user: ",userEmail);
const payload = {
jti: uuid,
sub: `az/${user.sub}`,
subType: "user",
name: user.displayName,
email: userEmail,
email_verified: true,
groups: JWT_USER_GROUPS,
};
const token = jsonwebtoken.sign(payload, privKey, signingOptions);
return token;
},
getQlikSessionCookie: async function (token) {
try {
const resp = await axios(`https://${TENANT_DOMAIN}/login/jwt-session`, {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
},
});
if (resp.status === 200) {
console.log("#QSProxy - login/jwt-session", resp.headers['set-cookie'])
return resp.headers['set-cookie']
.map((e) => {
return e.split(";")[0];
})
.join(";");
}
return "";
} catch (e) {
console.log("ERRRRRRROE", e);
return "";
}
},
};
const getStoreData = function (sidParsed) {
const store = passport.sessionStore;
return new Promise((resolve) => {
store.get(sidParsed, (err, session) => {
if (err) {
throw err;
}
return resolve(session);
});
});
};
router.get("/*.js", async (req, res) => {
setCors(res);
res.redirect(`https://${TENANT_DOMAIN}${req.path}`);
res.end();
});
// fetch resource from qlik using a redirect instead of proxy
// This endpoint is necessary when your web application uses the capability API.
router.get("/resources/*", async (req, res) => {
setCors(res);
res.redirect(`https://${TENANT_DOMAIN}${req.path}`);
res.end();
});
router.get('/assets/*', async (req, res) => {
setCors(res);
res.redirect(`https://${TENANT_DOMAIN}${req.path}`);
res.end();
});
// Issues the necessary pre-flight request to make sure the browser
// knows how to work with the web application.
router.options("/*", async (req, res) => {
setCors(res);
res.status(200).end();
});
function setCors(res) {
//res.set("Access-Control-Allow-Origin", "http://localhost:3000");
res.set("Access-Control-Allow-Methods", "GET, OPTIONS");
res.set("Access-Control-Allow-Headers", "Content-Type, x-proxy-session-id");
res.set("Access-Control-Allow-Credentials", "true");
}
// Intercepts a request to one of Qlik's REST APIs and proxies the request to
// Qlik Cloud.
router.get("/api/v1/*", passport.ensureAuthenticated, async (req, res) => {
setCors(res);
console.log("#QSProxy: ");
var session = req.session;
const reqHeaders = {};
if (session && session.id && session.qlikSession) {
console.log("# QSProxy - COOKIE FROM session", session.qlikSession);
reqHeaders.cookie = decodeURIComponent(session.qlikSession);
} else {
console.log("# QSProxy - New cookie request for user");
console.log(req.user);
// Read userEmail from cookies
const jwtToken = auth.generateToken(req.user);
console.log("# QSProxy - JWT token: ", jwtToken);
const qlikSession = await auth.getQlikSessionCookie(jwtToken);
session.qlikSession = encodeURIComponent(qlikSession);
reqHeaders.cookie = decodeURIComponent(session.qlikSession);
}
console.log("# QSProxy - qlikSession", reqHeaders.cookie);
const { status, data } = await axios(`https://${TENANT_DOMAIN}${req.path}`, {
headers: reqHeaders,
});
res.status(status).end(data);
});
router.get('/qlik-embed-iframe/*', async (req, res) => {
setCors(res);
res.redirect(`https://${TENANT_DOMAIN}${req.path}`);
res.end();
});
function init (server) {
// Websocket section for intercepting websocket requests from the
// frontend application. When the front end application communicates
// communicates with the backend using websockets, this set of
// functions will be invoked.
const wss = new WebSocketServer({ server });
wss.on("connection", async function connection(ws, req) {
let isOpened = false;
// WebSockets do not have access to session information.
// To get the session you need to parse the 1st-party cookie.
// This will give you access to the Qlik Cloud cookie in order
// to proxy requests.
const cookieString = req.headers.cookie;
let qlikCookie = "";
if (cookieString) {
const cookieParsed = cookie.parse(cookieString);
const appCookie = cookieParsed["connect.sid"];
if (appCookie) {
const sidParsed = cookieParser.signedCookie(appCookie, "secret");
var session = await getStoreData(sidParsed);
qlikCookie = decodeURIComponent(session.qlikSession);
}
}
const appId = req.url.match("/app/(.*)\\?")[1];
const csrfToken= qlikCookie.match("_csrfToken=(.*);")[1];
var wsConnUrl = `wss://${TENANT_DOMAIN}/app/${appId}?qlik-csrf-token=${csrfToken}`;
console.log("# QSProxy - wsConnUrl", wsConnUrl);
const qlikWebSocket = new WebSocket(
wsConnUrl,
{
headers: {
cookie: qlikCookie,
},
}
);
qlikWebSocket.on("error", console.error);
const openPromise = new Promise((resolve) => {
qlikWebSocket.on("open", function open() {
console.log("# QSProxy - wsConnUrl connnection open");
resolve();
});
});
ws.on("message", async function message(data) {
if (!isOpened) {
await openPromise;
isOpened = true;
}
qlikWebSocket.send(data.toString());
});
qlikWebSocket.on("message", function message(data) {
console.log("wsConnUrl message", data.toString());
ws.send(data.toString());
});
});
}
module.exports.router = router;
module.exports.init = init;

View File

@@ -16,6 +16,7 @@ const routesApiApikeys = require('./routes/api-apikeys');
const routesApiStats = require('./routes/api-stats');
const routesApiTraining = require('./routes/api-training');
const swaggerUi = require('swagger-ui-express');
const qsProxy = require("./routes/qsProxy");
const swaggerJsdoc = require('swagger-jsdoc');
const cookieParser = require('cookie-parser');
const bodyParser = require('body-parser');
@@ -99,6 +100,7 @@ app.use("/api/v1/deployopts", routesApiDeployOpts);
app.use("/api/v1/apikeys", routesApiApikeys);
app.use("/api/v1/stats", routesApiStats);
app.use("/api/v1/training", routesApiTraining);
app.use("/proxy", qsProxy.router);
function _isAllowedPath(path){
const allowedPaths = [ '/api-docs', '/arena', '/costexport', '/backendlogs', '/photos/user/' ];
@@ -214,9 +216,7 @@ dirs.forEach(d => {
/**
* Start App
*/
app.listen(3000, () => {
console.log(`Server listening on port 3000`)
});
var server;
if ( process.env.CERT_PFX_PASSWORD && process.env.CERT_PFX_FILENAME) {
var optionsHttps = {
@@ -224,9 +224,15 @@ if ( process.env.CERT_PFX_PASSWORD && process.env.CERT_PFX_FILENAME) {
passphrase: process.env.CERT_PFX_PASSWORD
};
https.createServer(optionsHttps, app).listen(3100, function(){
server = https.createServer(optionsHttps, app).listen(3100, function(){
console.log(`Secure server listening on port 3100`);
});
} else {
server = app.listen(3000, () => {
console.log(`Server listening on port 3000`)
});
}
qsProxy.init(server);

View File

@@ -1,5 +1,5 @@
import { BrowserModule } from '@angular/platform-browser';
import { NgModule, SecurityContext } from '@angular/core';
import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
@@ -11,6 +11,7 @@ import { AuthGuard } from './services/auth.guard';
import { ProvisionsService } from './services/provisions.service';
import { ScenariosService } from './services/scenarios.service';
import { UsersService } from './services/users.service';
import { QlikService } from './services/qs.service';
import { MDBBootstrapModule } from 'angular-bootstrap-md';
import { MarkdownModule, MarkedOptions, MarkedRenderer } from 'ngx-markdown';
@@ -113,7 +114,7 @@ export function markedOptions(): MarkedOptions {
SessionModalComponent,
SessionInfoModalComponent,
UserModalComponent,
TableSessionsComponent
TableSessionsComponent,
],
imports: [
BrowserModule,
@@ -138,8 +139,10 @@ export function markedOptions(): MarkedOptions {
AuthGuard,
FeatureGuard,
StatsService,
TrainingService
TrainingService,
QlikService
],
bootstrap: [AppComponent]
bootstrap: [AppComponent],
schemas: [ CUSTOM_ELEMENTS_SCHEMA ]
})
export class AppModule { }

View File

@@ -3,5 +3,26 @@
<p>
Cost dataset with history data from <b>June 2021 (Azure)</b> and <b>January 2022 (AWS)</b>. Updated on daily basis.
</p>
<!--
<iframe id="cost-analysis" *ngIf="qcsSheetUrl" class="cost-analysis" [src]="qcsSheetUrl" width="100%" height="1000px"></iframe>
-->
<div style="width:200px; height: 200px">
<qlik-embed
id="visualization"
ui="object"
app-id="1d2a43cf-8bc7-422c-90bd-d021cb232776"
object-id="GmhFFG"
></qlik-embed>
</div>
<div style="width: 100%; height:1000px;">
<qlik-embed
ui="analytics/sheet"
app-id="1d2a43cf-8bc7-422c-90bd-d021cb232776"
object-id="1ad3eb62-330d-45ac-8456-a6c8a76e044b"
>
</qlik-embed>
</div>
</div>

View File

@@ -17,6 +17,7 @@ export class CostComponent implements OnInit,OnDestroy {
ngOnInit(): void {
/*
this.subs = this._provisionsService.getCurrentQCSUser().subscribe( value => {
console.log("value", value);
this.qcsSheetUrl = this.sanitizer.bypassSecurityTrustResourceUrl(`${this._provisionsService.urlQlikServer}/single/?appid=1d2a43cf-8bc7-422c-90bd-d021cb232776&sheet=1ad3eb62-330d-45ac-8456-a6c8a76e044b&theme=breeze&opt=ctxmenu,currsel`);
@@ -24,13 +25,12 @@ export class CostComponent implements OnInit,OnDestroy {
console.log('oops', error);
const url = `${this._provisionsService.urlQlikServer}/login?qlik-web-integration-id=${this._provisionsService.webIntegrationId}&returnto=${window.location.href}`;
window.location.href = url;
});
});*/
}
ngOnDestroy(): void {
this.subs.unsubscribe();
//this.subs.unsubscribe();
}
}

View File

@@ -37,6 +37,9 @@
</div>
</div>
<div class="contentbox">
<div *ngIf="costData">
<b>{{getCost(info._id)}} spent so far</b>
</div>
<div *ngIf="!info.isDestroyed && info.statusVms">
Resources status:&nbsp;
<span>

View File

@@ -5,6 +5,7 @@ import { AlertService } from '../services/alert.service';
import { AuthGuard } from '../services/auth.guard';
import { ProvisionsService } from '../services/provisions.service';
import { ModalConfirmComponent } from './confirm.component';
import { QlikService } from '../services/qs.service';
@Component({
selector: 'qmi-modalinfo',
@@ -19,8 +20,9 @@ export class ModalInfoComponent implements OnInit, OnDestroy {
currentUser;
private _userId;
sharedUsers;
costData;
constructor( private modalService: MDBModalService, private _alertService: AlertService, private router: Router, public modalRef: MDBModalRef, private _provisionsService: ProvisionsService, private _auth: AuthGuard ) {
constructor( private modalService: MDBModalService, private _alertService: AlertService, private router: Router, public modalRef: MDBModalRef, private _provisionsService: ProvisionsService, private _auth: AuthGuard, private _qlikService: QlikService ) {
this._auth.getUserInfo().subscribe( value => {
this.currentUser = value;
this._userId = value? value._id : null;
@@ -49,6 +51,15 @@ export class ModalInfoComponent implements OnInit, OnDestroy {
this._refreshShares();
mains.unsubscribe();
});
this._qlikService.costSubject.subscribe(function(value){
console.log("value", value);
this.costData = value || {};
}.bind(this));
}
getCost(id){
return this.costData && this.costData[id]? this.costData[id].dollars : "n/a";
}
ngOnDestroy() {

View File

@@ -32,6 +32,9 @@
</div>
</div>
<div class="contentbox">
<div *ngIf="costData">
<b>{{getCost(provision._id)}} spent so far</b>
</div>
<div *ngIf="!provision.isDestroyed && provision.statusVms">
Resources status:&nbsp;
<span>

View File

@@ -6,6 +6,7 @@ import { MDBModalService } from 'angular-bootstrap-md';
import { AlertService } from '../services/alert.service';
import { Subscription } from 'rxjs';
import { ModalConfirmComponent } from '../modals/confirm.component';
import { QlikService } from '../services/qs.service';
@Component({
@@ -25,9 +26,10 @@ export class ProvComponent implements OnInit {
logShow: boolean = false;
sharedUsers;
acl = { "type": "view"};
costData;
constructor( private modalService: MDBModalService, private _alertService: AlertService, private _provisionsService: ProvisionsService, private _auth: AuthGuard, private route: ActivatedRoute ) {
constructor( private modalService: MDBModalService, private _alertService: AlertService, private _provisionsService: ProvisionsService, private _auth: AuthGuard, private route: ActivatedRoute, private _qlikService: QlikService ) {
this._auth.getUserInfo().subscribe( value => {
this._userId = value? value._id : null;
@@ -60,6 +62,20 @@ export class ProvComponent implements OnInit {
this._refreshEvents();
this._refreshShares();
});
this._qlikService.costSubject.subscribe(function(value){
console.log("value", value);
this.costData = value || {};
}.bind(this));
}
getCost(id) : string {
if (this.costData && this.costData[id] ) {
return this.costData[id].dollars;
}
return "n/a";
}

View File

@@ -1,5 +1,13 @@
<app-logs [show]="logShow" (onClose)="onLogsClose()" [type]="logstype" [selectedprov]="selectedprov"></app-logs>
<div style="margin-top: 80px; min-height: 650px;">
<!--<div style="width:200px; height: 200px">
<qlik-embed
id="visualization"
ui="object"
app-id="1d2a43cf-8bc7-422c-90bd-d021cb232776"
object-id="020620cf-458f-4f2a-92b2-84dfb21d593a"
></qlik-embed>
</div>-->
<h1>Provisions <span *ngIf="provisions && provisions.length">({{provisions.length}})</span></h1>
<div *ngIf="provisions && provisions.length" class="flexcontainer">
<div *ngFor="let provision of provisions; let i = index" class="box">
@@ -10,7 +18,10 @@
<div class="maintitle"><span *ngIf="provision._scenarioDoc">{{provision._scenarioDoc.title}}</span><span *ngIf="!provision._scenarioDoc">Old scenario (please destroy)</span></div>
<div style="font-size: 14px;">{{provision.scenario}} (v{{provision.scenarioVersion}}) <span *ngIf="provision.scenarioVersion !== provision._scenarioDoc.version" class="text-danger newversion">New version available!</span></div>
</div>
<div class="contentbox">
<div class="contentbox">
<div *ngIf="costData">
<b>{{getCost(provision._id)}} spent so far</b>
</div>
<div *ngIf="provision.statusVms">
Resources status:&nbsp;
<span>

View File

@@ -83,9 +83,9 @@ h1 {
.contentbox {
font-size: 12px;
padding: 10px;
min-height: 200px;
min-height: 220px;
&.destroyed {
min-height: 100px;
min-height: 120px;
}
}

View File

@@ -1,4 +1,4 @@
import { Component, OnInit } from '@angular/core';
import { Component, OnInit, AfterViewInit } from '@angular/core';
import { ProvisionsService } from '../services/provisions.service';
import { Subscription, timer} from 'rxjs';
import { switchMap } from 'rxjs/operators';
@@ -9,6 +9,7 @@ import { ModalInfoComponent } from '../modals/modalinfo.component';
import { ModalConfirmComponent } from '../modals/confirm.component';
import { ProvisionModalComponent } from '../modals/edit-provision.component';
import { ShareModalComponent } from '../modals/share.component';
import { QlikService } from '../services/qs.service';
@Component({
@@ -17,7 +18,7 @@ import { ShareModalComponent } from '../modals/share.component';
styleUrls: ['./provisions.component.scss'],
providers: [ProvisionsService]
})
export class ProvisionsComponent implements OnInit {
export class ProvisionsComponent implements OnInit, AfterViewInit {
private _userId;
provisions;
@@ -28,10 +29,15 @@ export class ProvisionsComponent implements OnInit {
logstype: String = 'provision';
public selectedprov: Object = null;
history: Boolean = false;
costData: any = null;
trigram;
constructor(private modalService: MDBModalService, private _alertService: AlertService, private _provisionsService: ProvisionsService, private _auth: AuthGuard) {
constructor(private modalService: MDBModalService, private _alertService: AlertService, private _provisionsService: ProvisionsService, private _auth: AuthGuard, private _qlikService: QlikService) {
this._auth.getUserInfo().subscribe( value => {
this._userId = value? value._id : null;
this.trigram = value? value.upn.split("@")[0] : null;
});
}
@@ -56,6 +62,13 @@ export class ProvisionsComponent implements OnInit {
}
})
this._qlikService.costSubject.subscribe(function(value){
console.log("value", value);
this.costData = value || {};
}.bind(this));
}
ngOnDestroy() {
@@ -70,6 +83,15 @@ export class ProvisionsComponent implements OnInit {
this._provisionsService.setSelectedProv(provision);
}
async ngAfterViewInit() {
//let qsEmbed = document.getElementById("visualization");
//this._qlikService.setCostDataAsync(qsEmbed);
}
getCost(id){
return this.costData && this.costData[id]? this.costData[id].dollars : "n/a";
}
del(provision): void {
this._provisionsService.delProvision(provision._id.toString(), this._userId).subscribe( res => {

View File

@@ -0,0 +1,94 @@
import { Injectable, EventEmitter } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class QlikService {
costSubject = new BehaviorSubject(null);
costData;
private formatMoney (num) {
const dollars = new Intl.NumberFormat(`en-US`, {
currency: `USD`,
style: 'currency',
}).format(num);
return dollars;
}
async setCostDataAsync( qsEmbed ) {
console.log("qsEmbed", qsEmbed);
if (!this.costData ) {
const refApi = await qsEmbed.getRefApi();
const doc = await refApi.getDoc();
const properties = {
qInfo: {
qType: 'my-straight-hypercube',
},
qHyperCubeDef: {
qDimensions: [
{
qDef: { qFieldDefs: ['_id'] },
},
{
qDef: { qFieldDefs: ['WD.Trigram'] },
},
{
qDef: { qFieldDefs: ['isActive'] },
},
{
qDef: { qFieldDefs: ['isDeleted'] },
}
],
qMeasures: [
{
qDef: { qDef: '=Sum(CostUSD)' },
},
],
qInitialDataFetch: [
{
qHeight: 2000,
qWidth: 5,
},
],
},
};
/*const field1 = await doc.getField("WD.Trigram");
await field1.selectValues([
{
qText: trigram,
},
], false, true);
*/
//await field1.lock();
await doc.clearAll();
const field1 = await doc.getField("isActive");
await field1.selectValues([
{
qText: "YES",
}
], false, true);
const obj = await doc.createSessionObject(properties);
const layout = await obj.getLayout();
let cost = {};
console.log(layout);
layout.qHyperCube.qDataPages[0].qMatrix.forEach(m=>{
cost[m[0].qText.toLowerCase()] = {amount: m[4].qNum, dollars: this.formatMoney(m[4].qNum)};
});
this.costData = cost;
this.costSubject.next(this.costData);
doc.clearAll();
} else {
console.log("DO NOTHING");
this.costSubject.next(this.costData);
}
}
}

View File

@@ -25,6 +25,7 @@
<thead class="sticky-top white-text">
<tr>
<th style="width: 255px;">Provision ID</th>
<th>Cost</th>
<th [mdbTableSort]="elements" sortBy="created" >Prov. Date <mdb-icon fas icon="sort"></mdb-icon></th>
<th [mdbTableSort]="elements" sortBy="scenario">Scenario <mdb-icon fas icon="sort"></mdb-icon></th>
<th [mdbTableSort]="elements" sortBy="deployOpts.location">Region <mdb-icon fas icon="sort"></mdb-icon></th>
@@ -43,6 +44,7 @@
<a href (click)="showLogs($event, provision, 'provision')" class="lui-text-info">{{ provision._id }}</a>
<div style="font-size: 10px;">{{provision.description}}</div>
</td>
<td (click)="openInfoModal(provision._id)" *ngIf="pagingIsDisabled || (i+1 >= mdbTablePagination.firstItemIndex && i < mdbTablePagination.lastItemIndex)">{{getCost(provision._id)}}</td>
<td (click)="openInfoModal(provision._id)" *ngIf="pagingIsDisabled || (i+1 >= mdbTablePagination.firstItemIndex && i < mdbTablePagination.lastItemIndex)">{{provision.created | date: 'dd-MM-yyyy H:mm'}}</td>
<td (click)="openInfoModal(provision._id)" *ngIf="pagingIsDisabled || (i+1 >= mdbTablePagination.firstItemIndex && i < mdbTablePagination.lastItemIndex)">
<div><mdb-icon mdbTooltip="External Access enabled" fas icon="globe-americas" *ngIf="provision.isExternalAccess" aria-hidden="true"></mdb-icon> {{provision.scenario}}</div>

View File

@@ -8,6 +8,7 @@ import { ModalConfirmComponent } from '../modals/confirm.component';
import { Subscription } from 'rxjs';
import { ProvisionModalComponent } from '../modals/edit-provision.component';
import * as moment from 'moment-timezone';
import { QlikService } from '../services/qs.service';
@Component({
selector: 'table-provisions',
@@ -48,11 +49,12 @@ export class TableProvisionsAdminComponent implements OnInit, OnDestroy, AfterVi
logShow: boolean = false;
logstype: String = 'provision';
timeInterval;
costData;
maxVisibleItems: number = 25;
constructor(private modalService: MDBModalService, private _alertService: AlertService, private cdRef: ChangeDetectorRef, private _provisionsService: ProvisionsService) {
constructor(private modalService: MDBModalService, private _alertService: AlertService, private cdRef: ChangeDetectorRef, private _provisionsService: ProvisionsService, private _qlikService: QlikService) {
}
@@ -110,6 +112,21 @@ export class TableProvisionsAdminComponent implements OnInit, OnDestroy, AfterVi
this.timeInterval = setInterval(() => {
this.nowTimeUTC = moment().utc().format("H:mm");
}, 60 * 1000);
this._qlikService.costSubject.subscribe(function(value){
console.log("value", value);
this.costData = value || {};
}.bind(this));
}
getCost(id) : string {
if (this.costData && this.costData[id] ) {
return this.costData[id].dollars;
}
return "n/a";
}

File diff suppressed because one or more lines are too long

View File

@@ -1,6 +1,8 @@
import { Component, OnInit } from '@angular/core';
import { Component, OnInit, AfterViewInit } from '@angular/core';
import { AuthGuard } from '../../services/auth.guard';
import { Subscription } from 'rxjs';
import { ProvisionsService } from 'src/app/services/provisions.service';
import { QlikService } from 'src/app/services/qs.service';
const _adminRoles = ['superadmin', 'admin'];
@@ -9,12 +11,12 @@ const _adminRoles = ['superadmin', 'admin'];
templateUrl: './header.component.html',
styleUrls: ['./header.component.scss']
})
export class HeaderComponent implements OnInit {
export class HeaderComponent implements OnInit, AfterViewInit {
user;
subs: Subscription;
constructor( private _auth: AuthGuard){
constructor( private _auth: AuthGuard, private _qlikService: QlikService){
this.subs = this._auth.getUserInfo().subscribe( value => {
this.user = value;
});
@@ -39,6 +41,9 @@ export class HeaderComponent implements OnInit {
event.target.src = 'https://ui-avatars.com/api/?name='+user.displayName+'&background=random&size=40'
}
ngAfterViewInit() {
}
private _isAdmin(user) : boolean {
return user && _adminRoles.includes(user.role);
}

View File

@@ -23,6 +23,9 @@
<div style="font-size: 14px;">{{provision.scenario}} (v{{provision.scenarioVersion}}) <span *ngIf="provision.scenarioVersion !== provision._scenarioDoc.version" class="text-danger newversion">New version available!</span></div>
</div>
<div class="contentbox">
<div *ngIf="costData">
<b>{{getCost(provision._id)}} spent so far</b>
</div>
<div *ngIf="provision.statusVms">
Resources status:&nbsp;
<span>

View File

@@ -7,6 +7,7 @@ import { ModalInfoComponent } from '../modals/modalinfo.component';
import { ProvisionModalComponent } from '../modals/edit-provision.component';
import { ShareModalComponent } from '../modals/share.component';
import { ModalConfirmComponent } from '../modals/confirm.component';
import { QlikService } from '../services/qs.service';
@Component({
@@ -25,11 +26,11 @@ export class UserDashboardComponent implements OnInit, OnDestroy{
public selectedprov: Object = null;
history: Boolean = false;
costData;
logShow: boolean = false;
logstype: String = 'provision';
constructor(private modalService: MDBModalService,private route: ActivatedRoute, private _provisionsService: ProvisionsService, private _userService: UsersService) { }
constructor(private modalService: MDBModalService,private route: ActivatedRoute, private _provisionsService: ProvisionsService, private _userService: UsersService, private _qlikService: QlikService) { }
ngOnInit() {
@@ -49,6 +50,19 @@ export class UserDashboardComponent implements OnInit, OnDestroy{
this.user = user;
})
});
this._qlikService.costSubject.subscribe(function(value){
console.log("value", value);
this.costData = value || {};
}.bind(this));
}
getCost(id) : string {
if (this.costData && this.costData[id] ) {
return this.costData[id].dollars;
}
return "n/a";
}
ngOnDestroy() {

View File

@@ -8,6 +8,23 @@
<link rel="icon" href="assets/favicon.ico">
<!-- Load environment variables -->
<script src="env.js"></script>
<script
crossorigin="anonymous"
type="application/javascript"
src="https://cdn.jsdelivr.net/npm/@qlik/embed-web-components"
data-host="https://qmicloud-dev.qliktech.com/proxy"
></script>
<!--<script
crossorigin="anonymous"
type="application/javascript"
src="https://cdn.jsdelivr.net/npm/@qlik/embed-web-components"
data-host="https://gear-presales.eu.qlikcloud.com"
data-web-integration-id="n4kMLH62hvXXC84q2vdfW15WUvrUw-HU"
data-cross-site-cookies="true"
></script>-->
</head>
<body>
<app-root></app-root>

View File

@@ -2856,11 +2856,11 @@ aws4@^1.8.0:
integrity sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==
axios@^0.21.1:
version "0.21.1"
resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.1.tgz#22563481962f4d6bde9a76d516ef0e5d3c09b2b8"
integrity sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==
version "0.21.4"
resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.4.tgz#c67b90dc0568e5c1cf2b0b858c43ba28e2eda575"
integrity sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==
dependencies:
follow-redirects "^1.10.0"
follow-redirects "^1.14.0"
babel-code-frame@^6.22.0:
version "6.26.0"
@@ -3979,6 +3979,11 @@ cookie@0.4.0:
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.0.tgz#beb437e7022b3b6d49019d088665303ebe9c14ba"
integrity sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==
cookie@^0.6.0:
version "0.6.0"
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.6.0.tgz#2798b04b071b0ecbff0dbb62a505a8efa4e19051"
integrity sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==
copy-concurrently@^1.0.0:
version "1.0.5"
resolved "https://registry.yarnpkg.com/copy-concurrently/-/copy-concurrently-1.0.5.tgz#92297398cae34937fcafd6ec8139c18051f0b5e0"
@@ -5639,11 +5644,16 @@ flush-write-stream@^1.0.0:
inherits "^2.0.3"
readable-stream "^2.3.6"
follow-redirects@^1.0.0, follow-redirects@^1.10.0:
follow-redirects@^1.0.0:
version "1.13.0"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.13.0.tgz#b42e8d93a2a7eea5ed88633676d6597bc8e384db"
integrity sha512-aq6gF1BEKje4a9i9+5jimNFIpq4Q1WiwBToeRK5NvZBd/TRsmW8BsJfOEGkr76TbOyPVD3OVDN910EcUNtRYEA==
follow-redirects@^1.14.0:
version "1.15.3"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.3.tgz#fe2f3ef2690afce7e82ed0b44db08165b207123a"
integrity sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==
font-awesome@^4.7.0:
version "4.7.0"
resolved "https://registry.yarnpkg.com/font-awesome/-/font-awesome-4.7.0.tgz#8fa8cf0411a1a31afd07b06d2902bb9fc815a133"
@@ -12083,6 +12093,11 @@ uuid@^8.3.0, uuid@^8.3.2:
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==
uuid@^9.0.1:
version "9.0.1"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.1.tgz#e188d4c8853cc722220392c424cd637f32293f30"
integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==
valid-url@^1.0.6:
version "1.0.9"
resolved "https://registry.yarnpkg.com/valid-url/-/valid-url-1.0.9.tgz#1c14479b40f1397a75782f115e4086447433a200"
@@ -12457,6 +12472,11 @@ ws@^7.0.0:
resolved "https://registry.yarnpkg.com/ws/-/ws-7.3.1.tgz#d0547bf67f7ce4f12a72dfe31262c68d7dc551c8"
integrity sha512-D3RuNkynyHmEJIpD2qrgVkc9DQ23OrN/moAwZX4L8DfvszsJxpjQuUq3LMx6HoYji9fbIOBY18XWBsAux1ZZUA==
ws@^8.14.2:
version "8.14.2"
resolved "https://registry.yarnpkg.com/ws/-/ws-8.14.2.tgz#6c249a806eb2db7a20d26d51e7709eab7b2e6c7f"
integrity sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==
ws@~3.3.1:
version "3.3.3"
resolved "https://registry.yarnpkg.com/ws/-/ws-3.3.3.tgz#f1cf84fe2d5e901ebce94efaece785f187a228f2"