Skip to content

Instantly share code, notes, and snippets.

@stacey-gammon
Last active March 12, 2018 17:00
Show Gist options
  • Save stacey-gammon/4287e43882b832395579603e1b881376 to your computer and use it in GitHub Desktop.
Save stacey-gammon/4287e43882b832395579603e1b881376 to your computer and use it in GitHub Desktop.
Dashboard and Embeddable Redux State Exploration

Dashboard Redux State Tree:

{
  id: string,
  title: string,
  
  timeRange: {
    from: string,
    to: string,
  },
  autoRefreshInterval: string,
  
  options: {
    timeStoredWithDashboard: boolean,
    useDarkTheme: boolean,
    useMargins: boolean
  }
  
  // We might need appliedFilters and pending filters...
  filters: {},
  
  panels: {
    [id]: {
      gridData: { // Used for positioning by ReactGridLayout
        x: int,
        y: int,
        w: int,
        h: int,
        i: string, // id
      },  
      
      // Configuration state is per panel data that is generally updated via the panel
      // context menu.
      configuration: {
        // Custom time range per panel
        customTimeRange: {},
        
        // if not undefined, this will be shown instead of the
        // title supplied by the embeddable
        customTitle: string,
         
        drillDownLinks: [
          {
            id: string,
            label: string,
            type: ?,
            url: string?,
            objectId: id?
          } 
        ],
        
        // A JSON object for the embeddable to store whatever it wants here which can
        // override other settings on the embeddable instance (for example, columns
        // on a saved search, or colors in a pie chart).  This can be any shape dictated
        // by the embeddable, dashboard knows nothing.
        // While not currently something exposed on the panel context menu, this is probably 
        // something we want to do, to allow users to "reset" this - so all values go back
        // to the stored state on the embeddable (e.g. whatever colors or columns, have most
        // recently been saved with the embeddable).
        // This is TWO WAY data - dashboard can tell the embeddable it changed (got reset, maybe
        // a manual modification of JSON data), *and* the embeddable can tell dashboard it changed
        // (user changed the color of a pie slice).
        embeddablePersonalization: {}
      },
      
      // State that dashboard expects the embeddable to give it.
      // @typedef {Object} EmbeddableData
      embeddable: {
        
        // This is state that might change after the embeddable has been initially rendered.
        // If we offer the embeddable a changeState function to call with new state, this is
        // the shape of the data we should expect back, while below is metadata that we only need
        // to grab once per render.
        // @typedef {Object} EmbeddableViewData
        view: {
          drillDownLink: {
            // Ids of the filters that were clicked on. These will be dynamically
            // applied to the drill down links customized for this panel. If these
            // are supplied the drill down menu should be displayed at the current
            // mouse location
            stagedFilters: FilterObject
                
          },
        
        },
        
        // Once set, metadata shouldn't change.
        // @typedef {Object} EmbeddableMetaData
        metadata {
          // Right now this is either visualization or saved search.
          type: string,
          // If this is true, then dashboard will show a "configure drill down
          // links" menu option in the context menu for the panel.
          supportsDrillDowns: boolean,
          editUrl: string,
          title: string,
          indexPatterns: {...},
          
          // Any shape (make no assumptions)!
          // But currently, it will have the id for visualizations and saved searches.
          embeddableConfiguration: {
            objectId: string?,
          }
         
        }
      }
    }
  },
}

Dashboard Stored Saved Object State

State we need to store with each dashboard object, essentially what the dashboard saved object should look like

Current representation:

_id: string // The id of the saved object is a special doc id field in elasticsearch.
_source: {
  type: string, // always will be "dashboard",
  updated_at: string,
  dashboard: {
    title: string,
    description: string,
    
    // Stores all panel data, including embeddablePersonalization data, and data needed to recreate the
    // embeddable (both data dashboard knows about and expects, e.g. type, and data it might not 
    // need to care about, e.g. id of the saved object for visualizations.
    panelsJSON: string,
    
    // Stuff in the options panel, like dark theme and use margins.
    optionsJSON: string,
    
    // A deprecated field that needs an official migration path before we can get rid of it completely.
    // Currently only migrated once an older dashboard is opened.
    uiStateJSON: string,
    
    // I'm actually not sure what this is for.  Shows up as just 1 in my index.  We store the kibana version
    // with panel data, but we should probably pull that out and store it only once per dashboard, not once
    // per panel.
    version: number,
    
    // Should the time range be saved with the dashboard?  
    timeRestore: boolean,
    
    // If timeRestore is true:
    timeTo: string,
    timeFrom: string,
    refreshInterval: {...}
    
    // Not sure why this is nested under a generic term rather than something dashboard specific. If it's
    // common to all saved objects, it should probably be under _source, not under _source.dashboard
    kibanaSavedObjectMeta: {
      // Stores filters saved with the dashboard.
      searchSourceJSON: string
    },
  }
}

Ideal representation

If we can make fields easily updateable and migratable, we should parse all that stuff out of JSON objects. Then we could search/query for different aspects.

