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>
0 commit comments