github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/pkg/ui/src/redux/uiData.ts (about)

     1  // Copyright 2018 The Cockroach Authors.
     2  //
     3  // Use of this software is governed by the Business Source License
     4  // included in the file licenses/BSL.txt.
     5  //
     6  // As of the Change Date specified in that file, in accordance with
     7  // the Business Source License, use of this software will be governed
     8  // by the Apache License, Version 2.0, included in the file
     9  // licenses/APL.txt.
    10  
    11  import _ from "lodash";
    12  import { Action, Dispatch } from "redux";
    13  import * as protobuf from "protobufjs/minimal";
    14  
    15  import * as protos from  "src/js/protos";
    16  import { PayloadAction } from "src/interfaces/action";
    17  import { getUIData, setUIData } from "src/util/api";
    18  import { AdminUIState } from "./state";
    19  
    20  export const SET = "cockroachui/uidata/SET_OPTIN";
    21  export const LOAD_ERROR = "cockroachui/uidata/LOAD_ERROR";
    22  export const SAVE_ERROR = "cockroachui/uidata/SAVE_ERROR";
    23  export const LOAD = "cockroachui/uidata/LOAD";
    24  export const LOAD_COMPLETE = "cockroachui/uidata/LOAD_COMPLETE";
    25  export const SAVE = "cockroachui/uidata/SAVE";
    26  export const SAVE_COMPLETE = "cockroachui/uidata/SAVE_COMPLETE";
    27  
    28  // Opt In Attribute Keys
    29  export const KEY_HELPUS: string = "helpus";
    30  // The "server." prefix denotes that this key is shared with the server, so
    31  // changes to this key must be synchronized with the server code.
    32  export const KEY_OPTIN: string = "server.optin-reporting";
    33  // Tracks whether the latest registration data has been synchronized with the
    34  // Cockroach Labs servers.
    35  export const KEY_REGISTRATION_SYNCHRONIZED = "registration_synchronized";
    36  
    37  /**
    38   * OptInAttributes tracks the values the user has provided when opting in to usage reporting
    39   */
    40  export class OptInAttributes {
    41    email: string = "";
    42    optin: boolean = null; // Did the user opt in/out of reporting usage
    43    /**
    44     * Number of times the user has dismissed the opt-in banner. This was made a
    45     * number instead of a boolean for a feature that was not implemented, and is
    46     * currently only ever set to null or 1.
    47     */
    48    dismissed: number = null;
    49    firstname: string = "";
    50    lastname: string = "";
    51    company: string = "";
    52    updates: boolean = null; // Did the user sign up for product/feature updates
    53  }
    54  
    55  // VERSION_DISMISSED_KEY is the uiData key on the server that tracks when the outdated banner
    56  // was last dismissed.
    57  export const VERSION_DISMISSED_KEY = "version_dismissed";
    58  
    59  // INSTRUCTIONS_BOX_COLLAPSED_KEY is the uiData key on the server that tracks whether the
    60  // instructions box on the cluster viz has been collapsed or not.
    61  export const INSTRUCTIONS_BOX_COLLAPSED_KEY = "clusterviz_instructions_box_collapsed";
    62  
    63  // RELEASE_NOTES_SIGNUP_DISMISSED_KEY is the uiData key on the server that tracks when the user
    64  // dismisses Release Nodes signup form.
    65  export const RELEASE_NOTES_SIGNUP_DISMISSED_KEY = "release_notes_signup_dismissed";
    66  
    67  export enum UIDataStatus {
    68    UNINITIALIZED, // Data has not been loaded yet.
    69    LOADING,
    70    LOADING_LOAD_ERROR, // Loading with an existing load error
    71    SAVING,
    72    SAVE_ERROR,
    73    LOAD_ERROR,
    74    VALID, // Data isn't loading/saving and has been successfully loaded/saved.
    75  }
    76  
    77  export class UIData {
    78    status: UIDataStatus = UIDataStatus.UNINITIALIZED;
    79    error: Error;
    80    data: any;
    81  }
    82  
    83  /**
    84   * UIDataState maintains the current values of fields that are persisted to the
    85   * server as UIData. Fields are maintained in this collection as untyped
    86   * objects.
    87   */
    88  export class UIDataState {
    89    [key: string]: UIData;
    90  }
    91  
    92  /**
    93   * Reducer which modifies a UIDataState.
    94   */
    95  export function uiDataReducer(state = new UIDataState(), action: Action): UIDataState {
    96    if (_.isNil(action)) {
    97      return state;
    98    }
    99  
   100    switch (action.type) {
   101      case SET: {
   102        const { key, value } = (action as PayloadAction<KeyValue>).payload;
   103        state = _.clone(state);
   104        state[key] = _.clone(state[key]) || new UIData();
   105        state[key].status = UIDataStatus.VALID;
   106        state[key].data = value;
   107        state[key].error = null;
   108        return state;
   109      }
   110      case SAVE: {
   111        const keys = (action as PayloadAction<string[]>).payload;
   112        state = _.clone(state);
   113        _.each(keys, (k) => {
   114          state[k] = _.clone(state[k]) || new UIData();
   115          state[k].status = UIDataStatus.SAVING;
   116        });
   117        return state;
   118      }
   119      case SAVE_ERROR: {
   120        // TODO(tamird): https://github.com/palantir/tslint/issues/2551
   121        //
   122        // tslint:disable-next-line:no-use-before-declare
   123        const { key: saveErrorKey, error: saveError } = (action as PayloadAction<KeyedError>).payload;
   124        state = _.clone(state);
   125        state[saveErrorKey] = _.clone(state[saveErrorKey]) || new UIData();
   126        state[saveErrorKey].status = UIDataStatus.SAVE_ERROR;
   127        state[saveErrorKey].error = saveError;
   128        return state;
   129      }
   130      case LOAD: {
   131        const keys = (action as PayloadAction<string[]>).payload;
   132        state = _.clone(state);
   133        _.each(keys, (k) => {
   134          state[k] = _.clone(state[k]) || new UIData();
   135          state[k].status = UIDataStatus.LOADING;
   136        });
   137        return state;
   138      }
   139      case LOAD_ERROR: {
   140        // TODO(tamird): https://github.com/palantir/tslint/issues/2551
   141        //
   142        // tslint:disable-next-line:no-use-before-declare
   143        const { key: loadErrorKey, error: loadError } = (action as PayloadAction<KeyedError>).payload;
   144        state = _.clone(state);
   145        state[loadErrorKey] = _.clone(state[loadErrorKey]) || new UIData();
   146        state[loadErrorKey].status = UIDataStatus.LOAD_ERROR;
   147        state[loadErrorKey].error = loadError;
   148        return state;
   149      }
   150      default:
   151        return state;
   152    }
   153  }
   154  
   155  /**
   156   * setUIDataKey sets the value of the given UIData key.
   157   */
   158  export function setUIDataKey(key: string, value: Object): PayloadAction<KeyValue> {
   159    return {
   160      type: SET,
   161      payload: { key, value },
   162    };
   163  }
   164  
   165  /**
   166   * errorUIData occurs when an asynchronous function related to UIData encounters
   167   * an error.
   168   */
   169  export function loadErrorUIData(key: string, error: Error): PayloadAction<KeyedError> {
   170    return {
   171      type: LOAD_ERROR,
   172      payload: { key, error },
   173    };
   174  }
   175  
   176  /**
   177   * errorUIData occurs when an asynchronous function related to UIData encounters
   178   * an error.
   179   */
   180  export function saveErrorUIData(key: string, error: Error): PayloadAction<KeyedError> {
   181    return {
   182      type: SAVE_ERROR,
   183      payload: { key, error },
   184    };
   185  }
   186  
   187  /**
   188   * loadUIData occurs when an asynchronous request to load UIData begins.
   189   */
   190  export function beginLoadUIData(keys: string[]): PayloadAction<string[]> {
   191    return {
   192      type: LOAD,
   193      payload: keys,
   194    };
   195  }
   196  
   197  /**
   198   * saveUIData occurs when an asynchronous request for UIData begins.
   199   */
   200  export function beginSaveUIData(keys: string[]): PayloadAction<string[]> {
   201    return {
   202      type: SAVE,
   203      payload: keys,
   204    };
   205  }
   206  
   207  /**
   208   * A generic KeyValue type used for convenience when calling saveUIData.
   209   */
   210  export interface KeyValue {
   211    key: string;
   212    value: Object;
   213  }
   214  
   215  /**
   216   * KeyedError associates an error with a key to use as an action payload.
   217   */
   218  export interface KeyedError {
   219    key: string;
   220    error: Error;
   221  }
   222  
   223  // HELPER FUNCTIONS
   224  
   225  // Returns true if the key exists and the data is valid.
   226  export function isValid(state: AdminUIState, key: string) {
   227    return state.uiData[key] && (state.uiData[key].status === UIDataStatus.VALID) || false;
   228  }
   229  
   230  // Returns contents of the data field if the key is valid, undefined otherwise.
   231  export function getData(state: AdminUIState, key: string) {
   232    return isValid(state, key) ? state.uiData[key].data : undefined;
   233  }
   234  
   235  // Returns true if the given key exists and is in the SAVING state.
   236  export function isSaving(state: AdminUIState, key: string) {
   237    return state.uiData[key] && (state.uiData[key].status === UIDataStatus.SAVING) || false;
   238  }
   239  
   240  // Returns true if the given key exists and is in the SAVING state.
   241  export function isLoading(state: AdminUIState, key: string) {
   242    return state.uiData[key] && (state.uiData[key].status === UIDataStatus.LOADING) || false;
   243  }
   244  
   245  // Returns true if the key exists and is in either the SAVING or LOADING state.
   246  export function isInFlight(state: AdminUIState, key: string) {
   247    return state.uiData[key] && ((state.uiData[key].status === UIDataStatus.SAVING) || (state.uiData[key].status === UIDataStatus.LOADING)) || false;
   248  }
   249  
   250  // Returns the error field if the key exists and is in the SAVE_ERROR state.
   251  // Returns null otherwise.
   252  export function getSaveError(state: AdminUIState, key: string): Error {
   253    return (state.uiData[key] && (state.uiData[key].status === UIDataStatus.SAVE_ERROR || state.uiData[key].status === UIDataStatus.SAVING)) ? state.uiData[key].error : null;
   254  }
   255  
   256  // Returns the error field if the key exists and is in the LOAD_ERROR state.
   257  // Returns null otherwise.
   258  export function getLoadError(state: AdminUIState, key: string): Error {
   259    return (state.uiData[key] && (state.uiData[key].status === UIDataStatus.LOAD_ERROR || state.uiData[key].status === UIDataStatus.LOADING)) ? state.uiData[key].error : null;
   260  }
   261  
   262  /**
   263   * saveUIData saves the value one (or more) UIData objects to the server. After
   264   * the values have been successfully persisted to the server, they are updated
   265   * in the local UIDataState store.
   266   */
   267  export function saveUIData(...values: KeyValue[]) {
   268    return (dispatch: Dispatch<Action, AdminUIState>, getState: () => AdminUIState): Promise<void> => {
   269      const state = getState();
   270      values = _.filter(values, (kv) => !isInFlight(state, kv.key));
   271      if (values.length === 0) {
   272        return;
   273      }
   274      dispatch(beginSaveUIData(_.map(values, (kv) => kv.key)));
   275  
   276      // Encode data for each UIData key.
   277      const request = new protos.cockroach.server.serverpb.SetUIDataRequest();
   278      _.each(values, (kv) => {
   279        const stringifiedValue = JSON.stringify(kv.value);
   280        const buffer = new Uint8Array(protobuf.util.utf8.length(stringifiedValue));
   281        protobuf.util.utf8.write(stringifiedValue, buffer, 0);
   282        request.key_values[kv.key] = buffer;
   283      });
   284  
   285      return setUIData(request).then((_response) => {
   286        // SetUIDataResponse is empty. A positive return indicates success.
   287        _.each(values, (kv) => dispatch(setUIDataKey(kv.key, kv.value)));
   288      }).catch((error) => {
   289        // TODO(maxlang): Fix error handling more comprehensively.
   290        // Tracked in #8699
   291        setTimeout(() => _.each(values, (kv) => dispatch(saveErrorUIData(kv.key, error))), 1000);
   292      });
   293    };
   294  }
   295  
   296  /**
   297   * loadUIData loads the values of the give UIData keys from the server.
   298   */
   299  export function loadUIData(...keys: string[]) {
   300    return (dispatch: Dispatch<Action, AdminUIState>, getState: () => AdminUIState): Promise<void> => {
   301      const state = getState();
   302      keys = _.filter(keys, (k) => !isInFlight(state, k));
   303      if (keys.length === 0) {
   304        return;
   305      }
   306      dispatch(beginLoadUIData(keys));
   307  
   308      return getUIData(new protos.cockroach.server.serverpb.GetUIDataRequest({ keys })).then((response) => {
   309        // Decode data for each UIData key.
   310        _.each(keys, (key) => {
   311          if (_.has(response.key_values, key)) {
   312            const buffer = response.key_values[key].value;
   313            dispatch(setUIDataKey(key, JSON.parse(protobuf.util.utf8.read(buffer, 0, buffer.byteLength))));
   314          } else {
   315            dispatch(setUIDataKey(key, undefined));
   316          }
   317        });
   318      }).catch((error) => {
   319        // TODO(maxlang): Fix error handling more comprehensively.
   320        // Tracked in #8699
   321        setTimeout(() => _.each(keys, (key) => dispatch(loadErrorUIData(key, error))), 1000);
   322      });
   323    };
   324  }