Skip to content

Commit 193e66c

Browse files
dniarcbtcmotorina0
authored
fix: dynamic instance type selection (#57)
* fix: dynamic instance type selection fix: vibe * green for gud * fix: only fetch version in testing mode --------- Co-authored-by: Arc <ben@arc.wales> Co-authored-by: Vlad Stan <stan.v.vlad@gmail.com>
1 parent d6d867f commit 193e66c

3 files changed

Lines changed: 251 additions & 23 deletions

File tree

src/boot/saas.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import axios from 'axios'
2+
import { env } from 'echarts'
23
import {secondsToDhm} from 'src/boot/utils'
34

45
const normalizeApiEnv = env => {
@@ -36,6 +37,11 @@ var saas = {
3637

3738
email: localStorage.getItem('email'),
3839

40+
isTestingMode: function () {
41+
const env = normalizeApiEnv(localStorage.getItem('apiEnv'))
42+
return (saas.email === 'alan@lnbits.com') || env === 'dev' || env === 'local'
43+
},
44+
3945
signup: async function (email, password, password2) {
4046
const {data} = await axios({
4147
method: 'POST',
@@ -118,6 +124,16 @@ var saas = {
118124
})
119125
},
120126

127+
getInstanceTypes: async function () {
128+
const response = await axios({
129+
method: 'GET',
130+
url: this.url('/instance/types'),
131+
withCredentials: true
132+
})
133+
134+
return response
135+
},
136+
121137
updateInstance: function (id, action) {
122138
return axios({
123139
method: 'PUT',

src/components/tables/InstancesTable.vue

Lines changed: 234 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -669,31 +669,62 @@
669669
<div class="text-h6">New LNbits Instance</div>
670670
</q-card-section>
671671
<q-card-section>
672-
You are about to create a new LNbits instance.
672+
Choose the image for your new instance.
673+
<q-select
674+
v-model="newInstanceDialog.selectedTag"
675+
:options="newInstanceDialog.options"
676+
option-label="label"
677+
option-value="value"
678+
emit-value
679+
map-options
680+
label="Instance image"
681+
dense
682+
outlined
683+
class="q-mt-md instance-image-select"
684+
:class="{
685+
'instance-image-select--first':
686+
newInstanceDialog.selectedTag ===
687+
newInstanceDialog.options[0]?.value
688+
}"
689+
popup-content-class="instance-image-select__menu"
690+
data-testid="instance-image-select"
691+
:loading="newInstanceDialog.loading"
692+
:disable="newInstanceDialog.loading"
693+
/>
694+
</q-card-section>
695+
<q-card-section v-if="newInstanceDialog.loading" class="text-center">
696+
<q-spinner-hourglass size="1.8rem" color="primary" />
697+
<div
698+
class="text-caption text-grey-7 q-mt-sm"
699+
data-testid="instance-types-loading"
700+
>
701+
Loading instance images...
702+
</div>
673703
</q-card-section>
674-
<q-card-actions v-if="showFeatureFlag" vertical class="q-pa-md q-ma-md q-gutter-md">
704+
<q-card-section v-else-if="newInstanceDialog.options.length === 0">
705+
<div
706+
class="text-subtitle1 text-grey-7"
707+
data-testid="instance-types-empty"
708+
>
709+
{{ newInstanceDialog.error || 'No instance images are available.' }}
710+
</div>
675711
<q-btn
676-
label="Advanced: I have my own bitcoin lightning funding source"
677-
color="primary"
712+
class="q-mt-sm"
678713
outline
679-
class="full-width"
680-
@click="confirmNewInstanceProvider('lnbits/lnbits:latest')"
714+
color="warning"
715+
label="Retry"
716+
data-testid="instance-types-retry"
717+
@click="retryLoadInstanceTypeOptions"
681718
/>
682-
<br />
683-
684-
<q-btn
685-
label="Simple: use Spark L2 to connect to bitcoin lightning"
686-
color="positive"
687-
class="full-width"
688-
@click="confirmNewInstanceProvider('lnbits/lnbits-sparkl2:latest')"
689-
/>
690-
</q-card-actions>
691-
<q-card-actions v-else vertical class="q-pa-md">
719+
</q-card-section>
720+
<q-card-actions class="q-pa-md">
692721
<q-btn
693-
label="OK, let's do it!"
722+
label="Create"
694723
color="primary"
695724
outline
696725
class="full-width"
726+
data-testid="confirm-instance-create"
727+
:disable="isCreateInstanceSubmitDisabled()"
697728
@click="confirmNewInstanceProvider()"
698729
/>
699730
</q-card-actions>
@@ -889,7 +920,11 @@ export default defineComponent({
889920
},
890921
newInstanceDialog: {
891922
show: false,
892-
action: null
923+
action: null,
924+
options: [],
925+
selectedTag: null,
926+
loading: false,
927+
error: null
893928
}
894929
}
895930
},
@@ -921,28 +956,195 @@ export default defineComponent({
921956
}
922957
},
923958
methods: {
924-
openNewInstanceDialog(action) {
959+
async openNewInstanceDialog(action) {
925960
this.newInstanceDialog.action = action
926961
this.newInstanceDialog.show = true
962+
this.newInstanceDialog.selectedTag = null
963+
this.newInstanceDialog.error = null
964+
if (action === 'on-demand' || action === 'plan-request') {
965+
await this.loadInstanceTypeOptions()
966+
}
927967
},
928968
async confirmNewInstanceProvider(provider) {
929969
const action = this.newInstanceDialog.action
970+
971+
const selectedTag = this.normalizeSelectedInstanceTypeTag(
972+
provider || this.newInstanceDialog.selectedTag
973+
)
974+
975+
if (this.isCreateInstanceSubmitDisabled(selectedTag)) {
976+
this.newInstanceDialog.error =
977+
this.newInstanceDialog.error || 'Please select an instance image.'
978+
this.q.notify({
979+
message: this.newInstanceDialog.error,
980+
color: 'negative'
981+
})
982+
return
983+
}
984+
985+
if (!selectedTag || !this.isInstanceTypeTagValid(selectedTag)) {
986+
this.newInstanceDialog.error = 'Please select an instance image.'
987+
this.q.notify({
988+
message: this.newInstanceDialog.error,
989+
color: 'negative'
990+
})
991+
return
992+
}
993+
930994
this.newInstanceDialog.show = false
931995
this.newInstanceDialog.action = null
932996
933997
if (action === 'on-demand') {
934-
const instance = await this.createInstance(provider)
998+
const instance = await this.createInstance(selectedTag)
935999
if (instance) {
9361000
await this.extendInstance(instance)
9371001
}
9381002
} else if (action === 'plan-request') {
939-
const instance = await this.createInstance(provider)
1003+
const instance = await this.createInstance(selectedTag)
9401004
if (instance) {
9411005
this.planDialog.instanceId = instance.id
9421006
await this.submitPlan()
9431007
}
9441008
}
9451009
},
1010+
normalizeSelectedInstanceTypeTag(value) {
1011+
if (typeof value === 'string') {
1012+
return value.trim()
1013+
}
1014+
1015+
if (value && typeof value === 'object') {
1016+
const candidate =
1017+
typeof value.value === 'string'
1018+
? value.value
1019+
: typeof value.tag === 'string'
1020+
? value.tag
1021+
: ''
1022+
1023+
return candidate.trim()
1024+
}
1025+
1026+
return ''
1027+
},
1028+
isInstanceTypeTagValid(tag) {
1029+
return this.normalizeSelectedInstanceTypeTag(tag).length > 0
1030+
},
1031+
isInstanceTypeOptionAvailable(tag) {
1032+
const normalizedTag = this.normalizeSelectedInstanceTypeTag(tag)
1033+
1034+
if (!this.isInstanceTypeTagValid(normalizedTag)) {
1035+
return false
1036+
}
1037+
1038+
return this.newInstanceDialog.options.some(
1039+
option => option.value === normalizedTag
1040+
)
1041+
},
1042+
isCreateInstanceSubmitDisabled(tag = this.newInstanceDialog.selectedTag) {
1043+
if (this.newInstanceDialog.loading) {
1044+
return true
1045+
}
1046+
1047+
if (this.newInstanceDialog.error) {
1048+
return true
1049+
}
1050+
1051+
if (!Array.isArray(this.newInstanceDialog.options)) {
1052+
return true
1053+
}
1054+
1055+
if (this.newInstanceDialog.options.length === 0) {
1056+
return true
1057+
}
1058+
1059+
return !this.isInstanceTypeOptionAvailable(tag)
1060+
},
1061+
async loadInstanceTypeOptions() {
1062+
this.newInstanceDialog.loading = true
1063+
this.newInstanceDialog.error = null
1064+
1065+
try {
1066+
let data = []
1067+
if (this.showFeatureFlag) {
1068+
const resp = await saas.getInstanceTypes()
1069+
data = resp.data
1070+
} else {
1071+
data = [
1072+
{
1073+
tag: 'lnbits',
1074+
label: 'LNbits Latest'
1075+
}
1076+
]
1077+
}
1078+
const types = Array.isArray(data) ? data : []
1079+
1080+
const options = types
1081+
.map(item => {
1082+
const tag = this.normalizeInstanceTypeTag(item?.tag)
1083+
const label = this.normalizeInstanceTypeLabel(item?.label)
1084+
1085+
if (!tag || !label) {
1086+
return null
1087+
}
1088+
1089+
return {
1090+
value: tag,
1091+
label
1092+
}
1093+
})
1094+
.filter(Boolean)
1095+
1096+
this.newInstanceDialog.options = options
1097+
this.newInstanceDialog.selectedTag = options[0]?.value || null
1098+
1099+
if (!options.length) {
1100+
this.newInstanceDialog.error = 'No instance images are available.'
1101+
this.newInstanceDialog.selectedTag = null
1102+
} else if (
1103+
this.isInstanceTypeTagValid(this.newInstanceDialog.selectedTag) &&
1104+
!options.some(
1105+
option =>
1106+
option.value ===
1107+
this.normalizeSelectedInstanceTypeTag(
1108+
this.newInstanceDialog.selectedTag
1109+
)
1110+
)
1111+
) {
1112+
this.newInstanceDialog.selectedTag = null
1113+
}
1114+
} catch (error) {
1115+
console.warn(error)
1116+
this.newInstanceDialog.error =
1117+
saas.mapErrorToString(error) || 'Failed to load instance images.'
1118+
this.newInstanceDialog.options = []
1119+
this.newInstanceDialog.selectedTag = null
1120+
this.q.notify({
1121+
message: 'Failed to load instance images',
1122+
caption: this.newInstanceDialog.error,
1123+
color: 'negative'
1124+
})
1125+
} finally {
1126+
this.newInstanceDialog.loading = false
1127+
}
1128+
},
1129+
async retryLoadInstanceTypeOptions() {
1130+
await this.loadInstanceTypeOptions()
1131+
},
1132+
normalizeInstanceTypeTag(value) {
1133+
if (typeof value !== 'string') {
1134+
return
1135+
}
1136+
1137+
const tag = value.trim()
1138+
return tag.length > 0 ? tag : undefined
1139+
},
1140+
normalizeInstanceTypeLabel(value) {
1141+
if (typeof value !== 'string') {
1142+
return
1143+
}
1144+
1145+
const label = value.trim()
1146+
return label.length > 0 ? label : undefined
1147+
},
9461148
showNewInstanceProvisioning: async function () {
9471149
this.selectPlan.show = false
9481150
if (this.selectPlan.method === 'one-time') {
@@ -1351,7 +1553,7 @@ export default defineComponent({
13511553
async created() {
13521554
try {
13531555
// temporary feature flag for alan
1354-
this.showFeatureFlag = saas.email === 'alan@lnbits.com'
1556+
this.showFeatureFlag = saas.isTestingMode()
13551557
13561558
this.inProgress = true
13571559
await this.refreshState()
@@ -1378,4 +1580,14 @@ export default defineComponent({
13781580
color: white;
13791581
background-color: rgba(0, 0, 0, 0.35);
13801582
}
1583+
1584+
.instance-image-select--first .q-field__native,
1585+
.instance-image-select--first .q-field__input,
1586+
.instance-image-select--first .q-field__native span {
1587+
color: #22c55e !important;
1588+
}
1589+
1590+
.instance-image-select__menu .q-item:nth-child(1) .q-item__label {
1591+
color: #22c55e;
1592+
}
13811593
</style>

src/layouts/MainLayout.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,7 @@ export default defineComponent({
185185
}
186186
try {
187187
// temporary feature flag for alan
188-
this.showFeatureFlag = saas.email === 'alan@lnbits.com'
188+
this.showFeatureFlag = saas.isTestingMode()
189189
} catch (error) {
190190
console.warn(error)
191191
} finally {

0 commit comments

Comments
 (0)