<commit_before>// Sonos speakers: http://www.sonos.com/system

var sonos       = require('sonos')
  , stringify   = require('json-stringify-safe')
  , url         = require('url')
  , util        = require('util')
  , validator   = require('validator')
  , xml2js      = require('xml2js')
  , devices     = require('./../../core/device')
  , steward     = require('./../../core/steward')
  , utility     = require('./../../core/utility')
  , discovery   = require('./../../discovery/discovery-ssdp')
  , media       = require('./../device-media')
  ;

var logger = media.logger;

var Sonos_Audio = exports.Device = function(deviceID, deviceUID, info) {
  var o, self;

  self = this;

  self.whatami = '/device/media/sonos/audio';
  self.deviceID = deviceID.toString();
  self.deviceUID = deviceUID;
  self.name = info.device.name;
  self.getName();

  self.url = info.url;
  self.sid = null;
  self.seq = 0;

  o = url.parse(info.url);
  self.sonos = new sonos.Sonos(o.hostname, o.port);
  self.status = 'idle';
  self.changed();
  self.info = { track: {}, mode: 'normal' };
  self.refreshID = null;

  self.sonos.getZoneAttrs(function(err, attrs) {
    if (err) return self.error(self, err,  'getZoneAttrs');

    self.setName(attrs.CurrentZoneName);
  });

  utility.broker.subscribe('actors', function(request, taskID, actor, perform, parameter) {
    if (actor !== ('device/' + self.deviceID)) return;

    if (request === 'perform') return self.perform(self, taskID, perform, parameter);
  });

  utility.broker.subscribe('discovery', function(method, headers, content) {
    if (method === 'notify') self.notify(self, headers, content);
  });

// we poll because '/MediaRenderer/RenderingControl/Event' doesn't inform us of changes in volume/mutedness
  self.refresh(self);

  self.jumpstart(self, '/MediaRenderer/AVTransport/Event');
};
util.inherits(Sonos_Audio, media.Device);

Sonos_Audio.prototype.jumpstart = function(self, path) {
  discovery.upnp_subscribe('device/' + self.deviceID, self.url, self.sid, path, function(err, state, response) {
    var i, secs;

    logger.debug('device/' + self.deviceID, { event   : 'subscribe'
                                            , state   : state
                                            , code    : response.statusCode
                                            , err     : stringify(err)
                                            , headers : stringify(response.headers)
                                            });

    if (err) {
      self.error(self, err, 'subscribe');
      setTimeout(function() { self.jumpstart(self, path); }, secs * 30 * 1000);
      return;
    }

    if ((response.statusCode !== 200) || (!response.headers.sid)) {
      self.sid = null;
      setTimeout(function() { self.jumpstart(self, path); }, secs * 30 * 1000);
      return;
    }

    self.sid = response.headers.sid;
    self.seq = 0;
    if (!!response.headers.timeout) {
      secs = response.headers.timeout;
      if ((i = secs.indexOf('Second-')) >= 0) secs = secs.substring(i + 7);
      secs = parseInt(secs, 10) - 1;
      if (secs <= 10) secs = 10;
      setTimeout(function() { self.jumpstart(self, path); }, secs * 1000);
    } else secs = 0;

    logger.info('device/' + self.deviceID, { subscribe: self.sid, path: path, sequence: self.seq, seconds: secs });
  });
};

Sonos_Audio.prototype.perform = function(self, taskID, perform, parameter) {
  var e, param0, params;

  try { params = JSON.parse(parameter); } catch(ex) { params = {}; }

  e = perform;
  param0 = null;
  switch (perform) {
    case 'set':
      if (!!params.name) {
        self.sonos.setName(params.name, function(err, data) {/* jshint unused: false */
          if (err) return self.error(self, err,  'setName');

          self.setName(params.name);
        });
      }
      if (!!params.mode) {
        param0 = { normal   : 'NORMAL'
                 , repeat   : 'REPEAT_ALL'
                 , shuffle  : 'SHUFFLE'
                 , shuffle1 : 'SHUFFLE_NOREPEAT'
                 }[params.mode.toLowerCase()];
        self.sonos.setPlayMode(param0, function(err, data) {/* jshint unused: false */
          if (err) return self.error(self, err,  'setPlayMode');
        });
      }
      if (!!params.position) {
        self.sonos.seek(Math.round(params.position / 1000), function(err, data) {/* jshint unused: false */
          if (err) return self.error(self, err,  'seek');
        });
      }
      if (!!params.volume) {
        self.sonos.setVolume(params.volume, function(err, data) {/* jshint unused: false */
          if (err) return self.error(self, err,  'setVolume');
        });
      }
      if (!!params.muted) {
        self.sonos.setMuted(params.muted === 'on' ? '1' : '0', function(err, data) {/* jshint unused: false */
          if (err) return self.error(self, err,  'setMuted');
        });
      }
      return true;

    case 'wake':
      return self.wake();

    case 'play':
      param0 = (!!params.url) && (params.url.length) ? devices.expand(params.url) : null;
      break;

    case 'queueNext':
      if (!params.url) return false;
      param0 = devices.expand(params.url);
      break;

    case 'stop':
    case 'pause':
    case 'next':
    case 'previous':
    case 'flush':
      break;

    default:
      return false;
  }

  if (!!param0) {
    self.sonos[e](param0, function(err, data) {/* jshint unused: false */
      if (err) self.error(self, err, e);
    });
  } else {
    self.sonos[e](function(err, data) {/* jshint unused: false */
      if (err) self.error(self, err, e);
    });
  }

  return steward.performed(taskID);
};

Sonos_Audio.prototype.notify = function(self, headers, content) {
  var parser = new xml2js.Parser();

  if ((headers.sid !== self.sid) || (headers.seq < self.seq)) return;
  self.seq = headers.seq + 1;

  try { parser.parseString(content, function(err, data) {
    if (err) {
      logger.error('device/' + self.deviceID, { event: 'xml2js.Parser', content: content, diagnostic: err.message });
      return;
    }

    if ((!data['e:propertyset'])
          || (!util.isArray(data['e:propertyset']['e:property']))
          || (!util.isArray(data['e:propertyset']['e:property'][0].LastChange))) return;

    parser.parseString(data['e:propertyset']['e:property'][0].LastChange[0], function(err, event) {
      var mode, status;

      if (err) {
        logger.error('device/' + self.deviceID,
                          { event      : 'xml2js.Parser'
                          , diagnostic : 'parseString'
                          , content    : data['e:propertyset']['e:property'][0].LastChange[0]
                          , exception  : err });
        return;
      }

      status = { PLAYING          : 'playing'
               , PAUSED_PLAYBACK  : 'paused'
               , TRANSITIONING    : 'busy'
               , STOPPED          : 'idle'
               }[event.Event.InstanceID[0].TransportState[0].$.val] || 'idle';
      mode   = { NORMAL           : 'normal'
               , REPEAT_ALL       : 'repeat'
               , SHUFFLE          : 'shuffle'
               , SHUFFLE_NOREPEAT : 'shuffle1'
               }[event.Event.InstanceID[0].CurrentPlayMode[0].$.val] || 'normal';

      if ((self.info.mode != mode) || (self.status !== status)) {
        self.info.mode = mode;
        self.status = status;
        self.changed();
        self.refresh(self);
      }

      parser.parseString(event.Event.InstanceID[0].CurrentTrackMetaData[0].$.val, function(err, didl) {
        var track;

        if (err) {
          logger.error('device/' + self.deviceID,
                            { event      : 'xml2js.Parser'
                            , diagnostic : 'parseString'
                            , content    : event.Event.InstanceID[0].CurrentTrackMetaData[0].$.val
                            , exception  : err });
          return;
        }

        track = self.sonos.parseDIDL(didl);
        if ((self.info.track.title !== track.title)
                || (self.info.track.artist !== track.artist)
                || (self.info.track.album !== track.album)
                || (self.info.track.albumArtURI !== track.albumArtURI)) {
          delete(track.albumArtURL);
          self.info.track = track;
          self.changed();
        }
      });
    });
  }); } catch(ex) { logger.error('device/' + self.deviceID, { event: 'notify', diagnostic: ex.message }); }
};

Sonos_Audio.prototype.refresh = function(self) {
  if (!!self.refreshID) { clearTimeout(self.refreshID); self.refreshID = null; }

  self.sonos.currentTrack(function(err, track) {
    if (err) return self.error(self, err,  'currentTrack');

     self.status = 'idle';

    if ((track !== undefined)
          && ((self.info.track.position !== (track.position * 1000))
               || (self.info.track.duration !== (track.duration * 1000)))) {
      delete(track.albumArtURL);
      self.info.track = track;
      self.info.track.position *= 1000;
      self.info.track.duration *= 1000;
      self.changed();
    }
  });

  self.sonos.getVolume(function(err, volume) {
    if (err) return self.error(self, err,  'getVolume');

    if ((volume !== undefined) && (self.info.volume !== volume)) {
      self.info.volume = volume;
      self.changed();
    }
  });

  self.sonos.getMuted(function(err, muted) {
    if (err) return self.error(self, err,  'getMuted');

    if ((muted !== undefined) && (self.info.muted !== (muted ? 'on' : 'off'))) {
      self.info.muted = muted ? 'on' : 'off';
      self.changed();
    }
  });

  self.refreshID = setTimeout (function() { self.refresh(self); }, (self.status === 'idle') ? (5 * 1000) : 500);
};

Sonos_Audio.prototype.error = function(self, err, event) {
  logger.error('device/' + self.deviceID, { event: event, diagnostic: err.message });
  if (self.status !== 'error') {
    self.status = 'error';
    self.changed();
  }
};

var validate_perform = function(perform, parameter) {
  var params = {}
    , result = { invalid: [], requires: [] }
    ;

  if (!!parameter) try { params = JSON.parse(parameter); } catch(ex) { result.invalid.push('parameter'); }

  switch (perform) {
    case 'wake':
      return true;

    case 'set':
      if (!parameter) {
        result.requires.push('parameter');
        return result;
      }
      if ((!!params.mode) && (!({ normal: true, repeat: true, shuffle: true, shuffle1: true }[params.mode.toLowerCase()]))) {
        result.invalid.push('mode');
      }
      if ((!!params.position) && (!media.validPosition(params.position))) result.invalid.push('position');
      if ((!!params.volume) && (!media.validVolume(params.volume))) result.invalid.push('volume');
      if ((!!params.muted) && (params.muted !== 'on') && (params.muted !== 'off')) result.invalid.push('volume');
      break;

    case 'play':
      if (!!params.url) try { validator.check(devices.expand(params.url)).isUrl(); } catch(ex) { result.invalid.push('url'); }
      break;

    case 'queueNext':
      if (!params.url) result.requires.push('url');
      else try { validator.check(devices.expand(params.url)).isUrl(); } catch(ex) { result.invalid.push('url'); }
      break;

    case 'stop':
    case 'pause':
    case 'next':
    case 'previous':
    case 'flush':
      break;

    default:
      result.invalid.push('perform');
      break;
  }

  return result;
};

var Sonos_Bridge = function(deviceID, deviceUID, info) {
  var self;

  self = this;

  self.whatami = '/device/gateway/sonos/bridge';
  self.deviceID = deviceID.toString();
  self.deviceUID = deviceUID;
  self.name = info.device.name;
  self.getName ();

  self.info = {};
  self.status = 'present';
  self.changed();

  utility.broker.subscribe('actors', function(request, taskID, actor, perform, parameter) {
    if (actor !== ('device/' + self.deviceID)) return;

    if (request === 'perform') return devices.perform(self, taskID, perform, parameter);
  });
};
util.inherits(Sonos_Bridge, media.Device);
Sonos_Bridge.prototype.perform = devices.perform;

exports.start = function() {
  steward.actors.device.media.sonos = steward.actors.device.media.sonos ||
      { $info     : { type: '/device/media/sonos' } };

  steward.actors.device.media.sonos.audio =
      { $info     : { type       : '/device/media/sonos/audio'
                    , observe    : [ ]
                    , perform    : [ 'play'
                                   , 'stop'
                                   , 'pause'
                                   , 'queueNext'
                                   , 'flush'
                                   , 'next'
                                   , 'previous'
                                   , 'wake'
                                   ]
                    , properties : { name    : true
                                   , status  : [ 'idle', 'playing', 'paused', 'busy', 'error' ]
                                   , track   : { title       : true
                                               , artist      : true
                                               , album       : true
                                               , albumArtURI : true
                                               , position    : 'milliseconds'
                                               , duration    : 'milliseconds'
                                               }
                                   , mode    : [ 'normal', 'repeat' , 'shuffle', 'shuffle1' ]
                                   , volume  : 'percentage'
                                   , muted   : [ 'on', 'off' ]
                                   }
                    }
      , $validate : { perform    : validate_perform }
      };
  devices.makers['urn:schemas-upnp-org:device:ZonePlayer:1'] = Sonos_Audio;

  steward.actors.device.gateway.sonos = steward.actors.device.gateway.sonos ||
      { $info     : { type: '/device/gateway/sonos' } };

  steward.actors.device.gateway.sonos.bridge =
      { $info     : { type       : '/device/gateway/sonos/bridge'
                    , observe    : [ ]
                    , perform    : [ 'wake' ]
                    , properties : { name    : true
                                   , status  : [ 'present' ]
                                   }
                    }
      , $validate : { perform    : devices.validate_perform }
      };
  devices.makers['Sonos ZoneBridge ZB100'] = Sonos_Bridge;
};
<commit_msg>sonos debugging<commit_after>// Sonos speakers: http://www.sonos.com/system

var sonos       = require('sonos')
  , stringify   = require('json-stringify-safe')
  , url         = require('url')
  , util        = require('util')
  , validator   = require('validator')
  , xml2js      = require('xml2js')
  , devices     = require('./../../core/device')
  , steward     = require('./../../core/steward')
  , utility     = require('./../../core/utility')
  , discovery   = require('./../../discovery/discovery-ssdp')
  , media       = require('./../device-media')
  ;

var logger = media.logger;

var Sonos_Audio = exports.Device = function(deviceID, deviceUID, info) {
  var o, self;

  self = this;

  self.whatami = '/device/media/sonos/audio';
  self.deviceID = deviceID.toString();
  self.deviceUID = deviceUID;
  self.name = info.device.name;
  self.getName();

  self.url = info.url;
  self.sid = null;
  self.seq = 0;

  o = url.parse(info.url);
  self.sonos = new sonos.Sonos(o.hostname, o.port);
  self.status = 'idle';
  self.changed();
  self.info = { track: {}, mode: 'normal' };
  self.refreshID = null;

  self.sonos.getZoneAttrs(function(err, attrs) {
    if (err) return self.error(self, err,  'getZoneAttrs');

    self.setName(attrs.CurrentZoneName);
  });

  utility.broker.subscribe('actors', function(request, taskID, actor, perform, parameter) {
    if (actor !== ('device/' + self.deviceID)) return;

    if (request === 'perform') return self.perform(self, taskID, perform, parameter);
  });

  utility.broker.subscribe('discovery', function(method, headers, content) {
    if (method === 'notify') self.notify(self, headers, content);
  });

// we poll because '/MediaRenderer/RenderingControl/Event' doesn't inform us of changes in volume/mutedness
  self.refresh(self);

  self.jumpstart(self, '/MediaRenderer/AVTransport/Event');
};
util.inherits(Sonos_Audio, media.Device);

Sonos_Audio.prototype.jumpstart = function(self, path) {
  discovery.upnp_subscribe('device/' + self.deviceID, self.url, self.sid, path, function(err, state, response) {
    var i, secs;

    logger.debug('device/' + self.deviceID, { event   : 'subscribe'
                                            , state   : state
                                            , code    : response.statusCode
                                            , err     : stringify(err)
                                            , headers : stringify(response.headers)
                                            });

    if (err) {
      self.error(self, err, 'subscribe');
      setTimeout(function() { self.jumpstart(self, path); }, secs * 30 * 1000);
      return;
    }

    if ((response.statusCode !== 200) || (!response.headers.sid)) {
      self.sid = null;
      setTimeout(function() { self.jumpstart(self, path); }, secs * 30 * 1000);
      return;
    }

    self.sid = response.headers.sid;
    self.seq = 0;
    if (!!response.headers.timeout) {
      secs = response.headers.timeout;
      if ((i = secs.indexOf('Second-')) >= 0) secs = secs.substring(i + 7);
      secs = parseInt(secs, 10) - 1;
      if (secs <= 10) secs = 10;
      setTimeout(function() { self.jumpstart(self, path); }, secs * 1000);
    } else secs = 0;

    logger.info('device/' + self.deviceID, { subscribe: self.sid, path: path, sequence: self.seq, seconds: secs });
  });
};

Sonos_Audio.prototype.perform = function(self, taskID, perform, parameter) {
  var e, param0, params;

  try { params = JSON.parse(parameter); } catch(ex) { params = {}; }

  e = perform;
  param0 = null;
  switch (perform) {
    case 'set':
      if (!!params.name) {
        self.sonos.setName(params.name, function(err, data) {/* jshint unused: false */
          if (err) return self.error(self, err,  'setName');

          self.setName(params.name);
        });
      }
      if (!!params.mode) {
        param0 = { normal   : 'NORMAL'
                 , repeat   : 'REPEAT_ALL'
                 , shuffle  : 'SHUFFLE'
                 , shuffle1 : 'SHUFFLE_NOREPEAT'
                 }[params.mode.toLowerCase()];
        self.sonos.setPlayMode(param0, function(err, data) {/* jshint unused: false */
          if (err) return self.error(self, err,  'setPlayMode');
        });
      }
      if (!!params.position) {
        self.sonos.seek(Math.round(params.position / 1000), function(err, data) {/* jshint unused: false */
          if (err) return self.error(self, err,  'seek');
        });
      }
      if (!!params.volume) {
        self.sonos.setVolume(params.volume, function(err, data) {/* jshint unused: false */
          if (err) return self.error(self, err,  'setVolume');
        });
      }
      if (!!params.muted) {
        self.sonos.setMuted(params.muted === 'on' ? '1' : '0', function(err, data) {/* jshint unused: false */
          if (err) return self.error(self, err,  'setMuted');
        });
      }
      return true;

    case 'wake':
      return self.wake();

    case 'play':
      param0 = (!!params.url) && (params.url.length) ? devices.expand(params.url) : null;
      break;

    case 'queueNext':
      if (!params.url) return false;
      param0 = devices.expand(params.url);
      break;

    case 'stop':
    case 'pause':
    case 'next':
    case 'previous':
    case 'flush':
      break;

    default:
      return false;
  }

console.log('>>> sonos perform: ' + e + JSON.stringify(param0 || {}));
  if (!!param0) {
    self.sonos[e](param0, function(err, data) {/* jshint unused: false */
      if (err) self.error(self, err, e);
    });
  } else {
    self.sonos[e](function(err, data) {/* jshint unused: false */
      if (err) self.error(self, err, e);
    });
  }

  return steward.performed(taskID);
};

Sonos_Audio.prototype.notify = function(self, headers, content) {
  var parser = new xml2js.Parser();

  if ((headers.sid !== self.sid) || (headers.seq < self.seq)) return;
  self.seq = headers.seq + 1;

  try { parser.parseString(content, function(err, data) {
    if (err) {
      logger.error('device/' + self.deviceID, { event: 'xml2js.Parser', content: content, diagnostic: err.message });
      return;
    }

    if ((!data['e:propertyset'])
          || (!util.isArray(data['e:propertyset']['e:property']))
          || (!util.isArray(data['e:propertyset']['e:property'][0].LastChange))) return;

    parser.parseString(data['e:propertyset']['e:property'][0].LastChange[0], function(err, event) {
      var mode, status;

      if (err) {
        logger.error('device/' + self.deviceID,
                          { event      : 'xml2js.Parser'
                          , diagnostic : 'parseString'
                          , content    : data['e:propertyset']['e:property'][0].LastChange[0]
                          , exception  : err });
        return;
      }

      status = { PLAYING          : 'playing'
               , PAUSED_PLAYBACK  : 'paused'
               , TRANSITIONING    : 'busy'
               , STOPPED          : 'idle'
               }[event.Event.InstanceID[0].TransportState[0].$.val] || 'idle';
      mode   = { NORMAL           : 'normal'
               , REPEAT_ALL       : 'repeat'
               , SHUFFLE          : 'shuffle'
               , SHUFFLE_NOREPEAT : 'shuffle1'
               }[event.Event.InstanceID[0].CurrentPlayMode[0].$.val] || 'normal';

      if ((self.info.mode != mode) || (self.status !== status)) {
        self.info.mode = mode;
        self.status = status;
        self.changed();
        self.refresh(self);
      }

      parser.parseString(event.Event.InstanceID[0].CurrentTrackMetaData[0].$.val, function(err, didl) {
        var track;

        if (err) {
          logger.error('device/' + self.deviceID,
                            { event      : 'xml2js.Parser'
                            , diagnostic : 'parseString'
                            , content    : event.Event.InstanceID[0].CurrentTrackMetaData[0].$.val
                            , exception  : err });
          return;
        }

        track = self.sonos.parseDIDL(didl);
        if ((self.info.track.title !== track.title)
                || (self.info.track.artist !== track.artist)
                || (self.info.track.album !== track.album)
                || (self.info.track.albumArtURI !== track.albumArtURI)) {
          delete(track.albumArtURL);
          self.info.track = track;
          self.changed();
        }
      });
    });
  }); } catch(ex) { logger.error('device/' + self.deviceID, { event: 'notify', diagnostic: ex.message }); }
};

Sonos_Audio.prototype.refresh = function(self) {
  if (!!self.refreshID) { clearTimeout(self.refreshID); self.refreshID = null; }

  self.sonos.currentTrack(function(err, track) {
    if (err) return self.error(self, err,  'currentTrack');

     self.status = 'idle';

    if ((track !== undefined)
          && ((self.info.track.position !== (track.position * 1000))
               || (self.info.track.duration !== (track.duration * 1000)))) {
      delete(track.albumArtURL);
      self.info.track = track;
      self.info.track.position *= 1000;
      self.info.track.duration *= 1000;
      self.changed();
    }
  });

  self.sonos.getVolume(function(err, volume) {
    if (err) return self.error(self, err,  'getVolume');

    if ((volume !== undefined) && (self.info.volume !== volume)) {
      self.info.volume = volume;
      self.changed();
    }
  });

  self.sonos.getMuted(function(err, muted) {
    if (err) return self.error(self, err,  'getMuted');

    if ((muted !== undefined) && (self.info.muted !== (muted ? 'on' : 'off'))) {
      self.info.muted = muted ? 'on' : 'off';
      self.changed();
    }
  });

  self.refreshID = setTimeout (function() { self.refresh(self); }, (self.status === 'idle') ? (5 * 1000) : 500);
};

Sonos_Audio.prototype.error = function(self, err, event) {
  logger.error('device/' + self.deviceID, { event: event, diagnostic: err.message });
  if (self.status !== 'error') {
    self.status = 'error';
    self.changed();
  }
};

var validate_perform = function(perform, parameter) {
  var params = {}
    , result = { invalid: [], requires: [] }
    ;

  if (!!parameter) try { params = JSON.parse(parameter); } catch(ex) { result.invalid.push('parameter'); }

  switch (perform) {
    case 'wake':
      return true;

    case 'set':
      if (!parameter) {
        result.requires.push('parameter');
        return result;
      }
      if ((!!params.mode) && (!({ normal: true, repeat: true, shuffle: true, shuffle1: true }[params.mode.toLowerCase()]))) {
        result.invalid.push('mode');
      }
      if ((!!params.position) && (!media.validPosition(params.position))) result.invalid.push('position');
      if ((!!params.volume) && (!media.validVolume(params.volume))) result.invalid.push('volume');
      if ((!!params.muted) && (params.muted !== 'on') && (params.muted !== 'off')) result.invalid.push('volume');
      break;

    case 'play':
      if (!!params.url) try { validator.check(devices.expand(params.url)).isUrl(); } catch(ex) { result.invalid.push('url'); }
      break;

    case 'queueNext':
      if (!params.url) result.requires.push('url');
      else try { validator.check(devices.expand(params.url)).isUrl(); } catch(ex) { result.invalid.push('url'); }
      break;

    case 'stop':
    case 'pause':
    case 'next':
    case 'previous':
    case 'flush':
      break;

    default:
      result.invalid.push('perform');
      break;
  }

  return result;
};

var Sonos_Bridge = function(deviceID, deviceUID, info) {
  var self;

  self = this;

  self.whatami = '/device/gateway/sonos/bridge';
  self.deviceID = deviceID.toString();
  self.deviceUID = deviceUID;
  self.name = info.device.name;
  self.getName ();

  self.info = {};
  self.status = 'present';
  self.changed();

  utility.broker.subscribe('actors', function(request, taskID, actor, perform, parameter) {
    if (actor !== ('device/' + self.deviceID)) return;

    if (request === 'perform') return devices.perform(self, taskID, perform, parameter);
  });
};
util.inherits(Sonos_Bridge, media.Device);
Sonos_Bridge.prototype.perform = devices.perform;

exports.start = function() {
  steward.actors.device.media.sonos = steward.actors.device.media.sonos ||
      { $info     : { type: '/device/media/sonos' } };

  steward.actors.device.media.sonos.audio =
      { $info     : { type       : '/device/media/sonos/audio'
                    , observe    : [ ]
                    , perform    : [ 'play'
                                   , 'stop'
                                   , 'pause'
                                   , 'queueNext'
                                   , 'flush'
                                   , 'next'
                                   , 'previous'
                                   , 'wake'
                                   ]
                    , properties : { name    : true
                                   , status  : [ 'idle', 'playing', 'paused', 'busy', 'error' ]
                                   , track   : { title       : true
                                               , artist      : true
                                               , album       : true
                                               , albumArtURI : true
                                               , position    : 'milliseconds'
                                               , duration    : 'milliseconds'
                                               }
                                   , mode    : [ 'normal', 'repeat' , 'shuffle', 'shuffle1' ]
                                   , volume  : 'percentage'
                                   , muted   : [ 'on', 'off' ]
                                   }
                    }
      , $validate : { perform    : validate_perform }
      };
  devices.makers['urn:schemas-upnp-org:device:ZonePlayer:1'] = Sonos_Audio;

  steward.actors.device.gateway.sonos = steward.actors.device.gateway.sonos ||
      { $info     : { type: '/device/gateway/sonos' } };

  steward.actors.device.gateway.sonos.bridge =
      { $info     : { type       : '/device/gateway/sonos/bridge'
                    , observe    : [ ]
                    , perform    : [ 'wake' ]
                    , properties : { name    : true
                                   , status  : [ 'present' ]
                                   }
                    }
      , $validate : { perform    : devices.validate_perform }
      };
  devices.makers['Sonos ZoneBridge ZB100'] = Sonos_Bridge;
};
<|endoftext|>


<commit_before>/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
/* eslint-disable camelcase */
import { Behavior, getChartMetadataRegistry } from '@superset-ui/core';

import { chart } from 'src/components/Chart/chartReducer';
import { initSliceEntities } from 'src/dashboard/reducers/sliceEntities';
import { getInitialState as getInitialNativeFilterState } from 'src/dashboard/reducers/nativeFilters';
import { applyDefaultFormData } from 'src/explore/store';
import { buildActiveFilters } from 'src/dashboard/util/activeDashboardFilters';
import findPermission, {
  canUserEditDashboard,
} from 'src/dashboard/util/findPermission';
import {
  DASHBOARD_FILTER_SCOPE_GLOBAL,
  dashboardFilter,
} from 'src/dashboard/reducers/dashboardFilters';
import {
  DASHBOARD_HEADER_ID,
  GRID_DEFAULT_CHART_WIDTH,
  GRID_COLUMN_COUNT,
  DASHBOARD_ROOT_ID,
} from 'src/dashboard/util/constants';
import {
  DASHBOARD_HEADER_TYPE,
  CHART_TYPE,
  ROW_TYPE,
} from 'src/dashboard/util/componentTypes';
import findFirstParentContainerId from 'src/dashboard/util/findFirstParentContainer';
import getEmptyLayout from 'src/dashboard/util/getEmptyLayout';
import getFilterConfigsFromFormdata from 'src/dashboard/util/getFilterConfigsFromFormdata';
import getLocationHash from 'src/dashboard/util/getLocationHash';
import newComponentFactory from 'src/dashboard/util/newComponentFactory';
import { TIME_RANGE } from 'src/visualizations/FilterBox/FilterBox';
import { URL_PARAMS } from 'src/constants';
import { getUrlParam } from 'src/utils/urlUtils';
import { FILTER_BOX_MIGRATION_STATES } from 'src/explore/constants';
import { ResourceStatus } from 'src/hooks/apiResources/apiResources';
import { FeatureFlag, isFeatureEnabled } from '../../featureFlags';
import extractUrlParams from '../util/extractUrlParams';
import getNativeFilterConfig from '../util/filterboxMigrationHelper';
import { updateColorSchema } from './dashboardInfo';

export const HYDRATE_DASHBOARD = 'HYDRATE_DASHBOARD';

export const hydrateDashboard =
  (
    dashboardData,
    chartData,
    filterboxMigrationState = FILTER_BOX_MIGRATION_STATES.NOOP,
    dataMaskApplied,
  ) =>
  (dispatch, getState) => {
    const { user, common, dashboardState } = getState();

    const { metadata } = dashboardData;
    const regularUrlParams = extractUrlParams('regular');
    const reservedUrlParams = extractUrlParams('reserved');
    const editMode = reservedUrlParams.edit === 'true';

    let preselectFilters = {};

    chartData.forEach(chart => {
      // eslint-disable-next-line no-param-reassign
      chart.slice_id = chart.form_data.slice_id;
    });
    try {
      // allow request parameter overwrite dashboard metadata
      preselectFilters =
        getUrlParam(URL_PARAMS.preselectFilters) ||
        JSON.parse(metadata.default_filters);
    } catch (e) {
      //
    }

    if (metadata?.shared_label_colors) {
      updateColorSchema(metadata, metadata?.shared_label_colors);
    }

    // Priming the color palette with user's label-color mapping provided in
    // the dashboard's JSON metadata
    if (metadata?.label_colors) {
      updateColorSchema(metadata, metadata?.label_colors);
    }

    // dashboard layout
    const { position_data } = dashboardData;
    // new dash: position_json could be {} or null
    const layout =
      position_data && Object.keys(position_data).length > 0
        ? position_data
        : getEmptyLayout();

    // create a lookup to sync layout names with slice names
    const chartIdToLayoutId = {};
    Object.values(layout).forEach(layoutComponent => {
      if (layoutComponent.type === CHART_TYPE) {
        chartIdToLayoutId[layoutComponent.meta.chartId] = layoutComponent.id;
      }
    });

    // find root level chart container node for newly-added slices
    const parentId = findFirstParentContainerId(layout);
    const parent = layout[parentId];
    let newSlicesContainer;
    let newSlicesContainerWidth = 0;

    const filterScopes = metadata?.filter_scopes || {};

    const chartQueries = {};
    const dashboardFilters = {};
    const slices = {};
    const sliceIds = new Set();
    chartData.forEach(slice => {
      const key = slice.slice_id;
      const form_data = {
        ...slice.form_data,
        url_params: {
          ...slice.form_data.url_params,
          ...regularUrlParams,
        },
      };
      chartQueries[key] = {
        ...chart,
        id: key,
        form_data,
        formData: applyDefaultFormData(form_data),
      };

      slices[key] = {
        slice_id: key,
        slice_url: slice.slice_url,
        slice_name: slice.slice_name,
        form_data: slice.form_data,
        viz_type: slice.form_data.viz_type,
        datasource: slice.form_data.datasource,
        description: slice.description,
        description_markeddown: slice.description_markeddown,
        owners: slice.owners,
        modified: slice.modified,
        changed_on: new Date(slice.changed_on).getTime(),
      };

      sliceIds.add(key);

      // if there are newly added slices from explore view, fill slices into 1 or more rows
      if (!chartIdToLayoutId[key] && layout[parentId]) {
        if (
          newSlicesContainerWidth === 0 ||
          newSlicesContainerWidth + GRID_DEFAULT_CHART_WIDTH > GRID_COLUMN_COUNT
        ) {
          newSlicesContainer = newComponentFactory(
            ROW_TYPE,
            (parent.parents || []).slice(),
          );
          layout[newSlicesContainer.id] = newSlicesContainer;
          parent.children.push(newSlicesContainer.id);
          newSlicesContainerWidth = 0;
        }

        const chartHolder = newComponentFactory(
          CHART_TYPE,
          {
            chartId: slice.slice_id,
          },
          (newSlicesContainer.parents || []).slice(),
        );

        layout[chartHolder.id] = chartHolder;
        newSlicesContainer.children.push(chartHolder.id);
        chartIdToLayoutId[chartHolder.meta.chartId] = chartHolder.id;
        newSlicesContainerWidth += GRID_DEFAULT_CHART_WIDTH;
      }

      // build DashboardFilters for interactive filter features
      if (slice.form_data.viz_type === 'filter_box') {
        const configs = getFilterConfigsFromFormdata(slice.form_data);
        let { columns } = configs;
        const { labels } = configs;
        if (preselectFilters[key]) {
          Object.keys(columns).forEach(col => {
            if (preselectFilters[key][col]) {
              columns = {
                ...columns,
                [col]: preselectFilters[key][col],
              };
            }
          });
        }

        const scopesByChartId = Object.keys(columns).reduce((map, column) => {
          const scopeSettings = {
            ...filterScopes[key],
          };
          const { scope, immune } = {
            ...DASHBOARD_FILTER_SCOPE_GLOBAL,
            ...scopeSettings[column],
          };

          return {
            ...map,
            [column]: {
              scope,
              immune,
            },
          };
        }, {});

        const componentId = chartIdToLayoutId[key];
        const directPathToFilter = (layout[componentId].parents || []).slice();
        directPathToFilter.push(componentId);
        if (
          [
            FILTER_BOX_MIGRATION_STATES.NOOP,
            FILTER_BOX_MIGRATION_STATES.SNOOZED,
          ].includes(filterboxMigrationState)
        ) {
          dashboardFilters[key] = {
            ...dashboardFilter,
            chartId: key,
            componentId,
            datasourceId: slice.form_data.datasource,
            filterName: slice.slice_name,
            directPathToFilter,
            columns,
            labels,
            scopes: scopesByChartId,
            isDateFilter: Object.keys(columns).includes(TIME_RANGE),
          };
        }
      }

      // sync layout names with current slice names in case a slice was edited
      // in explore since the layout was updated. name updates go through layout for undo/redo
      // functionality and python updates slice names based on layout upon dashboard save
      const layoutId = chartIdToLayoutId[key];
      if (layoutId && layout[layoutId]) {
        layout[layoutId].meta.sliceName = slice.slice_name;
      }
    });
    buildActiveFilters({
      dashboardFilters,
      components: layout,
    });

    // store the header as a layout component so we can undo/redo changes
    layout[DASHBOARD_HEADER_ID] = {
      id: DASHBOARD_HEADER_ID,
      type: DASHBOARD_HEADER_TYPE,
      meta: {
        text: dashboardData.dashboard_title,
      },
    };

    const dashboardLayout = {
      past: [],
      present: layout,
      future: [],
    };

    // find direct link component and path from root
    const directLinkComponentId = getLocationHash();
    let directPathToChild = [];
    if (layout[directLinkComponentId]) {
      directPathToChild = (layout[directLinkComponentId].parents || []).slice();
      directPathToChild.push(directLinkComponentId);
    }

    // should convert filter_box to filter component?
    let filterConfig = metadata?.native_filter_configuration || [];
    if (filterboxMigrationState === FILTER_BOX_MIGRATION_STATES.REVIEWING) {
      filterConfig = getNativeFilterConfig(
        chartData,
        filterScopes,
        preselectFilters,
      );
      metadata.native_filter_configuration = filterConfig;
      metadata.show_native_filters = true;
    }
    const nativeFilters = getInitialNativeFilterState({
      filterConfig,
    });
    metadata.show_native_filters =
      dashboardData?.metadata?.show_native_filters ??
      (isFeatureEnabled(FeatureFlag.DASHBOARD_NATIVE_FILTERS) &&
        [
          FILTER_BOX_MIGRATION_STATES.CONVERTED,
          FILTER_BOX_MIGRATION_STATES.REVIEWING,
          FILTER_BOX_MIGRATION_STATES.NOOP,
        ].includes(filterboxMigrationState));

    if (isFeatureEnabled(FeatureFlag.DASHBOARD_CROSS_FILTERS)) {
      // If user just added cross filter to dashboard it's not saving it scope on server,
      // so we tweak it until user will update scope and will save it in server
      Object.values(dashboardLayout.present).forEach(layoutItem => {
        const chartId = layoutItem.meta?.chartId;
        const behaviors =
          (
            getChartMetadataRegistry().get(
              chartQueries[chartId]?.formData?.viz_type,
            ) ?? {}
          )?.behaviors ?? [];

        if (!metadata.chart_configuration) {
          metadata.chart_configuration = {};
        }
        if (
          behaviors.includes(Behavior.INTERACTIVE_CHART) &&
          !metadata.chart_configuration[chartId]
        ) {
          metadata.chart_configuration[chartId] = {
            id: chartId,
            crossFilters: {
              scope: {
                rootPath: [DASHBOARD_ROOT_ID],
                excluded: [chartId], // By default it doesn't affects itself
              },
            },
          };
        }
      });
    }

    const { roles } = user;
    const canEdit = canUserEditDashboard(dashboardData, user);

    return dispatch({
      type: HYDRATE_DASHBOARD,
      data: {
        sliceEntities: { ...initSliceEntities, slices, isLoading: false },
        charts: chartQueries,
        // read-only data
        dashboardInfo: {
          ...dashboardData,
          metadata,
          userId: user.userId ? String(user.userId) : null, // legacy, please use state.user instead
          dash_edit_perm: canEdit,
          dash_save_perm: findPermission('can_save_dash', 'Superset', roles),
          dash_share_perm: findPermission(
            'can_share_dashboard',
            'Superset',
            roles,
          ),
          superset_can_explore: findPermission(
            'can_explore',
            'Superset',
            roles,
          ),
          superset_can_share: findPermission(
            'can_share_chart',
            'Superset',
            roles,
          ),
          superset_can_csv: findPermission('can_csv', 'Superset', roles),
          slice_can_edit: findPermission('can_slice', 'Superset', roles),
          common: {
            // legacy, please use state.common instead
            flash_messages: common?.flash_messages,
            conf: common?.conf,
          },
        },
        dataMask: dataMaskApplied,
        dashboardFilters,
        nativeFilters,
        dashboardState: {
          preselectNativeFilters: getUrlParam(URL_PARAMS.nativeFilters),
          sliceIds: Array.from(sliceIds),
          directPathToChild,
          directPathLastUpdated: Date.now(),
          focusedFilterField: null,
          expandedSlices: metadata?.expanded_slices || {},
          refreshFrequency: metadata?.refresh_frequency || 0,
          // dashboard viewers can set refresh frequency for the current visit,
          // only persistent refreshFrequency will be saved to backend
          shouldPersistRefreshFrequency: false,
          css: dashboardData.css || '',
          colorNamespace: metadata?.color_namespace || null,
          colorScheme: metadata?.color_scheme || null,
          editMode: canEdit && editMode,
          isPublished: dashboardData.published,
          hasUnsavedChanges: false,
          maxUndoHistoryExceeded: false,
          lastModifiedTime: dashboardData.changed_on,
          isRefreshing: false,
          isFiltersRefreshing: false,
          activeTabs: dashboardState?.activeTabs || [],
          filterboxMigrationState,
          datasetsStatus: ResourceStatus.LOADING,
        },
        dashboardLayout,
      },
    });
  };
<commit_msg>fix(dashboard): new created chart did not have high lighted effect when using the permalink of chart share in dashboard (#20411)<commit_after>/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
/* eslint-disable camelcase */
import { Behavior, getChartMetadataRegistry } from '@superset-ui/core';

import { chart } from 'src/components/Chart/chartReducer';
import { initSliceEntities } from 'src/dashboard/reducers/sliceEntities';
import { getInitialState as getInitialNativeFilterState } from 'src/dashboard/reducers/nativeFilters';
import { applyDefaultFormData } from 'src/explore/store';
import { buildActiveFilters } from 'src/dashboard/util/activeDashboardFilters';
import findPermission, {
  canUserEditDashboard,
} from 'src/dashboard/util/findPermission';
import {
  DASHBOARD_FILTER_SCOPE_GLOBAL,
  dashboardFilter,
} from 'src/dashboard/reducers/dashboardFilters';
import {
  DASHBOARD_HEADER_ID,
  GRID_DEFAULT_CHART_WIDTH,
  GRID_COLUMN_COUNT,
  DASHBOARD_ROOT_ID,
} from 'src/dashboard/util/constants';
import {
  DASHBOARD_HEADER_TYPE,
  CHART_TYPE,
  ROW_TYPE,
} from 'src/dashboard/util/componentTypes';
import findFirstParentContainerId from 'src/dashboard/util/findFirstParentContainer';
import getEmptyLayout from 'src/dashboard/util/getEmptyLayout';
import getFilterConfigsFromFormdata from 'src/dashboard/util/getFilterConfigsFromFormdata';
import getLocationHash from 'src/dashboard/util/getLocationHash';
import newComponentFactory from 'src/dashboard/util/newComponentFactory';
import { TIME_RANGE } from 'src/visualizations/FilterBox/FilterBox';
import { URL_PARAMS } from 'src/constants';
import { getUrlParam } from 'src/utils/urlUtils';
import { FILTER_BOX_MIGRATION_STATES } from 'src/explore/constants';
import { ResourceStatus } from 'src/hooks/apiResources/apiResources';
import { FeatureFlag, isFeatureEnabled } from '../../featureFlags';
import extractUrlParams from '../util/extractUrlParams';
import getNativeFilterConfig from '../util/filterboxMigrationHelper';
import { updateColorSchema } from './dashboardInfo';

export const HYDRATE_DASHBOARD = 'HYDRATE_DASHBOARD';

export const hydrateDashboard =
  (
    dashboardData,
    chartData,
    filterboxMigrationState = FILTER_BOX_MIGRATION_STATES.NOOP,
    dataMaskApplied,
  ) =>
  (dispatch, getState) => {
    const { user, common, dashboardState } = getState();

    const { metadata } = dashboardData;
    const regularUrlParams = extractUrlParams('regular');
    const reservedUrlParams = extractUrlParams('reserved');
    const editMode = reservedUrlParams.edit === 'true';

    let preselectFilters = {};

    chartData.forEach(chart => {
      // eslint-disable-next-line no-param-reassign
      chart.slice_id = chart.form_data.slice_id;
    });
    try {
      // allow request parameter overwrite dashboard metadata
      preselectFilters =
        getUrlParam(URL_PARAMS.preselectFilters) ||
        JSON.parse(metadata.default_filters);
    } catch (e) {
      //
    }

    if (metadata?.shared_label_colors) {
      updateColorSchema(metadata, metadata?.shared_label_colors);
    }

    // Priming the color palette with user's label-color mapping provided in
    // the dashboard's JSON metadata
    if (metadata?.label_colors) {
      updateColorSchema(metadata, metadata?.label_colors);
    }

    // dashboard layout
    const { position_data } = dashboardData;
    // new dash: position_json could be {} or null
    const layout =
      position_data && Object.keys(position_data).length > 0
        ? position_data
        : getEmptyLayout();

    // create a lookup to sync layout names with slice names
    const chartIdToLayoutId = {};
    Object.values(layout).forEach(layoutComponent => {
      if (layoutComponent.type === CHART_TYPE) {
        chartIdToLayoutId[layoutComponent.meta.chartId] = layoutComponent.id;
      }
    });

    // find root level chart container node for newly-added slices
    const parentId = findFirstParentContainerId(layout);
    const parent = layout[parentId];
    let newSlicesContainer;
    let newSlicesContainerWidth = 0;

    const filterScopes = metadata?.filter_scopes || {};

    const chartQueries = {};
    const dashboardFilters = {};
    const slices = {};
    const sliceIds = new Set();
    const slicesFromExploreCount = new Map();

    chartData.forEach(slice => {
      const key = slice.slice_id;
      const form_data = {
        ...slice.form_data,
        url_params: {
          ...slice.form_data.url_params,
          ...regularUrlParams,
        },
      };
      chartQueries[key] = {
        ...chart,
        id: key,
        form_data,
        formData: applyDefaultFormData(form_data),
      };

      slices[key] = {
        slice_id: key,
        slice_url: slice.slice_url,
        slice_name: slice.slice_name,
        form_data: slice.form_data,
        viz_type: slice.form_data.viz_type,
        datasource: slice.form_data.datasource,
        description: slice.description,
        description_markeddown: slice.description_markeddown,
        owners: slice.owners,
        modified: slice.modified,
        changed_on: new Date(slice.changed_on).getTime(),
      };

      sliceIds.add(key);

      // if there are newly added slices from explore view, fill slices into 1 or more rows
      if (!chartIdToLayoutId[key] && layout[parentId]) {
        if (
          newSlicesContainerWidth === 0 ||
          newSlicesContainerWidth + GRID_DEFAULT_CHART_WIDTH > GRID_COLUMN_COUNT
        ) {
          newSlicesContainer = newComponentFactory(
            ROW_TYPE,
            (parent.parents || []).slice(),
          );
          layout[newSlicesContainer.id] = newSlicesContainer;
          parent.children.push(newSlicesContainer.id);
          newSlicesContainerWidth = 0;
        }

        const chartHolder = newComponentFactory(
          CHART_TYPE,
          {
            chartId: slice.slice_id,
          },
          (newSlicesContainer.parents || []).slice(),
        );

        const count = (slicesFromExploreCount.get(slice.slice_id) ?? 0) + 1;
        chartHolder.id = `${CHART_TYPE}-explore-${slice.slice_id}-${count}`;
        slicesFromExploreCount.set(slice.slice_id, count);

        layout[chartHolder.id] = chartHolder;
        newSlicesContainer.children.push(chartHolder.id);
        chartIdToLayoutId[chartHolder.meta.chartId] = chartHolder.id;
        newSlicesContainerWidth += GRID_DEFAULT_CHART_WIDTH;
      }

      // build DashboardFilters for interactive filter features
      if (slice.form_data.viz_type === 'filter_box') {
        const configs = getFilterConfigsFromFormdata(slice.form_data);
        let { columns } = configs;
        const { labels } = configs;
        if (preselectFilters[key]) {
          Object.keys(columns).forEach(col => {
            if (preselectFilters[key][col]) {
              columns = {
                ...columns,
                [col]: preselectFilters[key][col],
              };
            }
          });
        }

        const scopesByChartId = Object.keys(columns).reduce((map, column) => {
          const scopeSettings = {
            ...filterScopes[key],
          };
          const { scope, immune } = {
            ...DASHBOARD_FILTER_SCOPE_GLOBAL,
            ...scopeSettings[column],
          };

          return {
            ...map,
            [column]: {
              scope,
              immune,
            },
          };
        }, {});

        const componentId = chartIdToLayoutId[key];
        const directPathToFilter = (layout[componentId].parents || []).slice();
        directPathToFilter.push(componentId);
        if (
          [
            FILTER_BOX_MIGRATION_STATES.NOOP,
            FILTER_BOX_MIGRATION_STATES.SNOOZED,
          ].includes(filterboxMigrationState)
        ) {
          dashboardFilters[key] = {
            ...dashboardFilter,
            chartId: key,
            componentId,
            datasourceId: slice.form_data.datasource,
            filterName: slice.slice_name,
            directPathToFilter,
            columns,
            labels,
            scopes: scopesByChartId,
            isDateFilter: Object.keys(columns).includes(TIME_RANGE),
          };
        }
      }

      // sync layout names with current slice names in case a slice was edited
      // in explore since the layout was updated. name updates go through layout for undo/redo
      // functionality and python updates slice names based on layout upon dashboard save
      const layoutId = chartIdToLayoutId[key];
      if (layoutId && layout[layoutId]) {
        layout[layoutId].meta.sliceName = slice.slice_name;
      }
    });
    buildActiveFilters({
      dashboardFilters,
      components: layout,
    });

    // store the header as a layout component so we can undo/redo changes
    layout[DASHBOARD_HEADER_ID] = {
      id: DASHBOARD_HEADER_ID,
      type: DASHBOARD_HEADER_TYPE,
      meta: {
        text: dashboardData.dashboard_title,
      },
    };

    const dashboardLayout = {
      past: [],
      present: layout,
      future: [],
    };

    // find direct link component and path from root
    const directLinkComponentId = getLocationHash();
    let directPathToChild = [];
    if (layout[directLinkComponentId]) {
      directPathToChild = (layout[directLinkComponentId].parents || []).slice();
      directPathToChild.push(directLinkComponentId);
    }

    // should convert filter_box to filter component?
    let filterConfig = metadata?.native_filter_configuration || [];
    if (filterboxMigrationState === FILTER_BOX_MIGRATION_STATES.REVIEWING) {
      filterConfig = getNativeFilterConfig(
        chartData,
        filterScopes,
        preselectFilters,
      );
      metadata.native_filter_configuration = filterConfig;
      metadata.show_native_filters = true;
    }
    const nativeFilters = getInitialNativeFilterState({
      filterConfig,
    });
    metadata.show_native_filters =
      dashboardData?.metadata?.show_native_filters ??
      (isFeatureEnabled(FeatureFlag.DASHBOARD_NATIVE_FILTERS) &&
        [
          FILTER_BOX_MIGRATION_STATES.CONVERTED,
          FILTER_BOX_MIGRATION_STATES.REVIEWING,
          FILTER_BOX_MIGRATION_STATES.NOOP,
        ].includes(filterboxMigrationState));

    if (isFeatureEnabled(FeatureFlag.DASHBOARD_CROSS_FILTERS)) {
      // If user just added cross filter to dashboard it's not saving it scope on server,
      // so we tweak it until user will update scope and will save it in server
      Object.values(dashboardLayout.present).forEach(layoutItem => {
        const chartId = layoutItem.meta?.chartId;
        const behaviors =
          (
            getChartMetadataRegistry().get(
              chartQueries[chartId]?.formData?.viz_type,
            ) ?? {}
          )?.behaviors ?? [];

        if (!metadata.chart_configuration) {
          metadata.chart_configuration = {};
        }
        if (
          behaviors.includes(Behavior.INTERACTIVE_CHART) &&
          !metadata.chart_configuration[chartId]
        ) {
          metadata.chart_configuration[chartId] = {
            id: chartId,
            crossFilters: {
              scope: {
                rootPath: [DASHBOARD_ROOT_ID],
                excluded: [chartId], // By default it doesn't affects itself
              },
            },
          };
        }
      });
    }

    const { roles } = user;
    const canEdit = canUserEditDashboard(dashboardData, user);

    return dispatch({
      type: HYDRATE_DASHBOARD,
      data: {
        sliceEntities: { ...initSliceEntities, slices, isLoading: false },
        charts: chartQueries,
        // read-only data
        dashboardInfo: {
          ...dashboardData,
          metadata,
          userId: user.userId ? String(user.userId) : null, // legacy, please use state.user instead
          dash_edit_perm: canEdit,
          dash_save_perm: findPermission('can_save_dash', 'Superset', roles),
          dash_share_perm: findPermission(
            'can_share_dashboard',
            'Superset',
            roles,
          ),
          superset_can_explore: findPermission(
            'can_explore',
            'Superset',
            roles,
          ),
          superset_can_share: findPermission(
            'can_share_chart',
            'Superset',
            roles,
          ),
          superset_can_csv: findPermission('can_csv', 'Superset', roles),
          slice_can_edit: findPermission('can_slice', 'Superset', roles),
          common: {
            // legacy, please use state.common instead
            flash_messages: common?.flash_messages,
            conf: common?.conf,
          },
        },
        dataMask: dataMaskApplied,
        dashboardFilters,
        nativeFilters,
        dashboardState: {
          preselectNativeFilters: getUrlParam(URL_PARAMS.nativeFilters),
          sliceIds: Array.from(sliceIds),
          directPathToChild,
          directPathLastUpdated: Date.now(),
          focusedFilterField: null,
          expandedSlices: metadata?.expanded_slices || {},
          refreshFrequency: metadata?.refresh_frequency || 0,
          // dashboard viewers can set refresh frequency for the current visit,
          // only persistent refreshFrequency will be saved to backend
          shouldPersistRefreshFrequency: false,
          css: dashboardData.css || '',
          colorNamespace: metadata?.color_namespace || null,
          colorScheme: metadata?.color_scheme || null,
          editMode: canEdit && editMode,
          isPublished: dashboardData.published,
          hasUnsavedChanges: false,
          maxUndoHistoryExceeded: false,
          lastModifiedTime: dashboardData.changed_on,
          isRefreshing: false,
          isFiltersRefreshing: false,
          activeTabs: dashboardState?.activeTabs || [],
          filterboxMigrationState,
          datasetsStatus: ResourceStatus.LOADING,
        },
        dashboardLayout,
      },
    });
  };
<|endoftext|>


<commit_before>/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
import { extent } from 'd3-array';
import { scaleThreshold } from 'd3-scale';
import { getSequentialSchemeRegistry, SequentialScheme } from '@superset-ui/color';
import { hexToRGB } from '../../modules/colors';

const DEFAULT_NUM_BUCKETS = 10;

export function getBreakPoints({
    break_points: formDataBreakPoints,
    num_buckets: formDataNumBuckets,
  }, features, accessor) {
  if (!features) {
    return [];
  }
  if (formDataBreakPoints === undefined || formDataBreakPoints.length === 0) {
    // compute evenly distributed break points based on number of buckets
    const numBuckets = formDataNumBuckets ? parseInt(formDataNumBuckets, 10) : DEFAULT_NUM_BUCKETS;
    const [minValue, maxValue] = extent(features, accessor);
    const delta = (maxValue - minValue) / numBuckets;
    const precision = delta === 0
      ? 0
      : Math.max(0, Math.ceil(Math.log10(1 / delta)));
    return Array(numBuckets + 1)
      .fill()
      .map((_, i) => (minValue + i * delta).toFixed(precision));
  }
  return formDataBreakPoints.sort((a, b) => parseFloat(a) - parseFloat(b));
}

export function getBreakPointColorScaler({
    break_points: formDataBreakPoints,
    num_buckets: formDataNumBuckets,
    linear_color_scheme: linearColorScheme,
    opacity,
  }, features, accessor) {
  const breakPoints = formDataBreakPoints || formDataNumBuckets
    ? getBreakPoints({
      break_points: formDataBreakPoints,
      num_buckets: formDataNumBuckets,
    }, features, accessor)
    : null;
  const colorScheme = Array.isArray(linearColorScheme)
    ? new SequentialScheme({
      id: 'custom',
      colors: linearColorScheme,
    })
    : getSequentialSchemeRegistry().get(linearColorScheme);

  let scaler;
  let maskPoint;
  if (breakPoints !== null) {
    // bucket colors into discrete colors
    const n = breakPoints.length - 1;
    const bucketedColors = n > 1
      ? colorScheme.getColors(n)
      : [colorScheme.colors[colorScheme.colors.length - 1]];

    // repeat ends
    const first = bucketedColors[0];
    const last = bucketedColors[bucketedColors.length - 1];
    bucketedColors.unshift(first);
    bucketedColors.push(last);

    const points = breakPoints.map(p => parseFloat(p));
    scaler = scaleThreshold().domain(points).range(bucketedColors);
    maskPoint = value => value > breakPoints[n] || value < breakPoints[0];
  } else {
    // interpolate colors linearly
    scaler = colorScheme.createLinearScale(extent(features, accessor));
    maskPoint = () => false;
  }

  return (d) => {
    const v = accessor(d);
    const c = hexToRGB(scaler(v));
    if (maskPoint(v)) {
      c[3] = 0;
    } else {
      c[3] = (opacity / 100.0) * 255;
    }
    return c;
  };
}

export function getBuckets(fd, features, accessor) {
  const breakPoints = getBreakPoints(fd, features, accessor);
  const colorScaler = getBreakPointColorScaler(fd, features, accessor);
  const buckets = {};
  breakPoints.slice(1).forEach((value, i) => {
    const range = breakPoints[i] + ' - ' + breakPoints[i + 1];
    const mid = 0.5 * (parseFloat(breakPoints[i]) + parseFloat(breakPoints[i + 1]));
    // fix polygon doesn't show
    const metricLabel = fd.metric ? fd.metric.label || fd.metric : null;
    buckets[range] = {
      color: colorScaler({ [metricLabel || fd.metric]: mid }),
      enabled: true,
    };
  });
  return buckets;
}
<commit_msg>Add extra bucket when max outside range (#7264)<commit_after>/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
import { extent } from 'd3-array';
import { scaleThreshold } from 'd3-scale';
import { getSequentialSchemeRegistry, SequentialScheme } from '@superset-ui/color';
import { hexToRGB } from '../../modules/colors';

const DEFAULT_NUM_BUCKETS = 10;

export function getBreakPoints({
    break_points: formDataBreakPoints,
    num_buckets: formDataNumBuckets,
  }, features, accessor) {
  if (!features) {
    return [];
  }
  if (formDataBreakPoints === undefined || formDataBreakPoints.length === 0) {
    // compute evenly distributed break points based on number of buckets
    const numBuckets = formDataNumBuckets ? parseInt(formDataNumBuckets, 10) : DEFAULT_NUM_BUCKETS;
    const [minValue, maxValue] = extent(features, accessor);
    const delta = (maxValue - minValue) / numBuckets;
    const precision = delta === 0
      ? 0
      : Math.max(0, Math.ceil(Math.log10(1 / delta)));
    const extraBucket = maxValue > maxValue.toFixed(precision) ? 1 : 0;
    return Array(numBuckets + 1 + extraBucket)
      .fill()
      .map((_, i) => (minValue + i * delta).toFixed(precision));
  }
  return formDataBreakPoints.sort((a, b) => parseFloat(a) - parseFloat(b));
}

export function getBreakPointColorScaler({
    break_points: formDataBreakPoints,
    num_buckets: formDataNumBuckets,
    linear_color_scheme: linearColorScheme,
    opacity,
  }, features, accessor) {
  const breakPoints = formDataBreakPoints || formDataNumBuckets
    ? getBreakPoints({
      break_points: formDataBreakPoints,
      num_buckets: formDataNumBuckets,
    }, features, accessor)
    : null;
  const colorScheme = Array.isArray(linearColorScheme)
    ? new SequentialScheme({
      id: 'custom',
      colors: linearColorScheme,
    })
    : getSequentialSchemeRegistry().get(linearColorScheme);

  let scaler;
  let maskPoint;
  if (breakPoints !== null) {
    // bucket colors into discrete colors
    const n = breakPoints.length - 1;
    const bucketedColors = n > 1
      ? colorScheme.getColors(n)
      : [colorScheme.colors[colorScheme.colors.length - 1]];

    // repeat ends
    const first = bucketedColors[0];
    const last = bucketedColors[bucketedColors.length - 1];
    bucketedColors.unshift(first);
    bucketedColors.push(last);

    const points = breakPoints.map(p => parseFloat(p));
    scaler = scaleThreshold().domain(points).range(bucketedColors);
    maskPoint = value => value > breakPoints[n] || value < breakPoints[0];
  } else {
    // interpolate colors linearly
    scaler = colorScheme.createLinearScale(extent(features, accessor));
    maskPoint = () => false;
  }

  return (d) => {
    const v = accessor(d);
    const c = hexToRGB(scaler(v));
    if (maskPoint(v)) {
      c[3] = 0;
    } else {
      c[3] = (opacity / 100.0) * 255;
    }
    return c;
  };
}

export function getBuckets(fd, features, accessor) {
  const breakPoints = getBreakPoints(fd, features, accessor);
  const colorScaler = getBreakPointColorScaler(fd, features, accessor);
  const buckets = {};
  breakPoints.slice(1).forEach((value, i) => {
    const range = breakPoints[i] + ' - ' + breakPoints[i + 1];
    const mid = 0.5 * (parseFloat(breakPoints[i]) + parseFloat(breakPoints[i + 1]));
    // fix polygon doesn't show
    const metricLabel = fd.metric ? fd.metric.label || fd.metric : null;
    buckets[range] = {
      color: colorScaler({ [metricLabel || fd.metric]: mid }),
      enabled: true,
    };
  });
  return buckets;
}
<|endoftext|>


<commit_before>// var sort_column = undefined;
// (document.querySelectorAll("th")).forEach((element, indx) => { if (element.textContent == "Path") {sort_column = indx} } );
var sort_column = 2;

let new_directory_name = document.getElementById("name");
let files = document.getElementById("files");

var url_base = window.location;
var current_path;
var editable = undefined;

async function refresh_list() {

    function compareValues(a, b) {
        if (a.value === b.value) {
          return 0;
        } else {
          return a.value < b.value ? -1 : 1;
        }
    }

    current_path = window.location.hash.substr(1);
    if (current_path == "") {
        current_path = "/";
    }
    // Do the fetch first because the browser will prompt for credentials.
    const response = await fetch(new URL("/fs" + current_path, url_base),
        {
            headers: {
                "Accept": "application/json"
            },
            credentials: "include"
        }
    );
    const data = await response.json();
    var new_children = [];
    var title = document.querySelector("title");
    title.textContent = current_path;
    var path = document.querySelector("#path");
    path.textContent = current_path;
    var template = document.querySelector('#row');

    if (editable === undefined) {
        const status = await fetch(new URL("/fs/", url_base),
            {
                method: "OPTIONS",
                credentials: "include"
            }
        );
        editable = status.headers.get("Access-Control-Allow-Methods").includes("DELETE");
        new_directory_name.disabled = !editable;
        files.disabled = !editable;
        if (!editable) {
            let usbwarning = document.querySelector("#usbwarning");
            usbwarning.style.display = "block";
        }
    }

    var dirCells = [];
    var dataCells = [];
    var index = 0;

    if (window.location.path != "/fs/") {
        var clone = template.content.cloneNode(true);
        var td = clone.querySelectorAll("td");
        td[0].textContent = "🗀";
        var path = clone.querySelector("a");
        let parent = new URL("..", "file://" + current_path);
        path.href = "#" + parent.pathname;
        path.textContent = "..";
        // Remove the delete button
        td[4].replaceChildren();

        var sortdata = {};
        sortdata.value = "..";
        sortdata.index = index;
        dirCells.push(sortdata);
        index += 1;

        new_children.push(clone);
    }

    for (const f of data) {
        // Clone the new row and insert it into the table
        var clone = template.content.cloneNode(true);
        var td = clone.querySelectorAll("td");
        var icon = "⬇";
        var file_path = current_path + f.name;
        let api_url = new URL("/fs" + file_path, url_base);
        let edit_url = "/edit/#" + file_path;
        if (f.directory) {
            file_path = "#" + file_path + "/";
            api_url += "/";
        } else {
            file_path = api_url;
        }

        if (f.directory) {
            icon = "🗀";
        } else if(f.name.endsWith(".txt") ||
                  f.name.endsWith(".py") ||
                  f.name.endsWith(".js") ||
                  f.name.endsWith(".json")) {
            icon = "🖹";
        } else if (f.name.endsWith(".html")) {
            icon = "🌐";
        }
        td[0].textContent = icon;
        td[1].textContent = f.file_size;
        var path = clone.querySelector("a");
        path.href = file_path;
        path.textContent = f.name;
        td[3].textContent = (new Date(f.modified_ns / 1000000)).toLocaleString();
        var delete_button = clone.querySelector("button.delete");
        delete_button.value = api_url;
        delete_button.disabled = !editable;
        delete_button.onclick = del;

        if (editable && !f.directory) {
            edit_url = new URL(edit_url, url_base);
            let edit_link = clone.querySelector(".edit_link");
            edit_link.href = edit_url
        }

        var dataCell = td[sort_column];

        var sortdata = {};
        sortdata.value = dataCell.textContent.toLowerCase().trim();
        sortdata.index = index;
        if (!f.directory) {
            dataCells.push(sortdata);
            index += 1;
        } else {
            dirCells.push(sortdata);
            index += 1;
        }

        new_children.push(clone);
    }

    dirCells.sort(compareValues);
    dataCells.sort(compareValues);

    var tbody = document.querySelector("tbody");

    // remove rows
    while (tbody.firstChild) {
      tbody.removeChild(tbody.lastChild);
    }

    // add sorted rows
    for (var i = 0; i < dirCells.length; i += 1) {
      tbody.appendChild(new_children[dirCells[i].index]);
    }
    for (var i = 0; i < dataCells.length; i += 1) {
      tbody.appendChild(new_children[dataCells[i].index]);
    }
}

async function find_devices() {
    const version_response = await fetch("/cp/version.json");
    if (version_response.ok) {
        url_base = new URL("/", window.location).href;
    } else {
        // TODO: Remove this when we've settled things. It is only used when this file isn't hosted
        // by a CP device.
        const response = await fetch("http://circuitpython.local/cp/devices.json");
        let url = new URL("/", response.url);
        url_base = url.href;
        const data = await response.json();
    }
    refresh_list();
}

async function mkdir(e) {
    const response = await fetch(
        new URL("/fs" + current_path + new_directory_name.value + "/", url_base),
        {
            method: "PUT",
            headers: {
                'X-Timestamp': Date.now()
            }
        }
    );
    if (response.ok) {
        refresh_list();
        new_directory_name.value = "";
        mkdir_button.disabled = true;
    }
}

async function upload(e) {
    for (const file of files.files) {
        let file_path = new URL("/fs" + current_path + file.name, url_base);
        const response = await fetch(file_path,
            {
                method: "PUT",
                headers: {
                    'Content-Type': 'application/octet-stream',
                    'X-Timestamp': file.lastModified
                },
                body: file
            }
        )
        if (response.ok) {
            refresh_list();
            files.value = "";
            upload_button.disabled = true;
        }
    }
}

async function del(e) {
    let fn = new URL(e.target.value);
    var prompt = "Delete " + fn.pathname.substr(3);
    if (e.target.value.endsWith("/")) {
        prompt += " and all of its contents?";
    } else {
        prompt += "?";
    }
    if (confirm(prompt)) {
        const response = await fetch(e.target.value,
            {
                method: "DELETE"
            }
        )
        if (response.ok) {
            refresh_list();
        }
    }
}

find_devices();

let mkdir_button = document.getElementById("mkdir");
mkdir_button.onclick = mkdir;

let upload_button = document.getElementById("upload");
upload_button.onclick = upload;

upload_button.disabled = files.files.length == 0;

files.onchange = () => {
    upload_button.disabled = files.files.length == 0;
}

mkdir_button.disabled = new_directory_name.value.length == 0;

new_directory_name.oninput = () => {
    mkdir_button.disabled = new_directory_name.value.length == 0;
}

window.onhashchange = refresh_list;
<commit_msg>Don't need a variable to identify 'Path' column<commit_after>let new_directory_name = document.getElementById("name");
let files = document.getElementById("files");

var url_base = window.location;
var current_path;
var editable = undefined;

async function refresh_list() {

    function compareValues(a, b) {
        if (a.value === b.value) {
          return 0;
        } else {
          return a.value < b.value ? -1 : 1;
        }
    }

    current_path = window.location.hash.substr(1);
    if (current_path == "") {
        current_path = "/";
    }
    // Do the fetch first because the browser will prompt for credentials.
    const response = await fetch(new URL("/fs" + current_path, url_base),
        {
            headers: {
                "Accept": "application/json"
            },
            credentials: "include"
        }
    );
    const data = await response.json();
    var new_children = [];
    var title = document.querySelector("title");
    title.textContent = current_path;
    var path = document.querySelector("#path");
    path.textContent = current_path;
    var template = document.querySelector('#row');

    if (editable === undefined) {
        const status = await fetch(new URL("/fs/", url_base),
            {
                method: "OPTIONS",
                credentials: "include"
            }
        );
        editable = status.headers.get("Access-Control-Allow-Methods").includes("DELETE");
        new_directory_name.disabled = !editable;
        files.disabled = !editable;
        if (!editable) {
            let usbwarning = document.querySelector("#usbwarning");
            usbwarning.style.display = "block";
        }
    }

    var dirCells = [];
    var dataCells = [];
    var index = 0;

    if (window.location.path != "/fs/") {
        var clone = template.content.cloneNode(true);
        var td = clone.querySelectorAll("td");
        td[0].textContent = "🗀";
        var path = clone.querySelector("a");
        let parent = new URL("..", "file://" + current_path);
        path.href = "#" + parent.pathname;
        path.textContent = "..";
        // Remove the delete button
        td[4].replaceChildren();

        var sortdata = {};
        sortdata.value = "..";
        sortdata.index = index;
        dirCells.push(sortdata);
        index += 1;

        new_children.push(clone);
    }

    for (const f of data) {
        // Clone the new row and insert it into the table
        var clone = template.content.cloneNode(true);
        var td = clone.querySelectorAll("td");
        var icon = "⬇";
        var file_path = current_path + f.name;
        let api_url = new URL("/fs" + file_path, url_base);
        let edit_url = "/edit/#" + file_path;
        if (f.directory) {
            file_path = "#" + file_path + "/";
            api_url += "/";
        } else {
            file_path = api_url;
        }

        if (f.directory) {
            icon = "🗀";
        } else if(f.name.endsWith(".txt") ||
                  f.name.endsWith(".py") ||
                  f.name.endsWith(".js") ||
                  f.name.endsWith(".json")) {
            icon = "🖹";
        } else if (f.name.endsWith(".html")) {
            icon = "🌐";
        }
        td[0].textContent = icon;
        td[1].textContent = f.file_size;
        var path = clone.querySelector("a");
        path.href = file_path;
        path.textContent = f.name;
        td[3].textContent = (new Date(f.modified_ns / 1000000)).toLocaleString();
        var delete_button = clone.querySelector("button.delete");
        delete_button.value = api_url;
        delete_button.disabled = !editable;
        delete_button.onclick = del;

        if (editable && !f.directory) {
            edit_url = new URL(edit_url, url_base);
            let edit_link = clone.querySelector(".edit_link");
            edit_link.href = edit_url
        }

        var dataCell = td[2];

        var sortdata = {};
        sortdata.value = dataCell.textContent.toLowerCase().trim();
        sortdata.index = index;
        if (!f.directory) {
            dataCells.push(sortdata);
            index += 1;
        } else {
            dirCells.push(sortdata);
            index += 1;
        }

        new_children.push(clone);
    }

    dirCells.sort(compareValues);
    dataCells.sort(compareValues);

    var tbody = document.querySelector("tbody");

    // remove rows
    while (tbody.firstChild) {
      tbody.removeChild(tbody.lastChild);
    }

    // add sorted rows
    for (var i = 0; i < dirCells.length; i += 1) {
      tbody.appendChild(new_children[dirCells[i].index]);
    }
    for (var i = 0; i < dataCells.length; i += 1) {
      tbody.appendChild(new_children[dataCells[i].index]);
    }
}

async function find_devices() {
    const version_response = await fetch("/cp/version.json");
    if (version_response.ok) {
        url_base = new URL("/", window.location).href;
    } else {
        // TODO: Remove this when we've settled things. It is only used when this file isn't hosted
        // by a CP device.
        const response = await fetch("http://circuitpython.local/cp/devices.json");
        let url = new URL("/", response.url);
        url_base = url.href;
        const data = await response.json();
    }
    refresh_list();
}

async function mkdir(e) {
    const response = await fetch(
        new URL("/fs" + current_path + new_directory_name.value + "/", url_base),
        {
            method: "PUT",
            headers: {
                'X-Timestamp': Date.now()
            }
        }
    );
    if (response.ok) {
        refresh_list();
        new_directory_name.value = "";
        mkdir_button.disabled = true;
    }
}

async function upload(e) {
    for (const file of files.files) {
        let file_path = new URL("/fs" + current_path + file.name, url_base);
        const response = await fetch(file_path,
            {
                method: "PUT",
                headers: {
                    'Content-Type': 'application/octet-stream',
                    'X-Timestamp': file.lastModified
                },
                body: file
            }
        )
        if (response.ok) {
            refresh_list();
            files.value = "";
            upload_button.disabled = true;
        }
    }
}

async function del(e) {
    let fn = new URL(e.target.value);
    var prompt = "Delete " + fn.pathname.substr(3);
    if (e.target.value.endsWith("/")) {
        prompt += " and all of its contents?";
    } else {
        prompt += "?";
    }
    if (confirm(prompt)) {
        const response = await fetch(e.target.value,
            {
                method: "DELETE"
            }
        )
        if (response.ok) {
            refresh_list();
        }
    }
}

find_devices();

let mkdir_button = document.getElementById("mkdir");
mkdir_button.onclick = mkdir;

let upload_button = document.getElementById("upload");
upload_button.onclick = upload;

upload_button.disabled = files.files.length == 0;

files.onchange = () => {
    upload_button.disabled = files.files.length == 0;
}

mkdir_button.disabled = new_directory_name.value.length == 0;

new_directory_name.oninput = () => {
    mkdir_button.disabled = new_directory_name.value.length == 0;
}

window.onhashchange = refresh_list;
<|endoftext|>


<commit_before>/**
 * Copyright (C) 2018-2020  The Software Heritage developers
 * See the AUTHORS file at the top-level directory of this distribution
 * License: GNU Affero General Public License version 3, or any later version
 * See top-level LICENSE file for more information
 */

import {handleFetchError} from 'utils/functions';

const limit = 100;
let linksPrev = [];
let linkNext = null;
let linkCurrent = null;
let inSearch = false;

function parseLinkHeader(s) {
  let re = /<(.+)>; rel="next"/;
  return s.match(re)[1];
}

function fixTableRowsStyle() {
  setTimeout(() => {
    $('#origin-search-results tbody tr').removeAttr('style');
  });
}

function clearOriginSearchResultsTable() {
  $('#origin-search-results tbody tr').remove();
}

function populateOriginSearchResultsTable(origins) {
  if (origins.length > 0) {
    $('#swh-origin-search-results').show();
    $('#swh-no-result').hide();
    clearOriginSearchResultsTable();
    let table = $('#origin-search-results tbody');
    for (let [i, origin] of origins.entries()) {
      let browseUrl = Urls.browse_origin(origin.url);
      let tableRow = `<tr id="origin-${i}" class="swh-search-result-entry swh-tr-hover-highlight">`;
      tableRow += `<td style="white-space: nowrap;"><a href="${encodeURI(browseUrl)}">${encodeURI(origin.url)}</a></td>`;
      tableRow += `<td id="visit-type-origin-${i}" style="width: 120px;"></td>`;
      tableRow += `<td class="swh-visit-status" id="visit-status-origin-${i}"><i title="Checking visit status" class="fa fa-refresh fa-spin"></i></td>`;
      tableRow += '
';
      table.append(tableRow);
      // get async latest visit snapshot and update visit status icon
      let latestSnapshotUrl = Urls.api_1_origin_visit_latest(origin.url);
      latestSnapshotUrl += '?require_snapshot=true';
      fetch(latestSnapshotUrl)
        .then(response => response.json())
        .then(data => {
          $(`#visit-type-origin-${i}`).text(data.type);
          $(`#visit-status-origin-${i}`).children().remove();
          if (data) {
            $(`#visit-status-origin-${i}`).append('');
          } else {
            $(`#visit-status-origin-${i}`).append('');
            if ($('#swh-filter-empty-visits').prop('checked')) {
              $(`#origin-${i}`).remove();
            }
          }
        });
    }
    fixTableRowsStyle();
  } else {
    $('#swh-origin-search-results').hide();
    $('#swh-no-result').text('No origins matching the search criteria were found.');
    $('#swh-no-result').show();
  }

  if (linkNext === null) {
    $('#origins-next-results-button').addClass('disabled');
  } else {
    $('#origins-next-results-button').removeClass('disabled');
  }

  if (linksPrev.length === 0) {
    $('#origins-prev-results-button').addClass('disabled');
  } else {
    $('#origins-prev-results-button').removeClass('disabled');
  }

  inSearch = false;
  setTimeout(() => {
    window.scrollTo(0, 0);
  });
}

function searchOriginsFirst(searchQueryText, limit) {
  let baseSearchUrl;
  let searchMetadata = $('#swh-search-origin-metadata').prop('checked');
  if (searchMetadata) {
    baseSearchUrl = Urls.api_1_origin_metadata_search() + '?fulltext=' + encodeURIComponent(searchQueryText);
  } else {
    baseSearchUrl = Urls.api_1_origin_search(searchQueryText) + '?';
  }

  let withVisit = $('#swh-search-origins-with-visit').prop('checked');
  let searchUrl = baseSearchUrl + `&limit=${limit}&with_visit=${withVisit}`;
  searchOrigins(searchUrl);
}

function searchOrigins(searchUrl) {
  clearOriginSearchResultsTable();
  $('.swh-loading').addClass('show');
  let response = fetch(searchUrl)
    .then(handleFetchError)
    .then(resp => {
      response = resp;
      return response.json();
    })
    .then(data => {
      // Save link to the current results page
      linkCurrent = searchUrl;
      // Save link to the next results page.
      linkNext = null;
      if (response.headers.has('Link')) {
        let parsedLink = parseLinkHeader(response.headers.get('Link'));
        if (parsedLink !== undefined) {
          linkNext = parsedLink;
        }
      }
      // prevLinks is updated by the caller, which is the one to know if
      // we're going forward or backward in the pages.

      $('.swh-loading').removeClass('show');
      populateOriginSearchResultsTable(data);
    })
    .catch(response => {
      $('.swh-loading').removeClass('show');
      inSearch = false;
      $('#swh-origin-search-results').hide();
      $('#swh-no-result').text(`Error ${response.status}: ${response.statusText}`);
      $('#swh-no-result').show();
    });
}

function doSearch() {
  $('#swh-no-result').hide();
  let searchQueryText = $('#origins-url-patterns').val();
  inSearch = true;
  if (searchQueryText.startsWith('swh:')) {
    // searchQueryText may be a PID so sending search queries to PID resolve endpoint
    let resolvePidUrl = Urls.api_1_resolve_swh_pid(searchQueryText);
    fetch(resolvePidUrl)
      .then(handleFetchError)
      .then(response => response.json())
      .then(data => {
        // pid has been successfully resolved,
        // so redirect to browse page
        window.location = data.browse_url;
      })
      .catch(response => {
        // display a useful error message if the input
        // looks like a swh pid
        response.json().then(data => {
          $('#swh-origin-search-results').hide();
          $('.swh-search-pagination').hide();
          $('#swh-no-result').text(data.reason);
          $('#swh-no-result').show();
        });

      });
  } else {
    // otherwise, proceed with origins search
    $('#swh-origin-search-results').show();
    $('.swh-search-pagination').show();
    searchOriginsFirst(searchQueryText, limit);
  }
}

export function initOriginSearch() {
  $(document).ready(() => {
    $('#swh-search-origins').submit(event => {
      event.preventDefault();
      let searchQueryText = $('#origins-url-patterns').val().trim();
      let withVisit = $('#swh-search-origins-with-visit').prop('checked');
      let withContent = $('#swh-filter-empty-visits').prop('checked');
      let searchMetadata = $('#swh-search-origin-metadata').prop('checked');
      let queryParameters = '?q=' + encodeURIComponent(searchQueryText);
      if (withVisit) {
        queryParameters += '&with_visit';
      }
      if (withContent) {
        queryParameters += '&with_content';
      }
      if (searchMetadata) {
        queryParameters += '&search_metadata';
      }
      // Update the url, triggering page reload and effective search
      window.location.search = queryParameters;
    });

    $('#origins-next-results-button').click(event => {
      if ($('#origins-next-results-button').hasClass('disabled') || inSearch) {
        return;
      }
      inSearch = true;
      linksPrev.push(linkCurrent);
      searchOrigins(linkNext);
      event.preventDefault();
    });

    $('#origins-prev-results-button').click(event => {
      if ($('#origins-prev-results-button').hasClass('disabled') || inSearch) {
        return;
      }
      inSearch = true;
      searchOrigins(linksPrev.pop());
      event.preventDefault();
    });

    let urlParams = new URLSearchParams(window.location.search);
    let query = urlParams.get('q');
    let withVisit = urlParams.has('with_visit');
    let withContent = urlParams.has('with_content');
    let searchMetadata = urlParams.has('search_metadata');
    if (query) {
      $('#origins-url-patterns').val(query);
      $('#swh-search-origins-with-visit').prop('checked', withVisit);
      $('#swh-filter-empty-visits').prop('checked', withContent);
      $('#swh-search-origin-metadata').prop('checked', searchMetadata);
      doSearch();
    }
  });
}
<commit_msg>browse/origin-search: To Use proper URL building in origin-search.js<commit_after>/**
 * Copyright (C) 2018-2020  The Software Heritage developers
 * See the AUTHORS file at the top-level directory of this distribution
 * License: GNU Affero General Public License version 3, or any later version
 * See top-level LICENSE file for more information
 */

import {handleFetchError} from 'utils/functions';

const limit = 100;
let linksPrev = [];
let linkNext = null;
let linkCurrent = null;
let inSearch = false;

function parseLinkHeader(s) {
  let re = /<(.+)>; rel="next"/;
  return s.match(re)[1];
}

function fixTableRowsStyle() {
  setTimeout(() => {
    $('#origin-search-results tbody tr').removeAttr('style');
  });
}

function clearOriginSearchResultsTable() {
  $('#origin-search-results tbody tr').remove();
}

function populateOriginSearchResultsTable(origins) {
  if (origins.length > 0) {
    $('#swh-origin-search-results').show();
    $('#swh-no-result').hide();
    clearOriginSearchResultsTable();
    let table = $('#origin-search-results tbody');
    for (let [i, origin] of origins.entries()) {
      let browseUrl = Urls.browse_origin(origin.url);
      let tableRow = `<tr id="origin-${i}" class="swh-search-result-entry swh-tr-hover-highlight">`;
      tableRow += `<td style="white-space: nowrap;"><a href="${encodeURI(browseUrl)}">${encodeURI(origin.url)}</a></td>`;
      tableRow += `<td id="visit-type-origin-${i}" style="width: 120px;"></td>`;
      tableRow += `<td class="swh-visit-status" id="visit-status-origin-${i}"><i title="Checking visit status" class="fa fa-refresh fa-spin"></i></td>`;
      tableRow += '
';
      table.append(tableRow);
      // get async latest visit snapshot and update visit status icon
      let latestSnapshotUrl = Urls.api_1_origin_visit_latest(origin.url);
      latestSnapshotUrl += '?require_snapshot=true';
      fetch(latestSnapshotUrl)
        .then(response => response.json())
        .then(data => {
          $(`#visit-type-origin-${i}`).text(data.type);
          $(`#visit-status-origin-${i}`).children().remove();
          if (data) {
            $(`#visit-status-origin-${i}`).append('');
          } else {
            $(`#visit-status-origin-${i}`).append('');
            if ($('#swh-filter-empty-visits').prop('checked')) {
              $(`#origin-${i}`).remove();
            }
          }
        });
    }
    fixTableRowsStyle();
  } else {
    $('#swh-origin-search-results').hide();
    $('#swh-no-result').text('No origins matching the search criteria were found.');
    $('#swh-no-result').show();
  }

  if (linkNext === null) {
    $('#origins-next-results-button').addClass('disabled');
  } else {
    $('#origins-next-results-button').removeClass('disabled');
  }

  if (linksPrev.length === 0) {
    $('#origins-prev-results-button').addClass('disabled');
  } else {
    $('#origins-prev-results-button').removeClass('disabled');
  }

  inSearch = false;
  setTimeout(() => {
    window.scrollTo(0, 0);
  });
}

function searchOriginsFirst(searchQueryText, limit) {
  let baseSearchUrl;
  let searchMetadata = $('#swh-search-origin-metadata').prop('checked');
  if (searchMetadata) {
    baseSearchUrl = new URL(Urls.api_1_origin_metadata_search(), window.location);
    baseSearchUrl.searchParams.append('fulltext', searchQueryText);
  } else {
    baseSearchUrl = new URL(Urls.api_1_origin_search(searchQueryText), window.location);
  }

  let withVisit = $('#swh-search-origins-with-visit').prop('checked');
  baseSearchUrl.searchParams.append('limit', limit);
  baseSearchUrl.searchParams.append('with_visit', withVisit);
  let searchUrl = baseSearchUrl.toString();
  searchOrigins(searchUrl);
}

function searchOrigins(searchUrl) {
  clearOriginSearchResultsTable();
  $('.swh-loading').addClass('show');
  let response = fetch(searchUrl)
    .then(handleFetchError)
    .then(resp => {
      response = resp;
      return response.json();
    })
    .then(data => {
      // Save link to the current results page
      linkCurrent = searchUrl;
      // Save link to the next results page.
      linkNext = null;
      if (response.headers.has('Link')) {
        let parsedLink = parseLinkHeader(response.headers.get('Link'));
        if (parsedLink !== undefined) {
          linkNext = parsedLink;
        }
      }
      // prevLinks is updated by the caller, which is the one to know if
      // we're going forward or backward in the pages.

      $('.swh-loading').removeClass('show');
      populateOriginSearchResultsTable(data);
    })
    .catch(response => {
      $('.swh-loading').removeClass('show');
      inSearch = false;
      $('#swh-origin-search-results').hide();
      $('#swh-no-result').text(`Error ${response.status}: ${response.statusText}`);
      $('#swh-no-result').show();
    });
}

function doSearch() {
  $('#swh-no-result').hide();
  let searchQueryText = $('#origins-url-patterns').val();
  inSearch = true;
  if (searchQueryText.startsWith('swh:')) {
    // searchQueryText may be a PID so sending search queries to PID resolve endpoint
    let resolvePidUrl = Urls.api_1_resolve_swh_pid(searchQueryText);
    fetch(resolvePidUrl)
      .then(handleFetchError)
      .then(response => response.json())
      .then(data => {
        // pid has been successfully resolved,
        // so redirect to browse page
        window.location = data.browse_url;
      })
      .catch(response => {
        // display a useful error message if the input
        // looks like a swh pid
        response.json().then(data => {
          $('#swh-origin-search-results').hide();
          $('.swh-search-pagination').hide();
          $('#swh-no-result').text(data.reason);
          $('#swh-no-result').show();
        });

      });
  } else {
    // otherwise, proceed with origins search
    $('#swh-origin-search-results').show();
    $('.swh-search-pagination').show();
    searchOriginsFirst(searchQueryText, limit);
  }
}

export function initOriginSearch() {
  $(document).ready(() => {
    $('#swh-search-origins').submit(event => {
      event.preventDefault();
      let searchQueryText = $('#origins-url-patterns').val().trim();
      let withVisit = $('#swh-search-origins-with-visit').prop('checked');
      let withContent = $('#swh-filter-empty-visits').prop('checked');
      let searchMetadata = $('#swh-search-origin-metadata').prop('checked');
      let queryParameters = new URLSearchParams();
      queryParameters.append('q', searchQueryText);
      if (withVisit) {
        queryParameters.append('with_visit', withVisit);
      }
      if (withContent) {
        queryParameters.append('with_content', withContent);
      }
      if (searchMetadata) {
        queryParameters.append('search_metadata', searchMetadata);
      }
      // Update the url, triggering page reload and effective search
      window.location.search = `?${queryParameters.toString()}`;
    });

    $('#origins-next-results-button').click(event => {
      if ($('#origins-next-results-button').hasClass('disabled') || inSearch) {
        return;
      }
      inSearch = true;
      linksPrev.push(linkCurrent);
      searchOrigins(linkNext);
      event.preventDefault();
    });

    $('#origins-prev-results-button').click(event => {
      if ($('#origins-prev-results-button').hasClass('disabled') || inSearch) {
        return;
      }
      inSearch = true;
      searchOrigins(linksPrev.pop());
      event.preventDefault();
    });

    let urlParams = new URLSearchParams(window.location.search);
    let query = urlParams.get('q');
    let withVisit = urlParams.has('with_visit');
    let withContent = urlParams.has('with_content');
    let searchMetadata = urlParams.has('search_metadata');
    if (query) {
      $('#origins-url-patterns').val(query);
      $('#swh-search-origins-with-visit').prop('checked', withVisit);
      $('#swh-filter-empty-visits').prop('checked', withContent);
      $('#swh-search-origin-metadata').prop('checked', searchMetadata);
      doSearch();
    }
  });
}
<|endoftext|>