<|start_filename|>client/app/pages/queries/hooks/useUpdateQueryTags.js<|end_filename|>
import { useCallback } from "react";
import useUpdateQuery from "./useUpdateQuery";
import recordEvent from "@/services/recordEvent";

export default function useUpdateQueryTags(query, onChange) {
  const updateQuery = useUpdateQuery(query, onChange);

  return useCallback(
    tags => {
      recordEvent("edit_tags", "query", query.id);
      updateQuery({ tags });
    },
    [query.id, updateQuery]
  );
}


<|start_filename|>client/app/services/location.js<|end_filename|>
import { isNil, isUndefined, isFunction, isObject, trimStart, mapValues, omitBy, extend } from "lodash";
import qs from "query-string";
import { createBrowserHistory } from "history";

const history = createBrowserHistory();

function normalizeLocation(rawLocation) {
  const { pathname, search, hash } = rawLocation;
  const result = {};

  result.path = pathname;
  result.search = mapValues(qs.parse(search), value => (isNil(value) ? true : value));
  result.hash = trimStart(hash, "#");
  result.url = `${pathname}${search}${hash}`;

  return result;
}

const location = {
  listen(handler) {
    if (isFunction(handler)) {
      return history.listen((unused, action) => handler(location, action));
    } else {
      return () => {};
    }
  },

  confirmChange(handler) {
    if (isFunction(handler)) {
      return history.block(nextLocation => {
        return handler(normalizeLocation(nextLocation), location);
      });
    } else {
      return () => {};
    }
  },

  update(newLocation, replace = false) {
    if (isObject(newLocation)) {
      // remap fields and remove undefined ones
      newLocation = omitBy(
        {
          pathname: newLocation.path,
          search: newLocation.search,
          hash: newLocation.hash,
        },
        isUndefined
      );

      // keep existing fields (!)
      newLocation = extend(
        {
          pathname: location.path,
          search: location.search,
          hash: location.hash,
        },
        newLocation
      );

      // serialize search and keep existing search parameters (!)
      if (isObject(newLocation.search)) {
        newLocation.search = omitBy(extend({}, location.search, newLocation.search), isNil);
        newLocation.search = mapValues(newLocation.search, value => (value === true ? null : value));
        newLocation.search = qs.stringify(newLocation.search);
      }
    }
    if (replace) {
      history.replace(newLocation);
    } else {
      history.push(newLocation);
    }
  },

  url: undefined,

  path: undefined,
  setPath(path, replace = false) {
    location.update({ path }, replace);
  },

  search: undefined,
  setSearch(search, replace = false) {
    location.update({ search }, replace);
  },

  hash: undefined,
  setHash(hash, replace = false) {
    location.update({ hash }, replace);
  },
};

function locationChanged() {
  extend(location, normalizeLocation(history.location));
}

history.listen(locationChanged);
locationChanged(); // init service

export default location;


<|start_filename|>client/app/services/utils.js<|end_filename|>
// eslint-disable-next-line import/prefer-default-export
export function absoluteUrl(url) {
  const urlObj = new URL(url, window.location);
  urlObj.protocol = window.location.protocol;
  urlObj.host = window.location.host;
  return urlObj.toString();
}


<|start_filename|>client/app/components/queries/editor-components/editorComponents.js<|end_filename|>
import { isArray, isNil, each } from "lodash";

const componentsRegistry = new Map();

export const QueryEditorComponents = {
  SCHEMA_BROWSER: "SchemaBrowser",
  QUERY_EDITOR: "QueryEditor",
};

export function registerEditorComponent(componentName, component, dataSourceTypes) {
  if (isNil(dataSourceTypes)) {
    dataSourceTypes = [null]; // use `null` entry for the default set of components
  }

  if (!isArray(dataSourceTypes)) {
    dataSourceTypes = [dataSourceTypes];
  }

  each(dataSourceTypes, dataSourceType => {
    componentsRegistry.set(dataSourceType, { ...componentsRegistry.get(dataSourceType), [componentName]: component });
  });
}

export function getEditorComponents(dataSourceType) {
  return { ...componentsRegistry.get(null), ...componentsRegistry.get(dataSourceType) };
}


<|start_filename|>client/app/components/items-list/hooks/useItemsListExtraActions.js<|end_filename|>
import { filter, includes, intersection } from "lodash";
import React, { useState, useMemo, useEffect, useCallback } from "react";
import Checkbox from "antd/lib/checkbox";
import { Columns } from "../components/ItemsTable";

export default function useItemsListExtraActions(controller, listColumns, ExtraActionsComponent) {
  const [actionsState, setActionsState] = useState({ isAvailable: false });
  const [selectedItems, setSelectedItems] = useState([]);

  // clear selection when page changes
  useEffect(() => {
    setSelectedItems([]);
  }, [controller.pageItems, actionsState.isAvailable]);

  const areAllItemsSelected = useMemo(() => {
    const allItems = controller.pageItems;
    if (allItems.length === 0 || selectedItems.length === 0) {
      return false;
    }
    return intersection(selectedItems, allItems).length === allItems.length;
  }, [selectedItems, controller.pageItems]);

  const toggleAllItems = useCallback(() => {
    if (areAllItemsSelected) {
      setSelectedItems([]);
    } else {
      setSelectedItems(controller.pageItems);
    }
  }, [areAllItemsSelected, controller.pageItems]);

  const toggleItem = useCallback(
    item => {
      if (includes(selectedItems, item)) {
        setSelectedItems(filter(selectedItems, s => s !== item));
      } else {
        setSelectedItems([...selectedItems, item]);
      }
    },
    [selectedItems]
  );

  const checkboxColumn = useMemo(
    () =>
      Columns.custom(
        (text, item) => <Checkbox checked={includes(selectedItems, item)} onChange={() => toggleItem(item)} />,
        {
          title: () => <Checkbox checked={areAllItemsSelected} onChange={toggleAllItems} />,
          field: "id",
          width: "1%",
        }
      ),
    [selectedItems, areAllItemsSelected, toggleAllItems, toggleItem]
  );

  const Component = useCallback(
    function ItemsListExtraActionsComponentWrapper(props) {
      // this check mostly needed to avoid eslint exhaustive deps warning
      if (!ExtraActionsComponent) {
        return null;
      }

      return <ExtraActionsComponent onStateChange={setActionsState} {...props} />;
    },
    [ExtraActionsComponent]
  );

  return useMemo(
    () => ({
      areExtraActionsAvailable: actionsState.isAvailable,
      listColumns: actionsState.isAvailable ? [checkboxColumn, ...listColumns] : listColumns,
      Component,
      selectedItems,
      setSelectedItems,
    }),
    [actionsState, listColumns, checkboxColumn, selectedItems, Component]
  );
}


<|start_filename|>client/app/services/organizationSettings.js<|end_filename|>
import { axios } from "@/services/axios";
import notification from "@/services/notification";

export default {
  get: () => axios.get("api/settings/organization"),
  save: (data, message = "Settings changes saved.") =>
    axios
      .post("api/settings/organization", data)
      .then(data => {
        notification.success(message);
        return data;
      })
      .catch(() => {
        notification.error("Failed saving changes.");
      }),
};


<|start_filename|>client/app/pages/dashboards/hooks/useDataSources.js<|end_filename|>
import { filter } from "lodash";
import { useState, useEffect } from "react";
import DataSource from "@/services/data-source";

/**
 * Provides a list of all data sources, as well as a boolean to say whether they've been loaded
 */
export default function useDataSources() {
  const [allDataSources, setAllDataSources] = useState([]);
  const [dataSourcesLoaded, setDataSourcesLoaded] = useState(false);
  const dataSources = filter(allDataSources, ds => !ds.view_only);

  useEffect(() => {
    let cancelDataSourceLoading = false;
    DataSource.query().then(data => {
      if (!cancelDataSourceLoading) {
        setDataSourcesLoaded(true);
        setAllDataSources(data);
      }
    });

    return () => {
      cancelDataSourceLoading = true;
    };
  }, []);

  return { dataSourcesLoaded, dataSources };
}


<|start_filename|>client/cypress/support/visualizations/table.js<|end_filename|>
export function expectTableToHaveLength(length) {
  cy.getByTestId("TableVisualization")
    .find("tbody tr")
    .should("have.length", length);
}

export function expectFirstColumnToHaveMembers(values) {
  cy.getByTestId("TableVisualization")
    .find("tbody tr td:first-child")
    .then($cell => Cypress.$.map($cell, item => Cypress.$(item).text()))
    .then(firstColumnCells => expect(firstColumnCells).to.have.members(values));
}


<|start_filename|>client/app/pages/queries/hooks/useAddToDashboardDialog.js<|end_filename|>
import { find } from "lodash";
import { useCallback } from "react";
import AddToDashboardDialog from "@/components/queries/AddToDashboardDialog";

export default function useAddToDashboardDialog(query) {
  return useCallback(
    visualizationId => {
      const visualization = find(query.visualizations, { id: visualizationId });
      AddToDashboardDialog.showModal({ visualization });
    },
    [query.visualizations]
  );
}


<|start_filename|>client/app/pages/queries/hooks/useDeleteVisualization.js<|end_filename|>
import { extend, filter } from "lodash";
import { useCallback } from "react";
import Visualization from "@/services/visualization";
import notification from "@/services/notification";
import useImmutableCallback from "@/lib/hooks/useImmutableCallback";

export default function useDeleteVisualization(query, onChange) {
  const handleChange = useImmutableCallback(onChange);

  return useCallback(
    visualizationId =>
      Visualization.delete({ id: visualizationId })
        .then(() => {
          const filteredVisualizations = filter(query.visualizations, v => v.id !== visualizationId);
          handleChange(extend(query.clone(), { visualizations: filteredVisualizations }));
        })
        .catch(() => {
          notification.error("Error deleting visualization.", "Maybe it's used in a dashboard?");
        }),
    [query, handleChange]
  );
}


<|start_filename|>client/cypress/integration/query-snippets/create_query_snippet_spec.js<|end_filename|>
describe("Create Query Snippet", () => {
  beforeEach(() => {
    cy.login();
    cy.visit("/query_snippets/new");
  });

  it("creates a query snippet with an empty description", () => {
    // delete existing "example-snippet"
    cy.request("GET", "api/query_snippets")
      .then(({ body }) => body.filter(snippet => snippet.trigger === "example-snippet"))
      .each(snippet => cy.request("DELETE", `api/query_snippets/${snippet.id}`));

    cy.getByTestId("QuerySnippetDialog").within(() => {
      cy.getByTestId("Trigger").type("example-snippet");
      cy.getByTestId("Snippet")
        .find(".ace_text-input")
        .type("SELECT 1", { force: true });
    });

    cy.getByTestId("SaveQuerySnippetButton").click();
  });
});


<|start_filename|>client/app/services/recordEvent.js<|end_filename|>
import { axios } from "@/services/axios";
import { debounce, extend } from "lodash";

let events = [];

const post = debounce(() => {
  const eventsToSend = events;
  events = [];

  axios.post("api/events", eventsToSend);
}, 1000);

export default function recordEvent(action, objectType, objectId, additionalProperties) {
  const event = {
    action,
    object_type: objectType,
    object_id: objectId,
    timestamp: Date.now() / 1000.0,
    screen_resolution: `${window.screen.width}x${window.screen.height}`,
  };
  extend(event, additionalProperties);
  events.push(event);

  post();
}


<|start_filename|>client/app/pages/queries/hooks/usePermissionsEditorDialog.js<|end_filename|>
import { useCallback } from "react";
import PermissionsEditorDialog from "@/components/PermissionsEditorDialog";

export default function usePermissionsEditorDialog(query) {
  return useCallback(() => {
    PermissionsEditorDialog.showModal({
      aclUrl: `api/queries/${query.id}/acl`,
      context: "query",
      author: query.user,
    });
  }, [query.id, query.user]);
}


<|start_filename|>client/cypress/integration/dashboard/sharing_spec.js<|end_filename|>
/* global cy */

import { editDashboard, shareDashboard, createQueryAndAddWidget } from "../../support/dashboard";

describe("Dashboard Sharing", () => {
  beforeEach(function() {
    cy.login();
    cy.createDashboard("Foo Bar").then(({ id }) => {
      this.dashboardId = id;
      this.dashboardUrl = `/dashboards/${id}`;
    });
    cy.updateOrgSettings({ disable_public_urls: false });
  });

  it("is unavailable when public urls feature is disabled", function() {
    const queryData = {
      query: "select 1",
    };

    const position = { autoHeight: false, sizeY: 6 };
    createQueryAndAddWidget(this.dashboardId, queryData, { position })
      .then(() => {
        cy.visit(this.dashboardUrl);
        return shareDashboard();
      })
      .then(secretAddress => {
        // disable the feature
        cy.updateOrgSettings({ disable_public_urls: true });

        // check the feature is disabled
        cy.visit(this.dashboardUrl);
        cy.getByTestId("DashboardMoreButton").should("exist");
        cy.getByTestId("OpenShareForm").should("not.exist");

        cy.logout();
        cy.visit(secretAddress);
        cy.wait(1500); // eslint-disable-line cypress/no-unnecessary-waiting
        cy.getByTestId("TableVisualization").should("not.exist");

        cy.login();
        cy.updateOrgSettings({ disable_public_urls: false });
      });
  });

  it("is possible if all queries are safe", function() {
    const options = {
      parameters: [
        {
          name: "foo",
          type: "number",
        },
      ],
    };

    const dashboardUrl = this.dashboardUrl;
    cy.createQuery({ options }).then(({ id: queryId }) => {
      cy.visit(dashboardUrl);
      editDashboard();
      cy.getByTestId("AddWidgetButton").click();
      cy.getByTestId("AddWidgetDialog").within(() => {
        cy.get(`.query-selector-result[data-test="QueryId${queryId}"]`).click();
      });
      cy.contains("button", "Add to Dashboard").click();
      cy.getByTestId("AddWidgetDialog").should("not.exist");
      cy.clickThrough(
        {
          button: `
        Done Editing
        Publish
      `,
        },
        `OpenShareForm
      PublicAccessEnabled`
      );

      cy.getByTestId("SecretAddress").should("exist");
    });
  });

  describe("is available to unauthenticated users", () => {
    it("when there are no parameters", function() {
      const queryData = {
        query: "select 1",
      };

      const position = { autoHeight: false, sizeY: 6 };
      createQueryAndAddWidget(this.dashboardId, queryData, { position }).then(() => {
        cy.visit(this.dashboardUrl);

        shareDashboard().then(secretAddress => {
          cy.logout();
          cy.visit(secretAddress);
          cy.getByTestId("TableVisualization", { timeout: 10000 }).should("exist");
          cy.percySnapshot("Successfully Shared Unparameterized Dashboard");
        });
      });
    });

    it("when there are only safe parameters", function() {
      const queryData = {
        query: "select '{{foo}}'",
        options: {
          parameters: [
            {
              name: "foo",
              type: "number",
              value: 1,
            },
          ],
        },
      };

      const position = { autoHeight: false, sizeY: 6 };
      createQueryAndAddWidget(this.dashboardId, queryData, { position }).then(() => {
        cy.visit(this.dashboardUrl);

        shareDashboard().then(secretAddress => {
          cy.logout();
          cy.visit(secretAddress);
          cy.getByTestId("TableVisualization", { timeout: 10000 }).should("exist");
          cy.percySnapshot("Successfully Shared Parameterized Dashboard");
        });
      });
    });

    it("even when there are suddenly some unsafe parameters", function() {
      const queryData = {
        query: "select 1",
      };

      // start out by creating a dashboard with no parameters & share it
      const position = { autoHeight: false, sizeY: 6 };
      createQueryAndAddWidget(this.dashboardId, queryData, { position })
        .then(() => {
          cy.visit(this.dashboardUrl);
          return shareDashboard();
        })
        .then(secretAddress => {
          const unsafeQueryData = {
            query: "select '{{foo}}'",
            options: {
              parameters: [
                {
                  name: "foo",
                  type: "text",
                  value: "oh snap!",
                },
              ],
            },
          };

          // then, after it is shared, add an unsafe parameterized query to it
          const secondWidgetPos = { autoHeight: false, col: 3, sizeY: 6 };
          createQueryAndAddWidget(this.dashboardId, unsafeQueryData, { position: secondWidgetPos }).then(() => {
            cy.logout();
            cy.title().should("eq", "Login to Redash"); // Make sure it's logged out
            cy.visit(secretAddress);
            cy.getByTestId("TableVisualization", { timeout: 10000 }).should("exist");
            cy.contains(
              ".alert",
              "This query contains potentially unsafe parameters" +
                " and cannot be executed on a shared dashboard or an embedded visualization."
            );
            cy.percySnapshot("Successfully Shared Parameterized Dashboard With Some Unsafe Queries");
          });
        });
    });
  });

  it("is not possible if some queries are not safe", function() {
    const options = {
      parameters: [
        {
          name: "foo",
          type: "text",
        },
      ],
    };

    const dashboardUrl = this.dashboardUrl;
    cy.createQuery({ options }).then(({ id: queryId }) => {
      cy.visit(dashboardUrl);
      editDashboard();
      cy.getByTestId("AddWidgetButton").click();
      cy.getByTestId("AddWidgetDialog").within(() => {
        cy.get(`.query-selector-result[data-test="QueryId${queryId}"]`).click();
      });
      cy.contains("button", "Add to Dashboard").click();
      cy.getByTestId("AddWidgetDialog").should("not.exist");
      cy.clickThrough(
        {
          button: `
        Done Editing
        Publish
      `,
        },
        "OpenShareForm"
      );

      cy.getByTestId("PublicAccessEnabled").should("be.disabled");
    });
  });
});


<|start_filename|>client/app/components/queries/editor-components/databricks/useDatabricksSchema.js<|end_filename|>
import { includes, has, get, map, first, isFunction, isEmpty, startsWith } from "lodash";
import { useEffect, useState, useMemo, useCallback, useRef } from "react";
import notification from "@/services/notification";
import DatabricksDataSource from "@/services/databricks-data-source";

function getDatabases(dataSource, refresh = false) {
  if (!dataSource) {
    return Promise.resolve([]);
  }

  return DatabricksDataSource.getDatabases(dataSource, refresh).catch(() => {
    notification.error("Failed to load Database list.", "Please try again later.");
    return Promise.reject();
  });
}

function getSchema(dataSource, databaseName, refresh = false) {
  if (!dataSource || !databaseName) {
    return Promise.resolve([]);
  }

  return DatabricksDataSource.getDatabaseTables(dataSource, databaseName, refresh).catch(() => {
    notification.error(`Failed to load tables for ${databaseName}.`, "Please try again later.");
    return Promise.reject();
  });
}

function addDisplayNameWithoutDatabaseName(schema, databaseName) {
  if (!databaseName) {
    return schema;
  }
  // add display name without {databaseName} + "."
  return map(schema, table => {
    const databaseNamePrefix = databaseName + ".";
    let displayName = table.name;
    if (startsWith(table.name, databaseNamePrefix)) {
      displayName = table.name.slice(databaseNamePrefix.length);
    }
    return { ...table, displayName };
  });
}

export default function useDatabricksSchema(dataSource, options = null, onOptionsUpdate = null) {
  const [databases, setDatabases] = useState([]);
  const [loadingDatabases, setLoadingDatabases] = useState(true);
  const [currentDatabaseName, setCurrentDatabaseName] = useState();
  const [schemas, setSchemas] = useState({});
  const [loadingSchema, setLoadingSchema] = useState(false);
  const [refreshing, setRefreshing] = useState(false);

  const setCurrentSchema = useCallback(
    schema =>
      setSchemas(currentSchemas => ({
        ...currentSchemas,
        [currentDatabaseName]: schema,
      })),
    [currentDatabaseName]
  );

  const currentDatabaseNameRef = useRef();
  currentDatabaseNameRef.current = currentDatabaseName;
  const loadTableColumns = useCallback(
    tableName => {
      // remove [databaseName.] from the tableName
      DatabricksDataSource.getTableColumns(
        dataSource,
        currentDatabaseName,
        tableName.substring(currentDatabaseName.length + 1)
      ).then(columns => {
        if (currentDatabaseNameRef.current === currentDatabaseName) {
          setSchemas(currentSchemas => {
            const schema = get(currentSchemas, currentDatabaseName, []);
            const updatedSchema = map(schema, table => {
              if (table.name === tableName) {
                return { ...table, columns, loading: false };
              }
              return table;
            });
            return {
              ...currentSchemas,
              [currentDatabaseName]: updatedSchema,
            };
          });
        }
      });
    },
    [dataSource, currentDatabaseName]
  );

  const schema = useMemo(() => {
    const currentSchema = get(schemas, currentDatabaseName, []);
    return addDisplayNameWithoutDatabaseName(currentSchema, currentDatabaseName);
  }, [schemas, currentDatabaseName]);

  const refreshAll = useCallback(() => {
    if (!refreshing) {
      setRefreshing(true);
      const getDatabasesPromise = getDatabases(dataSource, true).then(setDatabases);
      const getSchemasPromise = getSchema(dataSource, currentDatabaseName, true).then(({ schema }) =>
        setCurrentSchema(schema)
      );

      Promise.all([getSchemasPromise.catch(() => {}), getDatabasesPromise.catch(() => {})]).then(() =>
        setRefreshing(false)
      );
    }
  }, [dataSource, currentDatabaseName, setCurrentSchema, refreshing]);

  const schemasRef = useRef();
  schemasRef.current = schemas;
  useEffect(() => {
    let isCancelled = false;
    if (currentDatabaseName && !has(schemasRef.current, currentDatabaseName)) {
      setLoadingSchema(true);
      getSchema(dataSource, currentDatabaseName)
        .catch(() => Promise.resolve({ schema: [], has_columns: true }))
        .then(({ schema, has_columns }) => {
          if (!isCancelled) {
            if (!has_columns && !isEmpty(schema)) {
              schema = map(schema, table => ({ ...table, loading: true }));
              getSchema(dataSource, currentDatabaseName, true).then(({ schema }) => {
                if (!isCancelled) {
                  setCurrentSchema(schema);
                }
              });
            }
            setCurrentSchema(schema);
          }
        })
        .finally(() => {
          if (!isCancelled) {
            setLoadingSchema(false);
          }
        });
    }
    return () => {
      isCancelled = true;
    };
  }, [dataSource, currentDatabaseName, setCurrentSchema]);

  const defaultDatabaseNameRef = useRef();
  defaultDatabaseNameRef.current = get(options, "selectedDatabase", null);
  useEffect(() => {
    let isCancelled = false;
    setLoadingDatabases(true);
    setCurrentDatabaseName(undefined);
    setSchemas({});
    getDatabases(dataSource)
      .catch(() => Promise.resolve([]))
      .then(data => {
        if (!isCancelled) {
          setDatabases(data);

          // We set the database using this order:
          // 1. Currently selected value.
          // 2. Last used stored in localStorage.
          // 3. default database.
          // 4. first database in the list.
          let lastUsedDatabase =
            defaultDatabaseNameRef.current || localStorage.getItem(`lastSelectedDatabricksDatabase_${dataSource.id}`);

          if (!lastUsedDatabase) {
            lastUsedDatabase = includes(data, "default") ? "default" : first(data) || null;
          }

          setCurrentDatabaseName(lastUsedDatabase);
        }
      })
      .finally(() => {
        if (!isCancelled) {
          setLoadingDatabases(false);
        }
      });
    return () => {
      isCancelled = true;
    };
  }, [dataSource]);

  const setCurrentDatabase = useCallback(
    databaseName => {
      if (databaseName) {
        try {
          localStorage.setItem(`lastSelectedDatabricksDatabase_${dataSource.id}`, databaseName);
        } catch (e) {
          // `localStorage.setItem` may throw exception if there are no enough space - in this case it could be ignored
        }
      }
      setCurrentDatabaseName(databaseName);
      if (isFunction(onOptionsUpdate) && databaseName !== defaultDatabaseNameRef.current) {
        onOptionsUpdate({
          ...options,
          selectedDatabase: databaseName,
        });
      }
    },
    [dataSource.id, options, onOptionsUpdate]
  );

  return {
    databases,
    loadingDatabases,
    schema,
    loadingSchema,
    currentDatabaseName,
    setCurrentDatabase,
    loadTableColumns,
    refreshAll,
    refreshing,
  };
}


<|start_filename|>client/cypress/integration/alert/create_alert_spec.js<|end_filename|>
describe("Create Alert", () => {
  beforeEach(() => {
    cy.login();
  });

  it("renders the initial page and takes a screenshot", () => {
    cy.visit("/alerts/new");
    cy.getByTestId("QuerySelector").should("exist");
    cy.percySnapshot("Create Alert initial screen");
  });

  it("selects query and takes a screenshot", () => {
    cy.createQuery({ name: "Create Alert Query" }).then(({ id: queryId }) => {
      cy.visit("/alerts/new");
      cy.getByTestId("QuerySelector")
        .click()
        .type("Create Alert Query");
      cy.get(`.query-selector-result[data-test="QueryId${queryId}"]`).click();
      cy.getByTestId("Criteria").should("exist");
      cy.percySnapshot("Create Alert second screen");
    });
  });
});


<|start_filename|>client/app/pages/settings/components/prop-types.js<|end_filename|>
import PropTypes from "prop-types";

export const SettingsEditorPropTypes = {
  settings: PropTypes.object,
  values: PropTypes.object,
  onChange: PropTypes.func, // (key, value) => void
  loading: PropTypes.bool,
};

export const SettingsEditorDefaultProps = {
  settings: {},
  values: {},
  onChange: () => {},
  loading: false,
};


<|start_filename|>client/cypress/integration/user/logout_spec.js<|end_filename|>
describe("Logout", () => {
  beforeEach(() => {
    cy.login();
    cy.visit("/");
  });

  it("shows login page after logout", () => {
    cy.getByTestId("ProfileDropdown").click();
    // Wait until submenu appears and become interactive
    cy.wait(500); // eslint-disable-line cypress/no-unnecessary-waiting
    cy.getByTestId("LogOutButton")
      .should("be.visible")
      .click();

    cy.title().should("eq", "Login to Redash");
  });
});


<|start_filename|>client/app/components/items-list/classes/ItemsFetcher.js<|end_filename|>
import { identity, isFunction, isNil, isString } from "lodash";

class ItemsFetcher {
  _getRequest(state, context) {
    return this._originalGetRequest({}, context);
  }

  _processResults({ results, count }, state, context) {
    return {
      results: this._originalProcessResults(results, context),
      count,
    };
  }

  constructor({ getRequest, doRequest, processResults }) {
    this._originalGetRequest = isFunction(getRequest) ? getRequest : identity;
    this._originalDoRequest = doRequest;
    this._originalProcessResults = isFunction(processResults) ? processResults : identity;
  }

  fetch(changes, state, context) {
    const request = this._getRequest(state, context);
    return this._originalDoRequest(request, context).then(data => this._processResults(data, state, context));
  }
}

// For endpoints that return just an array with items; sorting and pagination
// is performed on client
export class PlainListFetcher extends ItemsFetcher {
  _allItems = [];

  _getRequest({ searchTerm, selectedTags }, context) {
    return this._originalGetRequest(
      {
        q: isString(searchTerm) && searchTerm !== "" ? searchTerm : undefined,
        tags: selectedTags,
      },
      context
    );
  }

  _processResults(data, { paginator, sorter }, context) {
    this._allItems = this._originalProcessResults(data, context);
    this._allItems = sorter.sort(this._allItems);
    return {
      results: paginator.getItemsForPage(this._allItems),
      count: this._allItems.length,
      allResults: this._allItems,
    };
  }

  fetch(changes, state, context) {
    // For plain lists we need to reload items from server only if tags or search changes.
    if (isNil(changes) || changes.tags || changes.sorting) {
      return super.fetch(changes, state, context);
    }
    // Sorting and pagination could be updated using previously fetched items.
    const { paginator, sorter } = state;
    if (changes.sorting) {
      this._allItems = sorter.sort(this._allItems);
    }
    return Promise.resolve({
      results: paginator.getItemsForPage(this._allItems),
      count: this._allItems.length,
      allResults: this._allItems,
    });
  }
}

// For endpoints that support server-side pagination (return object with
// items for current page and total items count)
export class PaginatedListFetcher extends ItemsFetcher {
  _getRequest({ paginator, sorter, searchTerm, selectedTags }, context) {
    return this._originalGetRequest(
      {
        page: paginator.page,
        page_size: paginator.itemsPerPage,
        order: sorter.compiled,
        q: isString(searchTerm) && searchTerm !== "" ? searchTerm : undefined,
        tags: selectedTags,
      },
      context
    );
  }
}


<|start_filename|>client/app/services/settingsMenu.js<|end_filename|>
import { isString, isObject, isFunction, extend, omit, sortBy, find, filter } from "lodash";
import { stripBase } from "@/components/ApplicationArea/Router";
import { currentUser } from "@/services/auth";

class SettingsMenuItem {
  constructor(menuItem) {
    extend(this, { pathPrefix: `/${menuItem.path}` }, omit(menuItem, ["isActive", "isAvailable"]));
    if (isFunction(menuItem.isActive)) {
      this.isActive = menuItem.isActive;
    }
    if (isFunction(menuItem.isAvailable)) {
      this.isAvailable = menuItem.isAvailable;
    }
  }

  isActive(path) {
    return path.startsWith(this.pathPrefix);
  }

  isAvailable() {
    return this.permission === undefined || currentUser.hasPermission(this.permission);
  }
}

class SettingsMenu {
  items = [];

  add(id, item) {
    id = isString(id) ? id : null;
    this.remove(id);
    if (isObject(item)) {
      this.items.push(new SettingsMenuItem({ ...item, id }));
      this.items = sortBy(this.items, "order");
    }
  }

  remove(id) {
    if (isString(id)) {
      this.items = filter(this.items, item => item.id !== id);
      // removing item does not change order of other items, so no need to sort
    }
  }

  getAvailableItems() {
    return filter(this.items, item => item.isAvailable());
  }

  getActiveItem(path) {
    const strippedPath = stripBase(path);
    return find(this.items, item => item.isActive(strippedPath));
  }
}

export default new SettingsMenu();


<|start_filename|>client/cypress/integration/visualizations/table/.mocks/all-cell-types.js<|end_filename|>
export const query = `
  SELECT
    314159.265359 AS num,
    'test' AS str,
    'hello, <b>world</b>' AS html,
    'hello, <b>world</b>' AS html2,
    'Link: http://example.com' AS html3,
    '1995-09-03T07:45' AS "date",
    true AS bool,
    '[{"a": 3.14, "b": "test", "c": [], "d": {}}, false, [null, 123], "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."]' AS json,
    'ukr' AS img,
    'redash' AS link
`;

export const config = {
  itemsPerPage: 25,
  columns: [
    {
      name: "num",
      displayAs: "number",
      numberFormat: "0.000",
    },
    {
      name: "str",
      displayAs: "string",
      allowHTML: true,
      highlightLinks: false,
    },
    {
      name: "html",
      displayAs: "string",
      allowHTML: true,
      highlightLinks: false,
    },
    {
      name: "html2",
      displayAs: "string",
      allowHTML: false,
      highlightLinks: false,
    },
    {
      name: "html3",
      displayAs: "string",
      allowHTML: true,
      highlightLinks: true,
    },
    {
      name: "date",
      displayAs: "datetime",
      dateTimeFormat: "D MMMM YYYY, h:mm A",
    },
    {
      name: "bool",
      displayAs: "boolean",
      booleanValues: ["No", "Yes"],
    },
    {
      name: "json",
      displayAs: "json",
    },
    {
      name: "img",
      displayAs: "image",
      imageUrlTemplate: "https://raw.githubusercontent.com/linssen/country-flag-icons/master/images/png/{{ @ }}.png",
      imageTitleTemplate: "ISO: {{ @ }}",
      imageWidth: "30",
      imageHeight: "",
    },
    {
      name: "link",
      displayAs: "link",
      linkUrlTemplate: "https://www.google.com.ua/search?q={{ @ }}",
      linkTextTemplate: "Search for '{{ @ }}'",
      linkTitleTemplate: "Search for '{{ @ }}'",
      linkOpenInNewTab: true,
    },
  ],
};


<|start_filename|>client/app/services/parameters/Parameter.js<|end_filename|>
import { isNull, isObject, isFunction, isUndefined, isEqual, has, omit, isArray, each } from "lodash";

class Parameter {
  constructor(parameter, parentQueryId) {
    this.title = parameter.title;
    this.name = parameter.name;
    this.type = parameter.type;
    this.global = parameter.global; // backward compatibility in Widget service
    this.parentQueryId = parentQueryId;

    // Used for meta-parameters (i.e. dashboard-level params)
    this.locals = [];

    // Used for URL serialization
    this.urlPrefix = "p_";
  }

  static getExecutionValue(param, extra = {}) {
    if (!isObject(param) || !isFunction(param.getExecutionValue)) {
      return null;
    }

    return param.getExecutionValue(extra);
  }

  static setValue(param, value) {
    if (!isObject(param) || !isFunction(param.setValue)) {
      return null;
    }

    return param.setValue(value);
  }

  get isEmpty() {
    return isNull(this.normalizedValue);
  }

  get hasPendingValue() {
    return this.pendingValue !== undefined && !isEqual(this.pendingValue, this.normalizedValue);
  }

  /** Get normalized value to be used in inputs */
  get normalizedValue() {
    return this.$$value;
  }

  isEmptyValue(value) {
    return isNull(this.normalizeValue(value));
  }

  // eslint-disable-next-line class-methods-use-this
  normalizeValue(value) {
    if (isUndefined(value)) {
      return null;
    }
    return value;
  }

  updateLocals() {
    if (isArray(this.locals)) {
      each(this.locals, local => {
        local.setValue(this.value);
      });
    }
  }

  setValue(value) {
    const normalizedValue = this.normalizeValue(value);
    this.value = normalizedValue;
    this.$$value = normalizedValue;

    this.updateLocals();
    this.clearPendingValue();
    return this;
  }

  /** Get execution value for a query */
  getExecutionValue() {
    return this.value;
  }

  setPendingValue(value) {
    this.pendingValue = this.normalizeValue(value);
  }

  applyPendingValue() {
    if (this.hasPendingValue) {
      this.setValue(this.pendingValue);
    }
  }

  clearPendingValue() {
    this.pendingValue = undefined;
  }

  /** Update URL with Parameter value */
  toUrlParams() {
    const prefix = this.urlPrefix;
    // `null` removes the parameter from the URL in case it exists
    return {
      [`${prefix}${this.name}`]: !this.isEmpty ? this.value : null,
    };
  }

  /** Set parameter value from the URL */
  fromUrlParams(query) {
    const prefix = this.urlPrefix;
    const key = `${prefix}${this.name}`;
    if (has(query, key)) {
      this.setValue(query[key]);
    }
  }

  toQueryTextFragment() {
    return `{{ ${this.name} }}`;
  }

  /** Get a saveable version of the Parameter by omitting unnecessary props */
  toSaveableObject() {
    return omit(this, ["$$value", "urlPrefix", "pendingValue", "parentQueryId"]);
  }
}

export default Parameter;


<|start_filename|>client/cypress/.eslintrc.js<|end_filename|>
module.exports = {
  extends: ["plugin:cypress/recommended"],
  plugins: ["cypress", "chai-friendly"],
  env: {
    "cypress/globals": true,
  },
  rules: {
    "func-names": ["error", "never"],
    "no-unused-expressions": 0,
    "chai-friendly/no-unused-expressions": 2,
  },
};


<|start_filename|>client/app/pages/queries/hooks/useAddVisualizationDialog.js<|end_filename|>
import { useState, useCallback, useEffect } from "react";
import useQueryFlags from "./useQueryFlags";
import useEditVisualizationDialog from "./useEditVisualizationDialog";

export default function useAddVisualizationDialog(query, queryResult, saveQuery, onChange) {
  const queryFlags = useQueryFlags(query);
  const editVisualization = useEditVisualizationDialog(query, queryResult, onChange);
  const [shouldOpenDialog, setShouldOpenDialog] = useState(false);

  useEffect(() => {
    if (!queryFlags.isNew && shouldOpenDialog) {
      setShouldOpenDialog(false);
      editVisualization();
    }
  }, [queryFlags.isNew, shouldOpenDialog, editVisualization]);

  return useCallback(() => {
    if (queryFlags.isNew) {
      setShouldOpenDialog(true);
      saveQuery();
    } else {
      editVisualization();
    }
  }, [queryFlags.isNew, saveQuery, editVisualization]);
}


<|start_filename|>client/app/components/dynamic-form/getFieldLabel.js<|end_filename|>
import { toHuman } from "@/lib/utils";

export default function getFieldLabel(field) {
  const { title, name } = field;
  return title || toHuman(name);
}


<|start_filename|>client/app/services/alert-subscription.js<|end_filename|>
import { axios } from "@/services/axios";

const AlertSubscription = {
  query: ({ alertId }) => axios.get(`api/alerts/${alertId}/subscriptions`),
  create: data => axios.post(`api/alerts/${data.alert_id}/subscriptions`, data),
  delete: data => axios.delete(`api/alerts/${data.alert_id}/subscriptions/${data.id}`),
};

export default AlertSubscription;


<|start_filename|>client/app/pages/users/hooks/useUserGroups.js<|end_filename|>
import { filter, includes, isArray } from "lodash";
import { useEffect, useMemo, useState } from "react";
import Group from "@/services/group";

export default function useUserGroups(user) {
  const [allGroups, setAllGroups] = useState([]);
  const [isLoading, setIsLoading] = useState(true);
  const groups = useMemo(() => filter(allGroups, group => includes(user.groupIds, group.id)), [allGroups, user]);

  useEffect(() => {
    let isCancelled = false;

    Group.query().then(groups => {
      if (!isCancelled) {
        setAllGroups(isArray(groups) ? groups : []);
        setIsLoading(false);
      }
    });
  }, []);

  return useMemo(() => ({ groups, allGroups, isLoading }), [groups, allGroups, isLoading]);
}


<|start_filename|>client/app/__tests__/mocks.js<|end_filename|>
import MockDate from "mockdate";

const date = new Date("2000-01-01T02:00:00.000");

MockDate.set(date);


<|start_filename|>client/app/lib/queryFormat.test.js<|end_filename|>
import { Query } from "@/services/query";
import * as queryFormat from "./queryFormat";

describe("QueryFormat.formatQuery", () => {
  test("returns same query text when syntax is not supported", () => {
    const unsupportedSyntax = "unsupported-syntax";
    const queryText = "select * from example";
    const isFormatQueryAvailable = queryFormat.isFormatQueryAvailable(unsupportedSyntax);
    const formattedQuery = queryFormat.formatQuery(queryText, unsupportedSyntax);

    expect(isFormatQueryAvailable).toBeFalsy();
    expect(formattedQuery).toBe(queryText);
  });

  describe("sql", () => {
    const syntax = "sql";

    test("returns the formatted query text", () => {
      const queryText = "select column1, column2 from example where column1 = 2";
      const expectedFormattedQueryText = [
        "select",
        "  column1,",
        "  column2",
        "from",
        "  example",
        "where",
        "  column1 = 2",
      ].join("\n");
      const isFormatQueryAvailable = queryFormat.isFormatQueryAvailable(syntax);
      const formattedQueryText = queryFormat.formatQuery(queryText, syntax);
      expect(isFormatQueryAvailable).toBeTruthy();
      expect(formattedQueryText).toBe(expectedFormattedQueryText);
    });

    test("still recognizes parameters after formatting", () => {
      const queryText = "select {{param1}}, {{ param2 }}, {{ date-range.start }} from example";
      const formattedQueryText = queryFormat.formatQuery(queryText, syntax);
      const queryParameters = new Query({ query: queryText }).getParameters().parseQuery();
      const formattedQueryParameters = new Query({ query: formattedQueryText }).getParameters().parseQuery();
      expect(formattedQueryParameters.sort()).toEqual(queryParameters.sort());
    });
  });

  describe("json", () => {
    const syntax = "json";

    test("returns the formatted query text", () => {
      const queryText = '{"collection": "example","limit": 10}';
      const expectedFormattedQueryText = '{\n    "collection": "example",\n    "limit": 10\n}';
      const isFormatQueryAvailable = queryFormat.isFormatQueryAvailable(syntax);
      const formattedQueryText = queryFormat.formatQuery(queryText, syntax);
      expect(isFormatQueryAvailable).toBeTruthy();
      expect(formattedQueryText).toBe(expectedFormattedQueryText);
    });
  });
});


<|start_filename|>client/app/pages/queries/hooks/useDuplicateQuery.js<|end_filename|>
import { noop, extend, pick } from "lodash";
import { useCallback, useState } from "react";
import url from "url";
import qs from "query-string";
import { Query } from "@/services/query";

function keepCurrentUrlParams(targetUrl) {
  const currentUrlParams = qs.parse(window.location.search);
  targetUrl = url.parse(targetUrl);
  const targetUrlParams = qs.parse(targetUrl.search);
  return url.format(
    extend(pick(targetUrl, ["protocol", "auth", "host", "pathname", "hash"]), {
      search: qs.stringify(extend(currentUrlParams, targetUrlParams)),
    })
  );
}

export default function useDuplicateQuery(query) {
  const [isDuplicating, setIsDuplicating] = useState(false);

  const duplicateQuery = useCallback(() => {
    // To prevent opening the same tab, name must be unique for each browser
    const tabName = `duplicatedQueryTab/${Math.random().toString()}`;

    // We should open tab here because this moment is a part of user interaction;
    // later browser will block such attempts
    const tab = window.open("", tabName);

    setIsDuplicating(true);
    Query.fork({ id: query.id })
      .then(newQuery => {
        tab.location = keepCurrentUrlParams(newQuery.getUrl(true));
      })
      .finally(() => {
        setIsDuplicating(false);
      });
  }, [query.id]);

  return [isDuplicating, isDuplicating ? noop : duplicateQuery];
}


<|start_filename|>client/app/pages/queries/hooks/useAddNewParameterDialog.js<|end_filename|>
import { map } from "lodash";
import { useCallback } from "react";
import EditParameterSettingsDialog from "@/components/EditParameterSettingsDialog";
import useImmutableCallback from "@/lib/hooks/useImmutableCallback";

export default function useAddNewParameterDialog(query, onParameterAdded) {
  const handleParameterAdded = useImmutableCallback(onParameterAdded);

  return useCallback(() => {
    EditParameterSettingsDialog.showModal({
      parameter: {
        title: null,
        name: "",
        type: "text",
        value: null,
      },
      existingParams: map(query.getParameters().get(), p => p.name),
    }).onClose(param => {
      const newQuery = query.clone();
      param = newQuery.getParameters().add(param);
      handleParameterAdded(newQuery, param);
    });
  }, [query, handleParameterAdded]);
}


<|start_filename|>client/cypress/integration/dashboard/filters_spec.js<|end_filename|>
import { createQueryAndAddWidget, editDashboard } from "../../support/dashboard";
import { expectTableToHaveLength, expectFirstColumnToHaveMembers } from "../../support/visualizations/table";

const SQL = `
SELECT 'a' AS stage1, 'a1' AS stage2, 11 AS value UNION ALL
SELECT 'a' AS stage1, 'a2' AS stage2, 12 AS value UNION ALL
SELECT 'a' AS stage1, 'a3' AS stage2, 45 AS value UNION ALL
SELECT 'a' AS stage1, 'a4' AS stage2, 54 AS value UNION ALL
SELECT 'b' AS stage1, 'b1' AS stage2, 33 AS value UNION ALL
SELECT 'b' AS stage1, 'b2' AS stage2, 73 AS value UNION ALL
SELECT 'b' AS stage1, 'b3' AS stage2, 90 AS value UNION ALL
SELECT 'c' AS stage1, 'c1' AS stage2, 19 AS value UNION ALL
SELECT 'c' AS stage1, 'c2' AS stage2, 92 AS value UNION ALL
SELECT 'c' AS stage1, 'c3' AS stage2, 63 AS value UNION ALL
SELECT 'c' AS stage1, 'c4' AS stage2, 44 AS value\
`;

describe("Dashboard Filters", () => {
  beforeEach(() => {
    cy.login();

    const queryData = {
      name: "Query Filters",
      query: `SELECT stage1 AS "stage1::filter", stage2, value FROM (${SQL}) q`,
    };
    cy.createDashboard("Dashboard Filters").then(dashboard => {
      createQueryAndAddWidget(dashboard.id, queryData)
        .as("widget1TestId")
        .then(() => createQueryAndAddWidget(dashboard.id, queryData, { position: { col: 4 } }))
        .as("widget2TestId")
        .then(() => cy.visit(`/dashboards/${dashboard.id}`));
    });
  });

  it("filters rows in a Table Visualization", function() {
    editDashboard();
    cy.getByTestId("DashboardFilters").should("not.exist");
    cy.getByTestId("DashboardFiltersCheckbox").click();

    cy.getByTestId("DashboardFilters").within(() => {
      cy.getByTestId("FilterName-stage1::filter")
        .find(".ant-select-selection-item")
        .should("have.text", "a");
    });

    cy.getByTestId(this.widget1TestId).within(() => {
      expectTableToHaveLength(4);
      expectFirstColumnToHaveMembers(["a", "a", "a", "a"]);

      cy.getByTestId("FilterName-stage1::filter")
        .find(".ant-select")
        .click();
    });

    cy.contains(".ant-select-item-option-content:visible", "b").click();

    cy.getByTestId(this.widget1TestId).within(() => {
      expectTableToHaveLength(3);
      expectFirstColumnToHaveMembers(["b", "b", "b"]);
    });

    // assert that changing one widget filter doesn't affect another

    cy.getByTestId(this.widget2TestId).within(() => {
      expectTableToHaveLength(4);
      expectFirstColumnToHaveMembers(["a", "a", "a", "a"]);
    });

    // assert that changing a global filter affects all widgets

    cy.getByTestId("DashboardFilters").within(() => {
      cy.getByTestId("FilterName-stage1::filter")
        .find(".ant-select")
        .click();
    });

    cy.contains(".ant-select-item-option-content:visible", "c").click();

    [this.widget1TestId, this.widget2TestId].forEach(widgetTestId =>
      cy.getByTestId(widgetTestId).within(() => {
        expectTableToHaveLength(4);
        expectFirstColumnToHaveMembers(["c", "c", "c", "c"]);
      })
    );
  });
});


<|start_filename|>client/cypress/support/index.js<|end_filename|>
/* global Cypress */

import "@cypress/code-coverage/support";
import "./commands";
import "./redash-api/index.js";

Cypress.env("dataSourceId", 1);

Cypress.on("uncaught:exception", err => {
  // Prevent ResizeObserver error from failing tests
  if (err && Cypress._.includes(err.message, "ResizeObserver loop limit exceeded")) {
    return false;
  }
});


<|start_filename|>client/app/services/policy/index.js<|end_filename|>
import DefaultPolicy from "./DefaultPolicy";

// eslint-disable-next-line import/no-mutable-exports
export let policy = new DefaultPolicy();

export function setPolicy(newPolicy) {
  policy = newPolicy;
}


<|start_filename|>client/app/services/getTags.js<|end_filename|>
import { axios } from "@/services/axios";

function processTags(data) {
  return data.tags || [];
}

export default function getTags(url) {
  return axios.get(url).then(processTags);
}


<|start_filename|>client/cypress/support/parameters.js<|end_filename|>
export function dragParam(paramName, offsetLeft, offsetTop) {
  cy.getByTestId(`DragHandle-${paramName}`)
    .trigger("mouseover")
    .trigger("mousedown");

  cy.get(".parameter-dragged .drag-handle")
    .trigger("mousemove", offsetLeft, offsetTop, { force: true })
    .trigger("mouseup", { force: true });
}

export function expectParamOrder(expectedOrder) {
  cy.get(".parameter-container label").each(($label, index) => expect($label).to.have.text(expectedOrder[index]));
}


<|start_filename|>client/app/pages/queries/hooks/useEditVisualizationDialog.js<|end_filename|>
import { extend, filter, find } from "lodash";
import { useCallback } from "react";
import EditVisualizationDialog from "@/components/visualizations/EditVisualizationDialog";
import useImmutableCallback from "@/lib/hooks/useImmutableCallback";

export default function useEditVisualizationDialog(query, queryResult, onChange) {
  const handleChange = useImmutableCallback(onChange);

  return useCallback(
    (visualizationId = null) => {
      const visualization = find(query.visualizations, { id: visualizationId }) || null;
      EditVisualizationDialog.showModal({
        query,
        visualization,
        queryResult,
      }).onClose(updatedVisualization => {
        const filteredVisualizations = filter(query.visualizations, v => v.id !== updatedVisualization.id);
        handleChange(
          extend(query.clone(), { visualizations: [...filteredVisualizations, updatedVisualization] }),
          updatedVisualization
        );
      });
    },
    [query, queryResult, handleChange]
  );
}


<|start_filename|>client/cypress/plugins/index.js<|end_filename|>
const percyHealthCheck = require("@percy/cypress/task"); // eslint-disable-line import/no-extraneous-dependencies, import/no-unresolved

module.exports = (on, config) => {
  if (config.env.coverage) {
    require("@cypress/code-coverage/task")(on, config);
  }
  on("task", percyHealthCheck);

  return config;
};


<|start_filename|>client/cypress/integration/settings/settings_tabs_spec.js<|end_filename|>
describe("Settings Tabs", () => {
  const regularUser = {
    name: "<NAME>",
    email: "<EMAIL>",
    password: "password",
  };

  const userTabs = ["Users", "Groups", "Query Snippets", "Account"];
  const adminTabs = ["Data Sources", "Alert Destinations", "General"];

  const expectSettingsTabsToBe = expectedTabs =>
    cy.getByTestId("SettingsScreenItem").then($list => {
      const listedPages = $list.toArray().map(el => el.text);
      expect(listedPages).to.have.members(expectedTabs);
    });

  before(() => {
    cy.login().then(() => cy.createUser(regularUser));
  });

  describe("For admin user", () => {
    beforeEach(() => {
      cy.logout();
      cy.login();
      cy.visit("/");
    });

    it("settings link should lead to Data Sources settings", () => {
      cy.getByTestId("SettingsLink")
        .should("exist")
        .should("have.attr", "href", "data_sources");
    });

    it("all tabs should be available", () => {
      cy.getByTestId("SettingsLink").click();
      expectSettingsTabsToBe([...userTabs, ...adminTabs]);
    });
  });

  describe("For regular user", () => {
    beforeEach(() => {
      cy.logout();
      cy.login(regularUser.email, regularUser.password);
      cy.visit("/");
    });

    it("settings link should lead to Users settings", () => {
      cy.getByTestId("SettingsLink")
        .should("exist")
        .should("have.attr", "href", "users");
    });

    it("limited set of settings tabs should be available", () => {
      cy.getByTestId("SettingsLink").click();
      expectSettingsTabsToBe(userTabs);
    });
  });
});


<|start_filename|>client/cypress/integration/visualizations/table/.mocks/multi-column-sort.js<|end_filename|>
export const query = `
  SELECT 3 AS a, 1 AS b, 'h' AS c UNION ALL
  SELECT 1 AS a, 1 AS b, 'b' AS c UNION ALL
  SELECT 2 AS a, 1 AS b, 'e' AS c UNION ALL
  SELECT 1 AS a, 3 AS b, 'd' AS c UNION ALL
  SELECT 2 AS a, 2 AS b, 'f' AS c UNION ALL
  SELECT 1 AS a, 1 AS b, 'a' AS c UNION ALL
  SELECT 3 AS a, 2 AS b, 'i' AS c UNION ALL
  SELECT 2 AS a, 3 AS b, 'g' AS c UNION ALL
  SELECT 1 AS a, 2 AS b, 'c' AS c UNION ALL
  SELECT 3 AS a, 3 AS b, 'j' AS c
`;

export const config = {
  itemsPerPage: 25,
  columns: [
    {
      name: "a",
      displayAs: "number",
      numberFormat: "0",
    },
    {
      name: "b",
      displayAs: "number",
      numberFormat: "0",
    },
    {
      name: "c",
      displayAs: "string",
    },
  ],
};


<|start_filename|>client/app/services/parameters/index.js<|end_filename|>
import Parameter from "./Parameter";
import TextParameter from "./TextParameter";
import NumberParameter from "./NumberParameter";
import EnumParameter from "./EnumParameter";
import QueryBasedDropdownParameter from "./QueryBasedDropdownParameter";
import DateParameter from "./DateParameter";
import DateRangeParameter from "./DateRangeParameter";

function createParameter(param, parentQueryId) {
  switch (param.type) {
    case "number":
      return new NumberParameter(param, parentQueryId);
    case "enum":
      return new EnumParameter(param, parentQueryId);
    case "query":
      return new QueryBasedDropdownParameter(param, parentQueryId);
    case "date":
    case "datetime-local":
    case "datetime-with-seconds":
      return new DateParameter(param, parentQueryId);
    case "date-range":
    case "datetime-range":
    case "datetime-range-with-seconds":
      return new DateRangeParameter(param, parentQueryId);
    default:
      return new TextParameter({ ...param, type: "text" }, parentQueryId);
  }
}

function cloneParameter(param) {
  return createParameter(param, param.parentQueryId);
}

export {
  Parameter,
  TextParameter,
  NumberParameter,
  EnumParameter,
  QueryBasedDropdownParameter,
  DateParameter,
  DateRangeParameter,
  createParameter,
  cloneParameter,
};


<|start_filename|>client/app/services/KeyboardShortcuts.js<|end_filename|>
import { each, filter, map, toLower, toString, trim, upperFirst, without } from "lodash";
import Mousetrap from "mousetrap";
import "mousetrap/plugins/global-bind/mousetrap-global-bind";

const modKey = /Mac|iPod|iPhone|iPad/.test(navigator.platform) ? "Cmd" : "Ctrl";
const altKey = /Mac|iPod|iPhone|iPad/.test(navigator.platform) ? "Option" : "Alt";

export function humanReadableShortcut(shortcut, limit = Infinity) {
  const modifiers = {
    mod: upperFirst(modKey),
    alt: upperFirst(altKey),
  };

  shortcut = toLower(toString(shortcut));
  shortcut = filter(map(shortcut.split(","), trim), s => s !== "").slice(0, limit);
  shortcut = map(shortcut, sc => {
    sc = filter(map(sc.split("+")), s => s !== "");
    return map(sc, s => modifiers[s] || upperFirst(s)).join(" + ");
  }).join(", ");

  return shortcut !== "" ? shortcut : null;
}

const handlers = {};

function onShortcut(event, shortcut) {
  event.preventDefault();
  event.retunValue = false;
  each(handlers[shortcut], fn => fn());
}

const KeyboardShortcuts = {
  modKey,
  altKey,

  bind: keymap => {
    each(keymap, (fn, key) => {
      const keys = key
        .toLowerCase()
        .split(",")
        .map(trim);
      each(keys, k => {
        handlers[k] = [...without(handlers[k], fn), fn];
        Mousetrap.bindGlobal(k, onShortcut);
      });
    });
  },

  unbind: keymap => {
    each(keymap, (fn, key) => {
      const keys = key
        .toLowerCase()
        .split(",")
        .map(trim);
      each(keys, k => {
        handlers[k] = without(handlers[k], fn);
        if (handlers[k].length === 0) {
          handlers[k] = undefined;
          Mousetrap.unbind(k);
        }
      });
    });
  },
};

export default KeyboardShortcuts;


<|start_filename|>client/app/pages/queries/hooks/useUnpublishQuery.js<|end_filename|>
import { useCallback } from "react";
import useUpdateQuery from "./useUpdateQuery";
import recordEvent from "@/services/recordEvent";

export default function useUnpublishQuery(query, onChange) {
  const updateQuery = useUpdateQuery(query, onChange);

  return useCallback(() => {
    recordEvent("toggle_published", "query", query.id);
    updateQuery({ is_draft: true });
  }, [query.id, updateQuery]);
}


<|start_filename|>client/app/services/alert.js<|end_filename|>
import { axios } from "@/services/axios";
import { merge } from "lodash";

// backwards compatibility
const normalizeCondition = {
  "greater than": ">",
  "less than": "<",
  equals: "=",
};

const transformResponse = data =>
  merge({}, data, {
    options: {
      op: normalizeCondition[data.options.op] || data.options.op,
    },
  });

const transformRequest = data => {
  const newData = Object.assign({}, data);
  if (newData.query_id === undefined) {
    newData.query_id = newData.query.id;
    newData.destination_id = newData.destinations;
    delete newData.query;
    delete newData.destinations;
  }

  return newData;
};

const saveOrCreateUrl = data => (data.id ? `api/alerts/${data.id}` : "api/alerts");

const Alert = {
  query: () => axios.get("api/alerts"),
  get: ({ id }) => axios.get(`api/alerts/${id}`).then(transformResponse),
  save: data => axios.post(saveOrCreateUrl(data), transformRequest(data)),
  delete: data => axios.delete(`api/alerts/${data.id}`),
  mute: data => axios.post(`api/alerts/${data.id}/mute`),
  unmute: data => axios.delete(`api/alerts/${data.id}/mute`),
};

export default Alert;


<|start_filename|>client/app/services/auth.test.js<|end_filename|>
import { currentUser } from "./auth";

describe("currentUser", () => {
  describe("currentUser.isAdmin", () => {
    it("returns state based on permission", () => {
      currentUser.permissions = ["admin"];

      expect(currentUser.isAdmin).toBeTruthy();

      currentUser.permissions = [];

      expect(currentUser.isAdmin).toBeFalsy();
    });

    it("allows setting admin status explicitly", () => {
      currentUser.permissions = [];
      currentUser.isAdmin = true;
      expect(currentUser.isAdmin).toBeTruthy();
      currentUser.permissions = ["admin"];
      currentUser.isAdmin = true;
      expect(currentUser.isAdmin).toBeTruthy();
      currentUser.permissions = ["admin"];
      currentUser.isAdmin = false;
      expect(currentUser.isAdmin).toBeFalsy();
      currentUser.permissions = [];
      currentUser.isAdmin = false;
      expect(currentUser.isAdmin).toBeFalsy();
    });
  });

  describe("currentUser.hasPermission", () => {
    it("let's override admin status", () => {
      currentUser.permissions = [""];
      currentUser.isAdmin = true;
      expect(currentUser.hasPermission("admin")).toBeTruthy();
      currentUser.permissions = [""];
      currentUser.isAdmin = false;
      expect(currentUser.hasPermission("admin")).toBeFalsy();
    });
  });
});


<|start_filename|>client/cypress/integration/user/create_user_spec.js<|end_filename|>
describe("Create User", () => {
  beforeEach(() => {
    cy.login();
    cy.visit("/users/new");
  });

  const fillUserFormAndSubmit = (name, email) => {
    cy.getByTestId("CreateUserDialog").within(() => {
      cy.getByTestId("Name").type(name);
      cy.getByTestId("Email").type(email);
    });
    cy.getByTestId("SaveUserButton").click();
  };

  it("creates a new user", () => {
    // delete existing "<EMAIL>"
    cy.request("GET", "api/users?q=new-user")
      .then(({ body }) => body.results.filter(user => user.email === "<EMAIL>"))
      .each(user => cy.request("DELETE", `api/users/${user.id}`));

    fillUserFormAndSubmit("New User", "<EMAIL>");

    cy.getByTestId("CreateUserErrorAlert").should("contain", "Email already taken");

    fillUserFormAndSubmit("{selectall}New User", "{selectall}<EMAIL>");
    cy.contains("Saved.");
  });
});


<|start_filename|>client/cypress/integration/alert/view_alert_spec.js<|end_filename|>
describe("View Alert", () => {
  beforeEach(function() {
    cy.login().then(() => {
      cy.createQuery({ query: "select 1 as col_name" })
        .then(({ id: queryId }) => cy.createAlert(queryId, { column: "col_name" }))
        .then(({ id: alertId }) => {
          this.alertId = alertId;
          this.alertUrl = `/alerts/${alertId}`;
        });
    });
  });

  it("renders the page and takes a screenshot", function() {
    cy.visit(this.alertUrl);
    cy.getByTestId("Criteria").should("exist");
    cy.percySnapshot("View Alert screen");
  });

  it("allows adding new destinations", function() {
    cy.visit(this.alertUrl);
    cy.getByTestId("AlertDestinations")
      .contains("Test Email Destination")
      .should("not.exist");

    cy.server();
    cy.route("GET", "**/api/destinations").as("Destinations");
    cy.route("GET", "**/api/alerts/*/subscriptions").as("Subscriptions");

    cy.visit(this.alertUrl);

    cy.wait(["@Destinations", "@Subscriptions"]);
    cy.getByTestId("ShowAddAlertSubDialog").click();
    cy.contains("Test Email Destination").click();
    cy.contains("Save").click();

    cy.getByTestId("AlertDestinations")
      .contains("Test Email Destination")
      .should("exist");
  });

  describe("Alert Destination permissions", () => {
    before(() => {
      cy.login();
      cy.createUser({
        name: "Example User",
        email: "<EMAIL>",
        password: "password",
      });
    });

    it("hides remove button from non-author", function() {
      cy.server();
      cy.route("GET", "**/api/alerts/*/subscriptions").as("Subscriptions");

      cy.logout()
        .then(() => cy.login()) // as admin
        .then(() => cy.addDestinationSubscription(this.alertId, "Test Email Destination"))
        .then(() => {
          cy.visit(this.alertUrl);

          // verify remove button appears for author
          cy.wait(["@Subscriptions"]);
          cy.getByTestId("AlertDestinations")
            .contains("Test Email Destination")
            .parent()
            .within(() => {
              cy.get(".remove-button")
                .as("RemoveButton")
                .should("exist");
            });

          return cy.logout().then(() => cy.login("<EMAIL>", "password"));
        })
        .then(() => {
          cy.visit(this.alertUrl);

          // verify remove button not shown for non-author
          cy.wait(["@Subscriptions"]);
          cy.get("@RemoveButton").should("not.exist");
        });
    });

    it("shows remove button for non-author admin", function() {
      cy.server();
      cy.route("GET", "**/api/alerts/*/subscriptions").as("Subscriptions");

      cy.logout()
        .then(() => cy.login("<EMAIL>", "password"))
        .then(() => cy.addDestinationSubscription(this.alertId, "Test Email Destination"))
        .then(() => {
          cy.visit(this.alertUrl);

          // verify remove button appears for author
          cy.wait(["@Subscriptions"]);
          cy.getByTestId("AlertDestinations")
            .contains("Test Email Destination")
            .parent()
            .within(() => {
              cy.get(".remove-button")
                .as("RemoveButton")
                .should("exist");
            });

          return cy.logout().then(() => cy.login()); // as admin
        })
        .then(() => {
          cy.visit(this.alertUrl);

          // verify remove button also appears for admin
          cy.wait(["@Subscriptions"]);
          cy.get("@RemoveButton").should("exist");
        });
    });
  });
});


<|start_filename|>client/app/pages/queries/hooks/useAutoLimitFlags.js<|end_filename|>
import { useCallback, useState } from "react";
import localOptions from "@/lib/localOptions";
import { get, extend } from "lodash";

function isAutoLimitAvailable(dataSource) {
  return get(dataSource, "supports_auto_limit", false);
}

export default function useAutoLimitFlags(dataSource, query, setQuery) {
  const isAvailable = isAutoLimitAvailable(dataSource);
  const [isChecked, setIsChecked] = useState(query.options.apply_auto_limit);
  query.options.apply_auto_limit = isChecked;

  const setAutoLimit = useCallback(
    state => {
      setIsChecked(state);
      localOptions.set("applyAutoLimit", state);
      setQuery(extend(query.clone(), { options: { ...query.options, apply_auto_limit: state } }));
    },
    [query, setQuery]
  );

  return [isAvailable, isChecked, setAutoLimit];
}


<|start_filename|>client/app/services/parameters/tests/TextParameter.test.js<|end_filename|>
import { createParameter } from "..";

describe("TextParameter", () => {
  let param;

  beforeEach(() => {
    param = createParameter({ name: "param", title: "Param", type: "text" });
  });

  describe("normalizeValue", () => {
    test("converts Strings", () => {
      const normalizedValue = param.normalizeValue("exampleString");
      expect(normalizedValue).toBe("exampleString");
    });

    test("converts Numbers", () => {
      const normalizedValue = param.normalizeValue(3);
      expect(normalizedValue).toBe("3");
    });

    describe("Empty values", () => {
      const emptyValues = [null, undefined, ""];

      test.each(emptyValues)("normalizes empty value '%s' as null", emptyValue => {
        const normalizedValue = param.normalizeValue(emptyValue);
        expect(normalizedValue).toBeNull();
      });
    });
  });
});


<|start_filename|>client/app/pages/queries/hooks/useUnsavedChangesAlert.js<|end_filename|>
import { useRef, useEffect } from "react";
import location from "@/services/location";

export default function useUnsavedChangesAlert(shouldShowAlert = false) {
  const shouldShowAlertRef = useRef();
  shouldShowAlertRef.current = shouldShowAlert;

  useEffect(() => {
    const unloadMessage = "You will lose your changes if you leave";
    const confirmMessage = `${unloadMessage}\n\nAre you sure you want to leave this page?`;
    // store original handler (if any)
    const savedOnBeforeUnload = window.onbeforeunload;

    window.onbeforeunload = function onbeforeunload() {
      return shouldShowAlertRef.current ? unloadMessage : undefined;
    };

    const unsubscribe = location.confirmChange((nextLocation, currentLocation) => {
      if (shouldShowAlertRef.current && nextLocation.path !== currentLocation.path) {
        return confirmMessage;
      }
    });

    return () => {
      window.onbeforeunload = savedOnBeforeUnload;
      unsubscribe();
    };
  }, []);
}


<|start_filename|>client/app/pages/queries-list/queries-list.css<|end_filename|>
.search input[type="text"],
.search button {
  height: 35px;
}

.page-queries-list .page-header-actions {
  width: 25%; /* same as sidebar */
  max-width: 350px; /* same as sidebar */
}

/* same rule as for sidebar */
@media (max-width: 990px) {
  .page-queries-list .page-header-actions {
    width: auto;
  }
}


<|start_filename|>client/app/pages/queries/hooks/useQueryFlags.js<|end_filename|>
import { isNil, isEmpty } from "lodash";
import { useMemo } from "react";
import { currentUser } from "@/services/auth";
import { policy } from "@/services/policy";

export default function useQueryFlags(query, dataSource = null) {
  dataSource = dataSource || { view_only: true };

  return useMemo(
    () => ({
      // state flags
      isNew: isNil(query.id),
      isDraft: query.is_draft,
      isArchived: query.is_archived,

      // permissions flags
      canCreate: currentUser.hasPermission("create_query"),
      canView: currentUser.hasPermission("view_query"),
      canEdit: currentUser.hasPermission("edit_query") && policy.canEdit(query),
      canViewSource: currentUser.hasPermission("view_source"),
      canExecute:
        !isEmpty(query.query) &&
        policy.canRun(query) &&
        (query.is_safe || (currentUser.hasPermission("execute_query") && !dataSource.view_only)),
      canFork: currentUser.hasPermission("edit_query") && !dataSource.view_only,
      canSchedule: currentUser.hasPermission("schedule_query"),
    }),
    [query, dataSource.view_only]
  );
}


<|start_filename|>client/app/pages/queries/hooks/useApiKeyDialog.js<|end_filename|>
import { useCallback } from "react";
import ApiKeyDialog from "@/components/queries/ApiKeyDialog";
import useImmutableCallback from "@/lib/hooks/useImmutableCallback";

export default function useApiKeyDialog(query, onChange) {
  const handleChange = useImmutableCallback(onChange);

  return useCallback(() => {
    ApiKeyDialog.showModal({ query }).onClose(handleChange);
  }, [query, handleChange]);
}


<|start_filename|>client/app/pages/queries/hooks/useQuery.js<|end_filename|>
import { isEmpty } from "lodash";
import { useState, useMemo } from "react";
import useUpdateQuery from "./useUpdateQuery";
import navigateTo from "@/components/ApplicationArea/navigateTo";

export default function useQuery(originalQuery) {
  const [query, setQuery] = useState(originalQuery);
  const [originalQuerySource, setOriginalQuerySource] = useState(originalQuery.query);
  const [originalAutoLimit, setOriginalAutoLimit] = useState(query.options.apply_auto_limit);

  const updateQuery = useUpdateQuery(query, updatedQuery => {
    // It's important to update URL first, and only then update state
    if (updatedQuery.id !== query.id) {
      // Don't reload page when saving new query
      navigateTo(updatedQuery.getUrl(true), true);
    }
    setQuery(updatedQuery);
    setOriginalQuerySource(updatedQuery.query);
    setOriginalAutoLimit(updatedQuery.options.apply_auto_limit);
  });

  return useMemo(
    () => ({
      query,
      setQuery,
      isDirty:
        query.query !== originalQuerySource ||
        (!isEmpty(query.query) && query.options.apply_auto_limit !== originalAutoLimit),
      saveQuery: () => updateQuery(),
    }),
    [query, originalQuerySource, updateQuery, originalAutoLimit]
  );
}


<|start_filename|>client/app/components/ApplicationArea/navigateTo.js<|end_filename|>
import location from "@/services/location";
import url from "@/services/url";
import { stripBase } from "./Router";

// When `replace` is set to `true` - it will just replace current URL
// without reloading current page (router will skip this location change)
export default function navigateTo(href, replace = false) {
  // Allow calling chain to roll up, and then navigate
  setTimeout(() => {
    const isExternal = stripBase(href) === false;
    if (isExternal) {
      window.location = href;
      return;
    }
    href = url.parse(href);
    location.update(
      {
        path: href.pathname,
        search: href.search,
        hash: href.hash,
      },
      replace
    );
  }, 10);
}


<|start_filename|>client/app/components/dashboards/dashboard-widget/index.js<|end_filename|>
export { default as VisualizationWidget } from "./VisualizationWidget";
export { default as TextboxWidget } from "./TextboxWidget";
export { default as RestrictedWidget } from "./RestrictedWidget";


<|start_filename|>client/cypress/support/tags/index.js<|end_filename|>
export function expectTagsToContain(tags = []) {
  cy.getByTestId("TagsControl").within(() => {
    cy.getByTestId("TagLabel")
      .should("have.length", tags.length)
      .each($tag => expect(tags).to.contain($tag.text()));
  });
}

export function typeInTagsSelectAndSave(text) {
  cy.getByTestId("EditTagsDialog").within(() => {
    cy.get(".ant-select")
      .find("input")
      .type(text, { force: true });

    cy.get(".ant-modal-header").click(); // hide dropdown options
    cy.contains("OK").click();
  });
}


<|start_filename|>client/cypress/integration/visualizations/counter_spec.js<|end_filename|>
/* global cy, Cypress */

const SQL = `
  SELECT 27182.8182846 AS a, 20000 AS b, 'lorem' AS c UNION ALL
  SELECT 31415.9265359 AS a, 40000 AS b, 'ipsum' AS c
`;

describe("Counter", () => {
  const viewportWidth = Cypress.config("viewportWidth");

  beforeEach(() => {
    cy.login();
    cy.createQuery({ query: SQL }).then(({ id }) => {
      cy.visit(`queries/${id}/source`);
      cy.getByTestId("ExecuteButton").click();
    });
    cy.getByTestId("NewVisualization").click();
    cy.getByTestId("VisualizationType").selectAntdOption("VisualizationType.COUNTER");
  });

  it("creates simple Counter", () => {
    cy.clickThrough(`
      Counter.General.ValueColumn
      Counter.General.ValueColumn.a
    `);

    cy.getByTestId("VisualizationPreview")
      .find(".counter-visualization-container")
      .should("exist");

    // wait a bit before taking snapshot
    cy.wait(500); // eslint-disable-line cypress/no-unnecessary-waiting
    cy.percySnapshot("Visualizations - Counter (with defaults)", { widths: [viewportWidth] });
  });

  it("creates Counter with custom label", () => {
    cy.clickThrough(`
      Counter.General.ValueColumn
      Counter.General.ValueColumn.a
    `);

    cy.fillInputs({
      "Counter.General.Label": "Custom Label",
    });

    cy.getByTestId("VisualizationPreview")
      .find(".counter-visualization-container")
      .should("exist");

    // wait a bit before taking snapshot
    cy.wait(500); // eslint-disable-line cypress/no-unnecessary-waiting
    cy.percySnapshot("Visualizations - Counter (custom label)", { widths: [viewportWidth] });
  });

  it("creates Counter with non-numeric value", () => {
    cy.clickThrough(`
      Counter.General.ValueColumn
      Counter.General.ValueColumn.c

      Counter.General.TargetValueColumn
      Counter.General.TargetValueColumn.c
    `);

    cy.fillInputs({
      "Counter.General.TargetValueRowNumber": "2",
    });

    cy.getByTestId("VisualizationPreview")
      .find(".counter-visualization-container")
      .should("exist");

    // wait a bit before taking snapshot
    cy.wait(500); // eslint-disable-line cypress/no-unnecessary-waiting
    cy.percySnapshot("Visualizations - Counter (non-numeric value)", { widths: [viewportWidth] });
  });

  it("creates Counter with target value (trend positive)", () => {
    cy.clickThrough(`
      Counter.General.ValueColumn
      Counter.General.ValueColumn.a

      Counter.General.TargetValueColumn
      Counter.General.TargetValueColumn.b
    `);

    cy.getByTestId("VisualizationPreview")
      .find(".counter-visualization-container")
      .should("exist");

    // wait a bit before taking snapshot
    cy.wait(500); // eslint-disable-line cypress/no-unnecessary-waiting
    cy.percySnapshot("Visualizations - Counter (target value + trend positive)", { widths: [viewportWidth] });
  });

  it("creates Counter with custom row number (trend negative)", () => {
    cy.clickThrough(`
      Counter.General.ValueColumn
      Counter.General.ValueColumn.a

      Counter.General.TargetValueColumn
      Counter.General.TargetValueColumn.b
    `);

    cy.fillInputs({
      "Counter.General.ValueRowNumber": "2",
      "Counter.General.TargetValueRowNumber": "2",
    });

    cy.getByTestId("VisualizationPreview")
      .find(".counter-visualization-container")
      .should("exist");

    // wait a bit before taking snapshot
    cy.wait(500); // eslint-disable-line cypress/no-unnecessary-waiting
    cy.percySnapshot("Visualizations - Counter (row number + trend negative)", { widths: [viewportWidth] });
  });

  it("creates Counter with count rows", () => {
    cy.clickThrough(`
      Counter.General.ValueColumn
      Counter.General.ValueColumn.a

      Counter.General.CountRows
    `);

    cy.getByTestId("VisualizationPreview")
      .find(".counter-visualization-container")
      .should("exist");

    // wait a bit before taking snapshot
    cy.wait(500); // eslint-disable-line cypress/no-unnecessary-waiting
    cy.percySnapshot("Visualizations - Counter (count rows)", { widths: [viewportWidth] });
  });

  it("creates Counter with formatting", () => {
    cy.clickThrough(`
      Counter.General.ValueColumn
      Counter.General.ValueColumn.a

      Counter.General.TargetValueColumn
      Counter.General.TargetValueColumn.b

      VisualizationEditor.Tabs.Format
    `);

    cy.fillInputs({
      "Counter.Formatting.DecimalPlace": "4",
      "Counter.Formatting.DecimalCharacter": ",",
      "Counter.Formatting.ThousandsSeparator": "`",
      "Counter.Formatting.StringPrefix": "$",
      "Counter.Formatting.StringSuffix": "%",
    });

    cy.getByTestId("VisualizationPreview")
      .find(".counter-visualization-container")
      .should("exist");

    // wait a bit before taking snapshot
    cy.wait(500); // eslint-disable-line cypress/no-unnecessary-waiting
    cy.percySnapshot("Visualizations - Counter (custom formatting)", { widths: [viewportWidth] });
  });

  it("creates Counter with target value formatting", () => {
    cy.clickThrough(`
      Counter.General.ValueColumn
      Counter.General.ValueColumn.a

      Counter.General.TargetValueColumn
      Counter.General.TargetValueColumn.b

      VisualizationEditor.Tabs.Format
      Counter.Formatting.FormatTargetValue
    `);

    cy.fillInputs({
      "Counter.Formatting.DecimalPlace": "4",
      "Counter.Formatting.DecimalCharacter": ",",
      "Counter.Formatting.ThousandsSeparator": "`",
      "Counter.Formatting.StringPrefix": "$",
      "Counter.Formatting.StringSuffix": "%",
    });

    cy.getByTestId("VisualizationPreview")
      .find(".counter-visualization-container")
      .should("exist");

    // wait a bit before taking snapshot
    cy.wait(500); // eslint-disable-line cypress/no-unnecessary-waiting
    cy.percySnapshot("Visualizations - Counter (format target value)", { widths: [viewportWidth] });
  });
});


<|start_filename|>client/app/pages/queries/hooks/useQueryParameters.js<|end_filename|>
import { isUndefined } from "lodash";
import { useEffect, useMemo, useState, useCallback } from "react";

export default function useQueryParameters(query) {
  const parameters = useMemo(() => query.getParametersDefs(), [query]);
  const [dirtyFlag, setDirtyFlag] = useState(query.getParameters().hasPendingValues());

  const updateDirtyFlag = useCallback(
    flag => {
      flag = isUndefined(flag) ? query.getParameters().hasPendingValues() : flag;
      setDirtyFlag(flag);
    },
    [query]
  );

  useEffect(() => {
    const updatedDirtyParameters = query.getParameters().hasPendingValues();
    if (updatedDirtyParameters !== dirtyFlag) {
      setDirtyFlag(updatedDirtyParameters);
    }
  }, [query, parameters, dirtyFlag]);

  return useMemo(() => [parameters, dirtyFlag, updateDirtyFlag], [parameters, dirtyFlag, updateDirtyFlag]);
}


<|start_filename|>client/app/services/notification.js<|end_filename|>
import notification from "antd/lib/notification";

notification.config({
  placement: "bottomRight",
  duration: 3,
});

const simpleNotification = {};

["success", "error", "info", "warning", "warn"].forEach(action => {
  // eslint-disable-next-line arrow-body-style
  simpleNotification[action] = (message, description = null, props = null) => {
    return notification[action]({ ...props, message, description });
  };
});

export default {
  // export Ant's notification and replace actions
  ...notification,
  ...simpleNotification,
};


<|start_filename|>client/app/services/parameters/tests/DateRangeParameter.test.js<|end_filename|>
import { createParameter } from "..";
import { getDynamicDateRangeFromString } from "../DateRangeParameter";
import moment from "moment";

describe("DateRangeParameter", () => {
  let type = "date-range";
  let param;

  beforeEach(() => {
    param = createParameter({ name: "param", title: "Param", type });
  });

  describe("getExecutionValue", () => {
    beforeEach(() => {
      param.setValue({ start: "2019-10-05 10:00:00", end: "2019-10-06 09:59:59" });
    });

    test("formats value as a string date", () => {
      const executionValue = param.getExecutionValue();
      expect(executionValue).toEqual({ start: "2019-10-05", end: "2019-10-06" });
    });

    describe("type is datetime-range", () => {
      beforeAll(() => {
        type = "datetime-range";
      });

      test("formats value as a string datetime", () => {
        const executionValue = param.getExecutionValue();
        expect(executionValue).toEqual({ start: "2019-10-05 10:00", end: "2019-10-06 09:59" });
      });
    });

    describe("type is datetime-range-with-seconds", () => {
      beforeAll(() => {
        type = "datetime-range-with-seconds";
      });

      test("formats value as a string datetime with seconds", () => {
        const executionValue = param.getExecutionValue();
        expect(executionValue).toEqual({ start: "2019-10-05 10:00:00", end: "2019-10-06 09:59:59" });
      });
    });
  });

  describe("normalizeValue", () => {
    test("recognizes dates from moment arrays", () => {
      const normalizedValue = param.normalizeValue([moment("2019-10-05"), moment("2019-10-06")]);
      expect(normalizedValue).toHaveLength(2);
      expect(normalizedValue[0].format("YYYY-MM-DD")).toBe("2019-10-05");
      expect(normalizedValue[1].format("YYYY-MM-DD")).toBe("2019-10-06");
    });

    test("recognizes dates from object", () => {
      const normalizedValue = param.normalizeValue({ start: "2019-10-05", end: "2019-10-06" });
      expect(normalizedValue).toHaveLength(2);
      expect(normalizedValue[0].format("YYYY-MM-DD")).toBe("2019-10-05");
      expect(normalizedValue[1].format("YYYY-MM-DD")).toBe("2019-10-06");
    });

    describe("Dynamic values", () => {
      test("recognizes dynamic values from string index", () => {
        const normalizedValue = param.normalizeValue("d_last_week");
        expect(normalizedValue).not.toBeNull();
        expect(normalizedValue).toEqual(getDynamicDateRangeFromString("d_last_week"));
      });

      test("recognizes dynamic values from a dynamic date range", () => {
        const dynamicDateRange = getDynamicDateRangeFromString("d_last_week");
        const normalizedValue = param.normalizeValue(dynamicDateRange);
        expect(normalizedValue).not.toBeNull();
        expect(normalizedValue).toEqual(dynamicDateRange);
      });
    });
  });
});


<|start_filename|>viz-lib/__tests__/mocks.js<|end_filename|>
import MockDate from "mockdate";

const date = new Date("2000-01-01T02:00:00.000");

MockDate.set(date);

Object.defineProperty(window, "matchMedia", {
  writable: true,
  value: jest.fn().mockImplementation(query => ({
    matches: false,
    media: query,
    onchange: null,
    addListener: jest.fn(), // deprecated
    removeListener: jest.fn(), // deprecated
    addEventListener: jest.fn(),
    removeEventListener: jest.fn(),
    dispatchEvent: jest.fn(),
  })),
});


<|start_filename|>client/cypress/integration/group/group_list_spec.js<|end_filename|>
describe("Group List", () => {
  beforeEach(() => {
    cy.login();
    cy.visit("/groups");
  });

  it("renders the page and takes a screenshot", () => {
    cy.getByTestId("GroupList")
      .should("exist")
      .and("contain", "admin")
      .and("contain", "default");

    cy.percySnapshot("Groups");
  });
});


<|start_filename|>client/app/lib/localOptions.js<|end_filename|>
const PREFIX = "localOptions:";

function get(key, defaultValue = undefined) {
  const fullKey = PREFIX + key;
  if (fullKey in window.localStorage) {
    return JSON.parse(window.localStorage.getItem(fullKey));
  }
  return defaultValue;
}

function set(key, value) {
  const fullKey = PREFIX + key;
  window.localStorage.setItem(fullKey, JSON.stringify(value));
}

export default {
  get,
  set,
};


<|start_filename|>redash/templates/invite.html<|end_filename|>
{% extends "layouts/signed_out.html" %}

{% block title %}Welcome to Redash!{% endblock %}

{% block content %}
<div class="fixed-width-page">
  <div class="bg-white tiled">
    <div class="m-b-25">
      {% if show_google_openid or show_saml_login or show_remote_user_login or show_ldap_login %}
        To create your account, please choose a password or login with your SSO provider.
      {% else %}
        To create your account, please choose a password.
      {% endif %}
    </div>

    {% with messages = get_flashed_messages() %}
      {% if messages %}
        {% for message in messages %}
          <div class="alert alert-danger" role="alert">{{ message }}</div>
        {% endfor %}
      {% endif %}
    {% endwith %}

    {% if show_google_openid %}
      <a href="{{ google_auth_url }}" class="login-button btn btn-default btn-block">
        <img src="/static/images/google_logo.svg">
        Login with Google
      </a>
    {% endif %}

    {% if show_saml_login %}
      <a href="{{ url_for('saml_auth.sp_initiated', org_slug=org_slug) }}" class="login-button btn btn-default btn-block">SAML Login</a>
    {% endif %}

    {% if show_remote_user_login %}
      <a href="{{ url_for('remote_user_auth.login') }}" class="login-button btn btn-default btn-block">Remote User Login</a>
    {% endif %}

    {% if show_ldap_login %}
      <a href="{{ url_for('ldap_auth.login') }}" class="login-button btn btn-default btn-block">LDAP/SSO Login</a>
    {% endif %}

    {% if show_google_openid or show_saml_login or show_remote_user_login or show_ldap_login %}
      <hr>
    {% endif %}

    <form role="form" method="post" name="invite">
      <input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
      <div class="form-group">
        <label for="password">Password</label>
        <input type="password" class="form-control" id="password" name="password">
      </div>
      <button type="submit" class="btn btn-primary btn-block m-t-25">Set Password</button>
    </form>
  </div>
</div>
{% endblock %}


<|start_filename|>redash/templates/emails/failures.html<|end_filename|>
<html>

<head>
    <style>
        body {
            margin: 0;
        }
    </style>
</head>

<body style="margin: 0;">
    <div
        style="background: #f6f8f9; font-family: arial; font-size: 14px; padding: 20px; color: #333; line-height: 20px">
        <h1 style="font-size: 14px; font-weight: normal;">Redash failed to run the following queries:</h1>

        {% for failure in failures %}
		    <div style="background: white;padding: 5px 20px 20px 20px; border-radius: 3px; box-shadow: 0 4px 9px -3px rgba(102,136,153,.15); margin-top: 20px">
		    	<h2 style="font-size: 16px; position: relative; padding-right: 90px;">{{failure.name}} <a href="{{base_url}}/queries/{{failure.id}}" style="background-color: #ff7964;font-size: 13px;color:  white;text-decoration: none;padding: 0 5px;display: inline-block;border-radius: 3px;font-weight: normal;position: absolute;right: 0;top:-1px">Go
		    			to Query</a></h2>
		    	<div>
		    		<p>
		    			Last failed: {{failure.failed_at}}
                        {% if failure.failure_count > 1 %}
                            <br />
                            <small style="color: #8c8c8c;font-size: 13px;">&nbsp; + {{failure.failure_count - 1}} more failures since last report</small>
                        {% endif %}
		    		</p>
		    		<h3 style="font-size: 14px; font-weight:normal; margin-bottom: 6px">Exception</h3>
		    		<div style="background: #ececec;padding: 8px;border-radius: 3px;font-family: monospace; word-break: break-all;">{{failure.failure_reason}}</div>
                </div>

                {% if failure.comment %}
                    <div style="margin-top: 25px;font-size: 13px;border-top: 1px solid #ececec;padding-top: 19px;">{{failure.comment}}</div>
                {% endif %}
            </div>
        {% endfor %}

    </div>
</body>

</html>


<|start_filename|>client/app/lib/hooks/useFullscreenHandler.js<|end_filename|>
import { has } from "lodash";
import { useEffect, useState } from "react";
import location from "@/services/location";

export default function useFullscreenHandler() {
  const [fullscreen, setFullscreen] = useState(has(location.search, "fullscreen"));
  useEffect(() => {
    document.body.classList.toggle("headless", fullscreen);
    location.setSearch({ fullscreen: fullscreen ? true : null }, true);
  }, [fullscreen]);

  const toggleFullscreen = () => setFullscreen(!fullscreen);
  return [fullscreen, toggleFullscreen];
}


<|start_filename|>client/cypress/support/visualizations/chart.js<|end_filename|>
/**
 * Asserts the preview canvas exists, then captures the g.points element, which should be generated by plotly and asserts whether it exists
 * @param should Passed to should expression after plot points are captured
 */
export function assertPlotPreview(should = "exist") {
  cy.getByTestId("VisualizationPreview")
    .find("g.plot")
    .should("exist")
    .find("g.points")
    .should(should);
}

export function createChartThroughUI(chartName, chartSpecificAssertionFn = () => {}) {
  cy.getByTestId("NewVisualization").click();
  cy.getByTestId("VisualizationType").selectAntdOption("VisualizationType.CHART");
  cy.getByTestId("VisualizationName")
    .clear()
    .type(chartName);

  chartSpecificAssertionFn();

  cy.server();
  cy.route("POST", "**/api/visualizations").as("SaveVisualization");

  cy.getByTestId("EditVisualizationDialog")
    .contains("button", "Save")
    .click();

  cy.getByTestId("QueryPageVisualizationTabs")
    .contains("span", chartName)
    .should("exist");

  cy.wait("@SaveVisualization").should("have.property", "status", 200);

  return cy.get("@SaveVisualization").then(xhr => {
    const { id, name, options } = xhr.response.body;
    return cy.wrap({ id, name, options });
  });
}

export function assertTabbedEditor(chartSpecificTabbedEditorAssertionFn = () => {}) {
  cy.getByTestId("Chart.GlobalSeriesType").should("exist");

  cy.getByTestId("VisualizationEditor.Tabs.Series").click();
  cy.getByTestId("VisualizationEditor")
    .find("table")
    .should("exist");

  cy.getByTestId("VisualizationEditor.Tabs.Colors").click();
  cy.getByTestId("VisualizationEditor")
    .find("table")
    .should("exist");

  cy.getByTestId("VisualizationEditor.Tabs.DataLabels").click();
  cy.getByTestId("VisualizationEditor")
    .getByTestId("Chart.DataLabels.ShowDataLabels")
    .should("exist");

  chartSpecificTabbedEditorAssertionFn();

  cy.getByTestId("VisualizationEditor.Tabs.General").click();
}

export function assertAxesAndAddLabels(xaxisLabel, yaxisLabel) {
  cy.getByTestId("VisualizationEditor.Tabs.XAxis").click();
  cy.getByTestId("Chart.XAxis.Type")
    .contains(".ant-select-selection-item", "Auto Detect")
    .should("exist");

  cy.getByTestId("Chart.XAxis.Name")
    .clear()
    .type(xaxisLabel);

  cy.getByTestId("VisualizationEditor.Tabs.YAxis").click();
  cy.getByTestId("Chart.LeftYAxis.Type")
    .contains(".ant-select-selection-item", "Linear")
    .should("exist");

  cy.getByTestId("Chart.LeftYAxis.Name")
    .clear()
    .type(yaxisLabel);

  cy.getByTestId("VisualizationEditor.Tabs.General").click();
}

export function createDashboardWithCharts(title, chartGetters, widgetsAssertionFn = () => {}) {
  cy.createDashboard(title).then(dashboard => {
    const dashboardUrl = `/dashboards/${dashboard.id}`;
    const widgetGetters = chartGetters.map(chartGetter => `${chartGetter}Widget`);

    chartGetters.forEach((chartGetter, i) => {
      const position = { autoHeight: false, sizeY: 8, sizeX: 3, col: (i % 2) * 3 };
      cy.get(`@${chartGetter}`)
        .then(chart => cy.addWidget(dashboard.id, chart.id, { position }))
        .as(widgetGetters[i]);
    });

    widgetsAssertionFn(widgetGetters, dashboardUrl);
  });
}


<|start_filename|>client/app/pages/queries/hooks/useVisualizationTabHandler.js<|end_filename|>
import { useState, useEffect, useMemo } from "react";
import { first, orderBy, find } from "lodash";
import location from "@/services/location";

export default function useVisualizationTabHandler(visualizations) {
  const firstVisualization = useMemo(() => first(orderBy(visualizations, ["id"])) || {}, [visualizations]);
  const [selectedTab, setSelectedTab] = useState(+location.hash || firstVisualization.id);

  useEffect(() => {
    const hashValue = selectedTab !== firstVisualization.id ? `${selectedTab}` : null;
    if (location.hash !== hashValue) {
      location.setHash(hashValue);
    }

    const unlisten = location.listen(() => {
      if (location.hash !== hashValue) {
        setSelectedTab(+location.hash || firstVisualization.id);
      }
    });
    return unlisten;
  }, [firstVisualization.id, selectedTab]);

  // make sure selectedTab is in visualizations
  useEffect(() => {
    if (!find(visualizations, { id: selectedTab })) {
      setSelectedTab(firstVisualization.id);
    }
  }, [firstVisualization.id, selectedTab, visualizations]);

  return useMemo(() => [selectedTab, setSelectedTab], [selectedTab]);
}


<|start_filename|>client/app/services/databricks-data-source.js<|end_filename|>
import { has } from "lodash";
import { axios } from "@/services/axios";
import DataSource from "@/services/data-source";
import { fetchDataFromJob } from "@/services/query-result";

function fetchDataFromJobOrReturnData(data) {
  return has(data, "job.id") ? fetchDataFromJob(data.job.id, 1000) : data;
}

function rejectErrorResponse(data) {
  return has(data, "error") ? Promise.reject(new Error(data.error.message)) : data;
}

export default {
  ...DataSource,
  getDatabases: ({ id }, refresh = false) => {
    const params = {};

    if (refresh) {
      params.refresh = true;
    }
    return axios
      .get(`api/databricks/databases/${id}`, { params })
      .then(fetchDataFromJobOrReturnData)
      .then(rejectErrorResponse);
  },
  getDatabaseTables: (data, databaseName, refresh = false) => {
    const params = {};

    if (refresh) {
      params.refresh = true;
    }
    return axios
      .get(`api/databricks/databases/${data.id}/${databaseName}/tables`, { params })
      .then(fetchDataFromJobOrReturnData)
      .then(rejectErrorResponse);
  },
  getTableColumns: (data, databaseName, tableName) =>
    axios
      .get(`api/databricks/databases/${data.id}/${databaseName}/columns/${tableName}`)
      .then(fetchDataFromJobOrReturnData)
      .then(rejectErrorResponse),
};


<|start_filename|>client/app/services/organizationStatus.js<|end_filename|>
import { axios } from "@/services/axios";

class OrganizationStatus {
  constructor() {
    this.objectCounters = {};
  }

  refresh() {
    return axios.get("api/organization/status").then(data => {
      this.objectCounters = data.object_counters;
      return this;
    });
  }
}

export default new OrganizationStatus();


<|start_filename|>client/app/services/parameters/tests/NumberParameter.test.js<|end_filename|>
import { createParameter } from "..";

describe("NumberParameter", () => {
  let param;

  beforeEach(() => {
    param = createParameter({ name: "param", title: "Param", type: "number" });
  });

  describe("normalizeValue", () => {
    test("converts Strings", () => {
      const normalizedValue = param.normalizeValue("15");
      expect(normalizedValue).toBe(15);
    });

    test("converts Numbers", () => {
      const normalizedValue = param.normalizeValue(42);
      expect(normalizedValue).toBe(42);
    });

    test("returns null when not possible to convert to number", () => {
      const normalizedValue = param.normalizeValue("notanumber");
      expect(normalizedValue).toBeNull();
    });
  });
});


<|start_filename|>client/app/pages/dashboards/hooks/useEditModeHandler.js<|end_filename|>
import { debounce, find, has, isMatch, map, pickBy } from "lodash";
import { useCallback, useEffect, useState } from "react";
import location from "@/services/location";
import notification from "@/services/notification";

export const DashboardStatusEnum = {
  SAVED: "saved",
  SAVING: "saving",
  SAVING_FAILED: "saving_failed",
};

function getChangedPositions(widgets, nextPositions = {}) {
  return pickBy(nextPositions, (nextPos, widgetId) => {
    const widget = find(widgets, { id: Number(widgetId) });
    const prevPos = widget.options.position;
    return !isMatch(prevPos, nextPos);
  });
}

export default function useEditModeHandler(canEditDashboard, widgets) {
  const [editingLayout, setEditingLayout] = useState(canEditDashboard && has(location.search, "edit"));
  const [dashboardStatus, setDashboardStatus] = useState(DashboardStatusEnum.SAVED);
  const [recentPositions, setRecentPositions] = useState([]);
  const [doneBtnClickedWhileSaving, setDoneBtnClickedWhileSaving] = useState(false);

  useEffect(() => {
    location.setSearch({ edit: editingLayout ? true : null }, true);
  }, [editingLayout]);

  useEffect(() => {
    if (doneBtnClickedWhileSaving && dashboardStatus === DashboardStatusEnum.SAVED) {
      setDoneBtnClickedWhileSaving(false);
      setEditingLayout(false);
    }
  }, [doneBtnClickedWhileSaving, dashboardStatus]);

  const saveDashboardLayout = useCallback(
    positions => {
      if (!canEditDashboard) {
        setDashboardStatus(DashboardStatusEnum.SAVED);
        return;
      }

      const changedPositions = getChangedPositions(widgets, positions);

      setDashboardStatus(DashboardStatusEnum.SAVING);
      setRecentPositions(positions);
      const saveChangedWidgets = map(changedPositions, (position, id) => {
        // find widget
        const widget = find(widgets, { id: Number(id) });

        // skip already deleted widget
        if (!widget) {
          return Promise.resolve();
        }

        return widget.save("options", { position });
      });

      return Promise.all(saveChangedWidgets)
        .then(() => setDashboardStatus(DashboardStatusEnum.SAVED))
        .catch(() => {
          setDashboardStatus(DashboardStatusEnum.SAVING_FAILED);
          notification.error("Error saving changes.");
        });
    },
    [canEditDashboard, widgets]
  );

  const saveDashboardLayoutDebounced = useCallback(
    (...args) => {
      setDashboardStatus(DashboardStatusEnum.SAVING);
      return debounce(() => saveDashboardLayout(...args), 2000)();
    },
    [saveDashboardLayout]
  );

  const retrySaveDashboardLayout = useCallback(() => saveDashboardLayout(recentPositions), [
    recentPositions,
    saveDashboardLayout,
  ]);

  const setEditing = useCallback(
    editing => {
      if (!editing && dashboardStatus !== DashboardStatusEnum.SAVED) {
        setDoneBtnClickedWhileSaving(true);
        return;
      }
      setEditingLayout(canEditDashboard && editing);
    },
    [dashboardStatus, canEditDashboard]
  );

  return {
    editingLayout: canEditDashboard && editingLayout,
    setEditingLayout: setEditing,
    saveDashboardLayout: editingLayout ? saveDashboardLayoutDebounced : saveDashboardLayout,
    retrySaveDashboardLayout,
    doneBtnClickedWhileSaving,
    dashboardStatus,
  };
}


<|start_filename|>client/app/pages/queries/hooks/useQueryDataSources.js<|end_filename|>
import { filter, find, toString } from "lodash";
import { useState, useMemo, useEffect } from "react";
import DataSource from "@/services/data-source";

export default function useQueryDataSources(query) {
  const [allDataSources, setAllDataSources] = useState([]);
  const [dataSourcesLoaded, setDataSourcesLoaded] = useState(false);
  const dataSources = useMemo(() => filter(allDataSources, ds => !ds.view_only || ds.id === query.data_source_id), [
    allDataSources,
    query.data_source_id,
  ]);
  const dataSource = useMemo(
    () => find(dataSources, ds => toString(ds.id) === toString(query.data_source_id)) || null,
    [query.data_source_id, dataSources]
  );

  useEffect(() => {
    let cancelDataSourceLoading = false;
    DataSource.query().then(data => {
      if (!cancelDataSourceLoading) {
        setDataSourcesLoaded(true);
        setAllDataSources(data);
      }
    });

    return () => {
      cancelDataSourceLoading = true;
    };
  }, []);

  return useMemo(() => ({ dataSourcesLoaded, dataSources, dataSource }), [dataSourcesLoaded, dataSources, dataSource]);
}


<|start_filename|>client/app/components/dynamic-form/fields/index.js<|end_filename|>
export { default as AceEditorField } from "./AceEditorField";
export { default as CheckboxField } from "./CheckboxField";
export { default as ContentField } from "./ContentField";
export { default as FileField } from "./FileField";
export { default as InputField } from "./InputField";
export { default as NumberField } from "./NumberField";
export { default as SelectField } from "./SelectField";
export { default as TextAreaField } from "./TextAreaField";


<|start_filename|>client/cypress/integration/settings/organization_settings_spec.js<|end_filename|>
describe("Settings", () => {
  beforeEach(() => {
    cy.login();
    cy.visit("/settings/general");
  });

  it("renders the page and takes a screenshot", () => {
    cy.getByTestId("OrganizationSettings").within(() => {
      cy.getByTestId("TimeFormatSelect").should("contain", "HH:mm");
    });

    cy.percySnapshot("Organization Settings");
  });

  it("can set date format setting", () => {
    cy.getByTestId("DateFormatSelect").click();
    cy.getByTestId("DateFormatSelect:YYYY-MM-DD").click();
    cy.getByTestId("OrganizationSettingsSaveButton").click();

    cy.createQuery({
      name: "test date format",
      query: "SELECT NOW()",
    }).then(({ id: queryId }) => {
      cy.visit(`/queries/${queryId}`);
      cy.findByText("Refresh Now").click();

      // "created at" field is formatted with the date format.
      cy.getByTestId("TableVisualization")
        .findAllByText(/\d{4}-\d{2}-\d{2}/)
        .should("exist");

      // set to a different format and expect a different result in the table
      cy.visit("/settings/general");
      cy.getByTestId("DateFormatSelect").click();
      cy.getByTestId("DateFormatSelect:MM/DD/YY").click();
      cy.getByTestId("OrganizationSettingsSaveButton").click();

      cy.visit(`/queries/${queryId}`);

      cy.getByTestId("TableVisualization")
        .findAllByText(/\d{2}\/\d{2}\/\d{2}/)
        .should("exist");
    });
  });
});


<|start_filename|>client/app/services/parameters/tests/Parameter.test.js<|end_filename|>
import {
  createParameter,
  TextParameter,
  NumberParameter,
  EnumParameter,
  QueryBasedDropdownParameter,
  DateParameter,
  DateRangeParameter,
} from "..";

describe("Parameter", () => {
  describe("create", () => {
    const parameterTypes = [
      ["text", TextParameter],
      ["number", NumberParameter],
      ["enum", EnumParameter],
      ["query", QueryBasedDropdownParameter],
      ["date", DateParameter],
      ["datetime-local", DateParameter],
      ["datetime-with-seconds", DateParameter],
      ["date-range", DateRangeParameter],
      ["datetime-range", DateRangeParameter],
      ["datetime-range-with-seconds", DateRangeParameter],
      [null, TextParameter],
    ];

    test.each(parameterTypes)("when type is '%s' creates a %p", (type, expectedClass) => {
      const parameter = createParameter({ name: "param", title: "Param", type });
      expect(parameter).toBeInstanceOf(expectedClass);
    });
  });
});


<|start_filename|>client/cypress/integration/user/login_spec.js<|end_filename|>
describe("Login", () => {
  beforeEach(() => {
    cy.visit("/login");
  });

  it("greets the user and take a screenshot", () => {
    cy.contains("h3", "Login to Redash");

    cy.wait(1000); // eslint-disable-line cypress/no-unnecessary-waiting
    cy.percySnapshot("Login");
  });

  it("shows message on failed login", () => {
    cy.getByTestId("Email").type("<EMAIL>");
    cy.getByTestId("Password").type("<PASSWORD>}");

    cy.getByTestId("ErrorMessage").should("contain", "Wrong email or password.");
  });

  it("navigates to homepage with successful login", () => {
    cy.getByTestId("Email").type("<EMAIL>");
    cy.getByTestId("Password").type("<PASSWORD>}");

    cy.title().should("eq", "Redash");
    cy.get(`img.profile__image_thumb[alt="Example Admin"]`).should("exist");

    cy.wait(1000); // eslint-disable-line cypress/no-unnecessary-waiting
    cy.percySnapshot("Homepage");
  });
});


<|start_filename|>client/cypress/integration/query/create_query_spec.js<|end_filename|>
describe("Create Query", () => {
  beforeEach(() => {
    cy.login();
    cy.visit("/queries/new");
  });

  it("executes and saves a query", () => {
    cy.clickThrough(`
      SelectDataSource
      SelectDataSource${Cypress.env("dataSourceId")}
    `);

    cy.getByTestId("QueryEditor")
      .get(".ace_text-input")
      .type("SELECT id, name FROM organizations{esc}", { force: true });

    cy.getByTestId("ExecuteButton")
      .should("be.enabled")
      .click();

    cy.getByTestId("TableVisualization").should("exist");
    cy.percySnapshot("Edit Query");

    cy.getByTestId("SaveButton").click();
    cy.url().should("match", /\/queries\/.+\/source/);
  });
});


<|start_filename|>client/app/pages/dashboards/hooks/useRefreshRateHandler.js<|end_filename|>
import { isNaN, max, min } from "lodash";
import { useEffect, useState, useMemo } from "react";
import location from "@/services/location";
import { policy } from "@/services/policy";
import useImmutableCallback from "@/lib/hooks/useImmutableCallback";

function getLimitedRefreshRate(refreshRate) {
  const allowedIntervals = policy.getDashboardRefreshIntervals();
  return max([30, min(allowedIntervals), refreshRate]);
}

function getRefreshRateFromUrl() {
  const refreshRate = parseFloat(location.search.refresh);
  return isNaN(refreshRate) ? null : getLimitedRefreshRate(refreshRate);
}

export default function useRefreshRateHandler(refreshDashboard) {
  const [refreshRate, setRefreshRate] = useState(getRefreshRateFromUrl());

  // `refreshDashboard` may change quite frequently (on every update of `dashboard` instance), but we
  // have to keep the same timer running, because timer will restart when re-creating, and instead of
  // running refresh every N seconds - it will run refresh every N seconds after last dashboard update
  // (which is not right obviously)
  const doRefreshDashboard = useImmutableCallback(refreshDashboard);

  // URL and timer should be updated only when `refreshRate` changes
  useEffect(() => {
    location.setSearch({ refresh: refreshRate || null }, true);
    if (refreshRate) {
      const refreshTimer = setInterval(doRefreshDashboard, refreshRate * 1000);
      return () => clearInterval(refreshTimer);
    }
  }, [refreshRate, doRefreshDashboard]);

  return useMemo(() => [refreshRate, rate => setRefreshRate(getLimitedRefreshRate(rate)), () => setRefreshRate(null)], [
    refreshRate,
  ]);
}


<|start_filename|>client/cypress/integration/visualizations/table/.mocks/large-dataset.js<|end_filename|>
const loremIpsum =
  "Lorem ipsum dolor sit amet consectetur adipiscing elit" +
  "sed do eiusmod tempor incididunt ut labore et dolore magna aliqua";

function pseudoRandom(seed) {
  const x = Math.sin(seed) * 10000;
  return x - Math.floor(x);
}

function randomString(index) {
  const n = pseudoRandom(index);
  const offset = Math.floor(n * loremIpsum.length);
  const length = Math.floor(n * 15) + 1;
  return loremIpsum.substr(offset, length).trim();
}

export const query = new Array(400)
  .fill(null) // we actually don't need these values, but `.map()` ignores undefined elements
  .map((unused, index) => `SELECT ${index} AS a, '${randomString(index)}' as b`)
  .join(" UNION ALL\n");

export const config = {
  itemsPerPage: 10,
  columns: [
    {
      name: "a",
      displayAs: "number",
      numberFormat: "0",
    },
    {
      name: "b",
      displayAs: "string",
    },
  ],
};


<|start_filename|>client/app/services/visualization.js<|end_filename|>
import { axios } from "@/services/axios";

const saveOrCreateUrl = data => (data.id ? `api/visualizations/${data.id}` : "api/visualizations");

const Visualization = {
  save: data => axios.post(saveOrCreateUrl(data), data),
  delete: data => axios.delete(`api/visualizations/${data.id}`),
};

export default Visualization;


<|start_filename|>client/app/components/ApplicationArea/ErrorMessage.test.js<|end_filename|>
import React from "react";
import { mount } from "enzyme";
import ErrorMessage from "./ErrorMessage";

const ErrorMessages = {
  UNAUTHORIZED: "It seems like you don’t have permission to see this page.",
  NOT_FOUND: "It seems like the page you're looking for cannot be found.",
  GENERIC: "It seems like we encountered an error. Try refreshing this page or contact your administrator.",
};

function mockAxiosError(status = 500, response = {}) {
  const error = new Error(`Failed with code ${status}.`);
  error.isAxiosError = true;
  error.response = { status, ...response };
  return error;
}

describe("Error Message", () => {
  const spyError = jest.spyOn(console, "error");

  beforeEach(() => {
    spyError.mockReset();
  });

  function expectErrorMessageToBe(error, errorMessage) {
    const component = mount(<ErrorMessage error={error} />);

    expect(component.find(".error-state__details h4").text()).toBe(errorMessage);
    expect(spyError).toHaveBeenCalledWith(error);
  }

  test("displays a generic message on adhoc errors", () => {
    expectErrorMessageToBe(new Error("technical information"), ErrorMessages.GENERIC);
  });

  test("displays a not found message on axios errors with 404 code", () => {
    expectErrorMessageToBe(mockAxiosError(404), ErrorMessages.NOT_FOUND);
  });

  test("displays a unauthorized message on axios errors with 401 code", () => {
    expectErrorMessageToBe(mockAxiosError(401), ErrorMessages.UNAUTHORIZED);
  });

  test("displays a unauthorized message on axios errors with 403 code", () => {
    expectErrorMessageToBe(mockAxiosError(403), ErrorMessages.UNAUTHORIZED);
  });

  test("displays a generic message on axios errors with 500 code", () => {
    expectErrorMessageToBe(mockAxiosError(500), ErrorMessages.GENERIC);
  });
});


<|start_filename|>client/cypress/integration/visualizations/chart_spec.js<|end_filename|>
/* global cy */

import { getWidgetTestId } from "../../support/dashboard";
import {
  assertAxesAndAddLabels,
  assertPlotPreview,
  assertTabbedEditor,
  createChartThroughUI,
  createDashboardWithCharts,
} from "../../support/visualizations/chart";

const SQL = `
  SELECT 'a' AS stage, 11 AS value1, 22 AS value2 UNION ALL
  SELECT 'a' AS stage, 12 AS value1, 41 AS value2 UNION ALL
  SELECT 'a' AS stage, 45 AS value1, 93 AS value2 UNION ALL
  SELECT 'a' AS stage, 54 AS value1, 79 AS value2 UNION ALL
  SELECT 'b' AS stage, 33 AS value1, 65 AS value2 UNION ALL
  SELECT 'b' AS stage, 73 AS value1, 50 AS value2 UNION ALL
  SELECT 'b' AS stage, 90 AS value1, 40 AS value2 UNION ALL
  SELECT 'c' AS stage, 19 AS value1, 33 AS value2 UNION ALL
  SELECT 'c' AS stage, 92 AS value1, 14 AS value2 UNION ALL
  SELECT 'c' AS stage, 63 AS value1, 65 AS value2 UNION ALL
  SELECT 'c' AS stage, 44 AS value1, 27 AS value2\
`;

describe("Chart", () => {
  beforeEach(() => {
    cy.login();
    cy.createQuery({ name: "Chart Visualization", query: SQL })
      .its("id")
      .as("queryId");
  });

  it("creates Bar charts", function() {
    cy.visit(`queries/${this.queryId}/source`);
    cy.getByTestId("ExecuteButton").click();

    const getBarChartAssertionFunction = (specificBarChartAssertionFn = () => {}) => () => {
      // checks for TabbedEditor standard tabs
      assertTabbedEditor();

      // standard chart should be bar
      cy.getByTestId("Chart.GlobalSeriesType").contains(".ant-select-selection-item", "Bar");

      // checks the plot canvas exists and is empty
      assertPlotPreview("not.exist");

      // creates a chart and checks it is plotted
      cy.getByTestId("Chart.ColumnMapping.x").selectAntdOption("Chart.ColumnMapping.x.stage");
      cy.getByTestId("Chart.ColumnMapping.y").selectAntdOption("Chart.ColumnMapping.y.value1");
      cy.getByTestId("Chart.ColumnMapping.y").selectAntdOption("Chart.ColumnMapping.y.value2");
      assertPlotPreview("exist");

      specificBarChartAssertionFn();
    };

    const chartTests = [
      {
        name: "Basic Bar Chart",
        alias: "basicBarChart",
        assertionFn: () => {
          assertAxesAndAddLabels("Stage", "Value");
        },
      },
      {
        name: "Horizontal Bar Chart",
        alias: "horizontalBarChart",
        assertionFn: () => {
          cy.getByTestId("Chart.SwappedAxes").check();
          cy.getByTestId("VisualizationEditor.Tabs.XAxis").should("have.text", "Y Axis");
          cy.getByTestId("VisualizationEditor.Tabs.YAxis").should("have.text", "X Axis");
        },
      },
      {
        name: "Stacked Bar Chart",
        alias: "stackedBarChart",
        assertionFn: () => {
          cy.getByTestId("Chart.Stacking").selectAntdOption("Chart.Stacking.Stack");
        },
      },
      {
        name: "Normalized Bar Chart",
        alias: "normalizedBarChart",
        assertionFn: () => {
          cy.getByTestId("Chart.NormalizeValues").check();
        },
      },
    ];

    chartTests.forEach(({ name, alias, assertionFn }) => {
      createChartThroughUI(name, getBarChartAssertionFunction(assertionFn)).as(alias);
    });

    const chartGetters = chartTests.map(({ alias }) => alias);

    const withDashboardWidgetsAssertionFn = (widgetGetters, dashboardUrl) => {
      cy.visit(dashboardUrl);
      widgetGetters.forEach(widgetGetter => {
        cy.get(`@${widgetGetter}`).then(widget => {
          cy.getByTestId(getWidgetTestId(widget)).within(() => {
            cy.get("g.points").should("exist");
          });
        });
      });
    };

    createDashboardWithCharts("Bar chart visualizations", chartGetters, withDashboardWidgetsAssertionFn);
    cy.percySnapshot("Visualizations - Charts - Bar");
  });
});


<|start_filename|>client/app/lib/hooks/useImmutableCallback.js<|end_filename|>
import { isFunction, noop } from "lodash";
import { useRef, useCallback } from "react";

// This hook wraps a potentially changeable function object and always returns the same
// function so it's safe to use it with other hooks: wrapper function stays the same,
// but will always call a latest wrapped function.
// A quick note regarding `react-hooks/exhaustive-deps`: since wrapper function doesn't
// change, it's safe to use it as a dependency, it will never trigger other hooks.
export default function useImmutableCallback(callback) {
  const callbackRef = useRef();
  callbackRef.current = isFunction(callback) ? callback : noop;

  return useCallback((...args) => callbackRef.current(...args), []);
}


<|start_filename|>client/cypress/integration/destination/create_destination_spec.js<|end_filename|>
describe("Create Destination", () => {
  beforeEach(() => {
    cy.login();
  });

  it("renders the page and takes a screenshot", function() {
    cy.visit("/destinations/new");
    cy.server();
    cy.route("**/api/destinations/types").as("DestinationTypesRequest");

    cy.wait("@DestinationTypesRequest")
      .then(({ response }) => response.body.filter(type => type.deprecated))
      .then(deprecatedTypes => deprecatedTypes.map(type => type.type))
      .as("deprecatedTypes");

    cy.getByTestId("PreviewItem")
      .then($previewItems => Cypress.$.map($previewItems, item => Cypress.$(item).attr("data-test-type")))
      .then(availableTypes => expect(availableTypes).not.to.contain.members(this.deprecatedTypes));

    cy.getByTestId("CreateSourceDialog").should("contain", "Email");
    cy.wait(1000); // eslint-disable-line cypress/no-unnecessary-waiting
    cy.percySnapshot("Create Destination - Types");
  });

  it("shows a custom error message when destination name is already taken", () => {
    cy.createDestination("Slack Destination", "slack").then(() => {
      cy.visit("/destinations/new");

      cy.getByTestId("SearchSource").type("Slack");
      cy.getByTestId("CreateSourceDialog")
        .contains("Slack")
        .click();

      cy.getByTestId("Name").type("Slack Destination");
      cy.getByTestId("CreateSourceSaveButton").click();

      cy.contains("Alert Destination with the name Slack Destination already exists.");
    });
  });
});
