summaryrefslogblamecommitdiffstats
path: root/webapp/src/components/GroupModuleGroupView.vue
blob: 04b0be0c47a2b2a47a3f698999055fe038bf6f41 (plain) (tree)
1
2
3
4
5
6
7
8
9
10


         
                   
                     
                       



                                 
                            
                            

                          
                      

                                      

         
                   
                      
                       
                                
                         

                                  
                              
                                   

                              
                      

                                               




          


























                                                                                                                                       
                              
                                                                                               











                                                                                                                                                      
                          

















                                                                                                                                                          

                            















                                                                                                                 
                                                                                                           
                            
                          








                                                                                                                                                                        
                        
                      
 














                                                                                                                                                         
                            
                          








                                                                                       

                          











                                                                                        
                              



                                                                                                                   

                        










                                                                                                              

             




                                                                      
                                              
                                             


                               


                   
                       




                  

                         
                          
             


            
                   
                      
                            

                 

                        
        
                  
                  

     
             
                                                       



                                              

                     
                                                                     

     

                                






                                                                                      

     
            
                                                                                                      
                        
                                
                                                                              
                                                                                              
      




                                                    




                                                    
                                              
                                                                          
                                                                             


                           
                                       

                                                                   
       
      
                 
                                                                                       
                                                                                       
                            

                                                
                        

                                

                                
        
                           
      


                                                                                                       
                    



                                      


                                                                                                                                              
     


                                                   





                                                                   

                       
                    


              

                  
                

                      

 


               



                      
                   

 









                                                                 
                                           
                  







                  

                                      
                  






                          




                      

 

                  

 
                 
                    

 

                

 
             


                     

 




                      
 


                      
 
            
                   
                    
                             


                      
 
        
<i18n>
{
  "en": {
    "info": "Info",
    "showall": "All",
    "groups": "Groups",
    "subgroups": "Subgroups",
    "clients": "Clients",
    "name": "Name",
    "description": "Description",
    "ipranges": "IP Ranges",
    "config": "iPXE Config",
    "parents": "Parents",
    "startIp": "Start IP",
    "endIp": "End IP",
    "selectParents": "Select parents",
    "more": "more"
  },
  "de": {
    "info": "Info",
    "showall": "Alle",
    "groups": "Groups",
    "subgroups": "Untergruppen",
    "clients": "Clients",
    "name": "Name",
    "description": "Beschreibung",
    "ipranges": "IP Bereiche",
    "config": "iPXE Konfiguration",
    "parents": "Übergruppen",
    "startIp": "Start IP",
    "endIp": "End IP",
    "selectParents": "Übergruppen auswählen",
    "more": "mehr"
  }
}
</i18n>