_id: string // The id of the saved object is a special doc id field in elasticsearch.
_source: {
  type: string, // always will be "dashboard",
  updated_at: string,
  dashboard: {
    title: string,
    description: string,
    kibanaVersion: string,
    // Should the time range be saved with the dashboard?  
    timeRestore: boolean,
    timeRange: {...},
    filters: {...},
    useDarkTheme: boolean,
    useMargins: boolean,
    
    // Should we keep this as JSON?  The benefit is that it's a lot easier to modify it without needing
    // to adjust the kibana index mapping. The downside is that then you can easily change something and
    // forget to handle BWC for older style dashboards.
    panels: {
      [id:string]: {
        gridData: {
          x: int,
          y: int,
          w: int,
          h: int,
        },
        
        // Optional panel specific time range
        timeRange: {...},
        
        // Optionally override the title given by the embeddable,
        customTitle: string,
        
        drillDownLinks: [
          {
            name: string,
            url: string?,
            objectId: string?,
          }
        ],
        
        // The type of embeddable. Used to look up the embeddable factory plugin so the
        // embeddable can be rendered.
        type: string,
        
        // Since we don't know the shape of this, we'll have to store it as JSON - we can't
        // create a mapping for it in our index. This will contain per panel embeddable state, such
        // as pie colors and saved search columns.
        embeddablePersonalizationJSON: string,
        
        // We won't know the shape of this either. This will contain data required by the embeddable
        // to render itself. Such as, visualizations and saved searches need an id in order to load
        // themselves. It differs from the personalization above because it's the same for every
        // embeddable and can't be changed by dashboard. We do want to expose functionality for
        // dashboard to wipe out the embeddable personalization data.
        // ** Is this separation from the above neccessary for storage purposes?? Should we reconsider this?
        embeddableConfigurationJSON: string,
        
      }
    }
  }
}

Dashboard and Embeddable communication

dashboard_app.js

// grab dashboard object from storage

// loop through panels array

For each panel:
  const metadata = embeddableFactoriers[panel.type].initialize(
  {
    type: panel.type,
    configuration: panel.embeddableConfiguration
  });
  
  // Keep this out of redux to avoid re-renders on data that doesn't change.
  // This is derived data that the selectors can get from.  Prefetched!
  embeddableMetaDataCache[panel.id] = metadata;

dashboard_panel.js:


// @typedef {Object} EmbeddableState - Data that dashbaord needs from the embeddable
// I think this will just be EmbeddableViewData plus a field for embeddablePersonalization
{
   
   // Before we have drilldown links expose, we need a way to communicate the filters to apply
   // immediately on a dashboard (but once applied don't really matter anymore to a visualization).
   applyFilters: {},
  
   // Once we expose drillDownLinks 
   drillDownLink: {
     // Ids of the filters that were clicked on. These will be dynamically
     // applied to the drill down links customized for this panel. If these
     // are supplied the drill down menu should be displayed at the current
     // mouse location
     stagedFilters: FilterObject
         
   },
   
   // Could be anything - colors on a pie slice, or columns in a saved search. Can be modified by
   // the embeddable or possibly by the dashboard - whether reset or if expose the actual object
   // for manual modification at the panel level on a dashboard...
   personalizationData: {},

}


// @typedef {Object} PanelState - data embeddable needs from dashboard in order to render itself.
{
  
  // Data stored on the dashboard, given to us by the embeddable.  We don't know or care what is in
  // here, but the embeddable needs it to recreate itself from storage.
  embeddableConfiguration: {
    objectId: string
  },
  
  filters: {...},
  
  timeRange: {...},
  
  
}

/**
 * @param {EmbeddableState} newEmbeddableState
 */
const changeState = (newEmbeddableState) => {
  // Convert newEmbeddableState to the Redux tree representation. It's not an exact transformation because:
  // 1. There is some data that is two way - both given to dashboard and dashboard can tell the embeddable to
  // change.  The redux tree formation separates these types of data.
  // 2. We can change our minds much easier about our redux tree set up if it's decoupled from our
  // plugin communication layer which we will have to support long term and BWC.
  
  For each known value inside of EmbeddableState that maps to our redux tree:
    dispatch(ACTION_TO_UPDATE, newEmbeddableState.value)
};

// Once again panelState should not map directly to our redux tree
const { update: fn } = embeddableFactory.render({ node, containerState, changeState });

EmbeddableFactory::render({ panelState }) {
 // mount myself then call
 
}



update(panelState);

Add panel action

// Add new panel
dispatch({
  type: "DASHBOARD_ADD_PANEL",
  
  embeddableType: "bar-chart",
  
  // Whatever data the embeddable needs to create an instance of this type. It will vary based on
  // the plugin type.
  embeddableConfiguration: {
    objectId: string
  },
});

// Selector
function getPanel(state, id) {
  const panel = state.panels[id];
  return {
    panel,
    embeddableMeta: embeddableFactories[panel.embeddableType].metadata,
  };
}

Embeddable State

Embeddable state is the data that the embeddable gives dashboard. It differs slightly from dashboard redux tree because some data we

{
    // Data 
    metadata: {
        // Right now this is either visualization or saved search.
        type: string,
        
        // If this is true, then dashboard will show a "configure drill down
        // links" menu option in the context menu for the panel.
        supportsDrillDowns: boolean,
    },
  },
  
  // Index patterns used by this embeddable. This information is currently
  // used by the filter on a dashboard for which fields to show in the
  // dropdown. Otherwise we'd have to show all fields over all indexes and
  // if no embeddables use those index patterns, there really is no point
  // to filtering on them.
  indexPatternIds: [id1, id2],
  
  // Dashboard navigates to this url when the user clicks 'Edit visualization'
  // in the panel context menu.
  editUrl: string,
  
  // Title to be shown in the panel. Can be overridden at the panel level.
  title: string,

  // 
  esQuery {...},
}

Corner Cases to check for

  1. We must ensure that if embeddables communicate changes to dashbaord via a state tree, that untouched state won't overwrite two way data.

For instance: embeddablePersonalization state goes both ways. Dashboard might tell the embeddable to wipe it out, but the embeddable might tell dashboard to udpate it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment