Skip to content

Commit

Permalink
ServiceForm autocompletes
Browse files Browse the repository at this point in the history
  • Loading branch information
proAlexandr committed Jul 6, 2019
1 parent 67d2733 commit 566cc51
Show file tree
Hide file tree
Showing 4 changed files with 214 additions and 12 deletions.
35 changes: 31 additions & 4 deletions src/renderer/components/shared/Loader.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,24 @@
<template>
<div class="loader" />
<div :class="className" />
</template>

<script>
export default {
name: 'Loader',
props: {
size: { type: String, default: 'm' }
},
computed: {
className() {
return {
loader: true,
[`loader_size_${this.size}`]: true
}
}
}
}
</script>

<style lang="scss">
@import '../../assets/styles/variables';
Expand All @@ -15,14 +32,24 @@
display: inline-block;
position: absolute;
top: 50%;
margin: -15px 0 0 -15px;
left: 50%;
width: 30px;
height: 30px;
border: 1px solid $color-primary;
border-radius: 50%;
border-bottom-color: transparent !important;
animation: spin 1s linear infinite;
box-sizing: border-box;
}
&.loader_size_s:before {
width: 20px;
height: 20px;
margin: -10px 0 0 -10px;
}
&.loader_size_m:before {
width: 30px;
height: 30px;
margin: -15px 0 0 -15px;
}
}
Expand Down
84 changes: 84 additions & 0 deletions src/renderer/components/shared/form/AutocompleteInput.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
<template>
<div class="autocomplete-input">
<BaseInput v-bind="$attrs" :value="value" v-on="listeners" />
<Popup v-if="(matchedOptions.length || loading) && focused" :position="'bottom'" :align="'both'">
<div v-if="loading" class="autocomplete-input__loader-container">
<Loader size="s" />
</div>

<ul v-else class="popup__actions">
<li v-for="option in matchedOptions" :key="option">
<Action @mousedown="handleOptionClick(option)">{{ option }}</Action>
</li>
</ul>
</Popup>
</div>
</template>

<script>
import BaseInput from './BaseInput'
import Popup from '../Popup'
import Action from '../Action'
import Loader from '../Loader'
export default {
name: 'AutocompleteInput',
components: { BaseInput, Popup, Action, Loader },
props: {
loading: { type: Boolean, default: false },
options: { type: Array, default: () => [] },
value: BaseInput.props.value // eslint-disable-line vue/require-default-prop
},
data() {
return {
focused: false
}
},
computed: {
matchedOptions() {
return this.options.filter(x => x.length !== this.value.length && x.startsWith(this.value))
},
listeners() {
return {
...this.$listeners,
focus: this.handleFocus,
blur: this.handleBlur
}
}
},
methods: {
handleOptionClick(option) {
this.$emit('input', option)
},
handleFocus() {
this.focused = true
this.$listeners.focus && this.$listeners.focus()
},
handleBlur() {
setTimeout(() => {
this.focused = false
this.$listeners.blur && this.$listeners.blur()
}, 100)
}
}
}
</script>

<style lang="scss">
.autocomplete-input {
position: relative;
.base-input {
width: 100%;
}
.popup__actions {
max-height: 200px;
overflow: auto;
}
}
.autocomplete-input__loader-container {
height: 40px;
}
</style>
98 changes: 91 additions & 7 deletions src/renderer/components/shared/service/ServiceForm.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@
</ControlGroup>

<ControlGroup label="Namespace" size="2" :attribute="$v.attributes.namespace">
<BaseInput v-model="$v.attributes.namespace.$model" />
<AutocompleteInput v-model="$v.attributes.namespace.$model"
:options="namespaces.data"
:loading="namespaces.loading"
@focus="handleNamespaceFocus" />
</ControlGroup>

<ControlGroup label="Kind" size="2" :attribute="$v.attributes.workloadType">
Expand All @@ -24,7 +27,11 @@
:disabled="!attributes.workloadType"
>
<template v-slot="slotProps">
<BaseInput v-model="slotProps.attribute.$model" v-bind="slotProps" />
<AutocompleteInput v-model="slotProps.attribute.$model"
v-bind="slotProps"
:options="resources.data"
:loading="resources.loading"
@focus="handleResourceNameFocus" />
</template>
</ControlGroup>