<template>
  <v-card style="margin-top: 24px">
    <v-tabs v-model="activeTab" grow hide-slider>
      <v-tab v-if="group.id !== 0" class="groupview-tab" :class="(group.id !== 0 && activeTab === 0) ? 'primary--text' : ''">
        <v-icon :color="(group.id !== 0 && activeTab === 0) ? 'primary' : ''">list_alt</v-icon>
        <span class="hidden-xs-only ml-2">Info</span>
      </v-tab>
      <v-tab v-if="group.id !== 'create'" class="groupview-tab" :class="activeTab === (group.id === 0 ? 0 : 1) ? 'primary--text' : ''">
        <v-icon class="mr-2" :color="activeTab === (group.id === 0 ? 0 : 1) ? 'primary' : ''">category</v-icon>
        {{ group.subgroups ? group.subgroups.length : 0 }}
        <span class="hidden-xs-only ml-1">{{ group.id > 0 ? $t('subgroups') : $t('groups') }}</span>
      </v-tab>
      <v-tab v-if="group.id !== 'create'" class="groupview-tab" :class="activeTab === (group.id === 0 ? 1 : 2) ? 'primary--text' : ''">
        <v-icon class="mr-2" :color="activeTab === (group.id === 0 ? 1 : 2) ? 'primary' : ''">computer</v-icon>
        {{ group.clients ? group.clients.length : 0 }}
        <span class="hidden-xs-only ml-1">{{ $t('clients') }}</span>
      </v-tab>

      <v-switch
        v-if="group.id !== 'create'"
        :input-value="group.tabShowAll"
        @change="setShowAll"
        class="show-toggle"
        :label="$t('showall')"
        hide-details
        color="primary"
      ></v-switch>

      <v-tabs-items touchless>
        <v-tab-item v-if="group.id !== 0" lazy :transition="false" :reverse-transition="false">
          <v-divider></v-divider>
          <v-card-text>
            <v-layout wrap>
              <v-flex lg4 sm6 xs12 order-lg1 order-xs2>
                <v-layout column>
                  <v-flex>
                    <div class="info-box">
                      <div class="body-2 info-heading"><v-icon>label</v-icon><span>{{ $t('name') }}</span></div>
                      <div class="info-text">
                        <v-text-field v-if="editMode" class="info-input" color="primary" v-model="info.name" hide-details tabindex="1"></v-text-field>
                        <div v-else>{{ group.name || '-' }}</div>
                      </div>
                    </div>
                  </v-flex>
                  <v-flex>
                    <div class="info-box">
                      <div class="body-2 info-heading"><v-icon>device_hub</v-icon><span>{{ $t('parents') }}</span></div>
                      <div class="info-text">
                        <select-box v-if="editMode" v-model="parents" :items="groupList" class="info-input" hide-details></select-box>
                        <div v-else class="chip-container non-selectable">
                          <v-tooltip v-for="parent in firstParents" :key="parent.id" top open-delay="800">
                            <template #activator="{ on }">
                              <v-chip v-on="on" small label style="width: calc(50% - 8px)" @click="openParent(parent)">
                                <span class="chip-text">{{ parent.name || parent.id }}</span>
                              </v-chip>
                            </template>
                            <span>{{ parent.name || parent.id }}</span>
                          </v-tooltip>
                          <span v-if="group.parents && group.parents.length > 5" class="and-more">+ {{ group.parents.length - 5 }} {{ $t('more') }}</span>
                          <span v-else-if="group.parents === undefined || group.parents.length === 0">-</span>
                        </div>
                      </div>
                    </div>
                  </v-flex>
                  <v-flex>
                    <div class="info-box">
                      <div class="body-2 info-heading"><v-icon>list</v-icon><span>{{ $t('config') }}</span></div>
                      <div class="info-text">
                        <v-select v-if="editMode"
                          class="info-input"
                          clearable
                          item-text="name"
                          item-value="id"
                          :menu-props="{ offsetY: '' }"
                          hide-details
                          color="primary"
                          v-model="info.configId"
                          :items="configList"
                        ></v-select>
                        <div v-else>{{ group.config ? (group.config.name || group.config.id) : '-' }}</div>
                      </div>
                    </div>
                  </v-flex>
                </v-layout>
              </v-flex>
              <v-flex lg4 sm6 xs12 order-lg2 order-xs3>
                <div class="info-box">
                  <div class="body-2 info-heading"><v-icon>description</v-icon><span>{{ $t('description') }}</span></div>
                  <div class="info-text">
                    <v-textarea v-if="editMode" class="info-input" rows="1" auto-grow hide-details color="primary" v-model="info.description" tabindex="2"></v-textarea>
                    <div v-else style="white-space: pre-wrap;">{{ group.description || '-' }}</div>
                  </div>
                </div>

                <div class="info-box">
                  <div class="body-2 info-heading"><v-icon>settings_ethernet</v-icon><span>{{ $t('ipranges') }}</span></div>
                  <div class="info-text">
                    <div v-if="editMode">
                      <div v-for="(iprange, index) in ipranges" :key="index">
                        <div class="iprange">
                        <v-btn class="iprange-button" small icon @click="removeIprange(index)"><v-icon>remove</v-icon></v-btn>
                        <v-text-field solo flat hide-details :label="$t('startIp')" color="primary" v-model="iprange.startIp" single-line></v-text-field>
                        <span class="ip-seperator">-</span>
                        <v-text-field solo flat hide-details :label="$t('endIp')" color="primary" v-model="iprange.endIp" single-line></v-text-field>
                        </div>
                        <v-divider></v-divider>
                      </div>
                      <div class="iprange-add-wrapper">
                        <v-btn class="iprange-button" small icon @click="addIprange"><v-icon>add</v-icon></v-btn>
                      </div>
                    </div>
                    <div v-else>
                      <table>
                        <tr v-for="(iprange, index) in group.ipranges" :key="index">
                          <td class="text-xs-right">{{ iprange.startIp }}</td>
                          <td class="ip-seperator">-</td>
                          <td>{{ iprange.endIp }}</td>
                        </tr>
                      </table>
                      <div v-if="group.ipranges && group.ipranges.length === 0">-</div>
                    </div>
                  </div>
                </div>
              </v-flex>
              <v-flex lg4 xs12 order-lg3 order-xs1 class="text-xs-right">
                <div class="info-box">
                  <div v-if="!editMode">
                    <v-btn color="error" flat @click="deleteGroup" class="info-buttons">
                      <v-icon left>delete</v-icon>{{ $t('delete') }}
                    </v-btn>
                    <v-btn color="primary" flat @click="editInfo" class="info-buttons">
                      <v-icon left>create</v-icon>{{ $t('edit') }}
                    </v-btn>
                  </div>
                  <div v-else>
                    <v-btn color="primary" flat @click="cancelEdit" class="info-buttons">{{ $t('cancel') }}</v-btn>
                    <v-btn color="primary" @click="saveData" class="info-buttons" tabindex="3">
                      <v-icon left>save</v-icon>{{ $t('save') }}
                    </v-btn>
                  </div>
                </div>
              </v-flex>
            </v-layout>
          </v-card-text>
        </v-tab-item>
        <v-tab-item v-if="group.id !== 'create'" lazy :transition="false" :reverse-transition="false">
          <group-module-group-list :tabIndex="tabIndex" :groupId="group.id" :groups="group.subgroups || []" />
        </v-tab-item>
        <v-tab-item v-if="group.id !== 'create'" lazy :transition="false" :reverse-transition="false">
          <group-module-client-list :tabIndex="tabIndex" :groupId="group.id" :clients="group.clients || []" />
        </v-tab-item>
      </v-tabs-items>
    </v-tabs>
  </v-card>
</template>

<script>
import GroupModuleGroupList from '@/components/GroupModuleGroupList'
import GroupModuleClientList from '@/components/GroupModuleClientList'
import SelectBox from '@/components/SelectBox'
import { mapState, mapMutations } from 'vuex'

export default {
  name: 'GroupModuleGroupView',
  props: {
    group: {
      type: Object,
      default: () => {}
    },
    tabIndex: {
      type: Number
    }
  },
  components: {
    GroupModuleGroupList,
    GroupModuleClientList,
    SelectBox
  },
  data () {
    return {
      activeTab: 0,
      editMode: false,
      showAllClicked: false,
      info: {
        name: '',
        description: '',
        configId: null
      },
      parents: [],
      ipranges: []
    }
  },
  computed: {
    ...mapState('groups', ['groupList', 'configList']),
    headers () {
      return [
        { key: 'name', text: this.$t('name') }
      ]
    },
    firstParents () {
      return this.group.parents ? this.group.parents.slice(0, 5) : []
    }
  },
  watch: {
    group (newValue, oldValue) {
      if (newValue.id === 'create') this.editInfo()
      else if (newValue.id !== oldValue.id) this.editMode = false
      else if (!oldValue.isPlaceholder) return

      if (newValue.subgroups.length) this.activeTab = (this.group.id === 0 ? 0 : 1)
      else if (newValue.clients.length) this.activeTab = (this.group.id === 0 ? 1 : 2)
      else this.activeTab = 0
    }
  },
  methods: {
    ...mapMutations('groups', ['setDialog', 'setActiveTab', 'adjustTabSlider', 'deleteFromTabChain']),
    setShowAll (value) {
      this.showAllClicked = true
      this.$store.commit('groups/setShowAll', { index: this.tabIndex, value })
      this.$store.dispatch('groups/loadGroup', { id: this.group.id, tabIndex: this.tabIndex })
    },
    removeIprange (index) {
      this.ipranges.splice(index, 1)
    },
    addIprange () {
      this.ipranges.push({ startIp: '', endIp: '' })
    },
    editInfo () {
      this.editMode = true
      this.info.name = this.group.name
      this.info.description = this.group.description
      this.info.configId = this.group.configId
      this.parents = this.group.parents ? this.group.parents.slice(0) : []
      this.ipranges = this.group.ipranges ? this.group.ipranges.slice(0) : []
    },
    cancelEdit () {
      this.editMode = false
      if (this.group.id === 'create') {
        this.deleteFromTabChain({ index: this.tabIndex, count: 1 })
        this.setActiveTab(this.tabIndex - 1)
      }
    },
    saveData () {
      this.info.configId = this.info.configId === undefined ? null : this.info.configId
      this.ipranges = this.ipranges.filter(iprange => iprange.startIp && iprange.endIp)
      this.adjustTabSlider()
      this.$store.dispatch('groups/saveGroup', {
        id: this.group.id,
        data: this.info,
        parents: this.parents,
        ipranges: this.ipranges,
        tabIndex: this.tabIndex,
        callback: this.updateUrl
      })
      this.editMode = false
    },
    deleteGroup () {
      this.setDialog({ show: true, info: { action: 'delete', type: 'group', selected: [this.group] } })
    },
    updateUrl (id) {
      this.$router.replace({
        name: 'GroupModule.group',
        params: { id, noReload: true }
      })
    },
    openParent (parent) {
      this.$store.dispatch('groups/loadGroup', { id: parent.id, name: parent.name, tabIndex: this.tabIndex, asParent: true, switchTab: true })
    }
  },
  created () {
    if (this.group.id === 'create') this.editInfo()
  }
}
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
.groupview-tab {
  text-transform: none;
  font-weight: bold;
}

.show-toggle {
  padding: 0 16px;
  margin: 0;
  flex: initial;
  display: flex;
  align-items: center;
}

.info-buttons {
  margin: 0;
}

.iprange {
  display: flex;
  align-items: center;
  min-height: 34px;
}

.iprange-add-wrapper {
  height: 32px;
  display: flex;
  align-items: center;
}

.iprange >>> .v-text-field.v-text-field--solo .v-input__control {
  min-height: 32px;
}

.iprange >>> .v-label, .iprange >>> input {
  font-size: 14px;
}

.chip-container {
  display: flex;
  flex-wrap: wrap;
  width: 100%;
}

.chip-container >>> .v-chip__content {
  width: 100%;
  cursor: pointer;
}

.chip-text {
  overflow: hidden;
  text-overflow: ellipsis;
}

.and-more {
  font-size: 13px;
  display: flex;
  align-items: center;
  margin: 4px 17px;
}

.ip-seperator {
  padding: 0 10px;
}

.iprange-button {
  margin: 0 8px 0 0;
}

.info-box {
  padding: 20px;
}

.info-input {
  margin: 0;
  padding: 0 0 1px 0;
  overflow: hidden;
}

.info-heading {
  display: flex;
  align-items: center;
  margin-bottom: 10px;
}

.info-heading > span {
  margin-left: 10px;
}

.info-text {
  overflow-x: auto;
  margin-left: 34px;
  font-family: 'Roboto Mono';
  min-height: 34px;
  display: flex;
  align-items: center;
}
</style>
an class="hl opt">); } /* No NAK, so reset the backoff */ state->nakoff = 1; if (type == DHCP_OFFER && state->state == STATE_INIT) { char *addr = strdup (inet_ntoa (dhcp->address)); if (dhcp->servername[0]) logger (LOG_INFO, "offered %s from %s `%s'", addr, inet_ntoa (dhcp->serveraddress), dhcp->servername); else logger (LOG_INFO, "offered %s from %s", addr, inet_ntoa (dhcp->serveraddress)); free (addr); logToQt(LOG_INFO, DHCP_OFFER, ""); #ifdef ENABLE_INFO if (options->test) { write_info (iface, dhcp, options, false); errno = 0; return (-1); } #endif _send_message (state, DHCP_REQUEST, options); state->state = STATE_REQUESTING; return (0); } if (type == DHCP_OFFER) { logger (LOG_INFO, "got subsequent offer of %s, ignoring ", inet_ntoa (dhcp->address)); return (0); } /* We should only be dealing with acks */ if (type != DHCP_ACK) { logger (LOG_ERR, "%d not an ACK or OFFER", type); return (0); } /* if we are here, than we received an ACK and can go on with configuration */ logToQt(LOG_INFO, DHCP_ACK, ""); switch (state->state) { case STATE_RENEW_REQUESTED: case STATE_REQUESTING: case STATE_RENEWING: case STATE_REBINDING: break; default: logger (LOG_ERR, "wrong state %d", state->state); } do_socket (state, SOCKET_CLOSED); #ifdef ENABLE_ARP if (options->doarp && iface->previous_address.s_addr != dhcp->address.s_addr) { errno = 0; logToQt(LOG_INFO, DHCPCD_ARP_TEST, ""); if (arp_claim (iface, dhcp->address)) { do_socket (state, SOCKET_OPEN); _send_message (state, DHCP_DECLINE, options); do_socket (state, SOCKET_CLOSED); free_dhcp (dhcp); memset (dhcp, 0, sizeof (*dhcp)); state->xid = 0; state->timeout = 0; state->state = STATE_INIT; /* RFC 2131 says that we should wait for 10 seconds * before doing anything else */ logger (LOG_INFO, "sleeping for 10 seconds"); ts.tv_sec = 10; ts.tv_nsec = 0; nanosleep (&ts, NULL); return (0); } else if (errno == EINTR) return (0); } #endif if (options->doinform) { if (options->request_address.s_addr != 0) dhcp->address = options->request_address; else dhcp->address = iface->previous_address; logger (LOG_INFO, "received approval for %s", inet_ntoa (dhcp->address)); if (iface->previous_netmask.s_addr != dhcp->netmask.s_addr) { add_address (iface->name, dhcp->address, dhcp->netmask, dhcp->broadcast); iface->previous_netmask.s_addr = dhcp->netmask.s_addr; } state->timeout = options->leasetime; if (state->timeout == 0) state->timeout = DEFAULT_LEASETIME; state->state = STATE_INIT; } else if (dhcp->leasetime == (unsigned) -1) { dhcp->renewaltime = dhcp->rebindtime = dhcp->leasetime; state->timeout = 1; /* So we wait for infinity */ logger (LOG_INFO, "leased %s for infinity", inet_ntoa (dhcp->address)); state->state = STATE_BOUND; } else { if (! dhcp->leasetime) { dhcp->leasetime = DEFAULT_LEASETIME; logger(LOG_INFO, "no lease time supplied, assuming %d seconds", dhcp->leasetime); } logger (LOG_INFO, "leased %s for %u seconds", inet_ntoa (dhcp->address), dhcp->leasetime); if (dhcp->rebindtime >= dhcp->leasetime) { dhcp->rebindtime = (dhcp->leasetime * 0.875); logger (LOG_ERR, "rebind time greater than lease " "time, forcing to %u seconds", dhcp->rebindtime); } if (dhcp->renewaltime > dhcp->rebindtime) { dhcp->renewaltime = (dhcp->leasetime * 0.5); logger (LOG_ERR, "renewal time greater than rebind time, " "forcing to %u seconds", dhcp->renewaltime); } if (! dhcp->renewaltime) { dhcp->renewaltime = (dhcp->leasetime * 0.5); logger (LOG_INFO, "no renewal time supplied, assuming %d seconds", dhcp->renewaltime); } else logger (LOG_DEBUG, "renew in %u seconds", dhcp->renewaltime); if (! dhcp->rebindtime) { dhcp->rebindtime = (dhcp->leasetime * 0.875); logger (LOG_INFO, "no rebind time supplied, assuming %d seconds", dhcp->rebindtime); } else logger (LOG_DEBUG, "rebind in %u seconds", dhcp->rebindtime); state->timeout = dhcp->renewaltime; state->state = STATE_BOUND; } state->xid = 0; logToQt(LOG_INFO, DHCPCD_CONFIGURE, ""); if (configure (options, iface, dhcp, true) == -1 && ! state->daemonised) return (-1); if (! state->daemonised && options->daemonise) { switch (daemonise (state->pidfd)) { case 0: state->daemonised = true; return (0); case -1: return (-1); default: state->persistent = true; state->forked = true; return (-1); } } return (0); } static int handle_packet (state_t *state, const options_t *options) { interface_t *iface = state->interface; bool valid = false; int type; struct dhcp_t *new_dhcp; dhcpmessage_t message; /* Allocate our buffer space for BPF. * We cannot do this until we have opened our socket as we don't * know how much of a buffer we need until then. */ if (! state->buffer) state->buffer = xmalloc (iface->buffer_length); state->buffer_len = iface->buffer_length; state->buffer_pos = 0; /* We loop through until our buffer is empty. * The benefit is that if we get >1 DHCP packet in our buffer and * the first one fails for any reason, we can use the next. */ memset (&message, 0, sizeof (message)); new_dhcp = xmalloc (sizeof (*new_dhcp)); do { if (get_packet (iface, (unsigned char *) &message, state->buffer, &state->buffer_len, &state->buffer_pos) == -1) break; if (state->xid != message.xid) { logger (LOG_DEBUG, "ignoring packet with xid 0x%x as it's not ours (0x%x)", message.xid, state->xid); continue; } logger (LOG_DEBUG, "got a packet with xid 0x%x", message.xid); memset (new_dhcp, 0, sizeof (*new_dhcp)); type = parse_dhcpmessage (new_dhcp, &message); if (type == -1) { logger (LOG_ERR, "failed to parse packet"); free_dhcp (new_dhcp); /* We don't abort on this, so return zero */ return (0); } /* If we got here then the DHCP packet is valid and appears to * be for us, so let's clear the buffer as we don't care about * any more DHCP packets at this point. */ valid = true; break; } while (state->buffer_pos != 0); /* No packets for us, so wait until we get one */ if (! valid) { free (new_dhcp); return (0); } /* new_dhcp is now our master DHCP message */ free_dhcp (state->dhcp); free (state->dhcp); state->dhcp = new_dhcp; new_dhcp = NULL; return (handle_dhcp (state, type, options)); } int dhcp_run (const options_t *options, int *pidfd) { interface_t *iface; state_t *state = NULL; struct pollfd fds[] = { { -1, POLLIN, 0 }, { -1, POLLIN, 0 } }; int retval = -1; int sig; if (! options) return (-1); /*read_interface : defined in interface.c*/ iface = read_interface (options->interface, options->metric); if (! iface) goto eexit; state = xzalloc (sizeof (*state)); state->dhcp = xzalloc (sizeof (*state->dhcp)); state->pidfd = pidfd; state->interface = iface; if (! client_setup (state, options)) goto eexit; if (signal_init () == -1) goto eexit; if (signal_setup () == -1) goto eexit; fds[POLLFD_SIGNAL].fd = signal_fd (); for (;;) { retval = wait_for_packet (fds, state, options); /* We should always handle our signals first */ if ((sig = (signal_read (&fds[POLLFD_SIGNAL]))) != -1) { if (handle_signal (sig, state, options)) retval = 0; else retval = -1; } else if (retval == 0) retval = handle_timeout (state, options); else if (retval > 0 && state->socket != SOCKET_CLOSED && fds[POLLFD_IFACE].revents & POLLIN) retval = handle_packet (state, options); else if (retval == -1 && errno == EINTR) { /* The interupt will be handled above */ retval = 0; } else { logger (LOG_ERR, "poll: %s", strerror (errno)); retval = -1; } if (retval != 0) break; } eexit: if (iface) { do_socket (state, SOCKET_CLOSED); drop_config (state, options); free_route (iface->previous_routes); free (iface->clientid); free (iface); } if (state) { if (state->forked) retval = 0; if (state->daemonised) unlink (options->pidfile); free_dhcp (state->dhcp); free (state->dhcp); free (state->buffer); free (state); } return (retval); }