> ## Documentation Index
> Fetch the complete documentation index at: https://docs.endorlabs.com/llms.txt
> Use this file to discover all available pages before exploring further.

# endorctl API Query Builder

> <Badge color="green">Beta</Badge> <br /> Use our API-based query builder to construct endorctl commands based on actual API operations and parameters.

export const ApiQueryBuilder = ({specUrl, specData: propSpecData}) => {
  const OPERATORS = [{
    value: '==',
    label: '== (equals)'
  }, {
    value: '!=',
    label: '!= (not equals)'
  }, {
    value: '>',
    label: '> (greater than)'
  }, {
    value: '>=',
    label: '>= (greater or equal)'
  }, {
    value: '<',
    label: '< (less than)'
  }, {
    value: '<=',
    label: '<= (less or equal)'
  }, {
    value: 'contains',
    label: 'contains'
  }, {
    value: 'not contains',
    label: 'not contains'
  }, {
    value: 'in',
    label: 'in'
  }, {
    value: 'not in',
    label: 'not in'
  }, {
    value: 'matches',
    label: 'matches (regex)'
  }, {
    value: 'exists',
    label: 'exists'
  }, {
    value: 'not exists',
    label: 'not exists'
  }];
  const ARRAY_OPS = new Set(['contains', 'not contains', 'in', 'not in']);
  const EXISTENCE_OPS = new Set(['exists', 'not exists']);
  const STRUCTURAL_FIELDS = new Set(['tenant_meta', 'meta', 'context', 'processing_status', 'propagate', 'tags', 'references']);
  const VALID_METHODS = new Set(['get', 'post', 'put', 'patch', 'delete']);
  const resolveRef = (schemaOrRef, schemas) => {
    if (!schemaOrRef) return null;
    const ref = schemaOrRef.$ref;
    if (!ref) return schemaOrRef;
    const name = ref.replace('#/components/schemas/', '').replace('#/definitions/', '');
    return schemas[name] || null;
  };
  const getRefName = schemaOrRef => {
    const ref = schemaOrRef?.$ref;
    if (!ref) return null;
    return ref.replace('#/components/schemas/', '').replace('#/definitions/', '');
  };
  const inferVerb = opName => {
    const lower = opName.toLowerCase();
    for (const verb of ['list', 'get', 'create', 'update', 'delete']) {
      if (lower.startsWith(verb)) return verb;
    }
    return 'get';
  };
  const singularize = name => {
    const irregulars = {
      Policies: 'Policy',
      Addresses: 'Address',
      Statuses: 'Status',
      Classes: 'Class'
    };
    if (irregulars[name]) return irregulars[name];
    if (name.length > 3 && name.endsWith('ies')) return name.slice(0, -3) + 'y';
    if (name.length > 3 && name.endsWith('ses')) return name.slice(0, -2);
    if (name.length > 1 && name.endsWith('s') && !name.endsWith('ss')) return name.slice(0, -1);
    return name;
  };
  const extractV1Resource = (rn, schemas) => {
    if (rn?.startsWith('v1') && schemas[rn]) return rn.substring(2);
    return null;
  };
  const resourceFromBody = (opData, schemas) => {
    const jsonSchema = opData.requestBody?.content?.['application/json']?.schema;
    if (jsonSchema) {
      const resolved = resolveRef(jsonSchema, schemas);
      const rn = getRefName(resolved?.properties?.object);
      const res = extractV1Resource(rn, schemas);
      if (res) return res;
    }
    for (const param of opData.parameters || []) {
      if (param.in !== 'body' || !param.schema) continue;
      const res = resolveRef(param.schema, schemas);
      const rn = getRefName(res?.properties?.object);
      const v1Res = extractV1Resource(rn, schemas);
      if (v1Res) return v1Res;
    }
    return null;
  };
  const resourceFromResponse = (opData, schemas) => {
    const jsonSchema = opData.responses?.['200']?.content?.['application/json']?.schema;
    if (!jsonSchema) return null;
    const rn = getRefName(jsonSchema);
    if (!rn || !schemas[rn]) return null;
    const resolved = schemas[rn];
    const listRn = getRefName(resolved?.properties?.list);
    if (listRn && schemas[listRn]) {
      const objectsItems = schemas[listRn]?.properties?.objects?.items;
      const itemRn = getRefName(objectsItems);
      const res = extractV1Resource(itemRn, schemas);
      if (res) return res;
    }
    return extractV1Resource(rn, schemas);
  };
  const inferResource = (opName, opData, schemas) => {
    if (opData && schemas) {
      const fromBody = resourceFromBody(opData, schemas);
      if (fromBody) return fromBody;
      const fromResp = resourceFromResponse(opData, schemas);
      if (fromResp) return fromResp;
    }
    return singularize(opName.replace(/^(List|Get|Create|Update|Delete)/, ''));
  };
  const tryRegisterOperation = (services, path, method, opData, sc) => {
    if (!opData || typeof opData !== 'object') return;
    const opId = opData.operationId;
    if (!opId?.includes('_')) return;
    const sep = opId.indexOf('_');
    const svcKey = opId.substring(0, sep);
    const opName = opId.substring(sep + 1);
    if (!services[svcKey]) {
      services[svcKey] = {
        label: svcKey.replace('Service', ' Service'),
        operations: {}
      };
    }
    if (services[svcKey].operations[opName]) return;
    services[svcKey].operations[opName] = {
      name: opName,
      verb: inferVerb(opName),
      resource: inferResource(opName, opData, sc),
      path,
      method,
      summary: opData.summary || '',
      opData
    };
  };
  const parseServices = (sd, sc) => {
    const services = {};
    for (const [path, pathData] of Object.entries(sd.paths || ({}))) {
      for (const [method, opData] of Object.entries(pathData)) {
        if (VALID_METHODS.has(method)) tryRegisterOperation(services, path, method, opData, sc);
      }
    }
    for (const svc of Object.values(services)) {
      for (const op of Object.values(svc.operations)) {
        if (sc['v1' + op.resource]) continue;
        const sibling = Object.values(svc.operations).find(s => s !== op && sc['v1' + s.resource]);
        if (sibling) op.resource = sibling.resource;
      }
    }
    return services;
  };
  const resolveNextInPath = (prop, schemas, visited) => {
    const rn = getRefName(prop);
    if (rn) {
      if (visited.has(rn)) return null;
      visited.add(rn);
      return schemas[rn] || null;
    }
    if (prop.properties) return prop;
    if (prop.type === 'array' && prop.items) {
      const itemRn = getRefName(prop.items);
      if (itemRn) {
        if (visited.has(itemRn)) return null;
        visited.add(itemRn);
        return schemas[itemRn] || null;
      }
      return prop.items?.properties ? prop.items : null;
    }
    return null;
  };
  const getSchemaAtPath = (resourceType, pathParts, schemas) => {
    let current = schemas['v1' + resourceType];
    const visited = new Set();
    for (const part of pathParts) {
      if (!current?.properties) return null;
      const prop = current.properties[part];
      if (!prop) return null;
      current = resolveNextInPath(prop, schemas, visited);
      if (!current) return null;
    }
    return current;
  };
  const getFieldsForResource = (resourceType, schemas) => {
    const schema = schemas['v1' + resourceType];
    if (!schema?.properties) return {
      fields: [],
      subFields: {}
    };
    const fields = Object.keys(schema.properties).sort((a, b) => a.localeCompare(b));
    const subFields = {};
    for (const [prop, propDef] of Object.entries(schema.properties)) {
      const resolved = resolveRef(propDef, schemas);
      if (resolved?.properties) {
        subFields[prop] = Object.keys(resolved.properties).sort((a, b) => a.localeCompare(b));
      }
    }
    return {
      fields,
      subFields
    };
  };
  const getChildFields = (resourceType, pathParts, schemas) => {
    const schema = getSchemaAtPath(resourceType, pathParts, schemas);
    return schema?.properties ? Object.keys(schema.properties).sort((a, b) => a.localeCompare(b)) : [];
  };
  const formatStringType = fieldDef => {
    if (fieldDef.format === 'date-time') return 'timestamp (RFC 3339)';
    return fieldDef.format === 'byte' ? 'byte string' : 'string';
  };
  const formatArrayType = fieldDef => {
    if (!fieldDef.items) return 'array';
    if (fieldDef.items.type === 'string') return 'array of strings';
    const itemRn = getRefName(fieldDef.items);
    if (itemRn) return 'array of ' + itemRn.replace(/^v1/, '');
    return fieldDef.items.type === 'object' ? 'array of objects' : 'array';
  };
  const formatDataType = fieldDef => {
    if (!fieldDef) return null;
    if (fieldDef.enum) return 'string (enum)';
    if (fieldDef.type === 'boolean') return 'boolean';
    if (fieldDef.type === 'string') return formatStringType(fieldDef);
    if (fieldDef.type === 'array') return formatArrayType(fieldDef);
    if (fieldDef.type === 'integer' || fieldDef.type === 'number') return fieldDef.type;
    if (fieldDef.type === 'object' || fieldDef.properties) return 'object';
    return null;
  };
  const getFieldInfo = (resourceType, pathParts, schemas) => {
    if (!pathParts?.length) return null;
    const lastField = pathParts[pathParts.length - 1];
    const container = pathParts.length === 1 ? schemas['v1' + resourceType] : getSchemaAtPath(resourceType, pathParts.slice(0, -1), schemas);
    const prop = container?.properties?.[lastField];
    if (!prop) return null;
    const resolved = resolveRef(prop, schemas);
    return {
      type: formatDataType(resolved || prop),
      desc: prop.description || resolved?.description || null
    };
  };
  const getBodySchema = (opData, schemas) => {
    const jsonSchema = opData?.requestBody?.content?.['application/json']?.schema;
    if (jsonSchema) return resolveRef(jsonSchema, schemas);
    for (const param of opData?.parameters || []) {
      if (param.in === 'body' && param.schema) return resolveRef(param.schema, schemas);
    }
    return null;
  };
  const collectUpdatableFields = (schema, schemas, prefix = '', depth = 0, visited = new Set()) => {
    if (depth > 4 || !schema?.properties) return [];
    const result = [];
    for (const [key, propSchema] of Object.entries(schema.properties)) {
      if (propSchema.readOnly) continue;
      const path = prefix ? prefix + '.' + key : key;
      const resolved = resolveRef(propSchema, schemas);
      const rn = getRefName(propSchema);
      if (rn && visited.has(rn)) {
        result.push({
          path,
          type: 'string',
          desc: propSchema.description || ''
        });
        continue;
      }
      const nextVisited = rn ? new Set([...visited, rn]) : visited;
      if (resolved?.properties && depth < 3 && !STRUCTURAL_FIELDS.has(key)) {
        result.push(...collectUpdatableFields(resolved, schemas, path, depth + 1, nextVisited));
      } else {
        result.push({
          path,
          type: resolved?.type || propSchema.type || 'string',
          desc: propSchema.description || resolved?.description || '',
          enumVals: resolved?.enum || propSchema.enum
        });
      }
    }
    return result;
  };
  const exampleStringValue = schema => {
    if (schema.enum) return schema.enum[0];
    if (schema.format === 'date-time') return new Date().toISOString();
    return schema.format === 'byte' ? '' : 'string';
  };
  const exampleValue = (schema, schemas, depth = 0, visited = new Set()) => {
    if (!schema) return null;
    const rn = getRefName(schema);
    if (rn) {
      if (visited.has(rn)) return {};
      if (!schemas[rn]) return null;
      return exampleValue(schemas[rn], schemas, depth, new Set([...visited, rn]));
    }
    if (schema.type === 'string') return exampleStringValue(schema);
    if (schema.type === 'number' || schema.type === 'integer') return 0;
    if (schema.type === 'boolean') return false;
    if (schema.type === 'array') return [];
    if (schema.type === 'object' || schema.properties) {
      return depth < 2 && schema.properties ? makeJsonTemplate(schema, schemas, depth + 1, visited) : {};
    }
    return null;
  };
  const makeJsonTemplate = (schema, schemas, depth = 0, visited = new Set()) => {
    if (depth > 3 || !schema) return {};
    const rn = getRefName(schema);
    if (rn) {
      if (visited.has(rn) || !schemas[rn]) return {};
      return makeJsonTemplate(schemas[rn], schemas, depth + 1, new Set([...visited, rn]));
    }
    if (schema.type !== 'object' || !schema.properties) {
      return exampleValue(schema, schemas, depth, visited);
    }
    const obj = {};
    for (const [key, prop] of Object.entries(schema.properties)) {
      if (!prop.readOnly) obj[key] = exampleValue(prop, schemas, depth + 1, visited);
    }
    return obj;
  };
  const fmtFilter = f => {
    const path = [f.field, ...f.subFields || []].filter(Boolean).join('.');
    if (EXISTENCE_OPS.has(f.operator)) return path + ' ' + f.operator;
    if (ARRAY_OPS.has(f.operator)) return path + ' ' + f.operator + ' [' + f.value + ']';
    return path + f.operator + '"' + f.value + '"';
  };
  const buildIdentifierFlags = (verb, resourceId) => {
    if (!['get', 'update', 'delete'].includes(verb) || !resourceId) return '';
    const id = resourceId.trim();
    return (/^[0-9a-f]{24,}$/i).test(id) ? ' --uuid ' + id : ' --name "' + id + '"';
  };
  const buildFilterExpression = p => {
    const parts = p.filters.filter(f => f.field && f.operator && (EXISTENCE_OPS.has(f.operator) || f.value)).map(fmtFilter);
    if (p.dateFrom) parts.push('meta.create_time>="' + p.dateFrom + 'T00:00:00Z"');
    if (p.dateTo) parts.push('meta.create_time<="' + p.dateTo + 'T23:59:59Z"');
    return parts.length ? ' --filter "' + parts.join(' and ') + '"' : '';
  };
  const buildListFlags = p => {
    let flags = buildFilterExpression(p);
    if (p.fieldMask) flags += ' --field-mask "' + p.fieldMask + '"';
    if (p.pageSize) flags += ' --page-size ' + p.pageSize;
    if (p.pageToken) flags += ' --page-token ' + p.pageToken;
    if (p.sortPath) flags += ' --sort-path "' + p.sortPath + '"';
    if (p.sortOrder && p.sortOrder !== 'SORT_ENTRY_SORT_ORDER_UNSPECIFIED') flags += ' --sort-order ' + p.sortOrder;
    if (p.countOnly) flags += ' --count';
    if (p.traverse) flags += ' --traverse';
    if (p.timeoutVal) flags += ' --timeout ' + p.timeoutVal + 's';
    return flags;
  };
  const assembleCommand = p => {
    let cmd = 'endorctl api ' + p.verb + ' -r ' + p.resource;
    if (p.namespace) cmd += ' --namespace "' + p.namespace + '"';
    cmd += buildIdentifierFlags(p.verb, p.resourceId);
    if (p.verb === 'list') cmd += buildListFlags(p);
    if (['create', 'update'].includes(p.verb) && p.body) {
      cmd += " --data '" + p.body + "'";
      if (p.verb === 'update' && p.updateMask) cmd += ' --field-mask "' + p.updateMask + '"';
    }
    if (p.outputType) cmd += ' --output-type ' + p.outputType;
    return cmd;
  };
  const filterIdRef = useRef(0);
  const [specData, setSpecData] = useState(propSpecData || null);
  const [loadError, setLoadError] = useState('');
  const [isDark, setIsDark] = useState(false);
  const [svc, setSvc] = useState('');
  const [op, setOp] = useState('');
  const [ns, setNs] = useState('');
  const [resId, setResId] = useState('');
  const [filters, setFilters] = useState([]);
  const [outType, setOutType] = useState('');
  const [pgSize, setPgSize] = useState('');
  const [pgToken, setPgToken] = useState('');
  const [sortPath, setSortPath] = useState('');
  const [sortOrder, setSortOrder] = useState('');
  const [fMask, setFMask] = useState('');
  const [countOnly, setCountOnly] = useState(false);
  const [traverse, setTraverse] = useState(false);
  const [timeoutVal, setTimeoutVal] = useState('');
  const [dateFrom, setDateFrom] = useState('');
  const [dateTo, setDateTo] = useState('');
  const [updFields, setUpdFields] = useState([]);
  const [body, setBody] = useState('');
  const [toast, setToast] = useState('');
  useEffect(() => {
    if (propSpecData) {
      setSpecData(propSpecData);
      return;
    }
    if (!specUrl) {
      setLoadError('No specUrl or specData provided.');
      return;
    }
    const ac = new AbortController();
    fetch(specUrl, {
      signal: ac.signal
    }).then(r => {
      if (!r.ok) throw new Error(r.status + ' ' + r.statusText);
      return r.json();
    }).then(data => {
      setSpecData(data);
    }).catch(err => {
      if (err.name !== 'AbortError') setLoadError('Unable to load API spec: ' + err.message);
    });
    return () => {
      ac.abort();
    };
  }, [specUrl, propSpecData]);
  useEffect(() => {
    const check = () => {
      const el = document.documentElement;
      setIsDark(el.dataset.theme === 'dark' || el.classList.contains('dark'));
    };
    check();
    const obs = new MutationObserver(check);
    obs.observe(document.documentElement, {
      attributes: true,
      attributeFilter: ['data-theme', 'class']
    });
    return () => {
      obs.disconnect();
    };
  }, []);
  useEffect(() => {
    if (!toast) return;
    const t = setTimeout(() => {
      setToast('');
    }, 2500);
    return () => {
      clearTimeout(t);
    };
  }, [toast]);
  const schemas = useMemo(() => {
    if (!specData) return {};
    return specData.components?.schemas || specData.definitions || ({});
  }, [specData]);
  const services = useMemo(() => {
    if (!specData) return {};
    return parseServices(specData, schemas);
  }, [specData, schemas]);
  const currentOp = useMemo(() => {
    if (!svc || !op || !services[svc]) return null;
    return services[svc].operations[op] || null;
  }, [services, svc, op]);
  const verb = currentOp ? currentOp.verb : '';
  const resource = currentOp ? currentOp.resource : '';
  const resourceInfo = useMemo(() => {
    if (!resource || !schemas) return {
      fields: [],
      subFields: {}
    };
    return getFieldsForResource(resource, schemas);
  }, [resource, schemas]);
  if (loadError) return <div style={{
    padding: '1rem',
    color: '#d32f2f',
    fontSize: '0.9rem'
  }}>{loadError}</div>;
  if (!specData) return <div style={{
    padding: '1rem',
    opacity: 0.6,
    fontSize: '0.9rem'
  }}>Loading API specification…</div>;
  const bg = isDark ? '#0d1117' : '#ffffff';
  const bgL = isDark ? '#161b22' : '#f6f8fa';
  const txt = isDark ? '#e6edf3' : '#1f2937';
  const bd = 'rgba(38,208,124,';
  const green = '#26D07C';
  const inputStyle = {
    width: '100%',
    padding: '0.5rem 0.75rem',
    borderRadius: '8px',
    border: '1px solid ' + bd + '0.2)',
    background: bg,
    color: txt,
    fontSize: '0.85rem',
    outline: 'none',
    boxSizing: 'border-box',
    transition: 'border-color 0.15s ease'
  };
  const selectStyle = {
    ...inputStyle,
    cursor: 'pointer',
    appearance: 'auto'
  };
  const labelStyle = {
    display: 'block',
    fontWeight: 600,
    fontSize: '0.8rem',
    marginBottom: '0.35rem',
    opacity: 0.9
  };
  const sectionStyle = {
    background: bgL,
    border: '1px solid ' + bd + '0.12)',
    borderRadius: '12px',
    padding: '0.85rem',
    marginTop: '0.75rem'
  };
  const rowStyle = {
    display: 'flex',
    gap: '0.75rem',
    flexWrap: 'wrap'
  };
  const cellStyle = {
    flex: 1,
    minWidth: '150px'
  };
  const btnPrimary = {
    padding: '0.45rem 0.85rem',
    border: 'none',
    borderRadius: '8px',
    background: green,
    color: isDark ? '#000' : '#fff',
    fontWeight: 600,
    fontSize: '0.8rem',
    cursor: 'pointer'
  };
  const btnOutline = {
    padding: '0.45rem 0.85rem',
    border: '1px solid ' + bd + '0.25)',
    borderRadius: '8px',
    background: 'transparent',
    color: txt,
    fontWeight: 600,
    fontSize: '0.8rem',
    cursor: 'pointer'
  };
  const btnDanger = {
    ...btnOutline,
    borderColor: '#dc3545',
    color: '#dc3545'
  };
  const codeStyle = {
    fontFamily: "Monaco, Menlo, 'Ubuntu Mono', monospace",
    fontSize: '0.82rem',
    background: isDark ? 'rgba(255,255,255,0.06)' : 'rgba(0,0,0,0.04)',
    border: '1px solid ' + bd + '0.15)',
    borderRadius: '8px',
    padding: '1rem',
    whiteSpace: 'pre-wrap',
    wordBreak: 'break-all',
    lineHeight: 1.5,
    color: txt
  };
  const resetFormFields = () => {
    setFilters([]);
    setResId('');
    setOutType('');
    setPgSize('');
    setPgToken('');
    setSortPath('');
    setSortOrder('');
    setFMask('');
    setCountOnly(false);
    setTraverse(false);
    setTimeoutVal('');
    setDateFrom('');
    setDateTo('');
    setUpdFields([]);
    setBody('');
  };
  const handleSvcChange = e => {
    setSvc(e.target.value);
    setOp('');
    resetFormFields();
  };
  const initOpFields = newOp => {
    const newCurrentOp = svc && services[svc] ? services[svc].operations[newOp] : null;
    if (!newCurrentOp) {
      setUpdFields([]);
      setBody('');
      return;
    }
    if (newCurrentOp.verb === 'update') {
      const bodySchema = getBodySchema(newCurrentOp.opData, schemas);
      const objSchema = bodySchema?.properties?.object ? resolveRef(bodySchema.properties.object, schemas) : null;
      if (objSchema) {
        const fields = collectUpdatableFields(objSchema, schemas);
        setUpdFields(fields.map(f => ({
          ...f,
          checked: false,
          value: ''
        })));
        setBody('');
        return;
      }
      setUpdFields([]);
      setBody('');
    } else if (newCurrentOp.verb === 'create') {
      const bs = getBodySchema(newCurrentOp.opData, schemas);
      setBody(bs ? JSON.stringify(makeJsonTemplate(bs, schemas), null, 2) : '{}');
      setUpdFields([]);
    } else {
      setUpdFields([]);
      setBody('');
    }
  };
  const handleOpChange = e => {
    const newOp = e.target.value;
    setOp(newOp);
    resetFormFields();
    initOpFields(newOp);
  };
  const addFilter = () => {
    filterIdRef.current += 1;
    setFilters(prev => [...prev, {
      id: filterIdRef.current,
      field: '',
      subFields: [],
      operator: '==',
      value: ''
    }]);
  };
  const removeFilter = id => {
    setFilters(prev => prev.filter(f => f.id !== id));
  };
  const updateFilter = (id, key, val) => {
    setFilters(prev => prev.map(f => {
      if (f.id !== id) return f;
      const updated = {
        ...f,
        [key]: val
      };
      if (key === 'field') updated.subFields = [];
      return updated;
    }));
  };
  const updateFilterSubField = (id, level, val) => {
    setFilters(prev => prev.map(f => {
      if (f.id !== id) return f;
      const newSubs = f.subFields.slice(0, level);
      if (val) newSubs.push(val);
      return {
        ...f,
        subFields: newSubs
      };
    }));
  };
  const toggleUpdField = idx => {
    setUpdFields(prev => prev.map((f, i) => i === idx ? {
      ...f,
      checked: !f.checked
    } : f));
  };
  const setUpdFieldValue = (idx, val) => {
    setUpdFields(prev => prev.map((f, i) => i === idx ? {
      ...f,
      value: val
    } : f));
  };
  const safeParseJson = str => {
    try {
      return JSON.parse(str);
    } catch {
      return str;
    }
  };
  const buildUpdateBody = () => {
    const obj = {};
    for (const f of updFields.filter(uf => uf.checked && uf.value !== '')) {
      const parts = f.path.split('.');
      let target = obj;
      for (const part of parts.slice(0, -1)) {
        if (!target[part]) target[part] = {};
        target = target[part];
      }
      let val = f.value;
      if (val === 'true') val = true; else if (val === 'false') val = false; else if (val.charAt(0) === '{' || val.charAt(0) === '[') val = safeParseJson(val); else if (!Number.isNaN(Number(val)) && val.trim() !== '') val = Number(val);
      target[parts[parts.length - 1]] = val;
    }
    return obj;
  };
  const buildUpdateMask = () => updFields.filter(f => f.checked && f.value !== '').map(f => f.path).join(',');
  const genCommand = () => {
    if (!currentOp) return '';
    let bodyStr = '';
    let mask = '';
    if (verb === 'create') bodyStr = body;
    if (verb === 'update') {
      mask = buildUpdateMask();
      if (mask) {
        bodyStr = JSON.stringify({
          request: {
            update_mask: mask
          },
          object: buildUpdateBody()
        });
      }
    }
    return assembleCommand({
      verb,
      resource,
      namespace: ns,
      resourceId: resId,
      filters,
      dateFrom,
      dateTo,
      fieldMask: fMask,
      pageSize: pgSize,
      pageToken: pgToken,
      sortPath,
      sortOrder,
      countOnly,
      traverse,
      timeoutVal,
      outputType: outType,
      body: bodyStr,
      updateMask: mask
    });
  };
  const command = currentOp ? genCommand() : '';
  const handleCopy = () => {
    navigator.clipboard.writeText(command).then(() => {
      setToast('Command copied!');
    }).catch(() => {
      setToast('Copy failed.');
    });
  };
  const buildSubFieldDropdowns = f => {
    const dropdowns = [];
    if (!f.field || !resource) return dropdowns;
    let level = 0;
    while (level < 10) {
      const pathSoFar = [f.field, ...f.subFields.slice(0, level)];
      const children = getChildFields(resource, pathSoFar, schemas);
      if (children.length === 0) break;
      const curLevel = level;
      const curVal = f.subFields[level] || '';
      const selectId = `aqb-f-${f.id}-sub-${curLevel}`;
      dropdowns.push(<div key={`sub-${curLevel}`} style={cellStyle}>
          <label htmlFor={selectId} style={labelStyle}>Sub-field</label>
          <select id={selectId} style={selectStyle} value={curVal} onChange={e => updateFilterSubField(f.id, curLevel, e.target.value)}>
            <option value="">Select sub-field…</option>
            {children.map(sf => <option key={sf} value={sf}>{sf}</option>)}
          </select>
        </div>);
      if (!curVal) break;
      level++;
    }
    return dropdowns;
  };
  const renderUpdateFieldInput = (f, i) => {
    const valInputId = `aqb-uf-${i}-val`;
    if (f.enumVals) {
      return <select id={valInputId} style={{
        ...selectStyle,
        flex: 1,
        minWidth: '120px'
      }} value={f.value} onChange={e => setUpdFieldValue(i, e.target.value)}>
          <option value="">Select…</option>
          {f.enumVals.map(v => <option key={v} value={v}>{v}</option>)}
        </select>;
    }
    if (f.type === 'boolean') {
      return <select id={valInputId} style={{
        ...selectStyle,
        flex: 1,
        minWidth: '80px'
      }} value={f.value} onChange={e => setUpdFieldValue(i, e.target.value)}>
          <option value="">Select…</option>
          <option value="true">true</option>
          <option value="false">false</option>
        </select>;
    }
    return <input id={valInputId} type="text" style={{
      ...inputStyle,
      flex: 1,
      minWidth: '120px'
    }} value={f.value} onChange={e => setUpdFieldValue(i, e.target.value)} placeholder={f.type === 'integer' || f.type === 'number' ? '0' : 'value'} />;
  };
  const sortedSvcKeys = Object.keys(services).sort((a, b) => a.localeCompare(b));
  const sortedOpKeys = svc && services[svc] ? Object.keys(services[svc].operations).sort((a, b) => a.localeCompare(b)) : [];
  return <div className="not-prose" style={{
    margin: '1rem 0',
    fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif"
  }}>
      <div style={{
    background: bg,
    border: '1px solid ' + bd + '0.2)',
    borderRadius: '16px',
    padding: '1.25rem',
    color: txt
  }}>

        <div style={{
    display: 'flex',
    gap: '0.75rem',
    alignItems: 'center',
    marginBottom: '1rem'
  }}>
          <div style={{
    width: '38px',
    height: '38px',
    borderRadius: '10px',
    display: 'flex',
    alignItems: 'center',
    justifyContent: 'center',
    background: bgL,
    border: '1px solid ' + bd + '0.2)',
    color: green,
    flexShrink: 0,
    fontSize: '1.1rem'
  }}>{'⚡'}</div>
          <div>
            <h3 style={{
    margin: '0 0 0.1rem',
    fontWeight: 600,
    fontSize: '1.1rem',
    color: txt
  }}>API Query Builder</h3>
            <p style={{
    margin: 0,
    fontSize: '0.82rem',
    opacity: 0.7
  }}>Build endorctl CLI commands interactively</p>
          </div>
        </div>

        <div style={rowStyle}>
          <div style={cellStyle}>
            <label htmlFor="aqb-svc" style={labelStyle}>Service</label>
            <select id="aqb-svc" style={selectStyle} value={svc} onChange={handleSvcChange}>
              <option value="">Select a service…</option>
              {sortedSvcKeys.map(k => <option key={k} value={k}>{services[k].label}</option>)}
            </select>
          </div>
          <div style={cellStyle}>
            <label htmlFor="aqb-op" style={labelStyle}>Operation</label>
            <select id="aqb-op" style={selectStyle} value={op} onChange={handleOpChange} disabled={!svc}>
              <option value="">Select an operation…</option>
              {sortedOpKeys.map(k => {
    const o = services[svc].operations[k];
    return <option key={k} value={k}>{k}{o.summary ? ' \u2014 ' + o.summary : ''}</option>;
  })}
            </select>
          </div>
        </div>

        {currentOp && <div style={{
    marginTop: '0.5rem',
    fontSize: '0.8rem',
    opacity: 0.7
  }}>
            <code style={{
    fontSize: '0.78rem'
  }}>{currentOp.method.toUpperCase()} {currentOp.path}</code>
            {' \u2014 Resource: '}<strong>{resource}</strong>{', Verb: '}<strong>{verb}</strong>
          </div>}

        <div style={{
    marginTop: '0.75rem'
  }}>
          <label htmlFor="aqb-ns" style={labelStyle}>Namespace</label>
          <input id="aqb-ns" type="text" style={inputStyle} value={ns} onChange={e => setNs(e.target.value)} placeholder="Enter namespace…" />
        </div>

        {(verb === 'get' || verb === 'update' || verb === 'delete') && <div style={{
    marginTop: '0.6rem'
  }}>
            <label htmlFor="aqb-res-id" style={labelStyle}>Resource Identifier (name or UUID)</label>
            <input id="aqb-res-id" type="text" style={inputStyle} value={resId} onChange={e => setResId(e.target.value)} placeholder="Enter name or UUID…" />
          </div>}

        {verb === 'list' && <div style={sectionStyle}>
            <div style={{
    display: 'flex',
    justifyContent: 'space-between',
    alignItems: 'center',
    marginBottom: '0.6rem'
  }}>
              <span style={{
    fontWeight: 700,
    fontSize: '0.85rem'
  }}>Filters</span>
              <button type="button" onClick={addFilter} style={btnPrimary}>+ Add Filter</button>
            </div>
            {filters.map(f => {
    const subFieldDropdowns = buildSubFieldDropdowns(f);
    const fullPath = [f.field, ...f.subFields].filter(Boolean);
    const fieldInfo = fullPath.length > 0 && resource ? getFieldInfo(resource, fullPath, schemas) : null;
    const typeStr = fieldInfo?.type ?? null;
    const descStr = fieldInfo?.desc ?? null;
    const fieldSelectId = `aqb-f-${f.id}-field`;
    const opSelectId = `aqb-f-${f.id}-op`;
    const valInputId = `aqb-f-${f.id}-val`;
    return <div key={f.id} style={{
      marginBottom: '0.6rem'
    }}>
                  <div style={{
      ...rowStyle,
      alignItems: 'flex-end'
    }}>
                    <div style={cellStyle}>
                      <label htmlFor={fieldSelectId} style={labelStyle}>Field</label>
                      <select id={fieldSelectId} style={selectStyle} value={f.field} onChange={e => updateFilter(f.id, 'field', e.target.value)}>
                        <option value="">Select field…</option>
                        {resourceInfo.fields.map(ff => <option key={ff} value={ff}>{ff}</option>)}
                      </select>
                    </div>
                    {subFieldDropdowns}
                    <div style={{
      ...cellStyle,
      maxWidth: '160px'
    }}>
                      <label htmlFor={opSelectId} style={labelStyle}>Operator</label>
                      <select id={opSelectId} style={selectStyle} value={f.operator} onChange={e => updateFilter(f.id, 'operator', e.target.value)}>
                        {OPERATORS.map(o => <option key={o.value} value={o.value}>{o.label}</option>)}
                      </select>
                    </div>
                    {!EXISTENCE_OPS.has(f.operator) && <div style={cellStyle}>
                        <label htmlFor={valInputId} style={labelStyle}>Value</label>
                        <input id={valInputId} type="text" style={inputStyle} value={f.value} onChange={e => updateFilter(f.id, 'value', e.target.value)} placeholder={typeStr ? 'Value (' + typeStr + ')' : 'Enter value…'} />
                      </div>}
                    <button type="button" onClick={() => removeFilter(f.id)} style={{
      ...btnDanger,
      alignSelf: 'flex-end',
      marginBottom: '1px'
    }}>Remove</button>
                  </div>
                  {descStr && <div style={{
      fontSize: '0.72rem',
      opacity: 0.55,
      marginTop: '0.2rem',
      paddingLeft: '0.25rem',
      lineHeight: 1.4
    }}>
                      {descStr}{typeStr ? ' (' + typeStr + ')' : ''}
                    </div>}
                </div>;
  })}
            {filters.length === 0 && <div style={{
    fontSize: '0.82rem',
    opacity: 0.6
  }}>No filters added. Click "+ Add Filter" to begin.</div>}
          </div>}

        {verb === 'list' && <div style={sectionStyle}>
            <span style={{
    fontWeight: 700,
    fontSize: '0.85rem',
    display: 'block',
    marginBottom: '0.6rem'
  }}>List Parameters</span>
            <div style={{
    ...rowStyle,
    marginBottom: '0.5rem'
  }}>
              <div style={cellStyle}>
                <label htmlFor="aqb-fmask" style={labelStyle}>Field Mask</label>
                <input id="aqb-fmask" type="text" style={inputStyle} value={fMask} onChange={e => setFMask(e.target.value)} placeholder="e.g. spec.level,uuid" />
              </div>
            </div>
            <div style={{
    ...rowStyle,
    marginBottom: '0.5rem'
  }}>
              <div style={cellStyle}>
                <label htmlFor="aqb-pgsize" style={labelStyle}>Page Size</label>
                <input id="aqb-pgsize" type="number" style={inputStyle} value={pgSize} onChange={e => setPgSize(e.target.value)} placeholder="100" />
              </div>
              <div style={cellStyle}>
                <label htmlFor="aqb-pgtoken" style={labelStyle}>Page Token</label>
                <input id="aqb-pgtoken" type="text" style={inputStyle} value={pgToken} onChange={e => setPgToken(e.target.value)} placeholder="Token from previous response" />
              </div>
            </div>
            <div style={{
    ...rowStyle,
    marginBottom: '0.5rem'
  }}>
              <div style={cellStyle}>
                <label htmlFor="aqb-sort-path" style={labelStyle}>Sort Path</label>
                <input id="aqb-sort-path" type="text" style={inputStyle} value={sortPath} onChange={e => setSortPath(e.target.value)} placeholder="e.g. meta.create_time" />
              </div>
              <div style={{
    ...cellStyle,
    maxWidth: '200px'
  }}>
                <label htmlFor="aqb-sort-order" style={labelStyle}>Sort Order</label>
                <select id="aqb-sort-order" style={selectStyle} value={sortOrder} onChange={e => setSortOrder(e.target.value)}>
                  <option value="">Default</option>
                  <option value="SORT_ENTRY_SORT_ORDER_ASC">Ascending</option>
                  <option value="SORT_ENTRY_SORT_ORDER_DESC">Descending</option>
                </select>
              </div>
            </div>
            <div style={{
    ...rowStyle,
    marginBottom: '0.5rem'
  }}>
              <div style={cellStyle}>
                <label htmlFor="aqb-date-from" style={labelStyle}>Date From</label>
                <input id="aqb-date-from" type="date" style={inputStyle} value={dateFrom} onChange={e => setDateFrom(e.target.value)} />
              </div>
              <div style={cellStyle}>
                <label htmlFor="aqb-date-to" style={labelStyle}>Date To</label>
                <input id="aqb-date-to" type="date" style={inputStyle} value={dateTo} onChange={e => setDateTo(e.target.value)} />
              </div>
            </div>
            <div style={{
    ...rowStyle,
    marginBottom: '0.5rem'
  }}>
              <div style={cellStyle}>
                <label htmlFor="aqb-timeout" style={labelStyle}>Timeout (seconds)</label>
                <input id="aqb-timeout" type="number" style={inputStyle} value={timeoutVal} onChange={e => setTimeoutVal(e.target.value)} placeholder="60" />
              </div>
              <div style={{
    display: 'flex',
    gap: '1.25rem',
    alignItems: 'center',
    flex: 1,
    paddingTop: '1.2rem'
  }}>
                <label htmlFor="aqb-count-only" style={{
    display: 'flex',
    gap: '0.4rem',
    alignItems: 'center',
    cursor: 'pointer',
    fontSize: '0.85rem'
  }}>
                  <input id="aqb-count-only" type="checkbox" checked={countOnly} onChange={e => setCountOnly(e.target.checked)} style={{
    accentColor: green
  }} /> Count only
                </label>
                <label htmlFor="aqb-traverse" style={{
    display: 'flex',
    gap: '0.4rem',
    alignItems: 'center',
    cursor: 'pointer',
    fontSize: '0.85rem'
  }}>
                  <input id="aqb-traverse" type="checkbox" checked={traverse} onChange={e => setTraverse(e.target.checked)} style={{
    accentColor: green
  }} /> Traverse
                </label>
              </div>
            </div>
          </div>}

        {verb === 'update' && <div style={sectionStyle}>
            <span style={{
    fontWeight: 700,
    fontSize: '0.85rem',
    display: 'block',
    marginBottom: '0.6rem'
  }}>Update Fields</span>
            <div style={{
    fontSize: '0.78rem',
    opacity: 0.6,
    marginBottom: '0.5rem'
  }}>Select fields to include in the update. Enter values for each checked field.</div>
            {updFields.length === 0 && <div style={{
    fontSize: '0.82rem',
    opacity: 0.6
  }}>No updatable fields found for this operation.</div>}
            {updFields.map((f, i) => {
    const checkboxId = `aqb-uf-${i}`;
    return <div key={f.path} style={{
      display: 'flex',
      gap: '0.6rem',
      alignItems: 'center',
      marginBottom: '0.4rem',
      flexWrap: 'wrap'
    }}>
                  <label htmlFor={checkboxId} style={{
      display: 'flex',
      gap: '0.4rem',
      alignItems: 'center',
      cursor: 'pointer',
      fontSize: '0.82rem',
      minWidth: '200px'
    }}>
                    <input id={checkboxId} type="checkbox" checked={f.checked} onChange={() => toggleUpdField(i)} style={{
      accentColor: green
    }} />
                    <code style={{
      fontSize: '0.78rem',
      opacity: 0.95
    }}>{f.path}</code>
                  </label>
                  {f.checked && renderUpdateFieldInput(f, i)}
                  {f.desc && f.checked && <div style={{
      fontSize: '0.72rem',
      opacity: 0.55,
      width: '100%',
      paddingLeft: '1.5rem'
    }}>{f.desc}</div>}
                </div>;
  })}
          </div>}

        {verb === 'create' && <div style={sectionStyle}>
            <span style={{
    fontWeight: 700,
    fontSize: '0.85rem',
    display: 'block',
    marginBottom: '0.6rem'
  }}>Request Body (JSON)</span>
            <div style={{
    fontSize: '0.78rem',
    opacity: 0.6,
    marginBottom: '0.5rem'
  }}>Edit the JSON template below. Fields are pre-populated from the API schema.</div>
            <textarea id="aqb-body" style={{
    ...inputStyle,
    minHeight: '200px',
    fontFamily: "Monaco, Menlo, 'Ubuntu Mono', monospace",
    fontSize: '0.8rem',
    lineHeight: 1.5,
    resize: 'vertical'
  }} value={body} onChange={e => setBody(e.target.value)} />
          </div>}

        {currentOp && <div style={{
    marginTop: '0.75rem'
  }}>
            <label htmlFor="aqb-out-type" style={labelStyle}>Output Type</label>
            <select id="aqb-out-type" style={selectStyle} value={outType} onChange={e => setOutType(e.target.value)}>
              <option value="">Default (table)</option>
              <option value="json">JSON</option>
              <option value="yaml">YAML</option>
            </select>
          </div>}

        {command && <div style={{
    marginTop: '1rem'
  }}>
            <div style={{
    display: 'flex',
    justifyContent: 'space-between',
    alignItems: 'center',
    marginBottom: '0.5rem'
  }}>
              <span style={{
    fontWeight: 700,
    fontSize: '0.9rem'
  }}>Generated Command</span>
              <button type="button" onClick={handleCopy} style={btnPrimary}>Copy</button>
            </div>
            <div style={codeStyle}>{command}</div>
          </div>}

        {toast && <div style={{
    marginTop: '0.5rem',
    padding: '0.45rem 0.75rem',
    background: bgL,
    border: '1px solid ' + bd + '0.2)',
    borderRadius: '8px',
    fontSize: '0.82rem',
    textAlign: 'center'
  }}>{toast}</div>}
      </div>
    </div>;
};

endorctl API is the most user-friendly way to interact with the Endor Labs REST API. It handles authentication automatically, provides built-in error handling with clear messages, and supports multiple output formats including json, yaml, and table.

## Prerequisites

Ensure that you complete the following prerequisites before running the commands generated by the API query builder:

* Install the latest version of [endorctl](/developers-api/cli/install-and-configure) before running the commands generated by the API query builder.
* Run `endorctl init` to authenticate with Endor Labs. See [endorctl init](/developers-api/cli/commands/init) for more information.

## About the API query builder

The API query builder allows you to construct [endorctl API](/developers-api/cli/commands/api) commands based on API operations and parameters. The tool currently supports all API operations available in [Top REST API Reference](https://docs.endorlabs.com/top-api/). You can refer to endorctl API command documentation to construct your own queries that are not supported by the API query builder. You can also refer to [Use Cases](/developers-api/rest-api/using-the-rest-api/use-cases) and [Advanced Use Cases](/developers-api/rest-api/using-the-rest-api/advanced-use-cases) to get examples of how you can use endorctl API commands.

The tool includes options for timeouts, date filtering, and pagination. It also simplifies complex queries with easy filtering, sorting, and field selection. endorctl also supports grouping queries, which is not currently supported by the API query builder. See [Grouping](/developers-api/rest-api/using-the-rest-api/grouping) for more information.

See the [Top REST API Reference](https://docs.endorlabs.com/top-api/) to get the details of request json for the operations that need a json request body. The request body appears in the API query builder for update operations, but you might need to modify it to fit your needs.

<ApiQueryBuilder specUrl="/api-reference/topapi.v3.json" />