Expand All @@ -51,18 +58,22 @@ import cloneDeep from 'clone-deep'
import { mapActions } from 'vuex'
import { required, minLength, integer, between } from 'vuelidate/lib/validators'
import { validationMixin } from 'vuelidate'
import { Core_v1Api, Extensions_v1beta1Api } from '@kubernetes/client-node' // eslint-disable-line camelcase
import * as workloadTypes from '../../../lib/constants/workload-types'
import * as resourceKinds from '../../../lib/constants/workload-types'
import * as clusterHelper from '../../../lib/helpers/cluster'
import BaseForm from '../form/BaseForm'
import BaseInput from '../form/BaseInput'
import BaseSelect from '../form/BaseSelect'
import Button from '../Button'
import ForwardsTable from './ForwardsTable'
import ControlGroup from '../form/ControlGroup'
import AutocompleteInput from '../form/AutocompleteInput'
export default {
components: {
AutocompleteInput,
ControlGroup,
BaseInput,
BaseForm,
Expand All @@ -82,7 +93,7 @@ export default {
namespace: { required },
workloadType: {
required,
oneOf: (value) => Object.values(workloadTypes).includes(value)
oneOf: (value) => Object.values(resourceKinds).includes(value)
},
workloadName: { required },
forwards: {
Expand All @@ -102,6 +113,16 @@ export default {
...this.getEmptyAttributes(),
clusterId: this.$route.params.clusterId,
...cloneDeep(this.initialAttributes)
},
namespaces: {
data: [],
loading: false,
clusterId: null
},
resources: {
data: [],
loading: false,
cacheKey: null
}
}
},
Expand All @@ -111,16 +132,31 @@ export default {
},
workloadTypeOptions() {
return [
[workloadTypes.POD, 'Pod'],
[workloadTypes.DEPLOYMENT, 'Deployment'],
[workloadTypes.SERVICE, 'Service']
[resourceKinds.POD, 'Pod'],
[resourceKinds.DEPLOYMENT, 'Deployment'],
[resourceKinds.SERVICE, 'Service']
]
},
submitButtonTitle() {
return this.serviceId ? `Save` : 'Add a resource'
},
backPath() {
return '/'
},
cluster() {
return this.$store.state.Clusters.items[this.attributes.clusterId]
},
coreApi() {
try {
return clusterHelper.buildApiClient(this.cluster, Core_v1Api)
} catch (e) {
console.error(e)
return null
}
},
resourcesCacheKey() {
const { clusterId, namespace, workloadType } = this.attributes
return `${clusterId}:${namespace}:${workloadType}`
}
},
methods: {
Expand All @@ -135,6 +171,54 @@ export default {
forwards: []
}
},
async handleNamespaceFocus() {
if (this.coreApi && this.namespaces.clusterId !== this.attributes.clusterId) {
this.namespaces.loading = true
try {
const namespaces = (await this.coreApi.listNamespace()).body.items
this.namespaces.data = namespaces.map(x => x.metadata.name)
this.namespaces.clusterId = this.cluster.id
} catch (e) {
console.error(e)
}
this.namespaces.loading = false
}
},
async handleResourceNameFocus() {
if (this.coreApi && this.resources.cacheKey !== this.resourcesCacheKey) {
this.resources.loading = true
try {
this.resources.data = await this.getResources(
this.coreApi,
this.attributes.workloadType,
this.attributes.namespace
)
this.resources.cacheKey = this.resourcesCacheKey
} catch (e) {
console.error(e)
}
this.resources.loading = false
}
},
async getResources(coreApi, kind, namespace) {
if (kind === resourceKinds.POD) {
const response = await coreApi.listNamespacedPod(namespace)
return response.body.items.map(x => x.metadata.name)
} else if (kind === resourceKinds.DEPLOYMENT) {
const extensionsApi = clusterHelper.buildApiClient(this.cluster, Extensions_v1beta1Api)
const response = await extensionsApi.listNamespacedDeployment(namespace)
return response.body.items.map(x => x.metadata.name)
} else if (kind === resourceKinds.SERVICE) {
const response = await coreApi.listNamespacedService(namespace)
return response.body.items.map(x => x.metadata.name)
}
return []
},
handleSubmit() {
const action = this.serviceId ? 'Services/updateService' : 'Services/createService'
Expand Down
9 changes: 8 additions & 1 deletion src/renderer/lib/helpers/cluster.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Core_v1Api } from '@kubernetes/client-node' // eslint-disable-line camelcase
import { Core_v1Api, KubeConfig } from '@kubernetes/client-node' // eslint-disable-line camelcase

import { k8nApiPrettyError } from './k8n-api-error'

Expand All @@ -23,3 +23,10 @@ export async function checkConnection(kubeConfig, context = null) {

return error
}

// You must catch errors manually
export function buildApiClient(cluster, api) {
const kubeConfig = new KubeConfig()
kubeConfig.loadFromString(cluster.config)
return kubeConfig.makeApiClient(api)
}

0 comments on commit 566cc51

Please sign in to comment.