Skip to content
GitLab
Menu
Projects
Groups
Snippets
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in / Register
Toggle navigation
Menu
Open sidebar
Open Subsurface Data Universe Software
Platform
Domain Data Mgmt Services
Seismic
Seismic DMS Suite
seismic-dms-service
Commits
65ee1cfd
Commit
65ee1cfd
authored
Apr 26, 2021
by
Walter D
Browse files
Merge branch 'ibm-lint-db-fixes' into 'master'
Removed linting errors. Added create db code. See merge request
!68
parents
0298033e
4e2367c1
Pipeline
#37152
passed with stages
in 11 minutes and 13 seconds
Changes
5
Pipelines
1
Hide whitespace changes
Inline
Side-by-side
src/cloud/providers/ibm/config.ts
View file @
65ee1cfd
...
...
@@ -20,7 +20,7 @@ export class IbmConfig extends Config {
public
static
COS_ENDPOINT
:
string
;
public
static
COS_S3_FORCEPATHSTYLE
:
boolean
;
public
static
COS_SIGNATUREVERSION
:
string
;
// IBM Keycloak
public
static
KEYCLOAK_BASEURL
:
string
;
public
static
KEYCLOAK_URL_TOKEN
:
string
;
...
...
@@ -31,7 +31,7 @@ export class IbmConfig extends Config {
public
static
KEYCLOAK_CLIENTID
:
string
;
public
static
KEYCLOAK_CLIENTSECRET
:
string
;
//IBM Document DB
//
IBM Document DB
public
static
DOC_DB_URL
:
string
;
public
static
DOC_DB_COLLECTION
:
string
;
public
static
DOC_DB_QUERY_RESULT_LIMIT
:
string
;
...
...
@@ -43,7 +43,7 @@ export class IbmConfig extends Config {
public
static
LEGAL_HOST
;
public
static
STORAGE_HOST
;
//Logger
//
Logger
public
static
LOGGER_LEVEL
;
public
async
init
():
Promise
<
void
>
{
...
...
@@ -54,17 +54,18 @@ export class IbmConfig extends Config {
IbmConfig
.
DES_SERVICE_HOST_STORAGE
=
process
.
env
.
DES_SERVICE_HOST_STORAGE
;
IbmConfig
.
IMP_SERVICE_ACCOUNT_SIGNER
=
process
.
env
.
IMP_SERVICE_ACCOUNT_SIGNER
;
IbmConfig
.
ENTITLEMENT_HOST
=
process
.
env
.
ENTITLEMENT_HOST
;
//DES_SERVICE_HOST replaced by new variable ENTITLEMENT_HOST
IbmConfig
.
LEGAL_HOST
=
process
.
env
.
LEGAL_HOST
;
//DES_SERVICE_HOST replaced by new variable LEGAL_HOST
IbmConfig
.
STORAGE_HOST
=
process
.
env
.
STORAGE_HOST
;
//DES_SERVICE_HOST replaced by new variable LEGAL_HOST
// DES_SERVICE_HOST replaced by new variable ENTITLEMENT_HOST
IbmConfig
.
ENTITLEMENT_HOST
=
process
.
env
.
ENTITLEMENT_HOST
;
IbmConfig
.
LEGAL_HOST
=
process
.
env
.
LEGAL_HOST
;
// DES_SERVICE_HOST replaced by new variable LEGAL_HOST
IbmConfig
.
STORAGE_HOST
=
process
.
env
.
STORAGE_HOST
;
// DES_SERVICE_HOST replaced by new variable LEGAL_HOST
// IBM COS
IbmConfig
.
COS_ACCESS_KEY_ID
=
process
.
env
.
COS_ACCESS_KEY_ID
;
IbmConfig
.
COS_SECRET_ACCESS_KEY
=
process
.
env
.
COS_SECRET_ACCESS_KEY
;
IbmConfig
.
COS_ENDPOINT
=
process
.
env
.
COS_ENDPOINT
;
IbmConfig
.
COS_S3_FORCEPATHSTYLE
=
process
.
env
.
COS_S3_FORCEPATHSTYLE
===
'
true
'
;
//string to boolean
IbmConfig
.
COS_S3_FORCEPATHSTYLE
=
process
.
env
.
COS_S3_FORCEPATHSTYLE
===
'
true
'
;
//
string to boolean
IbmConfig
.
COS_SIGNATUREVERSION
=
process
.
env
.
COS_SIGNATUREVERSION
;
// IBM Keycloak
IbmConfig
.
KEYCLOAK_BASEURL
=
process
.
env
.
KEYCLOAK_BASEURL
;
IbmConfig
.
KEYCLOAK_URL_TOKEN
=
process
.
env
.
KEYCLOAK_URL_TOKEN
;
...
...
@@ -74,8 +75,8 @@ export class IbmConfig extends Config {
IbmConfig
.
KEYCLOAK_CLIENTID
=
process
.
env
.
KEYCLOAK_CLIENTID
;
IbmConfig
.
KEYCLOAK_CLIENTSECRET
=
process
.
env
.
KEYCLOAK_CLIENTSECRET
;
IbmConfig
.
KEYCLOAK_REALM
=
process
.
env
.
KEYCLOAK_REALM
;
//IBM Document DB
//
IBM Document DB
IbmConfig
.
DOC_DB_URL
=
process
.
env
.
DB_URL
;
IbmConfig
.
DOC_DB_COLLECTION
=
process
.
env
.
DOC_DB_COLLECTION
;
IbmConfig
.
DOC_DB_QUERY_SELECT_FIELDS
=
process
.
env
.
DOC_DB_QUERY_SELECT_FIELDS
;
...
...
@@ -99,7 +100,7 @@ export class IbmConfig extends Config {
IbmConfig
.
DES_REDIS_INSTANCE_KEY
=
process
.
env
.
DES_REDIS_INSTANCE_KEY
IbmConfig
.
DES_REDIS_INSTANCE_TLS_DISABLE
=
process
.
env
.
CACHE_TLS_DISABLE
?
true
:
false
//Logger
//
Logger
IbmConfig
.
LOGGER_LEVEL
=
process
.
env
.
LOGGER_LEVEL
||
'
debug
'
;
Config
.
checkRequiredConfig
(
IbmConfig
.
LOCKSMAP_REDIS_INSTANCE_PORT
,
'
REDIS_INSTANCE_PORT
'
);
...
...
src/cloud/providers/ibm/cos.ts
View file @
65ee1cfd
...
...
@@ -13,7 +13,7 @@ let cosStorage;
export
class
Cos
extends
AbstractStorage
{
private
COS_SUBPROJECT_BUCKET_PREFIX
=
'
ss-
'
+
Config
.
SERVICE_ENV
;
public
constructor
()
{
super
();
logger
.
info
(
'
In Cos.constructor. Instantiating cos client.
'
);
...
...
@@ -26,9 +26,7 @@ export class Cos extends AbstractStorage {
});
}
//generate a random bucket name
// generate a random bucket name
public
randomBucketName
():
string
{
logger
.
info
(
'
In Cos.randomBucketName.
'
);
let
suffix
=
Math
.
random
().
toString
(
36
).
substring
(
2
,
16
);
...
...
@@ -40,13 +38,13 @@ export class Cos extends AbstractStorage {
return
this
.
COS_SUBPROJECT_BUCKET_PREFIX
+
'
-
'
+
suffix
;
}
//Create a new bucket
//
Create a new bucket
public
async
createBucket
(
bucketName
:
string
,
location
:
string
,
storageClass
:
string
):
Promise
<
void
>
{
logger
.
info
(
'
In Cos.createBucket.
'
);
logger
.
debug
(
bucketName
);
///not sure how to use ACLs
///
not sure how to use ACLs
const
bucketParams
=
{
Bucket
:
bucketName
,
CreateBucketConfiguration
:
{
...
...
@@ -55,7 +53,7 @@ export class Cos extends AbstractStorage {
}
};
cosStorage
.
createBucket
(
bucketParams
,
function
(
err
,
data
)
{
cosStorage
.
createBucket
(
bucketParams
,
(
err
,
data
)
=>
{
if
(
err
)
{
logger
.
error
(
'
Error while creating bucket. Error stack -
'
);
logger
.
error
(
err
.
stack
);
...
...
@@ -70,12 +68,12 @@ export class Cos extends AbstractStorage {
logger
.
info
(
'
Returning from Cos.createBucket.
'
);
}
//Cos bucket deletion
//
Cos bucket deletion
public
async
deleteBucket
(
bucketName
:
string
,
force
=
false
):
Promise
<
void
>
{
logger
.
info
(
'
In Cos.deleteBucket.
'
);
logger
.
debug
(
bucketName
);
var
params
=
{
Bucket
:
bucketName
};
cosStorage
.
deleteBucket
(
params
,
function
(
err
)
{
const
params
=
{
Bucket
:
bucketName
};
cosStorage
.
deleteBucket
(
params
,
(
err
)
=>
{
if
(
err
)
{
logger
.
error
(
'
Unable to delete bucket. Error stack
'
);
logger
.
error
(
err
.
stack
);
...
...
@@ -86,24 +84,24 @@ export class Cos extends AbstractStorage {
logger
.
info
(
'
Returning from Cos.deleteBucket.
'
);
}
//Deletion of files in Cos bucket
//
Deletion of files in Cos bucket
public
async
deleteFiles
(
bucketName
:
string
):
Promise
<
void
>
{
logger
.
info
(
'
In Cos.deleteFiles.
'
);
logger
.
debug
(
bucketName
);
var
self
=
this
;
cosStorage
.
listObjects
({
Bucket
:
bucketName
},
function
(
err
,
data
)
{
const
self
=
this
;
cosStorage
.
listObjects
({
Bucket
:
bucketName
},
(
err
,
data
)
=>
{
if
(
err
)
{
logger
.
error
(
"
error listing bucket objects
"
);
logger
.
error
(
'
error listing bucket objects
'
);
logger
.
error
(
err
.
stack
);
throw
err
;
}
var
items
=
data
.
Contents
;
const
items
=
data
.
Contents
;
if
(
!
items
||
items
.
length
<=
0
)
logger
.
info
(
'
No items to delete.
'
);
else
for
(
var
i
=
0
;
i
<
items
.
length
;
i
+=
1
)
{
var
objectKey
=
items
[
i
].
Key
;
for
(
const
i
of
items
)
{
const
objectKey
=
items
[
i
].
Key
;
logger
.
info
(
'
Object to be deleted. objectKey-
'
);
logger
.
debug
(
objectKey
);
self
.
deleteObject
(
bucketName
,
objectKey
);
...
...
@@ -112,15 +110,15 @@ export class Cos extends AbstractStorage {
logger
.
info
(
'
Returning from Cos.deleteFiles.
'
);
}
//Saving file in Cos bucket
//
Saving file in Cos bucket
public
async
saveObject
(
bucketName
:
string
,
objectName
:
string
,
data
:
string
):
Promise
<
void
>
{
logger
.
info
(
'
In Cos.saveObject.
'
);
logger
.
debug
(
bucketName
);
logger
.
debug
(
objectName
);
logger
.
debug
(
data
);
le
t
params
=
{
Bucket
:
bucketName
,
Key
:
objectName
,
Body
:
data
};
cons
t
params
=
{
Bucket
:
bucketName
,
Key
:
objectName
,
Body
:
data
};
cosStorage
.
putObject
(
params
,
function
(
err
,
data
)
{
cosStorage
.
putObject
(
params
,
(
err
,
result
)
=>
{
if
(
err
)
{
logger
.
error
(
'
Object not saved.
'
);
logger
.
error
(
err
.
stack
);
...
...
@@ -128,19 +126,19 @@ export class Cos extends AbstractStorage {
}
else
{
logger
.
info
(
"
Object saved.
"
);
logger
.
debug
(
data
);
logger
.
info
(
'
Object saved.
'
);
logger
.
debug
(
result
);
}
});
logger
.
info
(
'
Returning from Cos.saveObject.
'
);
}
//delete an object from a bucket
//
delete an object from a bucket
public
async
deleteObject
(
bucketName
:
string
,
objectName
:
string
):
Promise
<
void
>
{
///used to delete CDO file
///
used to delete CDO file
logger
.
info
(
'
In Cos.deleteObject.
'
);
le
t
params
=
{
Bucket
:
bucketName
,
Key
:
objectName
};
cosStorage
.
deleteObject
(
params
,
function
(
err
)
{
cons
t
params
=
{
Bucket
:
bucketName
,
Key
:
objectName
};
cosStorage
.
deleteObject
(
params
,
(
err
)
=>
{
if
(
err
)
{
logger
.
error
(
'
Unable to remove object
'
);
logger
.
error
(
err
.
stack
);
...
...
@@ -151,7 +149,7 @@ export class Cos extends AbstractStorage {
logger
.
info
(
'
Returning from Cos.deleteObject.
'
);
}
//delete multiple objects
//
delete multiple objects
public
async
deleteObjects
(
bucketName
:
string
,
prefix
:
string
,
async
:
boolean
=
false
):
Promise
<
void
>
{
logger
.
info
(
'
This function deletes bulk data uploaded by SDAPI/SDUTIL. Not implemented yet.
'
);
logger
.
debug
(
bucketName
);
...
...
@@ -160,9 +158,10 @@ export class Cos extends AbstractStorage {
await
Promise
.
resolve
();
}
//copy multiple objects (skip the dummy file)
///implemention aws sdk copyObject to copy dataset
public
async
copy
(
bucketIn
:
string
,
prefixIn
:
string
,
bucketOut
:
string
,
prefixOut
:
string
,
ownerEmail
:
string
):
Promise
<
void
>
{
// copy multiple objects (skip the dummy file)
/// implemention aws sdk copyObject to copy dataset
public
async
copy
(
bucketIn
:
string
,
prefixIn
:
string
,
bucketOut
:
string
,
prefixOut
:
string
,
ownerEmail
:
string
):
Promise
<
void
>
{
logger
.
info
(
'
In Cos.copy.
'
);
logger
.
info
(
'
Arguments passed:bucketIn,prefixIn...
'
);
logger
.
debug
(
bucketIn
);
...
...
@@ -171,24 +170,24 @@ export class Cos extends AbstractStorage {
logger
.
debug
(
prefixOut
);
logger
.
debug
(
ownerEmail
);
var
self
=
this
;
cosStorage
.
listObjects
({
Bucket
:
bucketIn
},
function
(
err
,
data
)
{
cosStorage
.
listObjects
({
Bucket
:
bucketIn
},
(
err
,
data
)
=>
{
if
(
err
)
{
logger
.
error
(
"
Error in listing objects.
"
);
logger
.
error
(
'
Error in listing objects.
'
);
logger
.
error
(
err
.
stack
);
throw
err
;
}
logger
.
info
(
"
Fetched objects.
"
);
logger
.
info
(
'
Fetched objects.
'
);
logger
.
debug
(
data
);
var
items
=
data
.
Contents
;
const
items
=
data
.
Contents
;
if
(
!
items
||
items
.
length
<=
0
)
logger
.
info
(
'
No items to copy.
'
);
else
for
(
var
i
=
0
;
i
<
items
.
length
;
i
+=
1
)
{
var
objectKey
=
items
[
i
].
Key
;
var
prefix
=
items
[
i
].
Key
.
split
(
'
/
'
)[
0
];
// for (var i = 0; i < items.length; i += 1) {
for
(
const
i
of
items
)
{
const
objectKey
=
items
[
i
].
Key
;
// let prefix = items[i].Key.split('/')[0];
logger
.
info
(
'
Object to be copied.
'
);
logger
.
debug
(
objectKey
);
}
...
...
@@ -196,9 +195,9 @@ export class Cos extends AbstractStorage {
logger
.
info
(
'
Returning from Cos.deleteObject.
'
);
}
//check bucket exists or not
//
check bucket exists or not
public
async
bucketExists
(
bucketName
:
string
):
Promise
<
boolean
>
{
//const result = await cosStorage.bucket(bucketName).exists();
//
const result = await cosStorage.bucket(bucketName).exists();
logger
.
info
(
'
In Cos.bucketExists.
'
);
const
bucketParams
=
{
Bucket
:
bucketName
...
...
src/cloud/providers/ibm/credentials.ts
View file @
65ee1cfd
/* Licensed Materials - Property of IBM */
/* (c) Copyright IBM Corp. 2020. All Rights Reserved.*/
import
{
Config
}
from
'
../../config
'
;
import
{
Utils
}
from
'
../../../shared
'
;
import
KcAdminClient
from
'
keycloak-admin
'
;
import
{
AbstractCredentials
,
CredentialsFactory
,
IAccessTokenModel
}
from
"
../../credentials
"
;
import
{
AbstractCredentials
,
CredentialsFactory
,
IAccessTokenModel
}
from
'
../../credentials
'
;
import
{
IbmConfig
}
from
'
./config
'
;
import
{
logger
}
from
'
./logger
'
;
...
...
@@ -49,23 +48,23 @@ export class Credentials extends AbstractCredentials {
logger
.
error
(
'
Authentication failure.
'
);
throw
new
Error
(
error
);
}
logger
.
info
(
'
Getting token by calling getAccessToken.
'
);
const
token
=
adminClient
.
getAccessToken
();
logger
.
info
(
'
Extracting token type and epiry value from token.
'
);
const
token
_t
ype
=
Utils
.
getPropertyFromTokenPayload
(
token
,
'
typ
'
);
const
token
_e
xpiry
:
number
=
+
Utils
.
getPropertyFromTokenPayload
(
token
,
'
exp
'
);
//conver
s
ted string to number
const
token
T
ype
=
Utils
.
getPropertyFromTokenPayload
(
token
,
'
typ
'
);
const
token
E
xpiry
:
number
=
+
Utils
.
getPropertyFromTokenPayload
(
token
,
'
exp
'
);
//
converted string to number
logger
.
info
(
'
Returning from Credentials.getStorageCredentials.
'
);
return
{
access_token
:
token
,
expires_in
:
token
_e
xpiry
,
token_type
:
token
_t
ype
,
expires_in
:
token
E
xpiry
,
token_type
:
token
T
ype
,
};
}
public
async
getServiceCredentials
():
Promise
<
string
>
{
logger
.
info
(
'
In Credentials.getServiceCredentials.
'
);
const
adminClient
=
new
KcAdminClient
();
...
...
@@ -96,7 +95,7 @@ export class Credentials extends AbstractCredentials {
logger
.
error
(
'
Authentication failure.
'
);
throw
new
Error
(
error
);
}
logger
.
info
(
'
Getting token by calling getAccessToken.
'
);
const
token
=
adminClient
.
getAccessToken
();
logger
.
debug
(
'
Token -
'
+
token
);
...
...
@@ -105,7 +104,7 @@ export class Credentials extends AbstractCredentials {
}
public
async
getServiceAccountAccessToken
():
Promise
<
IAccessTokenModel
>
{
//throw new Error("getServiceAccountAccessToken. Method not implemented.");
//
throw new Error("getServiceAccountAccessToken. Method not implemented.");
/*
const now = Math.floor(Date.now() / 1000);
if (this.serviceAccountAccessToken && this.serviceAccountAccessTokenExpiresIn > now) {
...
...
@@ -147,29 +146,29 @@ export class Credentials extends AbstractCredentials {
throw new Error(error);
//throw (Error.makeForHTTPRequest(error));
}*/
throw
new
Error
(
"
Checking if user is sysadmin. Work in progress.
"
);
throw
new
Error
(
'
Checking if user is sysadmin. Work in progress.
'
);
}
public
getServiceAccountEmail
():
Promise
<
string
>
{
logger
.
info
(
'
In Credentials.getServiceAccountEmail. Method not implemented.
'
);
throw
new
Error
(
"
getServiceAccountEmail. Method not implemented.
"
);
throw
new
Error
(
'
getServiceAccountEmail. Method not implemented.
'
);
}
public
getIAMResourceUrl
(
serviceSigner
:
string
):
string
{
///not implemented
///
not implemented
logger
.
info
(
'
In Credentials.getIAMResourceUrl. Method not implemented.
'
);
return
""
;
return
''
;
}
public
getAudienceForImpCredentials
():
string
{
logger
.
info
(
'
In Credentials.getAudienceForImpCredentials.
'
);
return
IbmConfig
.
KEYCLOAK_BASEURL
+
IbmConfig
.
KEYCLOAK_URL_TOKEN
;
///throw new Error("getAudienceForImpCredentials. Method not implemented.");
///
throw new Error("getAudienceForImpCredentials. Method not implemented.");
}
public
getPublicKeyCertificatesUrl
():
string
{
logger
.
info
(
'
In Credentials.getPublicKeyCertificatesUrl. Method not implemented.
'
);
throw
new
Error
(
"
getPublicKeyCertificatesUrl. Method not implemented.
"
);
throw
new
Error
(
'
getPublicKeyCertificatesUrl. Method not implemented.
'
);
}
}
\ No newline at end of file
src/cloud/providers/ibm/datastore.ts
View file @
65ee1cfd
...
...
@@ -2,6 +2,7 @@
/* (c) Copyright IBM Corp. 2020. All Rights Reserved.*/
import
{
AbstractJournal
,
AbstractJournalTransaction
,
IJournalQueryModel
,
IJournalTransaction
,
JournalFactory
}
from
'
../../journal
'
;
import
{
TenantModel
}
from
'
../../../services/tenant
'
;
import
cloudant
from
'
@cloudant/cloudant
'
;
import
{
Config
}
from
'
../../config
'
;
import
{
Utils
}
from
'
../../../shared/utils
'
...
...
@@ -12,23 +13,42 @@ let docDb;
@
JournalFactory
.
register
(
'
ibm
'
)
export
class
DatastoreDAO
extends
AbstractJournal
{
public
KEY
=
Symbol
(
'
id
'
);
private
dataPartition
:
string
;
public
constructor
(
{
projectId
,
keyFilename
}
)
{
public
constructor
(
tenant
:
TenantModel
)
{
super
();
logger
.
info
(
'
In datastore.constructor.
'
);
this
.
dataPartition
=
tenant
.
esd
.
indexOf
(
'
.
'
)
!==
-
1
?
tenant
.
esd
.
split
(
'
.
'
)[
0
]
:
tenant
.
esd
;
this
.
initDb
(
this
.
dataPartition
);
}
public
async
initDb
(
dataPartition
:
string
)
{
logger
.
info
(
'
In datastore.initDb.
'
);
const
dbUrl
=
IbmConfig
.
DOC_DB_URL
;
logger
.
debug
(
dbUrl
);
const
cloudantOb
=
cloudant
(
dbUrl
);
logger
.
info
(
'
DB
object
created. cloudantOb-
'
);
logger
.
info
(
'
DB
connection
created. cloudantOb-
'
);
logger
.
debug
(
cloudantOb
);
try
{
logger
.
debug
(
'
before connection
'
);
docDb
=
cloudantOb
.
db
.
use
(
IbmConfig
.
DOC_DB_COLLECTION
);
docDb
=
await
cloudantOb
.
db
.
get
(
IbmConfig
.
DOC_DB_COLLECTION
+
'
-
'
+
dataPartition
);
logger
.
debug
(
'
after connection
'
);
}
catch
(
err
)
{
logger
.
debug
(
'
catch of db connection code
'
);
if
(
err
.
statusCode
===
404
)
{
logger
.
debug
(
'
Database does not exist. Creating database.
'
);
await
cloudantOb
.
db
.
create
(
IbmConfig
.
DOC_DB_COLLECTION
+
'
-
'
+
dataPartition
)
logger
.
debug
(
'
Database created.
'
);
}
logger
.
debug
(
'
db connection error -
'
,
err
);
return
;
}
docDb
=
cloudantOb
.
db
.
use
(
IbmConfig
.
DOC_DB_COLLECTION
+
'
-
'
+
dataPartition
);
}
public
async
get
(
key
:
any
):
Promise
<
[
any
|
any
[]]
>
{
...
...
src/cloud/providers/ibm/logger.ts
View file @
65ee1cfd
...
...
@@ -2,7 +2,6 @@
/* (c) Copyright IBM Corp. 2020. All Rights Reserved.*/
import
{
getLogger
}
from
'
log4js
'
;
import
{
initParams
}
from
'
request-promise
'
;
import
{
AbstractLogger
,
LoggerFactory
}
from
'
../../logger
'
;
import
{
IbmConfig
}
from
'
./config
'
;
...
...
@@ -11,19 +10,19 @@ import { IbmConfig } from './config';
export
class
IbmLogger
extends
AbstractLogger
{
public
info
(
data
:
any
):
void
{
logger
.
info
(
data
);
logger
.
info
(
data
);
}
public
debug
(
data
:
any
):
void
{
logger
.
debug
(
data
);
logger
.
debug
(
data
);
}
public
error
(
data
:
any
):
void
{
logger
.
error
(
data
);
}
public
metric
(
key
:
string
,
data
:
any
):
void
{
logger
.
info
(
"
No Metric
"
);
logger
.
info
(
'
No Metric
'
);
}
}
...
...
Write
Preview
Supports
Markdown
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment