function getHeaderMenu()

in web-console/src/views/workbench-view/result-table-pane/result-table-pane.tsx [157:590]


  function getHeaderMenu(column: Column, headerIndex: number) {
    const header = column.name;
    const type = column.sqlType || column.nativeType;
    const ref = C(header);
    const prettyRef = prettyPrintSql(ref);

    const menuItems: JSX.Element[] = [];
    if (parsedQuery) {
      const noStar = !parsedQuery.hasStarInSelect();
      const selectExpression = parsedQuery.getSelectExpressionForIndex(headerIndex);

      const orderByExpression = parsedQuery.isValidSelectIndex(headerIndex)
        ? SqlLiteral.index(headerIndex)
        : ref;
      const descOrderBy = orderByExpression.toOrderByExpression('DESC');
      const ascOrderBy = orderByExpression.toOrderByExpression('ASC');
      const orderBy = parsedQuery.getOrderByForSelectIndex(headerIndex);

      if (orderBy) {
        const reverseOrderBy = orderBy.reverseDirection();
        const reverseOrderByDirection = reverseOrderBy.getEffectiveDirection();
        menuItems.push(
          <MenuItem
            key="order"
            icon={reverseOrderByDirection === 'ASC' ? IconNames.SORT_ASC : IconNames.SORT_DESC}
            text={`Order ${reverseOrderByDirection === 'ASC' ? 'ascending' : 'descending'}`}
            onClick={() => {
              handleQueryAction(q => q.changeOrderByExpressions([reverseOrderBy]));
            }}
          />,
        );
      } else {
        menuItems.push(
          <MenuItem
            key="order_desc"
            icon={IconNames.SORT_DESC}
            text="Order descending"
            onClick={() => {
              handleQueryAction(q => q.changeOrderByExpressions([descOrderBy]));
            }}
          />,
          <MenuItem
            key="order_asc"
            icon={IconNames.SORT_ASC}
            text="Order ascending"
            onClick={() => {
              handleQueryAction(q => q.changeOrderByExpressions([ascOrderBy]));
            }}
          />,
        );
      }

      // Expression changers
      if (selectExpression) {
        const underlyingExpression = selectExpression.getUnderlyingExpression();
        const columnValues = queryResult.getColumnByIndex(headerIndex)!;

        // Remove outer function
        if (underlyingExpression instanceof SqlFunction && underlyingExpression.getArg(0)) {
          menuItems.push(
            <MenuItem
              key="uncast"
              icon={IconNames.CROSS}
              text={`Remove outer ${underlyingExpression.getEffectiveFunctionName()} function`}
              onClick={() => {
                if (!selectExpression || !underlyingExpression) return;
                handleQueryAction(q =>
                  q.changeSelect(
                    headerIndex,
                    underlyingExpression.getArg(0)!.setAlias(selectExpression.getOutputName()),
                  ),
                );
              }}
            />,
          );
        }

        // Add a CAST
        menuItems.push(
          <MenuItem key="cast" icon={IconNames.EXCHANGE} text="Cast to...">
            {filterMap(CAST_TARGETS, asType => {
              if (asType === column.sqlType) return;
              return (
                <MenuItem
                  key={asType}
                  text={asType}
                  onClick={() => {
                    if (!selectExpression) return;
                    handleQueryAction(q =>
                      q.changeSelect(
                        headerIndex,
                        underlyingExpression
                          .cast(asType)
                          .setAlias(selectExpression.getOutputName()),
                      ),
                    );
                  }}
                />
              );
            })}
          </MenuItem>,
        );

        // Parse as JSON
        if (column.nativeType === 'STRING' && queryResult.getNumResults()) {
          if (columnValues.every(isStringJsonObject)) {
            menuItems.push(
              <MenuItem
                key="parse_json"
                icon={IconNames.DIAGRAM_TREE}
                text="Parse as JSON"
                onClick={() => {
                  if (!selectExpression) return;
                  handleQueryAction(q =>
                    q.changeSelect(
                      headerIndex,
                      F('TRY_PARSE_JSON', underlyingExpression).setAlias(
                        selectExpression.getOutputName(),
                      ),
                    ),
                  );
                }}
              />,
            );
          } else if (columnValues.every(isStringJsonString)) {
            menuItems.push(
              <MenuItem
                key="unquote_json_string"
                icon={IconNames.DOCUMENT_SHARE}
                text="Unquote JSON string"
                onClick={() => {
                  if (!selectExpression) return;
                  handleQueryAction(q =>
                    q.changeSelect(
                      headerIndex,
                      SqlFunction.jsonValue(
                        F('TRY_PARSE_JSON', underlyingExpression),
                        '$',
                        SqlType.VARCHAR,
                      ).setAlias(selectExpression.getOutputName()),
                    ),
                  );
                }}
              />,
            );
          }
        }

        // Extract a JSON path
        if (column.nativeType === 'COMPLEX<json>' && queryResult.getNumResults()) {
          const paths = getJsonPaths(
            filterMap(columnValues, (v: any) => {
              // Strangely, multi-stage-query-engine and broker deal with JSON differently
              if (v && typeof v === 'object') return v;
              try {
                return JSONBig.parse(v);
              } catch {
                return;
              }
            }),
          );

          if (paths.length) {
            menuItems.push(
              <MenuItem key="json_value" icon={IconNames.DIAGRAM_TREE} text="Get JSON value for...">
                {paths.map(path => {
                  return (
                    <MenuItem
                      key={path}
                      text={path}
                      onClick={() => {
                        if (!selectExpression) return;
                        handleQueryAction(q =>
                          q.addSelect(
                            SqlFunction.jsonValue(underlyingExpression, path).setAlias(
                              selectExpression.getOutputName() + path.replace(/^\$/, ''),
                            ),
                            { insertIndex: headerIndex + 1 },
                          ),
                        );
                      }}
                    />
                  );
                })}
              </MenuItem>,
            );
          }
        }
      }

      if (parsedQuery.isRealOutputColumnAtSelectIndex(headerIndex)) {
        const whereExpression = parsedQuery.getWhereExpression();
        if (whereExpression && whereExpression.containsColumnName(header)) {
          menuItems.push(
            <MenuItem
              key="remove_where"
              icon={IconNames.FILTER_REMOVE}
              text="Remove from WHERE clause"
              onClick={() => {
                handleQueryAction(q =>
                  q.changeWhereExpression(whereExpression.removeColumnFromAnd(header)),
                );
              }}
            />,
          );
        }

        const havingExpression = parsedQuery.getHavingExpression();
        if (havingExpression && havingExpression.containsColumnName(header)) {
          menuItems.push(
            <MenuItem
              key="remove_having"
              icon={IconNames.FILTER_REMOVE}
              text="Remove from HAVING clause"
              onClick={() => {
                handleQueryAction(q =>
                  q.changeHavingExpression(havingExpression.removeColumnFromAnd(header)),
                );
              }}
            />,
          );
        }
      }

      if (noStar) {
        menuItems.push(
          <MenuItem
            key="edit_column"
            icon={IconNames.EDIT}
            text="Edit column"
            onClick={() => {
              setEditingColumn(headerIndex);
            }}
          />,
        );
      }

      if (noStar && selectExpression) {
        if (column.isTimeColumn()) {
          menuItems.push(
            <TimeFloorMenuItem
              key="time_floor"
              expression={selectExpression}
              onChange={expression => {
                handleQueryAction(q => q.changeSelect(headerIndex, expression));
              }}
            />,
          );
        } else if (column.sqlType === 'TIMESTAMP') {
          menuItems.push(
            <MenuItem
              key="declare_time"
              icon={IconNames.TIME}
              text="Use as the primary time column"
              onClick={() => {
                handleQueryAction(q =>
                  q.changeSelect(headerIndex, selectExpression.as(TIME_COLUMN)),
                );
              }}
            />,
          );
        } else {
          // Not a time column -------------------------------------------
          const values = queryResult.rows.map(row => row[headerIndex]);
          const possibleDruidFormat = possibleDruidFormatForValues(values);
          const formatSql = possibleDruidFormat ? timeFormatToSql(possibleDruidFormat) : undefined;

          if (formatSql) {
            const newSelectExpression = formatSql.fillPlaceholders([
              selectExpression.getUnderlyingExpression(),
            ]);

            menuItems.push(
              <MenuItem
                key="parse_time"
                icon={IconNames.TIME}
                text={`Time parse as '${possibleDruidFormat}' and use as the primary time column`}
                onClick={() => {
                  handleQueryAction(q =>
                    q.changeSelect(headerIndex, newSelectExpression.as(TIME_COLUMN)),
                  );
                }}
              />,
            );
          }

          if (parsedQuery.hasGroupBy()) {
            if (parsedQuery.isGroupedOutputColumn(header)) {
              const convertToAggregate = (aggregate: SqlExpression) => {
                handleQueryAction(q =>
                  q.removeOutputColumn(header).addSelect(aggregate, {
                    insertIndex: 'last',
                  }),
                );
              };

              const underlyingSelectExpression = selectExpression.getUnderlyingExpression();

              menuItems.push(
                <MenuItem
                  key="convert_to_aggregate"
                  icon={IconNames.EXCHANGE}
                  text="Convert to aggregate"
                >
                  {oneOf(type, 'LONG', 'FLOAT', 'DOUBLE', 'BIGINT') && (
                    <>
                      <MenuItem
                        text="Convert to SUM(...)"
                        onClick={() => {
                          convertToAggregate(F.sum(underlyingSelectExpression).as(`sum_${header}`));
                        }}
                      />
                      <MenuItem
                        text="Convert to MIN(...)"
                        onClick={() => {
                          convertToAggregate(F.min(underlyingSelectExpression).as(`min_${header}`));
                        }}
                      />
                      <MenuItem
                        text="Convert to MAX(...)"
                        onClick={() => {
                          convertToAggregate(F.max(underlyingSelectExpression).as(`max_${header}`));
                        }}
                      />
                    </>
                  )}
                  <MenuItem
                    text="Convert to COUNT(DISTINCT ...)"
                    onClick={() => {
                      convertToAggregate(
                        F.countDistinct(underlyingSelectExpression).as(`unique_${header}`),
                      );
                    }}
                  />
                  <MenuItem
                    text="Convert to APPROX_COUNT_DISTINCT_DS_HLL(...)"
                    onClick={() => {
                      convertToAggregate(
                        F('APPROX_COUNT_DISTINCT_DS_HLL', underlyingSelectExpression).as(
                          `unique_${header}`,
                        ),
                      );
                    }}
                  />
                  <MenuItem
                    text="Convert to APPROX_COUNT_DISTINCT_DS_THETA(...)"
                    onClick={() => {
                      convertToAggregate(
                        F('APPROX_COUNT_DISTINCT_DS_THETA', underlyingSelectExpression).as(
                          `unique_${header}`,
                        ),
                      );
                    }}
                  />
                </MenuItem>,
              );
            } else {
              const groupByExpression = convertToGroupByExpression(selectExpression);
              if (groupByExpression) {
                menuItems.push(
                  <MenuItem
                    key="convert_to_group_by"
                    icon={IconNames.EXCHANGE}
                    text="Convert to group by"
                    onClick={() => {
                      handleQueryAction(q =>
                        q.removeOutputColumn(header).addSelect(groupByExpression, {
                          insertIndex: 'last-grouping',
                          addToGroupBy: 'end',
                        }),
                      );
                    }}
                  />,
                );
              }
            }
          }
        }
      }

      if (noStar) {
        menuItems.push(
          <MenuItem
            key="remove_column"
            icon={IconNames.CROSS}
            text="Remove column"
            onClick={() => {
              handleQueryAction(q => q.removeOutputColumn(header));
            }}
          />,
        );
      }
    } else {
      menuItems.push(
        <MenuItem
          key="copy_ref"
          icon={IconNames.CLIPBOARD}
          text={`Copy: ${prettyRef}`}
          onClick={() => {
            copyAndAlert(String(ref), `${prettyRef}' copied to clipboard`);
          }}
        />,
      );

      if (!runeMode) {
        const orderByExpression = ref;
        const descOrderBy = orderByExpression.toOrderByExpression('DESC');
        const ascOrderBy = orderByExpression.toOrderByExpression('ASC');
        const descOrderByPretty = prettyPrintSql(descOrderBy);
        const ascOrderByPretty = prettyPrintSql(descOrderBy);

        menuItems.push(
          <MenuItem
            key="copy_desc"
            icon={IconNames.CLIPBOARD}
            text={`Copy: ${descOrderByPretty}`}
            onClick={() =>
              copyAndAlert(descOrderBy.toString(), `'${descOrderByPretty}' copied to clipboard`)
            }
          />,
          <MenuItem
            key="copy_asc"
            icon={IconNames.CLIPBOARD}
            text={`Copy: ${ascOrderByPretty}`}
            onClick={() =>
              copyAndAlert(ascOrderBy.toString(), `'${ascOrderByPretty}' copied to clipboard`)
            }
          />,
        );
      }
    }

    return <Menu>{menuItems}</Menu>;
  }