export function SpanDetailPanel()

in dashboards-observability/public/components/trace_analytics/components/traces/span_detail_panel.tsx [24:245]


export function SpanDetailPanel(props: {
  http: HttpSetup;
  traceId: string;
  colorMap: any;
}) {
  const [data, setData] = useState({ gantt: [], table: [], ganttMaxX: 0 });
  const storedFilters = sessionStorage.getItem('TraceAnalyticsSpanFilters');
  const [spanFilters, setSpanFilters] = useState<Array<{ field: string; value: any }>>(
    storedFilters ? JSON.parse(storedFilters) : []
  );
  const [DSL, setDSL] = useState<any>({});

  const setSpanFiltersWithStorage = (newFilters: Array<{ field: string; value: any }>) => {
    setSpanFilters(newFilters);
    sessionStorage.setItem('TraceAnalyticsSpanFilters', JSON.stringify(newFilters));
  };

  const addSpanFilter = (field: string, value: any) => {
    const newFilters = [...spanFilters];
    const index = newFilters.findIndex(({ field: filterField }) => field === filterField);
    if (index === -1) {
      newFilters.push({ field, value });
    } else {
      newFilters.splice(index, 1, { field, value });
    }
    setSpanFiltersWithStorage(newFilters);
  };

  const removeSpanFilter = (field: string) => {
    const newFilters = [...spanFilters];
    const index = newFilters.findIndex(({ field: filterField }) => field === filterField);
    if (index !== -1) {
      newFilters.splice(index, 1);
      setSpanFiltersWithStorage(newFilters);
    }
  };

  const spanFiltersToDSL = () => {
    const DSL: any = {
      query: {
        bool: {
          must: [
            {
              term: {
                traceId: props.traceId,
              },
            },
          ],
          filter: [],
          should: [],
          must_not: [],
        },
      },
    };
    spanFilters.map(({ field, value }) => {
      if (value != null) {
        DSL.query.bool.must.push({
          term: {
            [field]: value,
          },
        });
      }
    });
    return DSL;
  };

  useEffect(() => {
    refresh();
  }, [props.colorMap, spanFilters]);

  const refresh = _.debounce(() => {
    if (_.isEmpty(props.colorMap)) return;
    const DSL = spanFiltersToDSL();
    setDSL(DSL);
    handleSpansGanttRequest(props.traceId, props.http, setData, props.colorMap, DSL);
  }, 150);

  const getSpanDetailLayout = (plotTraces: Plotly.Data[], maxX: number): Partial<Plotly.Layout> => {
    // get unique labels from traces
    const yLabels = plotTraces
      .map((d) => d.y[0])
      .filter((label, i, self) => self.indexOf(label) === i);
    // remove uuid when displaying y-ticks
    const yTexts = yLabels.map((label) => label.substring(0, label.length - 36));

    return {
      height: 25 * plotTraces.length + 60,
      width: 800,
      margin: {
        l: 260,
        r: 5,
        b: 30,
        t: 30,
      },
      xaxis: {
        ticksuffix: ' ms',
        side: 'top',
        color: '#91989c',
        showline: true,
        range: [0, maxX * 1.2],
      },
      yaxis: {
        showgrid: false,
        tickvals: yLabels,
        ticktext: yTexts,
      },
    };
  };

  const layout = useMemo(() => getSpanDetailLayout(data.gantt, data.ganttMaxX), [
    data.gantt,
    data.ganttMaxX,
  ]);

  const [currentSpan, setCurrentSpan] = useState('');

  const onClick = (event) => {
    if (!event?.points) return;
    const point = event.points[0];
    setCurrentSpan(point.data.spanId);
  };

  const renderFilters = useMemo(() => {
    return spanFilters.map(({ field, value }) => (
      <EuiFlexItem grow={false} key={`span-filter-badge-${field}`}>
        <EuiBadge
          iconType="cross"
          iconSide="right"
          iconOnClick={() => removeSpanFilter(field)}
          iconOnClickAriaLabel="remove current filter"
        >
          {`${field}: ${value}`}
        </EuiBadge>
      </EuiFlexItem>
    ));
  }, [spanFilters]);

  const onHover = () => {
    const dragLayer = document.getElementsByClassName('nsewdrag')?.[0];
    dragLayer.style.cursor = 'pointer';
  };

  const onUnhover = (pr) => {
    const dragLayer = document.getElementsByClassName('nsewdrag')?.[0];
    dragLayer.style.cursor = '';
  };

  const toggleOptions = [
    {
      id: 'timeline',
      label: 'Timeline',
    },
    {
      id: 'span_list',
      label: 'Span list',
    },
  ];
  const [toggleIdSelected, setToggleIdSelected] = useState(toggleOptions[0].id);

  const spanDetailTable = useMemo(
    () => (
      <SpanDetailTable
        http={props.http}
        hiddenColumns={['traceId', 'traceGroup']}
        DSL={DSL}
        openFlyout={(spanId: string) => setCurrentSpan(spanId)}
      />
    ),
    [DSL, setCurrentSpan]
  );

  return (
    <>
      <EuiPanel>
        <EuiFlexGroup>
          <EuiFlexItem>
            <PanelTitle title="Spans" totalItems={data.gantt.length / 2} />
          </EuiFlexItem>
          <EuiFlexItem grow={false}>
            <EuiButtonGroup
              legend="Select view of spans"
              options={toggleOptions}
              idSelected={toggleIdSelected}
              onChange={(id) => setToggleIdSelected(id)}
            />
          </EuiFlexItem>
        </EuiFlexGroup>
        {spanFilters.length > 0 && (
          <>
            <EuiSpacer size="s" />
            <EuiFlexGroup gutterSize="s" wrap>
              {renderFilters}
            </EuiFlexGroup>
          </>
        )}
        <EuiHorizontalRule margin="m" />
        <div style={{ overflowY: 'auto', maxHeight: 500 }}>
          {toggleIdSelected === 'timeline' ? (
            <Plt
              data={data.gantt}
              layout={layout}
              onClickHandler={onClick}
              onHoverHandler={onHover}
              onUnhoverHandler={onUnhover}
            />
          ) : (
            spanDetailTable
          )}
        </div>
      </EuiPanel>
      {!!currentSpan && (
        <SpanDetailFlyout
          http={props.http}
          spanId={currentSpan}
          isFlyoutVisible={!!currentSpan}
          closeFlyout={() => setCurrentSpan('')}
          addSpanFilter={addSpanFilter}
        />
      )}
    </>
  );
}