kirin-ri / memo

0 stars 0 forks source link

wwww #32

Open kirin-ri opened 1 month ago

kirin-ri commented 1 month ago
collections = ['api_node', 'metrics_node',
               'api_reference_edge', 'metrics_relation_edge']

for collection in collections:
    jsonOpen = open(f'{path}{filePath}graphdata/{collection}.json', 'r')
    jsonLoad = json.load(jsonOpen)
    if collection == "metrics_node":
        if PROJECT == "startpack":

    insertDocuments(collection=collection,
                    data=jsonLoad, database=arangoDb)
kirin-ri commented 1 month ago
    if collection == "metrics_node" and PROJECT != "startpack":
        # 假设 jsonLoad 是一个包含字典的列表
        for item in jsonLoad:
            for key, value in item.items():
                if value == "INDUSTRIAL":
                    item[key] = "a"
kirin-ri commented 1 month ago
  {
    "_key": "PARTSSTOCK_OUTOFSTOCK_MONTHLY",
    "metrics_id": "PARTSSTOCK_OUTOFSTOCK_MONTHLY",
    "disp_name": "部品在庫欠品:月次",
    "provide": false,
    "in_info": [
      {
        "db": "INDUSTRIAL_DNA_DB",
        "schema": "DATASET_SCHEMA",
        "table": "priprt_inventory",
        "column": [
          {
            "logicalName": "工場_生産拠点",
            "physicalName": "SUPPLIER_NAME",
            "type": "文字列"
          },
          {
            "logicalName": "一次部品_部品番号",
            "physicalName": "PARTS_NO",
            "type": "文字列"
          },
          {
            "logicalName": "在庫確認日",
            "physicalName": "STOCK_DATE",
            "type": "日付"
          },
          {
            "logicalName": "在庫数",
            "physicalName": "STOCK_QTY",
            "type": "数値"
          }
        ]
      },
      {
        "db": "INDUSTRIAL_DNA_DB",
        "schema": "DATASET_SCHEMA",
        "table": "components_master",
        "column": [
          {
            "logicalName": "構成品目_子品目",
            "physicalName": "COMPONENT",
            "type": "文字列"
          },
          {
            "logicalName": "品目コード_親品目",
            "physicalName": "ITEM_CODE",
            "type": "文字列"
          },
          {
            "logicalName": "員数",
            "physicalName": "PARTS_NUMBER",
            "type": "数値"
          }
        ]
      },
      {
        "db": "INDUSTRIAL_DNA_DB",
        "schema": "DATASET_SCHEMA",
        "table": "fg_prd_m_plan",
        "column": [
          {
            "logicalName": "工場_生産拠点",
            "physicalName": "FACTORY_NAME",
            "type": "文字列"
          },
          {
            "logicalName": "完成品_生産機種型番",
            "physicalName": "FINAL_PRODUCT",
            "type": "文字列"
          },
          {
            "logicalName": "生産日",
            "physicalName": "PRODUCTION_DATE",
            "type": "日付"
          },
          {
            "logicalName": "生産数量",
            "physicalName": "PRODUCTION_QTY",
            "type": "数値"
          }
        ]
      },
kirin-ri commented 1 month ago
    if collection == "metrics_node" and PROJECT != "startpack":
        # 遍历 JSON 数据并替换 'INDUSTRIAL' 为 'a'
        def replace_industrial(data):
            if isinstance(data, dict):
                for key, value in data.items():
                    if isinstance(value, str) and 'INDUSTRIAL' in value:
                        data[key] = value.replace('INDUSTRIAL', 'a')
                    else:
                        replace_industrial(value)
            elif isinstance(data, list):
                for index, item in enumerate(data):
                    if isinstance(item, str) and 'INDUSTRIAL' in item:
                        data[index] = item.replace('INDUSTRIAL', 'a')
                    else:
                        replace_industrial(item)

        # 调用递归函数替换所有 'INDUSTRIAL' 字符串
        replace_industrial(jsonLoad)
kirin-ri commented 1 month ago

コンフルエンスを説明いたします。 改修箇所の整理部分について、前回の中間レビュー後の変更点は赤字です。 データセット関連のコンフルを修正しようと思ってましたが、参照DBの情報が記載されていないため、更新はしていません。

サンプルデータ挿入の処理で接続先情報を更新しました。 また、中間レビュー時に挿入データの削除機能が対応していないため、追記しました。。 削除時の対象をPUBLICからDATASET_SCHEMAに変更しました。

メトリクス関連について、IN、OUT以外に中間レビュー時、計算データの削除の対応が漏れていたので、追記しました。 各メトリクスの入力出力参照DBを更新しました。 おなじく記載ないところは更新していません。 同じ修正を実施したので、各コンフルエンスの展開は割愛させていただきます。

データ分析管理画面は判定条件を更新しました。コンフルに処理詳細がDB記載がないため、更新はないです。

個社コピー・削除はsnowflakeの作成SCHEMAの変更とDPB/DPL部分をコメントアウトしました。

それ以外、動作確認中に2点対応しています。 まず個社コピー後、初回CI実行時にscm-metricsとの差分を見て動いているため、CR90などコピー時に削除されたメトリクスファイルもCIの対象内になっているため、エラーになりました。 初回の判定条件で削除ファイルを無視するため、shellを書き換えています。 diff-fitlerを追加しています。 改修内容は前スプリントのCICDテンプレート化に含まれています。 動作検証も個社コピー実施して確認しました。

個社コピーでArangoコピー処理は個社ごとに対応していませんでした。 Arangoコピー処理で個社名を判別し、DB名書き換える処理を追加しました。 既存環境は手動でArango情報を修正し、個社コピー機能で動作確認をしました。 dbとschema名が個社ごとに対応していることを確認しました。

改修点としては以上ですが、なにか質問ありますか?

動作確認は個社ごとからCATALOGAPの各機能をテストしました。

エビデンスとしては以上です。なにか気になるところございますでしょうか?

受け入れ条件に記載していた、データ追加、サンプルデータ挿入とメトリクス即時実行を実施します。 使用する環境は個社コピーによって作成先されたchangedbです。

kirin-ri commented 1 month ago
// fillter tag
function FilterTags({
  tags,
  setTypes,
}: {
  tags: Tag[];
  setTypes: React.Dispatch<React.SetStateAction<Tag[]>>;
}) {
  const handleClick = (item: Tag) => {
    // eslint-disable-next-line no-param-reassign
    item.selected = !item.selected;
    const t = tags.slice(0, tags.length);
    setTypes(t);
  };
  return (
    <div className="inline-form">
      <div className="inline-form-cat">フィルタータグ</div>
      <div className="inline-form-label">ジョブ種別</div>
      <div className="inline-form-group">
        {tags.map((item: Tag) => (
          <button
            type="button"
            className={`btn btn-tag${item.selected ? ' active' : ''}`}
            key={item.name}
            onClick={() => handleClick(item)}
          >
            {item.name}
          </button>
        ))}
      </div>
    </div>
  );
}
kirin-ri commented 1 month ago

import parse from 'html-react-parser'; import React, { useEffect, useLayoutEffect, useState } from 'react'; import ReactPaginate from 'react-paginate'; import { useHistory } from 'react-router-dom'; import Swal from 'sweetalert2'; import withReactContent from 'sweetalert2-react-content'; import { commonAjax } from '../../../components/commonAjax'; import { currentJST } from '../util/dateTimeUtil'; import { DialogModalOK, showDialog } from './dialogModal';

// eslint-disable-next-line @typescript-eslint/no-var-requires const moment = require('moment'); require('moment-duration-format');

const MySwal = withReactContent(Swal);

interface Props { match: { params: { id?: string; }; }; }

type JobEntry = { id: string; type: string; name: string; execDate: string; endDate: string | null; status: Status; warning: boolean; };

type JobDetailsTypes = { id: string; type: string; name: string; currPhase: number; totalPhase: number; phaseDesc: string; execDate: string; endDate: string | null; status: Status; warning: boolean; errorInfo: { message: string; description: string; userAction: string; dumpMsg: string | null; dumpData: string | null; } | null; externalQuery: { apiTablePhy?: string; putApiDataTran?: string; metrics?: string; putMetricsTran?: string; } | null; };

type Tag = { name: string; selected: boolean; };

type Status = 'running' | 'finished' | 'error';

function ProgressIndicator({ info }: { info?: JobDetailsTypes }) { const progList: JSX.Element[] = []; if (info) { for (let i = 0; i <= info.totalPhase + 1; i += 1) { let type; let stat; const content = null; if (i === 0) { type = 'progress-start'; } else if (i <= info.totalPhase) { type = 'progress-phase'; } else { type = 'progress-finish'; }

  if (i < info.currPhase) {
    stat = 'progress-done';
  } else if (i === info.currPhase) {
    if (info.status === 'running') {
      stat = 'progress-doing';
    } else if (info.status === 'finished') {
      stat = 'progress-done';
    } else {
      stat = 'progress-error';
    }
  } else if (i === info.totalPhase + 1) {
    if (info.status === 'finished') {
      stat = info.warning ? 'progress-warning' : 'progress-done';
    } else {
      stat = 'progress-todo';
    }
  } else {
    stat = 'progress-todo';
  }
  progList.push(
    <div className={`${type} ${stat}`} key={i}>
      {content}
    </div>,
  );
}

}

return (

{progList}
{info?.phaseDesc}

); }

const getJobStatus = (status: Status, warning: boolean) => { if (status === 'running') { return ( <> 実行中   {warning && ( )} </> ); } if (status === 'finished') { if (warning) { return 警告終了; } return 正常終了; } if (status === 'error') { return 異常終了; } if (status === 'canceled') { return 処理中断; } // eslint-disable-next-line react/jsx-no-useless-fragment return <>{status}</>; };

function ErrorDetailModal({ info }: { info?: JobDetailsTypes }) { const dumpMessage = (info?.errorInfo?.dumpMsg || '') .split(/(\r?\n)/) .map((line, idx) => ( // eslint-disable-next-line react/no-array-index-key

{line.match(/\r?\n/) ?
: parse(line)}
)); return (
メッセージ
{info?.errorInfo?.message}
詳細
{info?.errorInfo?.description && parse(info.errorInfo.description)}
アクション
{info?.errorInfo?.userAction && parse(info.errorInfo.userAction)}
内部メッセージ

Job ID: {info?.id}

{dumpMessage}

{info?.errorInfo?.dumpData}

); } function JobDetails({ id, load }: { id?: string; load: VoidFunction }) { const [currId, setCurrId] = useState(id); const [info, setInfo] = useState(); const [beginUpdate, setBeginUpdate] = useState(false); const [timer, setTimer] = useState(null); const fetchLatest = () => { commonAjax .axios() .get('/api/jobs/latest') .then((res) => { const { job } = res.data; setInfo(job); setCurrId(job.id); }); }; const update = () => { commonAjax .axios() .get(`/api/jobs/${currId}`) .then((res) => { const { job } = res.data; if (job) { if (job.status === 'running') { setBeginUpdate(true); } else if (timer) { clearTimeout(timer); setTimer(null); } } load(); setInfo(job); }); }; const handleErrorBtnClicked = () => { showDialog('errorDetailModal'); }; let elapsedTime = ''; let status = null; if (info) { // Calculate elapsed time since the job started const since = moment(info.execDate); const until = moment(info.endDate ?? currentJST()); elapsedTime = moment .duration(until.diff(since)) .format('hh:mm:ss', { trim: false }); status = getJobStatus(info.status, info.warning); } // ジョブ中断 const handleCancelClick = () => { commonAjax .axios() .post(`/api/jobs/cancel`, { jobId: currId, }) .then(() => { update(); }) .catch((err) => { MySwal.fire({ icon: 'error', title: `ERROR: ${err}`, toast: true, position: 'top-end', showConfirmButton: false, timer: 3000, }); }); }; // サンプルデータ挿入データ削除 const handleDeleteApiDataClick = () => { const targetData = { apiTablePhy: info?.externalQuery?.apiTablePhy, putApiDataTran: info?.externalQuery?.putApiDataTran, }; commonAjax .axios({ swalFire: false, loading: true }) .delete(`/api/api/deleteData`, { data: targetData, }) .then((res) => { if (!res.data.dataExistFlg) { MySwal.fire({ icon: 'error', title: `ERROR: ${res.data.message}`, toast: true, position: 'top-end', showConfirmButton: false, timer: 3000, }); } else { MySwal.fire({ icon: 'success', title: `Message: ${res.data.message}`, toast: true, position: 'top-end', showConfirmButton: false, timer: 3000, }); } }) .catch((err) => { MySwal.fire({ icon: 'error', title: `ERROR: ${err}`, toast: true, position: 'top-end', showConfirmButton: false, timer: 3000, }); }); }; // メトリクス実行データ削除 const handleDeleteMetricsDataClick = () => { const targetData = { metrics: info?.externalQuery?.metrics, putMetricsTran: info?.externalQuery?.putMetricsTran, }; commonAjax .axios({ swalFire: false, loading: true }) .delete(`/api/api/deleteMetricsData`, { data: targetData, }) .then((res) => { if (!res.data.dataExistFlg) { MySwal.fire({ icon: 'error', title: `ERROR: ${res.data.message}`, toast: true, position: 'top-end', showConfirmButton: false, timer: 3000, }); } else { MySwal.fire({ icon: 'success', title: `Message: ${res.data.message}`, toast: true, position: 'top-end', showConfirmButton: false, timer: 3000, }); } }) .catch((err) => { MySwal.fire({ icon: 'error', title: `ERROR: ${err}`, toast: true, position: 'top-end', showConfirmButton: false, timer: 3000, }); }); }; const cancelAndUpdateButtonHidden = info?.status === 'running' && info?.type === 'メトリクス即時実行'; const deleteApiDataButtonHidden = info?.type === 'サンプルデータ挿入'; const deleteMetricsDataButtonHidden = info?.type === 'メトリクス即時実行'; useEffect(() => { if (!currId) { fetchLatest(); } else if (currId !== id) { setCurrId(id); } }, [id]); useEffect(() => { let ignore = false; if (timer) { clearTimeout(timer); setTimer(null); } if (currId) { setTimeout(() => !ignore && update()); } return () => { ignore = true; }; }, [currId]); useLayoutEffect(() => { if (beginUpdate) { const newTimer = setTimeout(update, 3000); setTimer(newTimer); setBeginUpdate(false); } }, [beginUpdate]); return (
{ // eslint-disable-next-line no-nested-ternary cancelAndUpdateButtonHidden ? ( ) : deleteApiDataButtonHidden ? ( ) : ( deleteMetricsDataButtonHidden && ( ) ) }
ジョブID
{info?.id}
種別
{info?.type}
処理名
{info?.name}
実行日時
{info?.execDate}
終了日時
{info?.endDate}
所要時間
{elapsedTime}
状態
{status}  {info?.errorInfo && ( )}
); } // fillter tag function FilterTags({ tags, setTypes, }: { tags: Tag[]; setTypes: React.Dispatch>; }) { const handleClick = (item: Tag) => { // eslint-disable-next-line no-param-reassign item.selected = !item.selected; const t = tags.slice(0, tags.length); setTypes(t); }; return (
フィルタータグ
ジョブ種別
{tags.map((item: Tag) => ( ))}
); } function JobListEntry({ entry, onClick, }: { entry: JobEntry; onClick: (id: string) => void; }) { const handleClick = () => { onClick(entry.id); }; const status = getJobStatus(entry.status, entry.warning); return ( {entry.id} {entry.type} {entry.name} {status} {entry.execDate} {entry.endDate} ); } function JobList({ onJobSelected, load, list, types, setTypes, }: { onJobSelected: (id: string) => void; load: VoidFunction; list: JobEntry[] | undefined; types: Tag[]; setTypes: React.Dispatch>; }) { const perPage = 10; // const [types, setTypes] = useState([]); // const [list, setList] = useState(); const [offset, setOffset] = useState(0); useEffect(() => { load(); }, [types]); const handlePageChange = (data: { selected: number }) => { const page = data.selected; setOffset(page * perPage); }; if (list) { return (
{list.length > 0 && ( <> {list .filter( (e: JobEntry, idx: number) => idx >= offset && idx < offset + perPage, ) .map((e: JobEntry) => ( ))}
ID 種別 処理名 状態 実行日時 終了日時
)} {list.length === 0 && (
ジョブはありません
)}
); } // eslint-disable-next-line react/jsx-no-useless-fragment return <>; } function JobStatus(props: Props) { const { match } = props; const history = useHistory(); const [id, setId] = useState(match.params.id); const [list, setList] = useState(); const [types, setTypes] = useState([]); useEffect(() => { setId(match.params.id); }, [match.params.id, history]); const handleJobSelected = (targetId: string) => { setId(targetId); history.push(`/jobs/${targetId}`); }; // ジョブ終了後に一覧更新の必要があるため、メインコンポーネントに移動 const load = () => { const req = { types: types.filter((t) => t.selected).map((t) => t.name), }; commonAjax .axios() .post('/api/jobs', req) .then((res) => { const { data } = res; setList( data.list.sort( ( a: { status: string; id: string }, b: { status: string; id: string }, ) => // eslint-disable-next-line no-nested-ternary a.status === 'running' && b.status !== 'running' ? -1 : a.status !== 'running' && b.status === 'running' ? 1 : parseInt(b.id, 10) - parseInt(a.id, 10), ), ); if (!types.length) { setTypes( data.types.map((t: string) => ({ name: t, selected: true })), ); } }); }; return (

ジョブステータス確認

); } // const JOB_TYPES = [ // 'API追加', // 'API更新', // 'メトリクスデプロイ', // '新規メトリクス追加', // 'サンプルデータ挿入', // 'メトリクス改修', // ]; // , "個社環境コピー", "個社環境削除", "デモ環境初期化" // const SAMPLE_JOBS: { [key: string]: JobDetails } = { // '123': { // id: '123', // type: 'サンプルデータ挿入', // name: '完成品販売計画_月次 サンプルデータ挿入処理', // currPhase: 2, // totalPhase: 3, // phaseDesc: 'データ挿入中', // execDate: '2023-07-31 13:55:00', // endDate: null, // status: 'running' as Status, // warning: false, // errorInfo: null, // }, // '122': { // id: '122', // type: 'サンプルデータ挿入', // name: '完成品販売計画_月次 サンプルデータ挿入処理', // currPhase: 2, // totalPhase: 3, // phaseDesc: 'データ挿入中', // execDate: '2023-07-25 05:55:00', // endDate: null, // status: 'running' as Status, // warning: true, // errorInfo: { // message: 'データバリデーションエラー', // description: // '必要な情報が欠落しているか、データが規定の形式に適合していません。', // userAction: '入力内容を確認して再度お試しください。', // dumpMsg: null, // dumpData: null, // }, // }, // '121': { // id: '121', // type: 'サンプルデータ挿入', // name: '完成品販売計画_月次 サンプルデータ挿入処理', // currPhase: 3, // totalPhase: 3, // phaseDesc: 'リモートリポジトリをclone中', // execDate: '2023-07-24 13:43:55', // endDate: '2023-07-24 13:56:08', // status: 'finished' as Status, // warning: true, // errorInfo: { // message: 'データバリデーションエラー', // description: // '必要な情報が欠落しているか、データが規定の形式に適合していません。', // userAction: '入力内容を確認して再度お試しください。', // dumpMsg: null, // dumpData: null, // }, // }, // '120': { // id: '120', // type: 'API作成', // name: '"TEST API" 作成処理', // currPhase: 2, // totalPhase: 10, // phaseDesc: 'リモートリポジトリをclone中', // execDate: '2023-07-24 13:43:55', // endDate: '2023-07-24 13:56:08', // status: 'error' as Status, // warning: false, // errorInfo: { // message: 'リモートリポジトリのcloneに失敗', // description: // 'Githubの認証に失敗したため、リモートリポジトリがcloneできませんでした。', // userAction: // '本画面のスクリーンショットを取得し、サポートへお問い合わせください。', // dumpMsg: 'git.exc.GitCommandError', // dumpData: // "git.exc.GitCommandError: Cmd('git') failed due to: exit code(128)\n" + // "cmdline: git clone -v -b feature/ind_tmp https://*****:*****@github.com/i4platform/i4-dpb_script.git tmp/i4-dpb\nstderr: 'Cloning into 'tmp/i4-dpb'...\n" + // 'remote: Invalid username or password.\n' + // "fatal: Authentication failed for 'https://github.com/i4platform/i4-dpb_script.git/'", // }, // }, // '119': { // id: '119', // type: 'サンプルデータ挿入', // name: '完成品販売計画_月次 サンプルデータ挿入処理', // currPhase: 3, // totalPhase: 3, // phaseDesc: '処理終了', // execDate: '2023-07-25 05:55:00', // endDate: '2023-07-25 06:15:18', // status: 'finished' as Status, // warning: false, // errorInfo: null, // }, // }; // const TESTDATA_LIST = [ // { // id: '123', // type: 'サンプルデータ挿入', // name: '完成品販売計画_月次 サンプルデータ挿入処理', // execDate: '2023-07-24 14:43:55', // endDate: null, // status: 'running' as Status, // warning: false, // }, // { // id: '122', // type: 'サンプルデータ挿入', // name: '完成品販売計画_月次 サンプルデータ挿入処理', // execDate: '2023-07-24 11:23:55', // endDate: null, // status: 'running' as Status, // warning: true, // }, // { // id: '121', // type: 'サンプルデータ挿入', // name: '完成品販売_月次 サンプルデータ挿入処理', // execDate: '2023-07-24 10:23:55', // endDate: '2023-07-24 10:33:55', // status: 'finished' as Status, // warning: true, // }, // { // id: '120', // type: 'API更新', // name: '完成品販売計画_月次 API更新処理', // execDate: '2023-07-24 13:23:55', // endDate: '2023-07-24 13:43:55', // status: 'error' as Status, // warning: false, // }, // { // id: '119', // type: 'サンプルデータ挿入', // name: '完成品販売_月次 サンプルデータ挿入処理', // execDate: '2023-07-24 10:23:55', // endDate: '2023-07-24 10:33:55', // status: 'finished' as Status, // warning: false, // }, // { // id: '118', // type: 'サンプルデータ挿入', // name: '完成品販売_月次 サンプルデータ挿入処理', // execDate: '2023-07-24 10:23:55', // endDate: '2023-07-24 10:33:55', // status: 'finished' as Status, // warning: false, // }, // { // id: '117', // type: 'サンプルデータ挿入', // name: '完成品販売_月次 サンプルデータ挿入処理', // execDate: '2023-07-24 10:23:55', // endDate: '2023-07-24 10:33:55', // status: 'finished' as Status, // warning: false, // }, // { // id: '116', // type: 'サンプルデータ挿入', // name: '完成品販売_月次 サンプルデータ挿入処理', // execDate: '2023-07-24 10:23:55', // endDate: '2023-07-24 10:33:55', // status: 'finished' as Status, // warning: false, // }, // { // id: '115', // type: 'サンプルデータ挿入', // name: '完成品販売_月次 サンプルデータ挿入処理', // execDate: '2023-07-24 10:23:55', // endDate: '2023-07-24 10:33:55', // status: 'finished' as Status, // warning: false, // }, // { // id: '114', // type: 'サンプルデータ挿入', // name: '完成品販売_月次 サンプルデータ挿入処理', // execDate: '2023-07-24 10:23:55', // endDate: '2023-07-24 10:33:55', // status: 'finished' as Status, // warning: false, // }, // ]; export default JobStatus;
kirin-ri commented 1 month ago

`import parse from 'html-react-parser'; import React, { useEffect, useLayoutEffect, useState } from 'react'; import ReactPaginate from 'react-paginate'; import { useHistory } from 'react-router-dom'; import Swal from 'sweetalert2'; import withReactContent from 'sweetalert2-react-content'; import { commonAjax } from '../../../components/commonAjax'; import { currentJST } from '../util/dateTimeUtil'; import { DialogModalOK, showDialog } from './dialogModal';

// eslint-disable-next-line @typescript-eslint/no-var-requires const moment = require('moment'); require('moment-duration-format');

const MySwal = withReactContent(Swal);

interface Props { match: { params: { id?: string; }; }; }

type JobEntry = { id: string; type: string; name: string; execDate: string; endDate: string | null; status: Status; warning: boolean; };

type JobDetailsTypes = { id: string; type: string; name: string; currPhase: number; totalPhase: number; phaseDesc: string; execDate: string; endDate: string | null; status: Status; warning: boolean; errorInfo: { message: string; description: string; userAction: string; dumpMsg: string | null; dumpData: string | null; } | null; externalQuery: { apiTablePhy?: string; putApiDataTran?: string; metrics?: string; putMetricsTran?: string; } | null; };

type Tag = { name: string; selected: boolean; };

type Status = 'running' | 'finished' | 'error';

function ProgressIndicator({ info }: { info?: JobDetailsTypes }) { const progList: JSX.Element[] = []; if (info) { for (let i = 0; i <= info.totalPhase + 1; i += 1) { let type; let stat; const content = null; if (i === 0) { type = 'progress-start'; } else if (i <= info.totalPhase) { type = 'progress-phase'; } else { type = 'progress-finish'; }

  if (i < info.currPhase) {
    stat = 'progress-done';
  } else if (i === info.currPhase) {
    if (info.status === 'running') {
      stat = 'progress-doing';
    } else if (info.status === 'finished') {
      stat = 'progress-done';
    } else {
      stat = 'progress-error';
    }
  } else if (i === info.totalPhase + 1) {
    if (info.status === 'finished') {
      stat = info.warning ? 'progress-warning' : 'progress-done';
    } else {
      stat = 'progress-todo';
    }
  } else {
    stat = 'progress-todo';
  }
  progList.push(
    <div className={`${type} ${stat}`} key={i}>
      {content}
    </div>,
  );
}

}

return (

{progList}
{info?.phaseDesc}

); }

const getJobStatus = (status: Status, warning: boolean) => { if (status === 'running') { return ( <> 実行中   {warning && ( )} </> ); } if (status === 'finished') { if (warning) { return 警告終了; } return 正常終了; } if (status === 'error') { return 異常終了; } if (status === 'canceled') { return 処理中断; } // eslint-disable-next-line react/jsx-no-useless-fragment return <>{status}</>; };

function ErrorDetailModal({ info }: { info?: JobDetailsTypes }) { const dumpMessage = (info?.errorInfo?.dumpMsg || '') .split(/(\r?\n)/) .map((line, idx) => ( // eslint-disable-next-line react/no-array-index-key

{line.match(/\r?\n/) ?
: parse(line)}
)); return (
メッセージ
{info?.errorInfo?.message}
詳細
{info?.errorInfo?.description && parse(info.errorInfo.description)}
アクション
{info?.errorInfo?.userAction && parse(info.errorInfo.userAction)}
内部メッセージ

Job ID: {info?.id}

{dumpMessage}

{info?.errorInfo?.dumpData}

); } function JobDetails({ id, load }: { id?: string; load: VoidFunction }) { const [currId, setCurrId] = useState(id); const [info, setInfo] = useState(); const [beginUpdate, setBeginUpdate] = useState(false); const [timer, setTimer] = useState(null); const fetchLatest = () => { commonAjax .axios() .get('/api/jobs/latest') .then((res) => { const { job } = res.data; setInfo(job); setCurrId(job.id); }); }; const update = () => { commonAjax .axios() .get(`/api/jobs/${currId}`) .then((res) => { const { job } = res.data; if (job) { if (job.status === 'running') { setBeginUpdate(true); } else if (timer) { clearTimeout(timer); setTimer(null); } } load(); setInfo(job); }); }; const handleErrorBtnClicked = () => { showDialog('errorDetailModal'); }; let elapsedTime = ''; let status = null; if (info) { // Calculate elapsed time since the job started const since = moment(info.execDate); const until = moment(info.endDate ?? currentJST()); elapsedTime = moment .duration(until.diff(since)) .format('hh:mm:ss', { trim: false }); status = getJobStatus(info.status, info.warning); } // ジョブ中断 const handleCancelClick = () => { commonAjax .axios() .post(`/api/jobs/cancel`, { jobId: currId, }) .then(() => { update(); }) .catch((err) => { MySwal.fire({ icon: 'error', title: `ERROR: ${err}`, toast: true, position: 'top-end', showConfirmButton: false, timer: 3000, }); }); }; // サンプルデータ挿入データ削除 const handleDeleteApiDataClick = () => { const targetData = { apiTablePhy: info?.externalQuery?.apiTablePhy, putApiDataTran: info?.externalQuery?.putApiDataTran, }; commonAjax .axios({ swalFire: false, loading: true }) .delete(`/api/api/deleteData`, { data: targetData, }) .then((res) => { if (!res.data.dataExistFlg) { MySwal.fire({ icon: 'error', title: `ERROR: ${res.data.message}`, toast: true, position: 'top-end', showConfirmButton: false, timer: 3000, }); } else { MySwal.fire({ icon: 'success', title: `Message: ${res.data.message}`, toast: true, position: 'top-end', showConfirmButton: false, timer: 3000, }); } }) .catch((err) => { MySwal.fire({ icon: 'error', title: `ERROR: ${err}`, toast: true, position: 'top-end', showConfirmButton: false, timer: 3000, }); }); }; // メトリクス実行データ削除 const handleDeleteMetricsDataClick = () => { const targetData = { metrics: info?.externalQuery?.metrics, putMetricsTran: info?.externalQuery?.putMetricsTran, }; commonAjax .axios({ swalFire: false, loading: true }) .delete(`/api/api/deleteMetricsData`, { data: targetData, }) .then((res) => { if (!res.data.dataExistFlg) { MySwal.fire({ icon: 'error', title: `ERROR: ${res.data.message}`, toast: true, position: 'top-end', showConfirmButton: false, timer: 3000, }); } else { MySwal.fire({ icon: 'success', title: `Message: ${res.data.message}`, toast: true, position: 'top-end', showConfirmButton: false, timer: 3000, }); } }) .catch((err) => { MySwal.fire({ icon: 'error', title: `ERROR: ${err}`, toast: true, position: 'top-end', showConfirmButton: false, timer: 3000, }); }); }; const cancelAndUpdateButtonHidden = info?.status === 'running' && info?.type === 'メトリクス即時実行'; const deleteApiDataButtonHidden = info?.type === 'サンプルデータ挿入'; const deleteMetricsDataButtonHidden = info?.type === 'メトリクス即時実行'; useEffect(() => { if (!currId) { fetchLatest(); } else if (currId !== id) { setCurrId(id); } }, [id]); useEffect(() => { let ignore = false; if (timer) { clearTimeout(timer); setTimer(null); } if (currId) { setTimeout(() => !ignore && update()); } return () => { ignore = true; }; }, [currId]); useLayoutEffect(() => { if (beginUpdate) { const newTimer = setTimeout(update, 3000); setTimer(newTimer); setBeginUpdate(false); } }, [beginUpdate]); return (
{ // eslint-disable-next-line no-nested-ternary cancelAndUpdateButtonHidden ? ( ) : deleteApiDataButtonHidden ? ( ) : ( deleteMetricsDataButtonHidden && ( ) ) }
ジョブID
{info?.id}
種別
{info?.type}
処理名
{info?.name}
実行日時
{info?.execDate}
終了日時
{info?.endDate}
所要時間
{elapsedTime}
状態
{status}  {info?.errorInfo && ( )}
); } // fillter tag function FilterTags({ tags, setTypes, }: { tags: Tag[]; setTypes: React.Dispatch>; }) { const handleClick = (item: Tag) => { // eslint-disable-next-line no-param-reassign item.selected = !item.selected; const t = tags.slice(0, tags.length); setTypes(t); }; return (
フィルタータグ
ジョブ種別
{tags.map((item: Tag) => ( ))}
); } function JobListEntry({ entry, onClick, }: { entry: JobEntry; onClick: (id: string) => void; }) { const handleClick = () => { onClick(entry.id); }; const status = getJobStatus(entry.status, entry.warning); return ( {entry.id} {entry.type} {entry.name} {status} {entry.execDate} {entry.endDate} ); } function JobList({ onJobSelected, load, list, types, setTypes, }: { onJobSelected: (id: string) => void; load: VoidFunction; list: JobEntry[] | undefined; types: Tag[]; setTypes: React.Dispatch>; }) { const perPage = 10; // const [types, setTypes] = useState([]); // const [list, setList] = useState(); const [offset, setOffset] = useState(0); useEffect(() => { load(); }, [types]); const handlePageChange = (data: { selected: number }) => { const page = data.selected; setOffset(page * perPage); }; if (list) { return (
{list.length > 0 && ( <> {list .filter( (e: JobEntry, idx: number) => idx >= offset && idx < offset + perPage, ) .map((e: JobEntry) => ( ))}
ID 種別 処理名 状態 実行日時 終了日時
)} {list.length === 0 && (
ジョブはありません
)}
); } // eslint-disable-next-line react/jsx-no-useless-fragment return <>; } function JobStatus(props: Props) { const { match } = props; const history = useHistory(); const [id, setId] = useState(match.params.id); const [list, setList] = useState(); const [types, setTypes] = useState([]); useEffect(() => { setId(match.params.id); }, [match.params.id, history]); const handleJobSelected = (targetId: string) => { setId(targetId); history.push(`/jobs/${targetId}`); }; // ジョブ終了後に一覧更新の必要があるため、メインコンポーネントに移動 const load = () => { const req = { types: types.filter((t) => t.selected).map((t) => t.name), }; commonAjax .axios() .post('/api/jobs', req) .then((res) => { const { data } = res; setList( data.list.sort( ( a: { status: string; id: string }, b: { status: string; id: string }, ) => // eslint-disable-next-line no-nested-ternary a.status === 'running' && b.status !== 'running' ? -1 : a.status !== 'running' && b.status === 'running' ? 1 : parseInt(b.id, 10) - parseInt(a.id, 10), ), ); if (!types.length) { setTypes( data.types.map((t: string) => ({ name: t, selected: true })), ); } }); }; return (

ジョブステータス確認

); } // const JOB_TYPES = [ // 'API追加', // 'API更新', // 'メトリクスデプロイ', // '新規メトリクス追加', // 'サンプルデータ挿入', // 'メトリクス改修', // ]; // , "個社環境コピー", "個社環境削除", "デモ環境初期化" // const SAMPLE_JOBS: { [key: string]: JobDetails } = { // '123': { // id: '123', // type: 'サンプルデータ挿入', // name: '完成品販売計画_月次 サンプルデータ挿入処理', // currPhase: 2, // totalPhase: 3, // phaseDesc: 'データ挿入中', // execDate: '2023-07-31 13:55:00', // endDate: null, // status: 'running' as Status, // warning: false, // errorInfo: null, // }, // '122': { // id: '122', // type: 'サンプルデータ挿入', // name: '完成品販売計画_月次 サンプルデータ挿入処理', // currPhase: 2, // totalPhase: 3, // phaseDesc: 'データ挿入中', // execDate: '2023-07-25 05:55:00', // endDate: null, // status: 'running' as Status, // warning: true, // errorInfo: { // message: 'データバリデーションエラー', // description: // '必要な情報が欠落しているか、データが規定の形式に適合していません。', // userAction: '入力内容を確認して再度お試しください。', // dumpMsg: null, // dumpData: null, // }, // }, // '121': { // id: '121', // type: 'サンプルデータ挿入', // name: '完成品販売計画_月次 サンプルデータ挿入処理', // currPhase: 3, // totalPhase: 3, // phaseDesc: 'リモートリポジトリをclone中', // execDate: '2023-07-24 13:43:55', // endDate: '2023-07-24 13:56:08', // status: 'finished' as Status, // warning: true, // errorInfo: { // message: 'データバリデーションエラー', // description: // '必要な情報が欠落しているか、データが規定の形式に適合していません。', // userAction: '入力内容を確認して再度お試しください。', // dumpMsg: null, // dumpData: null, // }, // }, // '120': { // id: '120', // type: 'API作成', // name: '"TEST API" 作成処理', // currPhase: 2, // totalPhase: 10, // phaseDesc: 'リモートリポジトリをclone中', // execDate: '2023-07-24 13:43:55', // endDate: '2023-07-24 13:56:08', // status: 'error' as Status, // warning: false, // errorInfo: { // message: 'リモートリポジトリのcloneに失敗', // description: // 'Githubの認証に失敗したため、リモートリポジトリがcloneできませんでした。', // userAction: // '本画面のスクリーンショットを取得し、サポートへお問い合わせください。', // dumpMsg: 'git.exc.GitCommandError', // dumpData: // "git.exc.GitCommandError: Cmd('git') failed due to: exit code(128)\n" + // "cmdline: git clone -v -b feature/ind_tmp https://*****:*****@github.com/i4platform/i4-dpb_script.git tmp/i4-dpb\nstderr: 'Cloning into 'tmp/i4-dpb'...\n" + // 'remote: Invalid username or password.\n' + // "fatal: Authentication failed for 'https://github.com/i4platform/i4-dpb_script.git/'", // }, // }, // '119': { // id: '119', // type: 'サンプルデータ挿入', // name: '完成品販売計画_月次 サンプルデータ挿入処理', // currPhase: 3, // totalPhase: 3, // phaseDesc: '処理終了', // execDate: '2023-07-25 05:55:00', // endDate: '2023-07-25 06:15:18', // status: 'finished' as Status, // warning: false, // errorInfo: null, // }, // }; // const TESTDATA_LIST = [ // { // id: '123', // type: 'サンプルデータ挿入', // name: '完成品販売計画_月次 サンプルデータ挿入処理', // execDate: '2023-07-24 14:43:55', // endDate: null, // status: 'running' as Status, // warning: false, // }, // { // id: '122', // type: 'サンプルデータ挿入', // name: '完成品販売計画_月次 サンプルデータ挿入処理', // execDate: '2023-07-24 11:23:55', // endDate: null, // status: 'running' as Status, // warning: true, // }, // { // id: '121', // type: 'サンプルデータ挿入', // name: '完成品販売_月次 サンプルデータ挿入処理', // execDate: '2023-07-24 10:23:55', // endDate: '2023-07-24 10:33:55', // status: 'finished' as Status, // warning: true, // }, // { // id: '120', // type: 'API更新', // name: '完成品販売計画_月次 API更新処理', // execDate: '2023-07-24 13:23:55', // endDate: '2023-07-24 13:43:55', // status: 'error' as Status, // warning: false, // }, // { // id: '119', // type: 'サンプルデータ挿入', // name: '完成品販売_月次 サンプルデータ挿入処理', // execDate: '2023-07-24 10:23:55', // endDate: '2023-07-24 10:33:55', // status: 'finished' as Status, // warning: false, // }, // { // id: '118', // type: 'サンプルデータ挿入', // name: '完成品販売_月次 サンプルデータ挿入処理', // execDate: '2023-07-24 10:23:55', // endDate: '2023-07-24 10:33:55', // status: 'finished' as Status, // warning: false, // }, // { // id: '117', // type: 'サンプルデータ挿入', // name: '完成品販売_月次 サンプルデータ挿入処理', // execDate: '2023-07-24 10:23:55', // endDate: '2023-07-24 10:33:55', // status: 'finished' as Status, // warning: false, // }, // { // id: '116', // type: 'サンプルデータ挿入', // name: '完成品販売_月次 サンプルデータ挿入処理', // execDate: '2023-07-24 10:23:55', // endDate: '2023-07-24 10:33:55', // status: 'finished' as Status, // warning: false, // }, // { // id: '115', // type: 'サンプルデータ挿入', // name: '完成品販売_月次 サンプルデータ挿入処理', // execDate: '2023-07-24 10:23:55', // endDate: '2023-07-24 10:33:55', // status: 'finished' as Status, // warning: false, // }, // { // id: '114', // type: 'サンプルデータ挿入', // name: '完成品販売_月次 サンプルデータ挿入処理', // execDate: '2023-07-24 10:23:55', // endDate: '2023-07-24 10:33:55', // status: 'finished' as Status, // warning: false, // }, // ]; export default JobStatus; `
kirin-ri commented 1 month ago

`import parse from 'html-react-parser'; import React, { useEffect, useLayoutEffect, useState } from 'react'; import ReactPaginate from 'react-paginate'; import { useHistory } from 'react-router-dom'; import Swal from 'sweetalert2'; import withReactContent from 'sweetalert2-react-content'; import { commonAjax } from '../../../components/commonAjax'; import { currentJST } from '../util/dateTimeUtil'; import { DialogModalOK, showDialog } from './dialogModal';

// eslint-disable-next-line @typescript-eslint/no-var-requires const moment = require('moment'); require('moment-duration-format');

const MySwal = withReactContent(Swal);

interface Props { match: { params: { id?: string; }; }; }

type JobEntry = { id: string; type: string; name: string; execDate: string; endDate: string | null; status: Status; warning: boolean; };

type JobDetailsTypes = { id: string; type: string; name: string; currPhase: number; totalPhase: number; phaseDesc: string; execDate: string; endDate: string | null; status: Status; warning: boolean; errorInfo: { message: string; description: string; userAction: string; dumpMsg: string | null; dumpData: string | null; } | null; externalQuery: { apiTablePhy?: string; putApiDataTran?: string; metrics?: string; putMetricsTran?: string; } | null; };

type Tag = { name: string; selected: boolean; };

type Status = 'running' | 'finished' | 'error';

function ProgressIndicator({ info }: { info?: JobDetailsTypes }) { const progList: JSX.Element[] = []; if (info) { for (let i = 0; i <= info.totalPhase + 1; i += 1) { let type; let stat; const content = null; if (i === 0) { type = 'progress-start'; } else if (i <= info.totalPhase) { type = 'progress-phase'; } else { type = 'progress-finish'; }

  if (i < info.currPhase) {
    stat = 'progress-done';
  } else if (i === info.currPhase) {
    if (info.status === 'running') {
      stat = 'progress-doing';
    } else if (info.status === 'finished') {
      stat = 'progress-done';
    } else {
      stat = 'progress-error';
    }
  } else if (i === info.totalPhase + 1) {
    if (info.status === 'finished') {
      stat = info.warning ? 'progress-warning' : 'progress-done';
    } else {
      stat = 'progress-todo';
    }
  } else {
    stat = 'progress-todo';
  }
  progList.push(
    <div className={`${type} ${stat}`} key={i}>
      {content}
    </div>,
  );
}

}

return (

{progList}
{info?.phaseDesc}

); }

const getJobStatus = (status: Status, warning: boolean) => { if (status === 'running') { return ( <> 実行中   {warning && ( )} </> ); } if (status === 'finished') { if (warning) { return 警告終了; } return 正常終了; } if (status === 'error') { return 異常終了; } if (status === 'canceled') { return 処理中断; } // eslint-disable-next-line react/jsx-no-useless-fragment return <>{status}</>; };

function ErrorDetailModal({ info }: { info?: JobDetailsTypes }) { const dumpMessage = (info?.errorInfo?.dumpMsg || '') .split(/(\r?\n)/) .map((line, idx) => ( // eslint-disable-next-line react/no-array-index-key

{line.match(/\r?\n/) ?
: parse(line)}
)); return (
メッセージ
{info?.errorInfo?.message}
詳細
{info?.errorInfo?.description && parse(info.errorInfo.description)}
アクション
{info?.errorInfo?.userAction && parse(info.errorInfo.userAction)}
内部メッセージ

Job ID: {info?.id}

{dumpMessage}

{info?.errorInfo?.dumpData}

); } function JobDetails({ id, load }: { id?: string; load: VoidFunction }) { const [currId, setCurrId] = useState(id); const [info, setInfo] = useState(); const [beginUpdate, setBeginUpdate] = useState(false); const [timer, setTimer] = useState(null); const fetchLatest = () => { commonAjax .axios() .get('/api/jobs/latest') .then((res) => { const { job } = res.data; setInfo(job); setCurrId(job.id); }); }; const update = () => { commonAjax .axios() .get(`/api/jobs/${currId}`) .then((res) => { const { job } = res.data; if (job) { if (job.status === 'running') { setBeginUpdate(true); } else if (timer) { clearTimeout(timer); setTimer(null); } } load(); setInfo(job); }); }; const handleErrorBtnClicked = () => { showDialog('errorDetailModal'); }; let elapsedTime = ''; let status = null; if (info) { // Calculate elapsed time since the job started const since = moment(info.execDate); const until = moment(info.endDate ?? currentJST()); elapsedTime = moment .duration(until.diff(since)) .format('hh:mm:ss', { trim: false }); status = getJobStatus(info.status, info.warning); } // ジョブ中断 const handleCancelClick = () => { commonAjax .axios() .post(`/api/jobs/cancel`, { jobId: currId, }) .then(() => { update(); }) .catch((err) => { MySwal.fire({ icon: 'error', title: `ERROR: ${err}`, toast: true, position: 'top-end', showConfirmButton: false, timer: 3000, }); }); }; // サンプルデータ挿入データ削除 const handleDeleteApiDataClick = () => { const targetData = { apiTablePhy: info?.externalQuery?.apiTablePhy, putApiDataTran: info?.externalQuery?.putApiDataTran, }; commonAjax .axios({ swalFire: false, loading: true }) .delete(`/api/api/deleteData`, { data: targetData, }) .then((res) => { if (!res.data.dataExistFlg) { MySwal.fire({ icon: 'error', title: `ERROR: ${res.data.message}`, toast: true, position: 'top-end', showConfirmButton: false, timer: 3000, }); } else { MySwal.fire({ icon: 'success', title: `Message: ${res.data.message}`, toast: true, position: 'top-end', showConfirmButton: false, timer: 3000, }); } }) .catch((err) => { MySwal.fire({ icon: 'error', title: `ERROR: ${err}`, toast: true, position: 'top-end', showConfirmButton: false, timer: 3000, }); }); }; // メトリクス実行データ削除 const handleDeleteMetricsDataClick = () => { const targetData = { metrics: info?.externalQuery?.metrics, putMetricsTran: info?.externalQuery?.putMetricsTran, }; commonAjax .axios({ swalFire: false, loading: true }) .delete(`/api/api/deleteMetricsData`, { data: targetData, }) .then((res) => { if (!res.data.dataExistFlg) { MySwal.fire({ icon: 'error', title: `ERROR: ${res.data.message}`, toast: true, position: 'top-end', showConfirmButton: false, timer: 3000, }); } else { MySwal.fire({ icon: 'success', title: `Message: ${res.data.message}`, toast: true, position: 'top-end', showConfirmButton: false, timer: 3000, }); } }) .catch((err) => { MySwal.fire({ icon: 'error', title: `ERROR: ${err}`, toast: true, position: 'top-end', showConfirmButton: false, timer: 3000, }); }); }; const cancelAndUpdateButtonHidden = info?.status === 'running' && info?.type === 'メトリクス即時実行'; const deleteApiDataButtonHidden = info?.type === 'サンプルデータ挿入'; const deleteMetricsDataButtonHidden = info?.type === 'メトリクス即時実行'; useEffect(() => { if (!currId) { fetchLatest(); } else if (currId !== id) { setCurrId(id); } }, [id]); useEffect(() => { let ignore = false; if (timer) { clearTimeout(timer); setTimer(null); } if (currId) { setTimeout(() => !ignore && update()); } return () => { ignore = true; }; }, [currId]); useLayoutEffect(() => { if (beginUpdate) { const newTimer = setTimeout(update, 3000); setTimer(newTimer); setBeginUpdate(false); } }, [beginUpdate]); return (
{ // eslint-disable-next-line no-nested-ternary cancelAndUpdateButtonHidden ? ( ) : deleteApiDataButtonHidden ? ( ) : ( deleteMetricsDataButtonHidden && ( ) ) }
ジョブID
{info?.id}
種別
{info?.type}
処理名
{info?.name}
実行日時
{info?.execDate}
終了日時
{info?.endDate}
所要時間
{elapsedTime}
状態
{status}  {info?.errorInfo && ( )}
); } // fillter tag function FilterTags({ tags, setTypes, }: { tags: Tag[]; setTypes: React.Dispatch>; }) { const handleClick = (item: Tag) => { // eslint-disable-next-line no-param-reassign item.selected = !item.selected; const t = tags.slice(0, tags.length); setTypes(t); }; return (
フィルタータグ
ジョブ種別
{tags.map((item: Tag) => ( ))}
); } function JobListEntry({ entry, onClick, }: { entry: JobEntry; onClick: (id: string) => void; }) { const handleClick = () => { onClick(entry.id); }; const status = getJobStatus(entry.status, entry.warning); return ( {entry.id} {entry.type} {entry.name} {status} {entry.execDate} {entry.endDate} ); } function JobList({ onJobSelected, load, list, types, setTypes, }: { onJobSelected: (id: string) => void; load: VoidFunction; list: JobEntry[] | undefined; types: Tag[]; setTypes: React.Dispatch>; }) { const perPage = 10; // const [types, setTypes] = useState([]); // const [list, setList] = useState(); const [offset, setOffset] = useState(0); useEffect(() => { load(); }, [types]); const handlePageChange = (data: { selected: number }) => { const page = data.selected; setOffset(page * perPage); }; if (list) { return (
{list.length > 0 && ( <> {list .filter( (e: JobEntry, idx: number) => idx >= offset && idx < offset + perPage, ) .map((e: JobEntry) => ( ))}
ID 種別 処理名 状態 実行日時 終了日時
)} {list.length === 0 && (
ジョブはありません
)}
); } // eslint-disable-next-line react/jsx-no-useless-fragment return <>; } function JobStatus(props: Props) { const { match } = props; const history = useHistory(); const [id, setId] = useState(match.params.id); const [list, setList] = useState(); const [types, setTypes] = useState([]); useEffect(() => { setId(match.params.id); }, [match.params.id, history]); const handleJobSelected = (targetId: string) => { setId(targetId); history.push(`/jobs/${targetId}`); }; // ジョブ終了後に一覧更新の必要があるため、メインコンポーネントに移動 const load = () => { const req = { types: types.filter((t) => t.selected).map((t) => t.name), }; commonAjax .axios() .post('/api/jobs', req) .then((res) => { const { data } = res; setList( data.list.sort( ( a: { status: string; id: string }, b: { status: string; id: string }, ) => // eslint-disable-next-line no-nested-ternary a.status === 'running' && b.status !== 'running' ? -1 : a.status !== 'running' && b.status === 'running' ? 1 : parseInt(b.id, 10) - parseInt(a.id, 10), ), ); if (!types.length) { setTypes( data.types.map((t: string) => ({ name: t, selected: true })), ); } }); }; return (

ジョブステータス確認

); } // const JOB_TYPES = [ // 'API追加', // 'API更新', // 'メトリクスデプロイ', // '新規メトリクス追加', // 'サンプルデータ挿入', // 'メトリクス改修', // ]; // , "個社環境コピー", "個社環境削除", "デモ環境初期化" // const SAMPLE_JOBS: { [key: string]: JobDetails } = { // '123': { // id: '123', // type: 'サンプルデータ挿入', // name: '完成品販売計画_月次 サンプルデータ挿入処理', // currPhase: 2, // totalPhase: 3, // phaseDesc: 'データ挿入中', // execDate: '2023-07-31 13:55:00', // endDate: null, // status: 'running' as Status, // warning: false, // errorInfo: null, // }, // '122': { // id: '122', // type: 'サンプルデータ挿入', // name: '完成品販売計画_月次 サンプルデータ挿入処理', // currPhase: 2, // totalPhase: 3, // phaseDesc: 'データ挿入中', // execDate: '2023-07-25 05:55:00', // endDate: null, // status: 'running' as Status, // warning: true, // errorInfo: { // message: 'データバリデーションエラー', // description: // '必要な情報が欠落しているか、データが規定の形式に適合していません。', // userAction: '入力内容を確認して再度お試しください。', // dumpMsg: null, // dumpData: null, // }, // }, // '121': { // id: '121', // type: 'サンプルデータ挿入', // name: '完成品販売計画_月次 サンプルデータ挿入処理', // currPhase: 3, // totalPhase: 3, // phaseDesc: 'リモートリポジトリをclone中', // execDate: '2023-07-24 13:43:55', // endDate: '2023-07-24 13:56:08', // status: 'finished' as Status, // warning: true, // errorInfo: { // message: 'データバリデーションエラー', // description: // '必要な情報が欠落しているか、データが規定の形式に適合していません。', // userAction: '入力内容を確認して再度お試しください。', // dumpMsg: null, // dumpData: null, // }, // }, // '120': { // id: '120', // type: 'API作成', // name: '"TEST API" 作成処理', // currPhase: 2, // totalPhase: 10, // phaseDesc: 'リモートリポジトリをclone中', // execDate: '2023-07-24 13:43:55', // endDate: '2023-07-24 13:56:08', // status: 'error' as Status, // warning: false, // errorInfo: { // message: 'リモートリポジトリのcloneに失敗', // description: // 'Githubの認証に失敗したため、リモートリポジトリがcloneできませんでした。', // userAction: // '本画面のスクリーンショットを取得し、サポートへお問い合わせください。', // dumpMsg: 'git.exc.GitCommandError', // dumpData: // "git.exc.GitCommandError: Cmd('git') failed due to: exit code(128)\n" + // "cmdline: git clone -v -b feature/ind_tmp https://*****:*****@github.com/i4platform/i4-dpb_script.git tmp/i4-dpb\nstderr: 'Cloning into 'tmp/i4-dpb'...\n" + // 'remote: Invalid username or password.\n' + // "fatal: Authentication failed for 'https://github.com/i4platform/i4-dpb_script.git/'", // }, // }, // '119': { // id: '119', // type: 'サンプルデータ挿入', // name: '完成品販売計画_月次 サンプルデータ挿入処理', // currPhase: 3, // totalPhase: 3, // phaseDesc: '処理終了', // execDate: '2023-07-25 05:55:00', // endDate: '2023-07-25 06:15:18', // status: 'finished' as Status, // warning: false, // errorInfo: null, // }, // }; // const TESTDATA_LIST = [ // { // id: '123', // type: 'サンプルデータ挿入', // name: '完成品販売計画_月次 サンプルデータ挿入処理', // execDate: '2023-07-24 14:43:55', // endDate: null, // status: 'running' as Status, // warning: false, // }, // { // id: '122', // type: 'サンプルデータ挿入', // name: '完成品販売計画_月次 サンプルデータ挿入処理', // execDate: '2023-07-24 11:23:55', // endDate: null, // status: 'running' as Status, // warning: true, // }, // { // id: '121', // type: 'サンプルデータ挿入', // name: '完成品販売_月次 サンプルデータ挿入処理', // execDate: '2023-07-24 10:23:55', // endDate: '2023-07-24 10:33:55', // status: 'finished' as Status, // warning: true, // }, // { // id: '120', // type: 'API更新', // name: '完成品販売計画_月次 API更新処理', // execDate: '2023-07-24 13:23:55', // endDate: '2023-07-24 13:43:55', // status: 'error' as Status, // warning: false, // }, // { // id: '119', // type: 'サンプルデータ挿入', // name: '完成品販売_月次 サンプルデータ挿入処理', // execDate: '2023-07-24 10:23:55', // endDate: '2023-07-24 10:33:55', // status: 'finished' as Status, // warning: false, // }, // { // id: '118', // type: 'サンプルデータ挿入', // name: '完成品販売_月次 サンプルデータ挿入処理', // execDate: '2023-07-24 10:23:55', // endDate: '2023-07-24 10:33:55', // status: 'finished' as Status, // warning: false, // }, // { // id: '117', // type: 'サンプルデータ挿入', // name: '完成品販売_月次 サンプルデータ挿入処理', // execDate: '2023-07-24 10:23:55', // endDate: '2023-07-24 10:33:55', // status: 'finished' as Status, // warning: false, // }, // { // id: '116', // type: 'サンプルデータ挿入', // name: '完成品販売_月次 サンプルデータ挿入処理', // execDate: '2023-07-24 10:23:55', // endDate: '2023-07-24 10:33:55', // status: 'finished' as Status, // warning: false, // }, // { // id: '115', // type: 'サンプルデータ挿入', // name: '完成品販売_月次 サンプルデータ挿入処理', // execDate: '2023-07-24 10:23:55', // endDate: '2023-07-24 10:33:55', // status: 'finished' as Status, // warning: false, // }, // { // id: '114', // type: 'サンプルデータ挿入', // name: '完成品販売_月次 サンプルデータ挿入処理', // execDate: '2023-07-24 10:23:55', // endDate: '2023-07-24 10:33:55', // status: 'finished' as Status, // warning: false, // }, // ]; export default JobStatus; `
kirin-ri commented 1 month ago
import parse from 'html-react-parser';
import React, { useEffect, useLayoutEffect, useState } from 'react';
import ReactPaginate from 'react-paginate';
import { useHistory } from 'react-router-dom';
import Swal from 'sweetalert2';
import withReactContent from 'sweetalert2-react-content';
import { commonAjax } from '../../../components/commonAjax';
import { currentJST } from '../util/dateTimeUtil';
import { DialogModalOK, showDialog } from './dialogModal';

// eslint-disable-next-line @typescript-eslint/no-var-requires
const moment = require('moment');
require('moment-duration-format');

const MySwal = withReactContent(Swal);

interface Props {
  match: {
    params: {
      id?: string;
    };
  };
}

type JobEntry = {
  id: string;
  type: string;
  name: string;
  execDate: string;
  endDate: string | null;
  status: Status;
  warning: boolean;
};

type JobDetailsTypes = {
  id: string;
  type: string;
  name: string;
  currPhase: number;
  totalPhase: number;
  phaseDesc: string;
  execDate: string;
  endDate: string | null;
  status: Status;
  warning: boolean;
  errorInfo: {
    message: string;
    description: string;
    userAction: string;
    dumpMsg: string | null;
    dumpData: string | null;
  } | null;
  externalQuery: {
    apiTablePhy?: string;
    putApiDataTran?: string;
    metrics?: string;
    putMetricsTran?: string;
  } | null;
};

type Tag = {
  name: string;
  selected: boolean;
};

type Status = 'running' | 'finished' | 'error';

function ProgressIndicator({ info }: { info?: JobDetailsTypes }) {
  const progList: JSX.Element[] = [];
  if (info) {
    for (let i = 0; i <= info.totalPhase + 1; i += 1) {
      let type;
      let stat;
      const content = null;
      if (i === 0) {
        type = 'progress-start';
      } else if (i <= info.totalPhase) {
        type = 'progress-phase';
      } else {
        type = 'progress-finish';
      }

      if (i < info.currPhase) {
        stat = 'progress-done';
      } else if (i === info.currPhase) {
        if (info.status === 'running') {
          stat = 'progress-doing';
        } else if (info.status === 'finished') {
          stat = 'progress-done';
        } else {
          stat = 'progress-error';
        }
      } else if (i === info.totalPhase + 1) {
        if (info.status === 'finished') {
          stat = info.warning ? 'progress-warning' : 'progress-done';
        } else {
          stat = 'progress-todo';
        }
      } else {
        stat = 'progress-todo';
      }
      progList.push(
        <div className={`${type} ${stat}`} key={i}>
          {content}
        </div>,
      );
    }
  }

  return (
    <div className="progress-indicator">
      <div className="progress-arrow">{progList}</div>
      <div className="phase-description">{info?.phaseDesc}</div>
    </div>
  );
}

const getJobStatus = (status: Status, warning: boolean) => {
  if (status === 'running') {
    return (
      <>
        <span className="status status-running">実行中</span>
        &nbsp;
        {warning && (
          <i className="fa fa-exclamation-triangle status status-warning" />
        )}
      </>
    );
  }
  if (status === 'finished') {
    if (warning) {
      return <span className="status status-warning">警告終了</span>;
    }
    return <span className="status status-finished">正常終了</span>;
  }
  if (status === 'error') {
    return <span className="status status-error">異常終了</span>;
  }
  if (status === 'canceled') {
    return <span className="status status-warning">処理中断</span>;
  }
  // eslint-disable-next-line react/jsx-no-useless-fragment
  return <>{status}</>;
};

function ErrorDetailModal({ info }: { info?: JobDetailsTypes }) {
  const dumpMessage = (info?.errorInfo?.dumpMsg || '')
    .split(/(\r?\n)/)
    .map((line, idx) => (
      // eslint-disable-next-line react/no-array-index-key
      <React.Fragment key={idx}>
        {line.match(/\r?\n/) ? <br /> : parse(line)}
      </React.Fragment>
    ));
  return (
    <DialogModalOK id="errorDetailModal" title="エラー詳細情報" closeBtn>
      <div className="inline-form error-title">
        <div className="inline-form-label">メッセージ</div>
        <div className="inline-form-value">{info?.errorInfo?.message}</div>
      </div>
      <div className="inline-form error-desc">
        <div className="inline-form-label">詳細</div>
        <div className="inline-form-value">
          {info?.errorInfo?.description && parse(info.errorInfo.description)}
        </div>
      </div>
      <div className="inline-form error-user-action">
        <div className="inline-form-label">アクション</div>
        <div className="inline-form-value">
          {info?.errorInfo?.userAction && parse(info.errorInfo.userAction)}
        </div>
      </div>
      <div className="inline-form error-dump">
        <div className="inline-form-label">内部メッセージ</div>
        <div className="inline-form-value">
          <p>Job ID: {info?.id}</p>
          <p>{dumpMessage}</p>
          <p>{info?.errorInfo?.dumpData}</p>
        </div>
      </div>
    </DialogModalOK>
  );
}

function JobDetails({ id, load }: { id?: string; load: VoidFunction }) {
  const [currId, setCurrId] = useState<string | undefined>(id);
  const [info, setInfo] = useState<JobDetailsTypes>();
  const [beginUpdate, setBeginUpdate] = useState<boolean>(false);
  const [timer, setTimer] = useState<NodeJS.Timeout | null>(null);

  const fetchLatest = () => {
    commonAjax
      .axios()
      .get('/api/jobs/latest')
      .then((res) => {
        const { job } = res.data;
        setInfo(job);
        setCurrId(job.id);
      });
  };

  const update = () => {
    commonAjax
      .axios()
      .get(`/api/jobs/${currId}`)
      .then((res) => {
        const { job } = res.data;
        if (job) {
          if (job.status === 'running') {
            setBeginUpdate(true);
          } else if (timer) {
            clearTimeout(timer);
            setTimer(null);
          }
        }
        load();
        setInfo(job);
      });
  };

  const handleErrorBtnClicked = () => {
    showDialog('errorDetailModal');
  };

  let elapsedTime = '';
  let status = null;
  if (info) {
    // Calculate elapsed time since the job started
    const since = moment(info.execDate);
    const until = moment(info.endDate ?? currentJST());
    elapsedTime = moment
      .duration(until.diff(since))
      .format('hh:mm:ss', { trim: false });

    status = getJobStatus(info.status, info.warning);
  }

  // ジョブ中断
  const handleCancelClick = () => {
    commonAjax
      .axios()
      .post(`/api/jobs/cancel`, {
        jobId: currId,
      })
      .then(() => {
        update();
      })
      .catch((err) => {
        MySwal.fire({
          icon: 'error',
          title: `ERROR: ${err}`,
          toast: true,
          position: 'top-end',
          showConfirmButton: false,
          timer: 3000,
        });
      });
  };

  // サンプルデータ挿入データ削除
  const handleDeleteApiDataClick = () => {
    const targetData = {
      apiTablePhy: info?.externalQuery?.apiTablePhy,
      putApiDataTran: info?.externalQuery?.putApiDataTran,
    };
    commonAjax
      .axios({ swalFire: false, loading: true })
      .delete(`/api/api/deleteData`, {
        data: targetData,
      })
      .then((res) => {
        if (!res.data.dataExistFlg) {
          MySwal.fire({
            icon: 'error',
            title: `ERROR: ${res.data.message}`,
            toast: true,
            position: 'top-end',
            showConfirmButton: false,
            timer: 3000,
          });
        } else {
          MySwal.fire({
            icon: 'success',
            title: `Message: ${res.data.message}`,
            toast: true,
            position: 'top-end',
            showConfirmButton: false,
            timer: 3000,
          });
        }
      })
      .catch((err) => {
        MySwal.fire({
          icon: 'error',
          title: `ERROR: ${err}`,
          toast: true,
          position: 'top-end',
          showConfirmButton: false,
          timer: 3000,
        });
      });
  };

  // メトリクス実行データ削除
  const handleDeleteMetricsDataClick = () => {
    const targetData = {
      metrics: info?.externalQuery?.metrics,
      putMetricsTran: info?.externalQuery?.putMetricsTran,
    };
    commonAjax
      .axios({ swalFire: false, loading: true })
      .delete(`/api/api/deleteMetricsData`, {
        data: targetData,
      })
      .then((res) => {
        if (!res.data.dataExistFlg) {
          MySwal.fire({
            icon: 'error',
            title: `ERROR: ${res.data.message}`,
            toast: true,
            position: 'top-end',
            showConfirmButton: false,
            timer: 3000,
          });
        } else {
          MySwal.fire({
            icon: 'success',
            title: `Message: ${res.data.message}`,
            toast: true,
            position: 'top-end',
            showConfirmButton: false,
            timer: 3000,
          });
        }
      })
      .catch((err) => {
        MySwal.fire({
          icon: 'error',
          title: `ERROR: ${err}`,
          toast: true,
          position: 'top-end',
          showConfirmButton: false,
          timer: 3000,
        });
      });
  };

  const cancelAndUpdateButtonHidden =
    info?.status === 'running' && info?.type === 'メトリクス即時実行';
  const deleteApiDataButtonHidden = info?.type === 'サンプルデータ挿入';
  const deleteMetricsDataButtonHidden = info?.type === 'メトリクス即時実行';

  useEffect(() => {
    if (!currId) {
      fetchLatest();
    } else if (currId !== id) {
      setCurrId(id);
    }
  }, [id]);

  useEffect(() => {
    let ignore = false;
    if (timer) {
      clearTimeout(timer);
      setTimer(null);
    }
    if (currId) {
      setTimeout(() => !ignore && update());
    }

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

  useLayoutEffect(() => {
    if (beginUpdate) {
      const newTimer = setTimeout(update, 3000);
      setTimer(newTimer);
      setBeginUpdate(false);
    }
  }, [beginUpdate]);

  return (
    <div className="job-details">
      <div className="content-header-right btn-update">
        {
          // eslint-disable-next-line no-nested-ternary
          cancelAndUpdateButtonHidden ? (
            <button
              type="button"
              className="btn btn-danger mr-3"
              onClick={handleCancelClick}
            >
              中断
            </button>
          ) : deleteApiDataButtonHidden ? (
            <button
              type="button"
              className="btn btn-secondary"
              onClick={handleDeleteApiDataClick}
            >
              挿入データ削除
            </button>
          ) : (
            deleteMetricsDataButtonHidden && (
              <button
                type="button"
                className="btn btn-secondary"
                onClick={handleDeleteMetricsDataClick}
              >
                実行データ削除
              </button>
            )
          )
        }
      </div>
      <ProgressIndicator info={info} />
      <div className="inline-form-job-detail detail-id">
        <div className="inline-form-label detail-label">ジョブID</div>
        <div className="inline-form-value">{info?.id}</div>
      </div>
      <div className="inline-form-job-detail detail-type">
        <div className="inline-form-label detail-label">種別</div>
        <div className="inline-form-value">{info?.type}</div>
      </div>
      <div className="inline-form-job-detail detail-name">
        <div className="inline-form-label detail-label">処理名</div>
        <div className="inline-form-value">{info?.name}</div>
      </div>
      <div className="inline-form-job-detail detail-exec-date">
        <div className="inline-form-label detail-label">実行日時</div>
        <div className="inline-form-value">{info?.execDate}</div>
      </div>
      <div className="inline-form-job-detail detail-end-date">
        <div className="inline-form-label detail-label">終了日時</div>
        <div className="inline-form-value">{info?.endDate}</div>
      </div>
      <div className="inline-form-job-detail detail-elapsed-time">
        <div className="inline-form-label detail-label">所要時間</div>
        <div className="inline-form-value">{elapsedTime}</div>
      </div>
      <div className="inline-form-job-detail detail-status">
        <div className="inline-form-label detail-label">状態</div>
        <div className="inline-form-value">
          {status}&nbsp;
          {info?.errorInfo && (
            <button
              type="button"
              className="btn btn-small btn-primary"
              onClick={handleErrorBtnClicked}
            >
              エラー詳細
            </button>
          )}
        </div>
      </div>
      <ErrorDetailModal info={info} />
    </div>
  );
}

// fillter tag
function FilterTags({
  tags,
  setTypes,
}: {
  tags: Tag[];
  setTypes: React.Dispatch<React.SetStateAction<Tag[]>>;
}) {
  const handleClick = (item: Tag) => {
    // eslint-disable-next-line no-param-reassign
    item.selected = !item.selected;
    const t = tags.slice(0, tags.length);
    setTypes(t);
  };
  return (
    <div className="inline-form">
      <div className="inline-form-cat">フィルタータグ</div>
      <div className="inline-form-label">ジョブ種別</div>
      <div className="inline-form-group">
        {tags.map((item: Tag) => (
          <button
            type="button"
            className={`btn btn-tag${item.selected ? ' active' : ''}`}
            key={item.name}
            onClick={() => handleClick(item)}
          >
            {item.name}
          </button>
        ))}
      </div>
    </div>
  );
}

function JobListEntry({
  entry,
  onClick,
}: {
  entry: JobEntry;
  onClick: (id: string) => void;
}) {
  const handleClick = () => {
    onClick(entry.id);
  };

  const status = getJobStatus(entry.status, entry.warning);
  return (
    <tr className="job" onClick={handleClick}>
      <td>{entry.id}</td>
      <td>{entry.type}</td>
      <td className="w-50">{entry.name}</td>
      <td>{status}</td>
      <td>{entry.execDate}</td>
      <td>{entry.endDate}</td>
    </tr>
  );
}

function JobList({
  onJobSelected,
  load,
  list,
  types,
  setTypes,
}: {
  onJobSelected: (id: string) => void;
  load: VoidFunction;
  list: JobEntry[] | undefined;
  types: Tag[];
  setTypes: React.Dispatch<React.SetStateAction<Tag[]>>;
}) {
  const perPage = 10;
  // const [types, setTypes] = useState<Tag[]>([]);
  // const [list, setList] = useState<JobEntry[]>();
  const [offset, setOffset] = useState(0);

  useEffect(() => {
    load();
  }, [types]);

  const handlePageChange = (data: { selected: number }) => {
    const page = data.selected;
    setOffset(page * perPage);
  };

  if (list) {
    return (
      <div className="card card-primary jobs">
        <div className="card-body">
          {list.length > 0 && (
            <>
              <FilterTags tags={types} setTypes={setTypes} />
              <table className="table table-striped">
                <thead>
                  <tr>
                    <th>ID</th>
                    <th>種別</th>
                    <th>処理名</th>
                    <th>状態</th>
                    <th>実行日時</th>
                    <th>終了日時</th>
                  </tr>
                </thead>
                <tbody>
                  {list
                    .filter(
                      (e: JobEntry, idx: number) =>
                        idx >= offset && idx < offset + perPage,
                    )
                    .map((e: JobEntry) => (
                      <JobListEntry
                        entry={e}
                        onClick={onJobSelected}
                        key={e.id}
                      />
                    ))}
                </tbody>
              </table>
              <div className="d-flex justify-content-center">
                <ReactPaginate
                  pageCount={Math.ceil(list.length / perPage)}
                  marginPagesDisplayed={3}
                  pageRangeDisplayed={3}
                  onPageChange={handlePageChange}
                  containerClassName="pagination"
                  pageClassName="page-item"
                  pageLinkClassName="page-link"
                  activeClassName="active"
                  previousLabel="<"
                  nextLabel=">"
                  previousClassName="page-item"
                  nextClassName="page-item"
                  previousLinkClassName="page-link"
                  nextLinkClassName="page-link"
                  disabledClassName="disabled"
                  breakLabel="..."
                  breakClassName="page-item"
                  breakLinkClassName="page-link"
                />
              </div>
            </>
          )}
          {list.length === 0 && (
            <div className="no-jobs">ジョブはありません</div>
          )}
        </div>
      </div>
    );
  }
  // eslint-disable-next-line react/jsx-no-useless-fragment
  return <></>;
}

function JobStatus(props: Props) {
  const { match } = props;
  const history = useHistory();
  const [id, setId] = useState<string | undefined>(match.params.id);
  const [list, setList] = useState<JobEntry[]>();
  const [types, setTypes] = useState<Tag[]>([]);

  useEffect(() => {
    setId(match.params.id);
  }, [match.params.id, history]);

  const handleJobSelected = (targetId: string) => {
    setId(targetId);
    history.push(`/jobs/${targetId}`);
  };

  // ジョブ終了後に一覧更新の必要があるため、メインコンポーネントに移動
  const load = () => {
    const req = {
      types: types.filter((t) => t.selected).map((t) => t.name),
    };
    commonAjax
      .axios()
      .post('/api/jobs', req)
      .then((res) => {
        const { data } = res;
        setList(
          data.list.sort(
            (
              a: { status: string; id: string },
              b: { status: string; id: string },
            ) =>
              // eslint-disable-next-line no-nested-ternary
              a.status === 'running' && b.status !== 'running'
                ? -1
                : a.status !== 'running' && b.status === 'running'
                  ? 1
                  : parseInt(b.id, 10) - parseInt(a.id, 10),
          ),
        );
        if (!types.length) {
          setTypes(
            data.types.map((t: string) => ({ name: t, selected: true })),
          );
        }
      });
  };

  return (
    <div className="content-wrapper job-status">
      <section className="page-cover">
        <div className="page-cover-title-frame">
          <h1>ジョブステータス確認</h1>
        </div>
      </section>
      <section className="content">
        <div className="card card-primary status">
          <div className="card-body">
            <JobDetails id={id} load={load} />
          </div>
        </div>
        <JobList
          onJobSelected={handleJobSelected}
          load={load}
          list={list}
          types={types}
          setTypes={setTypes}
        />
      </section>
    </div>
  );
}

// const JOB_TYPES = [
//   'API追加',
//   'API更新',
//   'メトリクスデプロイ',
//   '新規メトリクス追加',
//   'サンプルデータ挿入',
//   'メトリクス改修',
// ]; // , "個社環境コピー", "個社環境削除", "デモ環境初期化"

// const SAMPLE_JOBS: { [key: string]: JobDetails } = {
//   '123': {
//     id: '123',
//     type: 'サンプルデータ挿入',
//     name: '完成品販売計画_月次 サンプルデータ挿入処理',
//     currPhase: 2,
//     totalPhase: 3,
//     phaseDesc: 'データ挿入中',
//     execDate: '2023-07-31 13:55:00',
//     endDate: null,
//     status: 'running' as Status,
//     warning: false,
//     errorInfo: null,
//   },
//   '122': {
//     id: '122',
//     type: 'サンプルデータ挿入',
//     name: '完成品販売計画_月次 サンプルデータ挿入処理',
//     currPhase: 2,
//     totalPhase: 3,
//     phaseDesc: 'データ挿入中',
//     execDate: '2023-07-25 05:55:00',
//     endDate: null,
//     status: 'running' as Status,
//     warning: true,
//     errorInfo: {
//       message: 'データバリデーションエラー',
//       description:
//         '必要な情報が欠落しているか、データが規定の形式に適合していません。',
//       userAction: '入力内容を確認して再度お試しください。',
//       dumpMsg: null,
//       dumpData: null,
//     },
//   },
//   '121': {
//     id: '121',
//     type: 'サンプルデータ挿入',
//     name: '完成品販売計画_月次 サンプルデータ挿入処理',
//     currPhase: 3,
//     totalPhase: 3,
//     phaseDesc: 'リモートリポジトリをclone中',
//     execDate: '2023-07-24 13:43:55',
//     endDate: '2023-07-24 13:56:08',
//     status: 'finished' as Status,
//     warning: true,
//     errorInfo: {
//       message: 'データバリデーションエラー',
//       description:
//         '必要な情報が欠落しているか、データが規定の形式に適合していません。',
//       userAction: '入力内容を確認して再度お試しください。',
//       dumpMsg: null,
//       dumpData: null,
//     },
//   },
//   '120': {
//     id: '120',
//     type: 'API作成',
//     name: '"TEST API" 作成処理',
//     currPhase: 2,
//     totalPhase: 10,
//     phaseDesc: 'リモートリポジトリをclone中',
//     execDate: '2023-07-24 13:43:55',
//     endDate: '2023-07-24 13:56:08',
//     status: 'error' as Status,
//     warning: false,
//     errorInfo: {
//       message: 'リモートリポジトリのcloneに失敗',
//       description:
//         'Githubの認証に失敗したため、リモートリポジトリがcloneできませんでした。',
//       userAction:
//         '本画面のスクリーンショットを取得し、サポートへお問い合わせください。',
//       dumpMsg: 'git.exc.GitCommandError',
//       dumpData:
//         "git.exc.GitCommandError: Cmd('git') failed due to: exit code(128)\n" +
//         "cmdline: git clone -v -b feature/ind_tmp https://*****:*****@github.com/i4platform/i4-dpb_script.git tmp/i4-dpb\nstderr: 'Cloning into 'tmp/i4-dpb'...\n" +
//         'remote: Invalid username or password.\n' +
//         "fatal: Authentication failed for 'https://github.com/i4platform/i4-dpb_script.git/'",
//     },
//   },
//   '119': {
//     id: '119',
//     type: 'サンプルデータ挿入',
//     name: '完成品販売計画_月次 サンプルデータ挿入処理',
//     currPhase: 3,
//     totalPhase: 3,
//     phaseDesc: '処理終了',
//     execDate: '2023-07-25 05:55:00',
//     endDate: '2023-07-25 06:15:18',
//     status: 'finished' as Status,
//     warning: false,
//     errorInfo: null,
//   },
// };

// const TESTDATA_LIST = [
//   {
//     id: '123',
//     type: 'サンプルデータ挿入',
//     name: '完成品販売計画_月次 サンプルデータ挿入処理',
//     execDate: '2023-07-24 14:43:55',
//     endDate: null,
//     status: 'running' as Status,
//     warning: false,
//   },
//   {
//     id: '122',
//     type: 'サンプルデータ挿入',
//     name: '完成品販売計画_月次 サンプルデータ挿入処理',
//     execDate: '2023-07-24 11:23:55',
//     endDate: null,
//     status: 'running' as Status,
//     warning: true,
//   },
//   {
//     id: '121',
//     type: 'サンプルデータ挿入',
//     name: '完成品販売_月次 サンプルデータ挿入処理',
//     execDate: '2023-07-24 10:23:55',
//     endDate: '2023-07-24 10:33:55',
//     status: 'finished' as Status,
//     warning: true,
//   },
//   {
//     id: '120',
//     type: 'API更新',
//     name: '完成品販売計画_月次 API更新処理',
//     execDate: '2023-07-24 13:23:55',
//     endDate: '2023-07-24 13:43:55',
//     status: 'error' as Status,
//     warning: false,
//   },
//   {
//     id: '119',
//     type: 'サンプルデータ挿入',
//     name: '完成品販売_月次 サンプルデータ挿入処理',
//     execDate: '2023-07-24 10:23:55',
//     endDate: '2023-07-24 10:33:55',
//     status: 'finished' as Status,
//     warning: false,
//   },
//   {
//     id: '118',
//     type: 'サンプルデータ挿入',
//     name: '完成品販売_月次 サンプルデータ挿入処理',
//     execDate: '2023-07-24 10:23:55',
//     endDate: '2023-07-24 10:33:55',
//     status: 'finished' as Status,
//     warning: false,
//   },
//   {
//     id: '117',
//     type: 'サンプルデータ挿入',
//     name: '完成品販売_月次 サンプルデータ挿入処理',
//     execDate: '2023-07-24 10:23:55',
//     endDate: '2023-07-24 10:33:55',
//     status: 'finished' as Status,
//     warning: false,
//   },
//   {
//     id: '116',
//     type: 'サンプルデータ挿入',
//     name: '完成品販売_月次 サンプルデータ挿入処理',
//     execDate: '2023-07-24 10:23:55',
//     endDate: '2023-07-24 10:33:55',
//     status: 'finished' as Status,
//     warning: false,
//   },
//   {
//     id: '115',
//     type: 'サンプルデータ挿入',
//     name: '完成品販売_月次 サンプルデータ挿入処理',
//     execDate: '2023-07-24 10:23:55',
//     endDate: '2023-07-24 10:33:55',
//     status: 'finished' as Status,
//     warning: false,
//   },
//   {
//     id: '114',
//     type: 'サンプルデータ挿入',
//     name: '完成品販売_月次 サンプルデータ挿入処理',
//     execDate: '2023-07-24 10:23:55',
//     endDate: '2023-07-24 10:33:55',
//     status: 'finished' as Status,
//     warning: false,
//   },
// ];

export default JobStatus;
kirin-ri commented 1 month ago
function FilterTags({
  tags,
  setTypes,
}: {
  tags: Tag[];
  setTypes: React.Dispatch<React.SetStateAction<Tag[]>>;
}) {
  const handleClick = (item: Tag) => {
    // 切换Tag的选择状态
    item.selected = !item.selected;
    const updatedTags = tags.map((tag) =>
      tag.name === item.name ? { ...tag, selected: item.selected } : tag
    );
    setTypes(updatedTags);
  };

  return (
    <div className="inline-form">
      <div className="inline-form-cat">フィルタータグ</div>
      <div className="inline-form-label">ジョブ種別</div>
      <div className="inline-form-group">
        {tags.map((item: Tag) => (
          <button
            type="button"
            className={`btn btn-tag${item.selected ? ' active' : ''}`}
            key={item.name}
            onClick={() => handleClick(item)}
          >
            {item.name}
          </button>
        ))}
      </div>
    </div>
  );
}
kirin-ri commented 1 month ago
useEffect(() => {
  const initialTags = types.map((t) => ({ name: t.name, selected: false })); // 默认设置未选择状态
  setTypes(initialTags);
}, []);
kirin-ri commented 1 month ago
const load = () => {
  const selectedTags = types.filter((tag) => tag.selected).map((tag) => tag.name);
  const req = {
    types: selectedTags.length ? selectedTags : [], // 如果没有选中Tag,则显示全部
  };

  commonAjax
    .axios()
    .post('/api/jobs', req)
    .then((res) => {
      const { data } = res;
      setList(
        data.list.sort(
          (
            a: { status: string; id: string },
            b: { status: string; id: string },
          ) =>
            a.status === 'running' && b.status !== 'running'
              ? -1
              : a.status !== 'running' && b.status === 'running'
              ? 1
              : parseInt(b.id, 10) - parseInt(a.id, 10),
        ),
      );
    });
};
kirin-ri commented 1 month ago
import React, { useEffect, useLayoutEffect, useState } from 'react';
import { Link } from 'react-router-dom';
import { commonAjax } from '../../../components/commonAjax';
import { Spacer } from '../spacer';
import * as Defs from './apiDefs';
import ApiUpdateResultModal from './apiUpdateResultModal';
import CreateNewApi from './createNewApi';
import { DialogModalOK, hideDialog, showDialog } from './dialogModal';
import EditApiList from './editApiList';
import NewAPIDtlModal from './newAPIDtlModal';
import NewSampleDataModal from './newSampleDataModal';
import SelectDeployApi from './selectDeployApi';

// main
function NewApiList() {
  // useState
  const [apiList, setApiList] = useState<Defs.Api[]>([]);
  const [tagList, setTags] = useState<Defs.Tag[]>([]);
  const [changeStatus, setChangeStatus] = useState<boolean>(false);
  const [enableDplApi, setEnableDplApi] = useState<boolean>(false);
  const [errorMsg, setErrorMsg] = useState<{}>({});
  const [deployJobId, setDeployJobId] = useState<string>('');
  const [isModalOpen, setIsModalOpen] = useState(false);

  /* API情報取得のリクエスト */
  useEffect(() => {
    (async () => {
      commonAjax
        .axios({ swalFire: false, loading: true })
        .get('/api/api')
        .then((res) => {
          const data = res.data.api;
          const resTags: React.SetStateAction<Defs.Tag[]> = [];
          let tmpTags: string[] = [];
          data.map((item: any) => {
            tmpTags = tmpTags.concat(item.tags);
          });
          tmpTags = Array.from(new Set(tmpTags));
          tmpTags.map((t: string) => {
            resTags.push({ tag_name: t, select: false });
          });
          setApiList(data);
          setTags(resTags);
          setEnableDplApi(res.data.enableDplApi);
        });
    })();
  }, [changeStatus]);

  useLayoutEffect(() => {
    if (deployJobId) {
      // ジョブ開始ダイアログ表示
      showDialog('api-deploy-job-start-notification');
    }
  }, [deployJobId]);

  const metricsProcess = process.env.REACT_APP_METRICS_PROCESS;
  const title = `${metricsProcess} データセット一覧`;

  return (
    <div className="content-wrapper api-list">
      <section className="page-cover">
        <h1>{title}</h1>
      </section>
      {/* Content Header (Page header) */}
      <section className="content-header">
        <div className="content-header-left">
          <h1>{title}</h1>
          <div className="content-header-desc">
            現在、設定されているデータセット一覧です
          </div>
        </div>
        {enableDplApi && (
          <div className="content-header-right">
            <button
              type="button"
              className="btn btn-secondary"
              data-toggle="modal"
              data-target="#editApiListModal"
              onClick={() => {
                setIsModalOpen(true);
              }}
            >
              編集
            </button>
            <button
              type="button"
              className="btn btn-primary"
              data-toggle="modal"
              data-target="#api-dtl-modal"
              onClick={() => {
                setIsModalOpen(true);
              }}
            >
              追加
            </button>
            <button
              type="button"
              className="btn-long-text btn-primary"
              data-toggle="modal"
              data-target="#selectDeployApiModal"
              onClick={() => {
                setIsModalOpen(true);
              }}
            >
              一括有効化
            </button>
          </div>
        )}
      </section>
      <section className="content">
        {/* filter tag */}
        <div className="inline-form">
          <div className="inline-form-cat">フィルタータグ</div>
          {FilterTags(tagList, setTags)}
        </div>
        {Spacer({ size: 30 })}
        {/* API List (Grid) */}
        {viewList(
          apiList,
          tagList,
          changeStatus,
          setChangeStatus,
          setErrorMsg,
          errorMsg,
          setDeployJobId,
        )}
      </section>
      {isModalOpen && (
        <>
          <CreateNewApi
            changeStatus={changeStatus}
            setChangeStatus={setChangeStatus}
          />
          <EditApiList
            val={apiList}
            changeStatus={changeStatus}
            setChangeStatus={setChangeStatus}
          />
          <SelectDeployApi
            val={apiList}
            changeStatus={changeStatus}
            setChangeStatus={setChangeStatus}
          />
        </>
      )}
      <ApiDeployJobStartNotification jobId={deployJobId} />
    </div>
  );
}

// fillter tag
function FilterTags(tags: Defs.Tag[], setTags: any) {
  return (
    <div className="inline-form-group">
      <div className="inline-form-label">分類</div>
      {tags.map((item: Defs.Tag) => {
        if (item.select) {
          return (
            <div
              className="btn btn-tag active"
              key={item.tag_name}
              onClick={() => {
                item.select = false;
                const t = tags.slice(0, tags.length);
                setTags(t);
              }}
            >
              {item.tag_name}
            </div>
          );
        }
        return (
          <div
            className="btn btn-tag"
            onClick={() => {
              item.select = true;
              const t = tags.slice(0, tags.length);
              setTags(t);
            }}
          >
            {item.tag_name}
          </div>
        );
      })}
    </div>
  );
}

function viewList(
  item: Defs.Api[],
  tagList: Defs.Tag[],
  changeStatus: boolean,
  setChangeStatus: any,
  setErrorMsg: any,
  errorMsg: any,
  setDeployJobId: any,
) {
  // Filter the API list based on selected tags
  const filteredList = item.filter((api) => {
    // If no tags are selected, show all
    const isAnyTagSelected = tagList.some((tag) => tag.select);
    if (!isAnyTagSelected) return true;

    // Show only the APIs that match at least one selected tag
    return api.tags.some((apiTag) =>
      tagList.some((tag) => tag.select && tag.tag_name === apiTag),
    );
  });

  const length = Math.ceil(filteredList.length / 3);
  let listItem: any = [];
  if (length !== 0) {
    listItem = transpose(
      new Array(length)
        .fill(0)
        .map((_, i) => filteredList.slice(i * 3, (i + 1) * 3)),
    );
  }

  return (
    <div className="list-wrapper">
      {listItem.map((column: Defs.Api[], columnIndex: number) => (
        <div className="apiList" key={columnIndex}>
          {column.map((val: Defs.Api) =>
            ApiCard(
              val,
              tagList,
              changeStatus,
              setChangeStatus,
              setErrorMsg,
              errorMsg,
              setDeployJobId,
            ),
          )}
        </div>
      ))}
    </div>
  );
}

// API Card
function ApiCard(
  api: Defs.Api,
  tagList: Defs.Tag[],
  changeStatus: boolean,
  setChangeStatus: any,
  setErrorMsg: any,
  errorMsg: any,
  setDeployJobId: any,
) {
  let selected = false;

  // タグが1つでも選択されているか確認
  const isAnyTagSelected = tagList.some((tag) => tag.select);

  tagList.map((item: Defs.Tag) => {
    api.tags.map((apiTag: string) => {
      if (apiTag === item.tag_name) {
        if (!isAnyTagSelected || item.select) {
          // タグが未選択の時、全て選択と同じ扱いにする
          selected = true;
        }
      }
    });
  });

  if (!errorMsg) {
    $(`#${api.physical_name}`).collapse('hide');
  }

  return (
    <div
      className={`card ${
        selected ? 'card-primary' : 'card-not-applicable not-applicable'
      } card-collapse-sample`}
    >
      <div
        className="card-header collapsed"
        data-toggle="collapse"
        data-target={`#card-body-${api.physical_name}`}
      >
        <div
          className="api-name-font"
          style={{
            fontSize: `clamp(0.6rem, ${
              29 / `${api.physical_name} / ${api.logical_name}`.length
            }vw, 1rem)`,
          }}
        >
          <i
            className={`fa fa-circle provide-icon-size ${
              selected
                ? api.provide
                  ? 'provide-icon-color'
                  : 'no-provide-icon-color'
                : undefined
            }`}
            aria-hidden="true"
          />
          {Spacer({ size: 10, horizontal: true })}
          {`${api.physical_name} / ${api.logical_name}`}
        </div>
      </div>
      <div className="card-body collapse" id={`card-body-${api.physical_name}`}>
        <div className="api-list-title">
          <p>データセット 連携項目 一覧</p>
          {Spacer({ size: 20 })}
        </div>
        {api.outinfo.map((val: Defs.TableInfo) => {
          const res: JSX.Element[] = [];
          val.column.map((item: Defs.Column) => {
            res.push(
              <div className="list-contents-grid" key={item.physicalName}>
                <p>{item.logicalName}</p>
              </div>,
            );
          });
          return res;
        })}
        {Spacer({ size: 20 })}
        <div className="text-danger error-msg">
          {errorMsg[api.physical_name]}
        </div>
        <br />
        {api.provide ? (
          <>
            <div className="btn-center">
              <button
                className="btn btn-secondary btn-secondary"
                data-toggle="modal"
                data-target={`#${api.physical_name}-dtl-modal`}
                data-backdrop="true"
              >
                サンプルデータ挿入 / データセット連携項目追加
              </button>
            </div>
            <NewAPIDtlModal val={api} />
            <NewSampleDataModal
              id={api.physical_name}
              logicalName={api.logical_name}
            />
            <ApiUpdateResultModal val={api} />
          </>
        ) : (
          <div className="btn-center">
            <button
              className="btn btn-secondary btn-secondary"
              onClick={() => {
                putDeploy(
                  api,
                  changeStatus,
                  setChangeStatus,
                  setErrorMsg,
                  setDeployJobId,
                );
              }}
            >
              有効化
            </button>
          </div>
        )}
      </div>
    </div>
  );
}

// transpose columns and rows
const transpose = (a: any[]) =>
  a[0].map((_: any, c: string | number) => a.map((r) => r[c]).filter(Boolean));

// put api(deploy)
function putDeploy(
  api: Defs.Api,
  changeStatus: boolean,
  setChangeStatus: any,
  setErrorMsg: any,
  setDeployJobId: any,
) {
  if (api) {
    if (!api.provide) {
      (async () => {
        const errorMsgList: { [key: string]: string } = {
          [api.physical_name]: '',
        };
        commonAjax
          .axios({ swalFire: false, loading: true })
          .put('/api/api/deploy', { id: [api.physical_name] })
          .then((res) => {
            if (res.data.errorMsg != null) {
              const { errorMsg } = res.data;
              console.log({ errorMsg });
              errorMsgList[api.physical_name] = errorMsg;
              setErrorMsg(errorMsgList);
            } else {
              setChangeStatus(!changeStatus);
              setErrorMsg(errorMsgList);
              setDeployJobId(res.data.jobId);
            }
          });
      })();
    }
  }
}

function ApiDeployJobStartNotification({ jobId }: { jobId?: string }) {
  return (
    <DialogModalOK
      id="api-deploy-job-start-notification"
      title="処理を開始しました"
    >
      <p>
        データセット有効化処理を開始しました。
        <br />
        完了には時間がかかりますのでお待ちください。
        <br />
        処理の状況は下記画面で確認できます。
      </p>
      <Link
        to={`/jobs/${jobId}`}
        className="btn btn-secondary"
        target="_blank"
        onClick={() => hideDialog('api-deploy-job-start-notification')}
      >
        ジョブステータス確認
      </Link>
    </DialogModalOK>
  );
}

export default NewApiList;
kirin-ri commented 1 month ago
import React, { useEffect, useLayoutEffect, useState } from 'react';
import { Link } from 'react-router-dom';
import { commonAjax } from '../../../components/commonAjax';
import { Spacer } from '../spacer';
import * as Defs from './apiDefs';
import ApiUpdateResultModal from './apiUpdateResultModal';
import CreateNewApi from './createNewApi';
import { DialogModalOK, hideDialog, showDialog } from './dialogModal';
import EditApiList from './editApiList';
import NewAPIDtlModal from './newAPIDtlModal';
import NewSampleDataModal from './newSampleDataModal';
import SelectDeployApi from './selectDeployApi';

// main
function NewApiList() {
  // useState
  const [apiList, setApiList] = useState<Defs.Api[]>([]);
  const [tagList, setTags] = useState<Defs.Tag[]>([]);
  const [changeStatus, setChangeStatus] = useState<boolean>(false);
  const [enableDplApi, setEnableDplApi] = useState<boolean>(false);
  const [errorMsg, setErrorMsg] = useState<{}>({});
  const [deployJobId, setDeployJobId] = useState<string>('');
  const [isModalOpen, setIsModalOpen] = useState<string | null>(null); // 状态控制哪一个モーダル打开

  /* API情報取得のリクエスト */
  useEffect(() => {
    (async () => {
      commonAjax
        .axios({ swalFire: false, loading: true })
        .get('/api/api')
        .then((res) => {
          const data = res.data.api;
          const resTags: React.SetStateAction<Defs.Tag[]> = [];
          let tmpTags: string[] = [];
          data.map((item: any) => {
            tmpTags = tmpTags.concat(item.tags);
          });
          tmpTags = Array.from(new Set(tmpTags));
          tmpTags.map((t: string) => {
            resTags.push({ tag_name: t, select: false });
          });
          setApiList(data);
          setTags(resTags);
          setEnableDplApi(res.data.enableDplApi);
        });
    })();
  }, [changeStatus]);

  useLayoutEffect(() => {
    if (deployJobId) {
      // ジョブ開始ダイアログ表示
      showDialog('api-deploy-job-start-notification');
    }
  }, [deployJobId]);

  const metricsProcess = process.env.REACT_APP_METRICS_PROCESS;
  const title = `${metricsProcess} データセット一覧`;

  useEffect(() => {
    return () => {
      // 清理掉多余的モーダル状态
      setIsModalOpen(null);
    };
  }, []);

  return (
    <div className="content-wrapper api-list">
      <section className="page-cover">
        <h1>{title}</h1>
      </section>
      {/* Content Header (Page header) */}
      <section className="content-header">
        <div className="content-header-left">
          <h1>{title}</h1>
          <div className="content-header-desc">
            現在、設定されているデータセット一覧です
          </div>
        </div>
        {enableDplApi && (
          <div className="content-header-right">
            <button
              type="button"
              className="btn btn-secondary"
              onClick={() => {
                setIsModalOpen('editApiList');
              }}
            >
              編集
            </button>
            <button
              type="button"
              className="btn btn-primary"
              onClick={() => {
                setIsModalOpen('apiDtl');
              }}
            >
              追加
            </button>
            <button
              type="button"
              className="btn-long-text btn-primary"
              onClick={() => {
                setIsModalOpen('selectDeployApi');
              }}
            >
              一括有効化
            </button>
          </div>
        )}
      </section>
      <section className="content">
        {/* filter tag */}
        <div className="inline-form">
          <div className="inline-form-cat">フィルタータグ</div>
          {FilterTags(tagList, setTags)}
        </div>
        {Spacer({ size: 30 })}
        {/* API List (Grid) */}
        {viewList(
          apiList,
          tagList,
          changeStatus,
          setChangeStatus,
          setErrorMsg,
          errorMsg,
          setDeployJobId,
        )}
      </section>
      {isModalOpen === 'editApiList' && (
        <EditApiList
          val={apiList}
          changeStatus={changeStatus}
          setChangeStatus={setChangeStatus}
        />
      )}
      {isModalOpen === 'apiDtl' && (
        <NewAPIDtlModal
          id="api-dtl-modal"
          changeStatus={changeStatus}
          setChangeStatus={setChangeStatus}
        />
      )}
      {isModalOpen === 'selectDeployApi' && (
        <SelectDeployApi
          val={apiList}
          changeStatus={changeStatus}
          setChangeStatus={setChangeStatus}
        />
      )}
      <ApiDeployJobStartNotification jobId={deployJobId} />
    </div>
  );
}

// fillter tag
function FilterTags(tags: Defs.Tag[], setTags: any) {
  return (
    <div className="inline-form-group">
      <div className="inline-form-label">分類</div>
      {tags.map((item: Defs.Tag) => {
        if (item.select) {
          return (
            <div
              className="btn btn-tag active"
              key={item.tag_name}
              onClick={() => {
                item.select = false;
                const t = tags.slice(0, tags.length);
                setTags(t);
              }}
            >
              {item.tag_name}
            </div>
          );
        }
        return (
          <div
            className="btn btn-tag"
            key={item.tag_name}
            onClick={() => {
              item.select = true;
              const t = tags.slice(0, tags.length);
              setTags(t);
            }}
          >
            {item.tag_name}
          </div>
        );
      })}
    </div>
  );
}

function viewList(
  item: Defs.Api[],
  tagList: Defs.Tag[],
  changeStatus: boolean,
  setChangeStatus: any,
  setErrorMsg: any,
  errorMsg: any,
  setDeployJobId: any,
) {
  // Filter the API list based on selected tags
  const filteredList = item.filter((api) => {
    // If no tags are selected, show all
    const isAnyTagSelected = tagList.some((tag) => tag.select);
    if (!isAnyTagSelected) return true;

    // Show only the APIs that match at least one selected tag
    return api.tags.some((apiTag) =>
      tagList.some((tag) => tag.select && tag.tag_name === apiTag),
    );
  });

  const length = Math.ceil(filteredList.length / 3);
  let listItem: any = [];
  if (length !== 0) {
    listItem = transpose(
      new Array(length)
        .fill(0)
        .map((_, i) => filteredList.slice(i * 3, (i + 1) * 3)),
    );
  }

  return (
    <div className="list-wrapper">
      {listItem.map((column: Defs.Api[], columnIndex: number) => (
        <div className="apiList" key={columnIndex}>
          {column.map((val: Defs.Api) =>
            ApiCard(
              val,
              tagList,
              changeStatus,
              setChangeStatus,
              setErrorMsg,
              errorMsg,
              setDeployJobId,
            ),
          )}
        </div>
      ))}
    </div>
  );
}

// API Card
function ApiCard(
  api: Defs.Api,
  tagList: Defs.Tag[],
  changeStatus: boolean,
  setChangeStatus: any,
  setErrorMsg: any,
  errorMsg: any,
  setDeployJobId: any,
) {
  let selected = false;

  // タグが1つでも選択されているか確認
  const isAnyTagSelected = tagList.some((tag) => tag.select);

  tagList.map((item: Defs.Tag) => {
    api.tags.map((apiTag: string) => {
      if (apiTag === item.tag_name) {
        if (!isAnyTagSelected || item.select) {
          // タグが未選択の時、全て選択と同じ扱いにする
          selected = true;
        }
      }
    });
  });

  if (!errorMsg) {
    $(`#${api.physical_name}`).collapse('hide');
  }

  return (
    <div
      className={`card ${
        selected ? 'card-primary' : 'card-not-applicable not-applicable'
      } card-collapse-sample`}
    >
      <div
        className="card-header collapsed"
        data-toggle="collapse"
        data-target={`#card-body-${api.physical_name}`}
      >
        <div
          className="api-name-font"
          style={{
            fontSize: `clamp(0.6rem, ${
              29 / `${api.physical_name} / ${api.logical_name}`.length
            }vw, 1rem)`,
          }}
        >
          <i
            className={`fa fa-circle provide-icon-size ${
              selected
                ? api.provide
                  ? 'provide-icon-color'
                  : 'no-provide-icon-color'
                : undefined
            }`}
            aria-hidden="true"
          />
          {Spacer({ size: 10, horizontal: true })}
          {`${api.physical_name} / ${api.logical_name}`}
        </div>
      </div>
      <div className="card-body collapse" id={`card-body-${api.physical_name}`}>
        <div className="api-list-title">
          <p>データセット 連携項目 一覧</p>
          {Spacer({ size: 20 })}
        </div>
        {api.outinfo.map((val: Defs.TableInfo) => {
          const res: JSX.Element[] = [];
          val.column.map((item: Defs.Column) => {
            res.push(
              <div className="list-contents-grid" key={item.physicalName}>
                <p>{item.logicalName}</p>
              </div>,
            );
          });
          return res;
        })}
        {Spacer({ size: 20 })}
        <div className="text-danger error-msg">
          {errorMsg[api.physical_name]}
        </div>
        <br />
        {api.provide ? (
          <>
            <div className="btn-center">
              <button
                className="btn btn-secondary btn-secondary"
                onClick={() => setIsModalOpen(`${api.physical_name}-dtl-modal`)}
              >
                サンプルデータ挿入 / データセット連携項目追加
              </button>
            </div>
            {isModalOpen === `${api.physical_name}-dtl-modal` && (
              <>
                <NewAPIDtlModal val={api} />
                <NewSampleDataModal
                  id={api.physical_name}
                  logicalName={api.logical_name}
                />
                <ApiUpdateResultModal val={api} />
              </>
            )}
          </>
        ) : (
          <div className="btn-center">
            <button
              className="btn btn-secondary btn-secondary"
              onClick={() => {
                putDeploy(
                  api,
                  changeStatus,
                  setChangeStatus,
                  setErrorMsg,
                  setDeployJobId,
                );
              }}
            >
              有効化
            </button>
          </div>
        )}
      </div>
    </div>
  );
}

// transpose columns and rows
const transpose = (a: any[]) =>
  a[0].map((_: any, c: string | number) => a.map((r) => r[c]).filter(Boolean));

// put api(deploy)
function putDeploy(
  api: Defs.Api,
  changeStatus: boolean,
  setChangeStatus: any,
  setErrorMsg: any,
  setDeployJobId: any,
) {
  if (api) {
    if (!api.provide) {
      (async () => {
        const errorMsgList: { [key: string]: string } = {
          [api.physical_name]: '',
        };
        commonAjax
          .axios({ swalFire: false, loading: true })
          .put('/api/api/deploy', { id: [api.physical_name] })
          .then((res) => {
            if (res.data.errorMsg != null) {
              const { errorMsg } = res.data;
              console.log({ errorMsg });
              errorMsgList[api.physical_name] = errorMsg;
              setErrorMsg(errorMsgList);
            } else {
              setChangeStatus(!changeStatus);
              setErrorMsg(errorMsgList);
              setDeployJobId(res.data.jobId);
            }
          });
      })();
    }
  }
}

function ApiDeployJobStartNotification({ jobId }: { jobId?: string }) {
  return (
    <DialogModalOK
      id="api-deploy-job-start-notification"
      title="処理を開始しました"
    >
      <p>
        データセット有効化処理を開始しました。
        <br />
        完了には時間がかかりますのでお待ちください。
        <br />
        処理の状況は下記画面で確認できます。
      </p>
      <Link
        to={`/jobs/${jobId}`}
        className="btn btn-secondary"
        target="_blank"
        onClick={() => hideDialog('api-deploy-job-start-notification')}
      >
        ジョブステータス確認
      </Link>
    </DialogModalOK>
  );
}

export default NewApiList;
kirin-ri commented 1 month ago
ERROR in src/app2/components/pages/apiList.tsx:137:11
TS2322: Type '{ id: string; changeStatus: boolean; setChangeStatus: Dispatch<SetStateAction<boolean>>; }' is not assignable to type 'IntrinsicAttributes & { val: Api; }'.
  Property 'id' does not exist on type 'IntrinsicAttributes & { val: Api; }'.
    135 |       {isModalOpen === 'apiDtl' && (
    136 |         <NewAPIDtlModal
  > 137 |           id="api-dtl-modal"
        |           ^^
    138 |           changeStatus={changeStatus}
    139 |           setChangeStatus={setChangeStatus}
    140 |         />

ERROR in src/app2/components/pages/apiList.tsx:334:32
TS2304: Cannot find name 'setIsModalOpen'.
    332 |               <button
    333 |                 className="btn btn-secondary btn-secondary"
  > 334 |                 onClick={() => setIsModalOpen(`${api.physical_name}-dtl-modal`)}
        |                                ^^^^^^^^^^^^^^
    335 |               >
    336 |                 サンプルデータ挿入 / データセット連携項目追加
    337 |               </button>

ERROR in src/app2/components/pages/apiList.tsx:339:14
TS2304: Cannot find name 'isModalOpen'.
    337 |               </button>
    338 |             </div>
  > 339 |             {isModalOpen === `${api.physical_name}-dtl-modal` && (
        |              ^^^^^^^^^^^
    340 |               <>
    341 |                 <NewAPIDtlModal val={api} />
    342 |                 <NewSampleDataModal
kirin-ri commented 1 month ago
import React, { useEffect, useLayoutEffect, useState } from 'react';
import { Link } from 'react-router-dom';
import { commonAjax } from '../../../components/commonAjax';
import { Spacer } from '../spacer';
import * as Defs from './apiDefs';
import ApiUpdateResultModal from './apiUpdateResultModal';
import CreateNewApi from './createNewApi';
import { DialogModalOK, hideDialog, showDialog } from './dialogModal';
import EditApiList from './editApiList';
import NewAPIDtlModal from './newAPIDtlModal';
import NewSampleDataModal from './newSampleDataModal';
import SelectDeployApi from './selectDeployApi';

// main
function NewApiList() {
  // useState
  const [apiList, setApiList] = useState<Defs.Api[]>([]);
  const [tagList, setTags] = useState<Defs.Tag[]>([]);
  const [changeStatus, setChangeStatus] = useState<boolean>(false);
  const [enableDplApi, setEnableDplApi] = useState<boolean>(false);
  const [errorMsg, setErrorMsg] = useState<{}>({});
  const [deployJobId, setDeployJobId] = useState<string>('');
  const [isModalOpen, setIsModalOpen] = useState<string | null>(null); // 修正1: 增加 isModalOpen 和 setIsModalOpen 状态

  /* API情報取得のリクエスト */
  useEffect(() => {
    (async () => {
      commonAjax
        .axios({ swalFire: false, loading: true })
        .get('/api/api')
        .then((res) => {
          const data = res.data.api;
          const resTags: React.SetStateAction<Defs.Tag[]> = [];
          let tmpTags: string[] = [];
          data.map((item: any) => {
            tmpTags = tmpTags.concat(item.tags);
          });
          tmpTags = Array.from(new Set(tmpTags));
          tmpTags.map((t: string) => {
            resTags.push({ tag_name: t, select: false });
          });
          setApiList(data);
          setTags(resTags);
          setEnableDplApi(res.data.enableDplApi);
        });
    })();
  }, [changeStatus]);

  useLayoutEffect(() => {
    if (deployJobId) {
      // ジョブ開始ダイアログ表示
      showDialog('api-deploy-job-start-notification');
    }
  }, [deployJobId]);

  const metricsProcess = process.env.REACT_APP_METRICS_PROCESS;
  const title = `${metricsProcess} データセット一覧`;

  useEffect(() => {
    return () => {
      // 清理掉多余的モーダル状态
      setIsModalOpen(null);
    };
  }, []);

  return (
    <div className="content-wrapper api-list">
      <section className="page-cover">
        <h1>{title}</h1>
      </section>
      {/* Content Header (Page header) */}
      <section className="content-header">
        <div className="content-header-left">
          <h1>{title}</h1>
          <div className="content-header-desc">
            現在、設定されているデータセット一覧です
          </div>
        </div>
        {enableDplApi && (
          <div className="content-header-right">
            <button
              type="button"
              className="btn btn-secondary"
              onClick={() => {
                setIsModalOpen('editApiList'); // 修正2: 使用 setIsModalOpen 来控制モーダル的打开
              }}
            >
              編集
            </button>
            <button
              type="button"
              className="btn btn-primary"
              onClick={() => {
                setIsModalOpen('apiDtl'); // 修正2: 控制 API详情モーダル的显示
              }}
            >
              追加
            </button>
            <button
              type="button"
              className="btn-long-text btn-primary"
              onClick={() => {
                setIsModalOpen('selectDeployApi'); // 修正2: 控制一括有効化モーダル
              }}
            >
              一括有効化
            </button>
          </div>
        )}
      </section>
      <section className="content">
        {/* filter tag */}
        <div className="inline-form">
          <div className="inline-form-cat">フィルタータグ</div>
          {FilterTags(tagList, setTags)}
        </div>
        {Spacer({ size: 30 })}
        {/* API List (Grid) */}
        {viewList(
          apiList,
          tagList,
          changeStatus,
          setChangeStatus,
          setErrorMsg,
          errorMsg,
          setDeployJobId,
        )}
      </section>

      {isModalOpen === 'editApiList' && ( // 修正3: 根据 isModalOpen 状态控制モーダル显示
        <EditApiList
          val={apiList}
          changeStatus={changeStatus}
          setChangeStatus={setChangeStatus}
        />
      )}

      {isModalOpen === 'apiDtl' && (
        <NewAPIDtlModal
          changeStatus={changeStatus}
          setChangeStatus={setChangeStatus}
        />
      )}

      {isModalOpen === 'selectDeployApi' && (
        <SelectDeployApi
          val={apiList}
          changeStatus={changeStatus}
          setChangeStatus={setChangeStatus}
        />
      )}

      <ApiDeployJobStartNotification jobId={deployJobId} />
    </div>
  );
}

// fillter tag
function FilterTags(tags: Defs.Tag[], setTags: any) {
  return (
    <div className="inline-form-group">
      <div className="inline-form-label">分類</div>
      {tags.map((item: Defs.Tag) => {
        if (item.select) {
          return (
            <div
              className="btn btn-tag active"
              key={item.tag_name}
              onClick={() => {
                item.select = false;
                const t = tags.slice(0, tags.length);
                setTags(t);
              }}
            >
              {item.tag_name}
            </div>
          );
        }
        return (
          <div
            className="btn btn-tag"
            key={item.tag_name}
            onClick={() => {
              item.select = true;
              const t = tags.slice(0, tags.length);
              setTags(t);
            }}
          >
            {item.tag_name}
          </div>
        );
      })}
    </div>
  );
}

function viewList(
  item: Defs.Api[],
  tagList: Defs.Tag[],
  changeStatus: boolean,
  setChangeStatus: any,
  setErrorMsg: any,
  errorMsg: any,
  setDeployJobId: any,
) {
  // Filter the API list based on selected tags
  const filteredList = item.filter((api) => {
    // If no tags are selected, show all
    const isAnyTagSelected = tagList.some((tag) => tag.select);
    if (!isAnyTagSelected) return true;

    // Show only the APIs that match at least one selected tag
    return api.tags.some((apiTag) =>
      tagList.some((tag) => tag.select && tag.tag_name === apiTag),
    );
  });

  const length = Math.ceil(filteredList.length / 3);
  let listItem: any = [];
  if (length !== 0) {
    listItem = transpose(
      new Array(length)
        .fill(0)
        .map((_, i) => filteredList.slice(i * 3, (i + 1) * 3)),
    );
  }

  return (
    <div className="list-wrapper">
      {listItem.map((column: Defs.Api[], columnIndex: number) => (
        <div className="apiList" key={columnIndex}>
          {column.map((val: Defs.Api) =>
            ApiCard(
              val,
              tagList,
              changeStatus,
              setChangeStatus,
              setErrorMsg,
              errorMsg,
              setDeployJobId,
            ),
          )}
        </div>
      ))}
    </div>
  );
}

// API Card
function ApiCard(
  api: Defs.Api,
  tagList: Defs.Tag[],
  changeStatus: boolean,
  setChangeStatus: any,
  setErrorMsg: any,
  errorMsg: any,
  setDeployJobId: any,
) {
  let selected = false;

  // タグが1つでも選択されているか確認
  const isAnyTagSelected = tagList.some((tag) => tag.select);

  tagList.map((item: Defs.Tag) => {
    api.tags.map((apiTag: string) => {
      if (apiTag === item.tag_name) {
        if (!isAnyTagSelected || item.select) {
          // タグが未選択の時、全て選択と同じ扱いにする
          selected = true;
        }
      }
    });
  });

  if (!errorMsg) {
    $(`#${api.physical_name}`).collapse('hide');
  }

  return (
    <div
      className={`card ${
        selected ? 'card-primary' : 'card-not-applicable not-applicable'
      } card-collapse-sample`}
    >
      <div
        className="card-header collapsed"
        data-toggle="collapse"
        data-target={`#card-body-${api.physical_name}`}
      >
        <div
          className="api-name-font"
          style={{
            fontSize: `clamp(0.6rem, ${
              29 / `${api.physical_name} / ${api.logical_name}`.length
            }vw, 1rem)`,
          }}
        >
          <i
            className={`fa fa-circle provide-icon-size ${
              selected
                ? api.provide
                  ? 'provide-icon-color'
                  : 'no-provide-icon-color'
                : undefined
            }`}
            aria-hidden="true"
          />
          {Spacer({ size: 10, horizontal: true })}
          {`${api.physical_name} / ${api.logical_name}`}
        </div>
      </div>
      <div className="card-body collapse" id={`card-body-${api.physical_name}`}>
        <div className="api-list-title">
          <p>データセット 連携項目 一覧</p>
          {Spacer({ size: 20 })}
        </div>
        {api.outinfo.map((val: Defs.TableInfo) => {
          const res: JSX.Element[] = [];
          val.column.map((item: Defs.Column) => {
            res.push(
              <div className="list-contents-grid" key={item.physicalName}>
                <p>{item.logicalName}</p>
              </div>,
            );
          });
          return res;
        })}
        {Spacer({ size: 20 })}
        <div className="text-danger error-msg">
          {errorMsg[api.physical_name]}
        </div>
        <br />
        {api.provide ? (
          <>
            <div className="btn-center">
              <button
                className="btn btn-secondary btn-secondary"
                onClick={() => setIsModalOpen(`${api.physical_name}-dtl-modal`)} // 修正4: 使用状态控制每个API的モーダル
              >
                サンプルデータ挿入 / データセット連携項目追加
              </button>
            </div>
            {isModalOpen === `${api.physical_name}-dtl-modal` && ( // 修正5: 控制显示 API详情モーダル
              <>
                <NewAPIDtlModal val={api} />
                <NewSampleDataModal
                  id={api.physical_name}
                  logicalName={api.logical_name}
                />
                <ApiUpdateResultModal val={api} />
              </>
            )}
          </>
        ) : (
          <div className="btn-center">
            <button
              className="btn btn-secondary btn-secondary"
              onClick={() => {
                putDeploy(
                  api,
                  changeStatus,
                  setChangeStatus,
                  setErrorMsg,
                  setDeployJobId,
                );
              }}
            >
              有効化
            </button>
          </div>
        )}
      </div>
    </div>
  );
}

// transpose columns and rows
const transpose = (a: any[]) =>
  a[0].map((_: any, c: string | number) => a.map((r) => r[c]).filter(Boolean));

// put api(deploy)
function putDeploy(
  api: Defs.Api,
  changeStatus: boolean,
  setChangeStatus: any,
  setErrorMsg: any,
  setDeployJobId: any,
) {
  if (api) {
    if (!api.provide) {
      (async () => {
        const errorMsgList: { [key: string]: string } = {
          [api.physical_name]: '',
        };
        commonAjax
          .axios({ swalFire: false, loading: true })
          .put('/api/api/deploy', { id: [api.physical_name] })
          .then((res) => {
            if (res.data.errorMsg != null) {
              const { errorMsg } = res.data;
              console.log({ errorMsg });
              errorMsgList[api.physical_name] = errorMsg;
              setErrorMsg(errorMsgList);
            } else {
              setChangeStatus(!changeStatus);
              setErrorMsg(errorMsgList);
              setDeployJobId(res.data.jobId);
            }
          });
      })();
    }
  }
}

function ApiDeployJobStartNotification({ jobId }: { jobId?: string }) {
  return (
    <DialogModalOK
      id="api-deploy-job-start-notification"
      title="処理を開始しました"
    >
      <p>
        データセット有効化処理を開始しました。
        <br />
        完了には時間がかかりますのでお待ちください。
        <br />
        処理の状況は下記画面で確認できます。
      </p>
      <Link
        to={`/jobs/${jobId}`}
        className="btn btn-secondary"
        target="_blank"
        onClick={() => hideDialog('api-deploy-job-start-notification')}
      >
        ジョブステータス確認
      </Link>
    </DialogModalOK>
  );
}

export default NewApiList;
kirin-ri commented 1 month ago
ERROR in src/app2/components/pages/apiList.tsx:139:11
TS2322: Type '{ changeStatus: boolean; setChangeStatus: Dispatch<SetStateAction<boolean>>; }' is not assignable to type 'IntrinsicAttributes & { val: Api; }'.
  Property 'changeStatus' does not exist on type 'IntrinsicAttributes & { val: Api; }'.
    137 |       {isModalOpen === 'apiDtl' && (
    138 |         <NewAPIDtlModal
  > 139 |           changeStatus={changeStatus}
        |           ^^^^^^^^^^^^
    140 |           setChangeStatus={setChangeStatus}
    141 |         />
    142 |       )}

ERROR in src/app2/components/pages/apiList.tsx:337:32
TS2304: Cannot find name 'setIsModalOpen'.
    335 |               <button
    336 |                 className="btn btn-secondary btn-secondary"
  > 337 |                 onClick={() => setIsModalOpen(`${api.physical_name}-dtl-modal`)} // 修正4: 使用状态控制每个API的モーダル
        |                                ^^^^^^^^^^^^^^
    338 |               >
    339 |                 サンプルデータ挿入 / データセット連携項目追加
    340 |               </button>

ERROR in src/app2/components/pages/apiList.tsx:342:14
TS2304: Cannot find name 'isModalOpen'.
    340 |               </button>
    341 |             </div>
  > 342 |             {isModalOpen === `${api.physical_name}-dtl-modal` && ( // 修正5: 控制显示 API详情モーダル
        |              ^^^^^^^^^^^
    343 |               <>
    344 |                 <NewAPIDtlModal val={api} />
    345 |                 <NewSampleDataModal
kirin-ri commented 1 month ago
import React, { useEffect, useLayoutEffect, useState } from 'react';
import { Link } from 'react-router-dom';
import { commonAjax } from '../../../components/commonAjax';
import { Spacer } from '../spacer';
import * as Defs from './apiDefs';
import ApiUpdateResultModal from './apiUpdateResultModal';
import CreateNewApi from './createNewApi';
import { DialogModalOK, hideDialog, showDialog } from './dialogModal';
import EditApiList from './editApiList';
import NewAPIDtlModal from './newAPIDtlModal';
import NewSampleDataModal from './newSampleDataModal';
import SelectDeployApi from './selectDeployApi';

// main
function NewApiList() {
  // useState
  const [apiList, setApiList] = useState<Defs.Api[]>([]);
  const [tagList, setTags] = useState<Defs.Tag[]>([]);
  const [changeStatus, setChangeStatus] = useState<boolean>(false);
  const [enableDplApi, setEnableDplApi] = useState<boolean>(false);
  const [errorMsg, setErrorMsg] = useState<{}>({});
  const [deployJobId, setDeployJobId] = useState<string>('');
  const [isModalOpen, setIsModalOpen] = useState<string | null>(null); // 定义 isModalOpen 和 setIsModalOpen

  /* API情報取得のリクエスト */
  useEffect(() => {
    (async () => {
      commonAjax
        .axios({ swalFire: false, loading: true })
        .get('/api/api')
        .then((res) => {
          const data = res.data.api;
          const resTags: React.SetStateAction<Defs.Tag[]> = [];
          let tmpTags: string[] = [];
          data.map((item: any) => {
            tmpTags = tmpTags.concat(item.tags);
          });
          tmpTags = Array.from(new Set(tmpTags));
          tmpTags.map((t: string) => {
            resTags.push({ tag_name: t, select: false });
          });
          setApiList(data);
          setTags(resTags);
          setEnableDplApi(res.data.enableDplApi);
        });
    })();
  }, [changeStatus]);

  useLayoutEffect(() => {
    if (deployJobId) {
      // ジョブ開始ダイアログ表示
      showDialog('api-deploy-job-start-notification');
    }
  }, [deployJobId]);

  const metricsProcess = process.env.REACT_APP_METRICS_PROCESS;
  const title = `${metricsProcess} データセット一覧`;

  useEffect(() => {
    return () => {
      // 清理掉多余的モーダル状态
      setIsModalOpen(null);
    };
  }, []);

  return (
    <div className="content-wrapper api-list">
      <section className="page-cover">
        <h1>{title}</h1>
      </section>
      {/* Content Header (Page header) */}
      <section className="content-header">
        <div className="content-header-left">
          <h1>{title}</h1>
          <div className="content-header-desc">
            現在、設定されているデータセット一覧です
          </div>
        </div>
        {enableDplApi && (
          <div className="content-header-right">
            <button
              type="button"
              className="btn btn-secondary"
              onClick={() => {
                setIsModalOpen('editApiList');
              }}
            >
              編集
            </button>
            <button
              type="button"
              className="btn btn-primary"
              onClick={() => {
                setIsModalOpen('apiDtl');
              }}
            >
              追加
            </button>
            <button
              type="button"
              className="btn-long-text btn-primary"
              onClick={() => {
                setIsModalOpen('selectDeployApi');
              }}
            >
              一括有効化
            </button>
          </div>
        )}
      </section>
      <section className="content">
        {/* filter tag */}
        <div className="inline-form">
          <div className="inline-form-cat">フィルタータグ</div>
          {FilterTags(tagList, setTags)}
        </div>
        {Spacer({ size: 30 })}
        {/* API List (Grid) */}
        {viewList(
          apiList,
          tagList,
          changeStatus,
          setChangeStatus,
          setErrorMsg,
          errorMsg,
          setDeployJobId,
        )}
      </section>

      {isModalOpen === 'editApiList' && (
        <EditApiList
          val={apiList}
          changeStatus={changeStatus}
          setChangeStatus={setChangeStatus}
        />
      )}

      {isModalOpen === 'apiDtl' && (
        <NewAPIDtlModal val={api} /> {/* 移除 changeStatus 和 setChangeStatus */}
      )}

      {isModalOpen === 'selectDeployApi' && (
        <SelectDeployApi
          val={apiList}
          changeStatus={changeStatus}
          setChangeStatus={setChangeStatus}
        />
      )}

      <ApiDeployJobStartNotification jobId={deployJobId} />
    </div>
  );
}

// fillter tag
function FilterTags(tags: Defs.Tag[], setTags: any) {
  return (
    <div className="inline-form-group">
      <div className="inline-form-label">分類</div>
      {tags.map((item: Defs.Tag) => {
        if (item.select) {
          return (
            <div
              className="btn btn-tag active"
              key={item.tag_name}
              onClick={() => {
                item.select = false;
                const t = tags.slice(0, tags.length);
                setTags(t);
              }}
            >
              {item.tag_name}
            </div>
          );
        }
        return (
          <div
            className="btn btn-tag"
            key={item.tag_name}
            onClick={() => {
              item.select = true;
              const t = tags.slice(0, tags.length);
              setTags(t);
            }}
          >
            {item.tag_name}
          </div>
        );
      })}
    </div>
  );
}

function viewList(
  item: Defs.Api[],
  tagList: Defs.Tag[],
  changeStatus: boolean,
  setChangeStatus: any,
  setErrorMsg: any,
  errorMsg: any,
  setDeployJobId: any,
) {
  // Filter the API list based on selected tags
  const filteredList = item.filter((api) => {
    // If no tags are selected, show all
    const isAnyTagSelected = tagList.some((tag) => tag.select);
    if (!isAnyTagSelected) return true;

    // Show only the APIs that match at least one selected tag
    return api.tags.some((apiTag) =>
      tagList.some((tag) => tag.select && tag.tag_name === apiTag),
    );
  });

  const length = Math.ceil(filteredList.length / 3);
  let listItem: any = [];
  if (length !== 0) {
    listItem = transpose(
      new Array(length)
        .fill(0)
        .map((_, i) => filteredList.slice(i * 3, (i + 1) * 3)),
    );
  }

  return (
    <div className="list-wrapper">
      {listItem.map((column: Defs.Api[], columnIndex: number) => (
        <div className="apiList" key={columnIndex}>
          {column.map((val: Defs.Api) =>
            ApiCard(
              val,
              tagList,
              changeStatus,
              setChangeStatus,
              setErrorMsg,
              errorMsg,
              setDeployJobId,
            ),
          )}
        </div>
      ))}
    </div>
  );
}

// API Card
function ApiCard(
  api: Defs.Api,
  tagList: Defs.Tag[],
  changeStatus: boolean,
  setChangeStatus: any,
  setErrorMsg: any,
  errorMsg: any,
  setDeployJobId: any,
) {
  let selected = false;

  // タグが1つでも選択されているか確認
  const isAnyTagSelected = tagList.some((tag) => tag.select);

  tagList.map((item: Defs.Tag) => {
    api.tags.map((apiTag: string) => {
      if (apiTag === item.tag_name) {
        if (!isAnyTagSelected || item.select) {
          // タグが未選択の時、全て選択と同じ扱いにする
          selected = true;
        }
      }
    });
  });

  if (!errorMsg) {
    $(`#${api.physical_name}`).collapse('hide');
  }

  return (
    <div
      className={`card ${
        selected ? 'card-primary' : 'card-not-applicable not-applicable'
      } card-collapse-sample`}
    >
      <div
        className="card-header collapsed"
        data-toggle="collapse"
        data-target={`#card-body-${api.physical_name}`}
      >
        <div
          className="api-name-font"
          style={{
            fontSize: `clamp(0.6rem, ${
              29 / `${api.physical_name} / ${api.logical_name}`.length
            }vw, 1rem)`,
          }}
        >
          <i
            className={`fa fa-circle provide-icon-size ${
              selected
                ? api.provide
                  ? 'provide-icon-color'
                  : 'no-provide-icon-color'
                : undefined
            }`}
            aria-hidden="true"
          />
          {Spacer({ size: 10, horizontal: true })}
          {`${api.physical_name} / ${api.logical_name}`}
        </div>
      </div>
      <div className="card-body collapse" id={`card-body-${api.physical_name}`}>
        <div className="api-list-title">
          <p>データセット 連携項目 一覧</p>
          {Spacer({ size: 20 })}
        </div>
        {api.outinfo.map((val: Defs.TableInfo) => {
          const res: JSX.Element[] = [];
          val.column.map((item: Defs.Column) => {
            res.push(
              <div className="list-contents-grid" key={item.physicalName}>
                <p>{item.logicalName}</p>
              </div>,
            );
          });
          return res;
        })}
        {Spacer({ size: 20 })}
        <div className="text-danger error-msg">
          {errorMsg[api.physical_name]}
        </div>
        <br />
        {api.provide ? (
          <>
            <div className="btn-center">
              <button
                className="btn btn-secondary btn-secondary"
                onClick={() => setIsModalOpen(`${api.physical_name}-dtl-modal`)}
              >
                サンプルデータ挿入 / データセット連携項目追加
              </button>
            </div>
            {isModalOpen === `${api.physical_name}-dtl-modal` && (
              <>
                <NewAPIDtlModal val={api} />
                <NewSampleDataModal
                  id={api.physical_name}
                  logicalName={api.logical_name}
                />
                <ApiUpdateResultModal val={api} />
              </>
            )}
          </>
        ) : (
          <div className="btn-center">
            <button
              className="btn btn-secondary btn-secondary"
              onClick={() => {
                putDeploy(
                  api,
                  changeStatus,
                  setChangeStatus,
                  setErrorMsg,
                  setDeployJobId,
                );
              }}
            >
              有効化
            </button>
          </div>
        )}
      </div>
    </div>
  );
}

// transpose columns and rows
const transpose = (a: any[]) =>
  a[0].map((_: any, c: string | number) => a.map((r) => r[c]).filter(Boolean));

// put api(deploy)
function putDeploy(
  api: Defs.Api,
  changeStatus: boolean,
  setChangeStatus: any,
  setErrorMsg: any,
  setDeployJobId: any,
) {
  if (api) {
    if (!api.provide) {
      (async () => {
        const errorMsgList: { [key: string]: string } = {
          [api.physical_name]: '',
        };
        commonAjax
          .axios({ swalFire: false, loading: true })
          .put('/api/api/deploy', { id: [api.physical_name] })
          .then((res) => {
            if (res.data.errorMsg != null) {
              const { errorMsg } = res.data;
              console.log({ errorMsg });
              errorMsgList[api.physical_name] = errorMsg;
              setErrorMsg(errorMsgList);
            } else {
              setChangeStatus(!changeStatus);
              setErrorMsg(errorMsgList);
              setDeployJobId(res.data.jobId);
            }
          });
      })();
    }
  }
}

function ApiDeployJobStartNotification({ jobId }: { jobId?: string }) {
  return (
    <DialogModalOK
      id="api-deploy-job-start-notification"
      title="処理を開始しました"
    >
      <p>
        データセット有効化処理を開始しました。
        <br />
        完了には時間がかかりますのでお待ちください。
        <br />
        処理の状況は下記画面で確認できます。
      </p>
      <Link
        to={`/jobs/${jobId}`}
        className="btn btn-secondary"
        target="_blank"
        onClick={() => hideDialog('api-deploy-job-start-notification')}
      >
        ジョブステータス確認
      </Link>
    </DialogModalOK>
  );
}

export default NewApiList;
kirin-ri commented 1 month ago
Failed to compile.

SyntaxError: /home/uenv/q_li/Desktop/catalog-web-app/client/src/app2/components/pages/apiList.tsx: Unexpected token, expected "," (138:37)
  136 |
  137 |       {isModalOpen === 'apiDtl' && (
> 138 |         <NewAPIDtlModal val={api} /> {/* 移除 changeStatus 和 setChangeStatus */}
      |                                      ^
  139 |       )}
  140 |
  141 |       {isModalOpen === 'selectDeployApi' && (
ERROR in ./src/app2/components/pages/apiList.tsx
Module build failed (from ./node_modules/babel-loader/lib/index.js):
SyntaxError: /home/uenv/q_li/Desktop/catalog-web-app/client/src/app2/components/pages/apiList.tsx: Unexpected token, expected "," (138:37)

  136 |
  137 |       {isModalOpen === 'apiDtl' && (
> 138 |         <NewAPIDtlModal val={api} /> {/* 移除 changeStatus 和 setChangeStatus */}
      |                                      ^
  139 |       )}
  140 |
  141 |       {isModalOpen === 'selectDeployApi' && (
    at instantiate (/home/uenv/q_li/Desktop/catalog-web-app/client/node_modules/@babel/parser/lib/index.js:60:32)
    at constructor (/home/uenv/q_li/Desktop/catalog-web-app/client/node_modules/@babel/parser/lib/index.js:355:12)
    at TypeScriptParserMixin.raise (/home/uenv/q_li/Desktop/catalog-web-app/client/node_modules/@babel/parser/lib/index.js:3204:19)
    at TypeScriptParserMixin.unexpected (/home/uenv/q_li/Desktop/catalog-web-app/client/node_modules/@babel/parser/lib/index.js:3234:16)
    at TypeScriptParserMixin.expect (/home/uenv/q_li/Desktop/catalog-web-app/client/node_modules/@babel/parser/lib/index.js:3571:28)
    at TypeScriptParserMixin.parseParenAndDistinguishExpression (/home/uenv/q_li/Desktop/catalog-web-app/client/node_modules/@babel/parser/lib/index.js:11450:14)
    at TypeScriptParserMixin.parseExprAtom (/home/uenv/q_li/Desktop/catalog-web-app/client/node_modules/@babel/parser/lib/index.js:11115:23)
    at TypeScriptParserMixin.parseExprAtom (/home/uenv/q_li/Desktop/catalog-web-app/client/node_modules/@babel/parser/lib/index.js:6918:20)
    at TypeScriptParserMixin.parseExprSubscripts (/home/uenv/q_li/Desktop/catalog-web-app/client/node_modules/@babel/parser/lib/index.js:10841:23)
    at TypeScriptParserMixin.parseUpdate (/home/uenv/q_li/Desktop/catalog-web-app/client/node_modules/@babel/parser/lib/index.js:10824:21)
    at TypeScriptParserMixin.parseMaybeUnary (/home/uenv/q_li/Desktop/catalog-web-app/client/node_modules/@babel/parser/lib/index.js:10800:23)
    at TypeScriptParserMixin.parseMaybeUnary (/home/uenv/q_li/Desktop/catalog-web-app/client/node_modules/@babel/parser/lib/index.js:9684:18)
    at TypeScriptParserMixin.parseMaybeUnaryOrPrivate (/home/uenv/q_li/Desktop/catalog-web-app/client/node_modules/@babel/parser/lib/index.js:10638:61)
    at TypeScriptParserMixin.parseExprOpBaseRightExpr (/home/uenv/q_li/Desktop/catalog-web-app/client/node_modules/@babel/parser/lib/index.js:10731:34)
    at TypeScriptParserMixin.parseExprOpRightExpr (/home/uenv/q_li/Desktop/catalog-web-app/client/node_modules/@babel/parser/lib/index.js:10726:21)
    at TypeScriptParserMixin.parseExprOp (/home/uenv/q_li/Desktop/catalog-web-app/client/node_modules/@babel/parser/lib/index.js:10689:27)
    at TypeScriptParserMixin.parseExprOp (/home/uenv/q_li/Desktop/catalog-web-app/client/node_modules/@babel/parser/lib/index.js:9190:18)
    at TypeScriptParserMixin.parseExprOp (/home/uenv/q_li/Desktop/catalog-web-app/client/node_modules/@babel/parser/lib/index.js:10697:21)
    at TypeScriptParserMixin.parseExprOp (/home/uenv/q_li/Desktop/catalog-web-app/client/node_modules/@babel/parser/lib/index.js:9190:18)
    at TypeScriptParserMixin.parseExprOps (/home/uenv/q_li/Desktop/catalog-web-app/client/node_modules/@babel/parser/lib/index.js:10647:17)
    at TypeScriptParserMixin.parseMaybeConditional (/home/uenv/q_li/Desktop/catalog-web-app/client/node_modules/@babel/parser/lib/index.js:10620:23)
    at TypeScriptParserMixin.parseMaybeAssign (/home/uenv/q_li/Desktop/catalog-web-app/client/node_modules/@babel/parser/lib/index.js:10581:21)
    at TypeScriptParserMixin.parseMaybeAssign (/home/uenv/q_li/Desktop/catalog-web-app/client/node_modules/@babel/parser/lib/index.js:9631:20)
    at TypeScriptParserMixin.parseExpressionBase (/home/uenv/q_li/Desktop/catalog-web-app/client/node_modules/@babel/parser/lib/index.js:10535:23)
    at /home/uenv/q_li/Desktop/catalog-web-app/client/node_modules/@babel/parser/lib/index.js:10531:39
    at TypeScriptParserMixin.allowInAnd (/home/uenv/q_li/Desktop/catalog-web-app/client/node_modules/@babel/parser/lib/index.js:12231:12)
    at TypeScriptParserMixin.parseExpression (/home/uenv/q_li/Desktop/catalog-web-app/client/node_modules/@babel/parser/lib/index.js:10531:17)
    at TypeScriptParserMixin.jsxParseExpressionContainer (/home/uenv/q_li/Desktop/catalog-web-app/client/node_modules/@babel/parser/lib/index.js:6778:31)
    at TypeScriptParserMixin.jsxParseElementAt (/home/uenv/q_li/Desktop/catalog-web-app/client/node_modules/@babel/parser/lib/index.js:6857:36)
    at TypeScriptParserMixin.jsxParseElement (/home/uenv/q_li/Desktop/catalog-web-app/client/node_modules/@babel/parser/lib/index.js:6901:17)
    at TypeScriptParserMixin.parseExprAtom (/home/uenv/q_li/Desktop/catalog-web-app/client/node_modules/@babel/parser/lib/index.js:6913:19)
    at TypeScriptParserMixin.parseExprSubscripts (/home/uenv/q_li/Desktop/catalog-web-app/client/node_modules/@babel/parser/lib/index.js:10841:23)
    at TypeScriptParserMixin.parseUpdate (/home/uenv/q_li/Desktop/catalog-web-app/client/node_modules/@babel/parser/lib/index.js:10824:21)
    at TypeScriptParserMixin.parseMaybeUnary (/home/uenv/q_li/Desktop/catalog-web-app/client/node_modules/@babel/parser/lib/index.js:10800:23)
    at TypeScriptParserMixin.parseMaybeUnary (/home/uenv/q_li/Desktop/catalog-web-app/client/node_modules/@babel/parser/lib/index.js:9684:18)
    at TypeScriptParserMixin.parseMaybeUnaryOrPrivate (/home/uenv/q_li/Desktop/catalog-web-app/client/node_modules/@babel/parser/lib/index.js:10638:61)
    at TypeScriptParserMixin.parseExprOps (/home/uenv/q_li/Desktop/catalog-web-app/client/node_modules/@babel/parser/lib/index.js:10643:23)
    at TypeScriptParserMixin.parseMaybeConditional (/home/uenv/q_li/Desktop/catalog-web-app/client/node_modules/@babel/parser/lib/index.js:10620:23)
    at TypeScriptParserMixin.parseMaybeAssign (/home/uenv/q_li/Desktop/catalog-web-app/client/node_modules/@babel/parser/lib/index.js:10581:21)
    at /home/uenv/q_li/Desktop/catalog-web-app/client/node_modules/@babel/parser/lib/index.js:9620:39
    at TypeScriptParserMixin.tryParse (/home/uenv/q_li/Desktop/catalog-web-app/client/node_modules/@babel/parser/lib/index.js:3578:20)
    at TypeScriptParserMixin.parseMaybeAssign (/home/uenv/q_li/Desktop/catalog-web-app/client/node_modules/@babel/parser/lib/index.js:9620:18)
    at /home/uenv/q_li/Desktop/catalog-web-app/client/node_modules/@babel/parser/lib/index.js:10551:39
    at TypeScriptParserMixin.allowInAnd (/home/uenv/q_li/Desktop/catalog-web-app/client/node_modules/@babel/parser/lib/index.js:12231:12)
    at TypeScriptParserMixin.parseMaybeAssignAllowIn (/home/uenv/q_li/Desktop/catalog-web-app/client/node_modules/@babel/parser/lib/index.js:10551:17)
    at TypeScriptParserMixin.parseParenAndDistinguishExpression (/home/uenv/q_li/Desktop/catalog-web-app/client/node_modules/@babel/parser/lib/index.js:11464:28)
    at TypeScriptParserMixin.parseExprAtom (/home/uenv/q_li/Desktop/catalog-web-app/client/node_modules/@babel/parser/lib/index.js:11115:23)
    at TypeScriptParserMixin.parseExprAtom (/home/uenv/q_li/Desktop/catalog-web-app/client/node_modules/@babel/parser/lib/index.js:6918:20)
    at TypeScriptParserMixin.parseExprSubscripts (/home/uenv/q_li/Desktop/catalog-web-app/client/node_modules/@babel/parser/lib/index.js:10841:23)
    at TypeScriptParserMixin.parseUpdate (/home/uenv/q_li/Desktop/catalog-web-app/client/node_modules/@babel/parser/lib/index.js:10824:21)

webpack compiled with 1 error
ERROR in src/app2/components/pages/apiList.tsx:138:30
TS2304: Cannot find name 'api'.
    136 |
    137 |       {isModalOpen === 'apiDtl' && (
  > 138 |         <NewAPIDtlModal val={api} /> {/* 移除 changeStatus 和 setChangeStatus */}
        |                              ^^^
    139 |       )}
    140 |
    141 |       {isModalOpen === 'selectDeployApi' && (

ERROR in src/app2/components/pages/apiList.tsx:138:38
TS1005: ')' expected.
    136 |
    137 |       {isModalOpen === 'apiDtl' && (
  > 138 |         <NewAPIDtlModal val={api} /> {/* 移除 changeStatus 和 setChangeStatus */}
        |                                      ^
    139 |       )}
    140 |
    141 |       {isModalOpen === 'selectDeployApi' && (

ERROR in src/app2/components/pages/apiList.tsx:139:8
TS1381: Unexpected token. Did you mean `{'}'}` or `&rbrace;`?
    137 |       {isModalOpen === 'apiDtl' && (
    138 |         <NewAPIDtlModal val={api} /> {/* 移除 changeStatus 和 setChangeStatus */}
  > 139 |       )}
        |        ^
    140 |
    141 |       {isModalOpen === 'selectDeployApi' && (
    142 |         <SelectDeployApi

ERROR in src/app2/components/pages/apiList.tsx:334:32
TS2304: Cannot find name 'setIsModalOpen'.
    332 |               <button
    333 |                 className="btn btn-secondary btn-secondary"
  > 334 |                 onClick={() => setIsModalOpen(`${api.physical_name}-dtl-modal`)}
        |                                ^^^^^^^^^^^^^^
    335 |               >
    336 |                 サンプルデータ挿入 / データセット連携項目追加
    337 |               </button>

ERROR in src/app2/components/pages/apiList.tsx:339:14
TS2304: Cannot find name 'isModalOpen'.
    337 |               </button>
    338 |             </div>
  > 339 |             {isModalOpen === `${api.physical_name}-dtl-modal` && (
        |              ^^^^^^^^^^^
    340 |               <>
    341 |                 <NewAPIDtlModal val={api} />
    342 |                 <NewSampleDataModal
kirin-ri commented 1 month ago
import React, { useEffect, useLayoutEffect, useState } from 'react';
import { Link } from 'react-router-dom';
import { commonAjax } from '../../../components/commonAjax';
import { Spacer } from '../spacer';
import * as Defs from './apiDefs';
import ApiUpdateResultModal from './apiUpdateResultModal';
import CreateNewApi from './createNewApi';
import { DialogModalOK, hideDialog, showDialog } from './dialogModal';
import EditApiList from './editApiList';
import NewAPIDtlModal from './newAPIDtlModal';
import NewSampleDataModal from './newSampleDataModal';
import SelectDeployApi from './selectDeployApi';

// main
function NewApiList() {
  // useState
  const [apiList, setApiList] = useState<Defs.Api[]>([]);
  const [tagList, setTags] = useState<Defs.Tag[]>([]);
  const [changeStatus, setChangeStatus] = useState<boolean>(false);
  const [enableDplApi, setEnableDplApi] = useState<boolean>(false);
  const [errorMsg, setErrorMsg] = useState<{}>({});
  const [deployJobId, setDeployJobId] = useState<string>('');
  const [isModalOpen, setIsModalOpen] = useState<string | null>(null); // 定义 isModalOpen 和 setIsModalOpen

  /* API情報取得のリクエスト */
  useEffect(() => {
    (async () => {
      commonAjax
        .axios({ swalFire: false, loading: true })
        .get('/api/api')
        .then((res) => {
          const data = res.data.api;
          const resTags: React.SetStateAction<Defs.Tag[]> = [];
          let tmpTags: string[] = [];
          data.forEach((item: any) => {
            tmpTags = tmpTags.concat(item.tags);
          });
          tmpTags = Array.from(new Set(tmpTags));
          tmpTags.forEach((t: string) => {
            resTags.push({ tag_name: t, select: false });
          });
          setApiList(data);
          setTags(resTags);
          setEnableDplApi(res.data.enableDplApi);
        });
    })();
  }, [changeStatus]);

  useLayoutEffect(() => {
    if (deployJobId) {
      showDialog('api-deploy-job-start-notification');
    }
  }, [deployJobId]);

  const metricsProcess = process.env.REACT_APP_METRICS_PROCESS;
  const title = `${metricsProcess} データセット一覧`;

  useEffect(() => {
    return () => {
      setIsModalOpen(null);
    };
  }, []);

  return (
    <div className="content-wrapper api-list">
      <section className="page-cover">
        <h1>{title}</h1>
      </section>
      <section className="content-header">
        <div className="content-header-left">
          <h1>{title}</h1>
          <div className="content-header-desc">現在、設定されているデータセット一覧です</div>
        </div>
        {enableDplApi && (
          <div className="content-header-right">
            <button type="button" className="btn btn-secondary" onClick={() => setIsModalOpen('editApiList')}>
              編集
            </button>
            <button type="button" className="btn btn-primary" onClick={() => setIsModalOpen('apiDtl')}>
              追加
            </button>
            <button type="button" className="btn-long-text btn-primary" onClick={() => setIsModalOpen('selectDeployApi')}>
              一括有効化
            </button>
          </div>
        )}
      </section>
      <section className="content">
        {/* filter tag */}
        <div className="inline-form">
          <div className="inline-form-cat">フィルタータグ</div>
          {FilterTags(tagList, setTags)}
        </div>
        {Spacer({ size: 30 })}
        {/* API List */}
        {viewList(apiList, tagList, changeStatus, setChangeStatus, setErrorMsg, errorMsg, setDeployJobId)}
      </section>

      {isModalOpen === 'editApiList' && <EditApiList val={apiList} changeStatus={changeStatus} setChangeStatus={setChangeStatus} />}
      {isModalOpen === 'apiDtl' && (
        <NewAPIDtlModal val={apiList[0]} /> {/* 确保此处传递有效的 api 对象 */}
      )}
      {isModalOpen === 'selectDeployApi' && (
        <SelectDeployApi val={apiList} changeStatus={changeStatus} setChangeStatus={setChangeStatus} />
      )}

      <ApiDeployJobStartNotification jobId={deployJobId} />
    </div>
  );
}

// filter tag
function FilterTags(tags: Defs.Tag[], setTags: any) {
  return (
    <div className="inline-form-group">
      <div className="inline-form-label">分類</div>
      {tags.map((item: Defs.Tag) => (
        <div
          className={`btn btn-tag ${item.select ? 'active' : ''}`}
          key={item.tag_name}
          onClick={() => {
            item.select = !item.select;
            setTags([...tags]);
          }}
        >
          {item.tag_name}
        </div>
      ))}
    </div>
  );
}

function viewList(item: Defs.Api[], tagList: Defs.Tag[], changeStatus: boolean, setChangeStatus: any, setErrorMsg: any, errorMsg: any, setDeployJobId: any) {
  const filteredList = item.filter((api) => {
    const isAnyTagSelected = tagList.some((tag) => tag.select);
    if (!isAnyTagSelected) return true;
    return api.tags.some((apiTag) => tagList.some((tag) => tag.select && tag.tag_name === apiTag));
  });

  const length = Math.ceil(filteredList.length / 3);
  const listItem = length !== 0 ? transpose(new Array(length).fill(0).map((_, i) => filteredList.slice(i * 3, (i + 1) * 3))) : [];

  return (
    <div className="list-wrapper">
      {listItem.map((column: Defs.Api[], columnIndex: number) => (
        <div className="apiList" key={columnIndex}>
          {column.map((val: Defs.Api) => (
            <ApiCard key={val.physical_name} api={val} tagList={tagList} changeStatus={changeStatus} setChangeStatus={setChangeStatus} setErrorMsg={setErrorMsg} errorMsg={errorMsg} setDeployJobId={setDeployJobId} />
          ))}
        </div>
      ))}
    </div>
  );
}

function ApiCard(api: Defs.Api, tagList: Defs.Tag[], changeStatus: boolean, setChangeStatus: any, setErrorMsg: any, errorMsg: any, setDeployJobId: any) {
  let selected = false;
  const isAnyTagSelected = tagList.some((tag) => tag.select);

  tagList.forEach((item: Defs.Tag) => {
    api.tags.forEach((apiTag: string) => {
      if (apiTag === item.tag_name) {
        if (!isAnyTagSelected || item.select) {
          selected = true;
        }
      }
    });
  });

  return (
    <div className={`card ${selected ? 'card-primary' : 'card-not-applicable not-applicable'} card-collapse-sample`}>
      <div className="card-header collapsed" data-toggle="collapse" data-target={`#card-body-${api.physical_name}`}>
        <div
          className="api-name-font"
          style={{
            fontSize: `clamp(0.6rem, ${29 / `${api.physical_name} / ${api.logical_name}`.length}vw, 1rem)`,
          }}
        >
          <i className={`fa fa-circle provide-icon-size ${selected ? (api.provide ? 'provide-icon-color' : 'no-provide-icon-color') : undefined}`} aria-hidden="true" />
          {Spacer({ size: 10, horizontal: true })}
          {`${api.physical_name} / ${api.logical_name}`}
        </div>
      </div>
      <div className="card-body collapse" id={`card-body-${api.physical_name}`}>
        <div className="api-list-title">
          <p>データセット 連携項目 一覧</p>
          {Spacer({ size: 20 })}
        </div>
        {api.outinfo.map((val: Defs.TableInfo) => {
          const res: JSX.Element[] = [];
          val.column.map((item: Defs.Column) => {
            res.push(
              <div className="list-contents-grid" key={item.physicalName}>
                <p>{item.logicalName}</p>
              </div>
            );
          });
          return res;
        })}
        {Spacer({ size: 20 })}
        <div className="text-danger error-msg">
          {errorMsg[api.physical_name]}
        </div>
        <br />
        {api.provide ? (
          <>
            <div className="btn-center">
              <button
                className="btn btn-secondary btn-secondary"
                onClick={() => setIsModalOpen(`${api.physical_name}-dtl-modal`)}
              >
                サンプルデータ挿入 / データセット連携項目追加
              </button>
            </div>
            {isModalOpen === `${api.physical_name}-dtl-modal` && (
              <>
                <NewAPIDtlModal val={api} />
                <NewSampleDataModal
                  id={api.physical_name}
                  logicalName={api.logical_name}
                />
                <ApiUpdateResultModal val={api} />
              </>
            )}
          </>
        ) : (
          <div className="btn-center">
            <button
              className="btn btn-secondary btn-secondary"
              onClick={() => {
                putDeploy(
                  api,
                  changeStatus,
                  setChangeStatus,
                  setErrorMsg,
                  setDeployJobId,
                );
              }}
            >
              有効化
            </button>
          </div>
        )}
      </div>
    </div>
  );
}

// transpose columns and rows
const transpose = (a: any[]) =>
  a[0].map((_: any, c: string | number) => a.map((r) => r[c]).filter(Boolean));

// put api(deploy)
function putDeploy(
  api: Defs.Api,
  changeStatus: boolean,
  setChangeStatus: any,
  setErrorMsg: any,
  setDeployJobId: any,
) {
  if (api) {
    if (!api.provide) {
      (async () => {
        const errorMsgList: { [key: string]: string } = {
          [api.physical_name]: '',
        };
        commonAjax
          .axios({ swalFire: false, loading: true })
          .put('/api/api/deploy', { id: [api.physical_name] })
          .then((res) => {
            if (res.data.errorMsg != null) {
              const { errorMsg } = res.data;
              console.log({ errorMsg });
              errorMsgList[api.physical_name] = errorMsg;
              setErrorMsg(errorMsgList);
            } else {
              setChangeStatus(!changeStatus);
              setErrorMsg(errorMsgList);
              setDeployJobId(res.data.jobId);
            }
          });
      })();
    }
  }
}

function ApiDeployJobStartNotification({ jobId }: { jobId?: string }) {
  return (
    <DialogModalOK
      id="api-deploy-job-start-notification"
      title="処理を開始しました"
    >
      <p>
        データセット有効化処理を開始しました。
        <br />
        完了には時間がかかりますのでお待ちください。
        <br />
        処理の状況は下記画面で確認できます。
      </p>
      <Link
        to={`/jobs/${jobId}`}
        className="btn btn-secondary"
        target="_blank"
        onClick={() => hideDialog('api-deploy-job-start-notification')}
      >
        ジョブステータス確認
      </Link>
    </DialogModalOK>
  );
}

export default NewApiList;
kirin-ri commented 1 month ago
import React, { useEffect, useLayoutEffect, useState } from 'react';
import { Link } from 'react-router-dom';
import { commonAjax } from '../../../components/commonAjax';
import { Spacer } from '../spacer';
import * as Defs from './apiDefs';
import ApiUpdateResultModal from './apiUpdateResultModal';
import CreateNewApi from './createNewApi';
import { DialogModalOK, hideDialog, showDialog } from './dialogModal';
import EditApiList from './editApiList';
import NewAPIDtlModal from './newAPIDtlModal';
import NewSampleDataModal from './newSampleDataModal';
import SelectDeployApi from './selectDeployApi';

// main
function NewApiList() {
  // useState
  const [apiList, setApiList] = useState<Defs.Api[]>([]);
  const [tagList, setTags] = useState<Defs.Tag[]>([]);
  const [changeStatus, setChangeStatus] = useState<boolean>(false);
  const [enableDplApi, setEnableDplApi] = useState<boolean>(false);
  const [errorMsg, setErrorMsg] = useState<{}>({});
  const [deployJobId, setDeployJobId] = useState<string>('');
  const [isModalOpen, setIsModalOpen] = useState(false);

  /* API情報取得のリクエスト */
  useEffect(() => {
    (async () => {
      commonAjax
        .axios({ swalFire: false, loading: true })
        .get('/api/api')
        .then((res) => {
          const data = res.data.api;
          const resTags: React.SetStateAction<Defs.Tag[]> = [];
          let tmpTags: string[] = [];
          data.map((item: any) => {
            tmpTags = tmpTags.concat(item.tags);
          });
          tmpTags = Array.from(new Set(tmpTags));
          tmpTags.map((t: string) => {
            resTags.push({ tag_name: t, select: false });
          });
          setApiList(data);
          setTags(resTags);
          setEnableDplApi(res.data.enableDplApi);
        });
    })();
  }, [changeStatus]);

  useLayoutEffect(() => {
    if (deployJobId) {
      // ジョブ開始ダイアログ表示
      showDialog('api-deploy-job-start-notification');
    }
  }, [deployJobId]);

  const metricsProcess = process.env.REACT_APP_METRICS_PROCESS;
  const title = `${metricsProcess} データセット一覧`;

  return (
    <div className="content-wrapper api-list">
      <section className="page-cover">
        <h1>{title}</h1>
      </section>
      {/* Content Header (Page header) */}
      <section className="content-header">
        <div className="content-header-left">
          <h1>{title}</h1>
          <div className="content-header-desc">
            現在、設定されているデータセット一覧です
          </div>
        </div>
        {enableDplApi && (
          <div className="content-header-right">
            <button
              type="button"
              className="btn btn-secondary"
              data-toggle="modal"
              data-target="#editApiListModal"
              onClick={() => {
                setIsModalOpen(true);
              }}
            >
              編集
            </button>
            <button
              type="button"
              className="btn btn-primary"
              data-toggle="modal"
              data-target="#api-dtl-modal"
              onClick={() => {
                setIsModalOpen(true);
              }}
            >
              追加
            </button>
            <button
              type="button"
              className="btn-long-text btn-primary"
              data-toggle="modal"
              data-target="#selectDeployApiModal"
              onClick={() => {
                setIsModalOpen(true);
              }}
            >
              一括有効化
            </button>
          </div>
        )}
      </section>
      <section className="content">
        {/* filter tag */}
        <div className="inline-form">
          <div className="inline-form-cat">フィルタータグ</div>
          {FilterTags(tagList, setTags)}
        </div>
        {Spacer({ size: 30 })}
        {/* API List (Grid) */}
        {viewList(
          apiList,
          tagList,
          changeStatus,
          setChangeStatus,
          setErrorMsg,
          errorMsg,
          setDeployJobId,
        )}
      </section>
      {isModalOpen && (
        <>
          <CreateNewApi
            changeStatus={changeStatus}
            setChangeStatus={setChangeStatus}
          />
          <EditApiList
            val={apiList}
            changeStatus={changeStatus}
            setChangeStatus={setChangeStatus}
          />
          <SelectDeployApi
            val={apiList}
            changeStatus={changeStatus}
            setChangeStatus={setChangeStatus}
          />
        </>
      )}
      <ApiDeployJobStartNotification jobId={deployJobId} />
    </div>
  );
}

// fillter tag
function FilterTags(tags: Defs.Tag[], setTags: any) {
  return (
    <div className="inline-form-group">
      <div className="inline-form-label">分類</div>
      {tags.map((item: Defs.Tag) => {
        if (item.select) {
          return (
            <div
              className="btn btn-tag active"
              key={item.tag_name}
              onClick={() => {
                item.select = false;
                const t = tags.slice(0, tags.length);
                setTags(t);
              }}
            >
              {item.tag_name}
            </div>
          );
        }
        return (
          <div
            className="btn btn-tag"
            onClick={() => {
              item.select = true;
              const t = tags.slice(0, tags.length);
              setTags(t);
            }}
          >
            {item.tag_name}
          </div>
        );
      })}
    </div>
  );
}

function viewList(
  item: Defs.Api[],
  tagList: Defs.Tag[],
  changeStatus: boolean,
  setChangeStatus: any,
  setErrorMsg: any,
  errorMsg: any,
  setDeployJobId: any,
) {
  // Filter the API list based on selected tags
  const filteredList = item.filter((api) => {
    // If no tags are selected, show all
    const isAnyTagSelected = tagList.some((tag) => tag.select);
    if (!isAnyTagSelected) return true;

    // Show only the APIs that match at least one selected tag
    return api.tags.some((apiTag) =>
      tagList.some((tag) => tag.select && tag.tag_name === apiTag),
    );
  });

  const length = Math.ceil(filteredList.length / 3);
  let listItem: any = [];
  if (length !== 0) {
    listItem = transpose(
      new Array(length)
        .fill(0)
        .map((_, i) => filteredList.slice(i * 3, (i + 1) * 3)),
    );
  }

  return (
    <div className="list-wrapper">
      {listItem.map((column: Defs.Api[], columnIndex: number) => (
        <div className="apiList" key={columnIndex}>
          {column.map((val: Defs.Api) =>
            ApiCard(
              val,
              tagList,
              changeStatus,
              setChangeStatus,
              setErrorMsg,
              errorMsg,
              setDeployJobId,
            ),
          )}
        </div>
      ))}
    </div>
  );
}

// API Card
function ApiCard(
  api: Defs.Api,
  tagList: Defs.Tag[],
  changeStatus: boolean,
  setChangeStatus: any,
  setErrorMsg: any,
  errorMsg: any,
  setDeployJobId: any,
) {
  let selected = false;

  // タグが1つでも選択されているか確認
  const isAnyTagSelected = tagList.some((tag) => tag.select);

  tagList.map((item: Defs.Tag) => {
    api.tags.map((apiTag: string) => {
      if (apiTag === item.tag_name) {
        if (!isAnyTagSelected || item.select) {
          // タグが未選択の時、全て選択と同じ扱いにする
          selected = true;
        }
      }
    });
  });

  if (!errorMsg) {
    $(`#${api.physical_name}`).collapse('hide');
  }

  return (
    <div
      className={`card ${
        selected ? 'card-primary' : 'card-not-applicable not-applicable'
      } card-collapse-sample`}
    >
      <div
        className="card-header collapsed"
        data-toggle="collapse"
        data-target={`#card-body-${api.physical_name}`}
      >
        <div
          className="api-name-font"
          style={{
            fontSize: `clamp(0.6rem, ${
              29 / `${api.physical_name} / ${api.logical_name}`.length
            }vw, 1rem)`,
          }}
        >
          <i
            className={`fa fa-circle provide-icon-size ${
              selected
                ? api.provide
                  ? 'provide-icon-color'
                  : 'no-provide-icon-color'
                : undefined
            }`}
            aria-hidden="true"
          />
          {Spacer({ size: 10, horizontal: true })}
          {`${api.physical_name} / ${api.logical_name}`}
        </div>
      </div>
      <div className="card-body collapse" id={`card-body-${api.physical_name}`}>
        <div className="api-list-title">
          <p>データセット 連携項目 一覧</p>
          {Spacer({ size: 20 })}
        </div>
        {api.outinfo.map((val: Defs.TableInfo) => {
          const res: JSX.Element[] = [];
          val.column.map((item: Defs.Column) => {
            res.push(
              <div className="list-contents-grid" key={item.physicalName}>
                <p>{item.logicalName}</p>
              </div>,
            );
          });
          return res;
        })}
        {Spacer({ size: 20 })}
        <div className="text-danger error-msg">
          {errorMsg[api.physical_name]}
        </div>
        <br />
        {api.provide ? (
          <>
            <div className="btn-center">
              <button
                className="btn btn-secondary btn-secondary"
                data-toggle="modal"
                data-target={`#${api.physical_name}-dtl-modal`}
                data-backdrop="true"
              >
                サンプルデータ挿入 / データセット連携項目追加
              </button>
            </div>
            <NewAPIDtlModal val={api} />
            <NewSampleDataModal
              id={api.physical_name}
              logicalName={api.logical_name}
            />
            <ApiUpdateResultModal val={api} />
          </>
        ) : (
          <div className="btn-center">
            <button
              className="btn btn-secondary btn-secondary"
              onClick={() => {
                putDeploy(
                  api,
                  changeStatus,
                  setChangeStatus,
                  setErrorMsg,
                  setDeployJobId,
                );
              }}
            >
              有効化
            </button>
          </div>
        )}
      </div>
    </div>
  );
}

// transpose columns and rows
const transpose = (a: any[]) =>
  a[0].map((_: any, c: string | number) => a.map((r) => r[c]).filter(Boolean));

// put api(deploy)
function putDeploy(
  api: Defs.Api,
  changeStatus: boolean,
  setChangeStatus: any,
  setErrorMsg: any,
  setDeployJobId: any,
) {
  if (api) {
    if (!api.provide) {
      (async () => {
        const errorMsgList: { [key: string]: string } = {
          [api.physical_name]: '',
        };
        commonAjax
          .axios({ swalFire: false, loading: true })
          .put('/api/api/deploy', { id: [api.physical_name] })
          .then((res) => {
            if (res.data.errorMsg != null) {
              const { errorMsg } = res.data;
              console.log({ errorMsg });
              errorMsgList[api.physical_name] = errorMsg;
              setErrorMsg(errorMsgList);
            } else {
              setChangeStatus(!changeStatus);
              setErrorMsg(errorMsgList);
              setDeployJobId(res.data.jobId);
            }
          });
      })();
    }
  }
}

function ApiDeployJobStartNotification({ jobId }: { jobId?: string }) {
  return (
    <DialogModalOK
      id="api-deploy-job-start-notification"
      title="処理を開始しました"
    >
      <p>
        データセット有効化処理を開始しました。
        <br />
        完了には時間がかかりますのでお待ちください。
        <br />
        処理の状況は下記画面で確認できます。
      </p>
      <Link
        to={`/jobs/${jobId}`}
        className="btn btn-secondary"
        target="_blank"
        onClick={() => hideDialog('api-deploy-job-start-notification')}
      >
        ジョブステータス確認
      </Link>
    </DialogModalOK>
  );
}

export default NewApiList;
kirin-ri commented 1 month ago
import React, { useEffect, useLayoutEffect, useState } from 'react';
import { Link } from 'react-router-dom';
import { commonAjax } from '../../../components/commonAjax';
import { Spacer } from '../spacer';
import * as Defs from './apiDefs';
import ApiUpdateResultModal from './apiUpdateResultModal';
import CreateNewApi from './createNewApi';
import { DialogModalOK, hideDialog, showDialog } from './dialogModal';
import EditApiList from './editApiList';
import NewAPIDtlModal from './newAPIDtlModal';
import NewSampleDataModal from './newSampleDataModal';
import SelectDeployApi from './selectDeployApi';

// main
function NewApiList() {
  // useState
  const [apiList, setApiList] = useState<Defs.Api[]>([]);
  const [tagList, setTags] = useState<Defs.Tag[]>([]);
  const [changeStatus, setChangeStatus] = useState<boolean>(false);
  const [enableDplApi, setEnableDplApi] = useState<boolean>(false);
  const [errorMsg, setErrorMsg] = useState<{}>({});
  const [deployJobId, setDeployJobId] = useState<string>('');
  const [isModalOpen, setIsModalOpen] = useState<string | null>(null); // 定义 isModalOpen 和 setIsModalOpen

  /* API情報取得のリクエスト */
  useEffect(() => {
    (async () => {
      commonAjax
        .axios({ swalFire: false, loading: true })
        .get('/api/api')
        .then((res) => {
          const data = res.data.api;
          const resTags: React.SetStateAction<Defs.Tag[]> = [];
          let tmpTags: string[] = [];
          data.forEach((item: any) => {
            tmpTags = tmpTags.concat(item.tags);
          });
          tmpTags = Array.from(new Set(tmpTags));
          tmpTags.forEach((t: string) => {
            resTags.push({ tag_name: t, select: false });
          });
          setApiList(data);
          setTags(resTags);
          setEnableDplApi(res.data.enableDplApi);
        });
    })();
  }, [changeStatus]);

  useLayoutEffect(() => {
    if (deployJobId) {
      showDialog('api-deploy-job-start-notification');
    }
  }, [deployJobId]);

  const metricsProcess = process.env.REACT_APP_METRICS_PROCESS;
  const title = `${metricsProcess} データセット一覧`;

  // 当组件卸载或更新时,清空 modal 状态
  useEffect(() => {
    return () => {
      setIsModalOpen(null);
    };
  }, []);

  // 控制 modal 打开和关闭的函数
  const handleModalOpen = (modalType: string) => {
    if (isModalOpen !== modalType) {
      setIsModalOpen(modalType);
    } else {
      setIsModalOpen(null); // 如果同样的 modal 已经打开,则关闭
    }
  };

  return (
    <div className="content-wrapper api-list">
      <section className="page-cover">
        <h1>{title}</h1>
      </section>
      <section className="content-header">
        <div className="content-header-left">
          <h1>{title}</h1>
          <div className="content-header-desc">現在、設定されているデータセット一覧です</div>
        </div>
        {enableDplApi && (
          <div className="content-header-right">
            <button type="button" className="btn btn-secondary" onClick={() => handleModalOpen('editApiList')}>
              編集
            </button>
            <button type="button" className="btn btn-primary" onClick={() => handleModalOpen('apiDtl')}>
              追加
            </button>
            <button type="button" className="btn-long-text btn-primary" onClick={() => handleModalOpen('selectDeployApi')}>
              一括有効化
            </button>
          </div>
        )}
      </section>
      <section className="content">
        {/* filter tag */}
        <div className="inline-form">
          <div className="inline-form-cat">フィルタータグ</div>
          {FilterTags(tagList, setTags)}
        </div>
        {Spacer({ size: 30 })}
        {/* API List */}
        {viewList(apiList, tagList, changeStatus, setChangeStatus, setErrorMsg, errorMsg, setDeployJobId)}
      </section>

      {/* 根据 isModalOpen 的状态来渲染不同的 modal */}
      {isModalOpen === 'editApiList' && <EditApiList val={apiList} changeStatus={changeStatus} setChangeStatus={setChangeStatus} />}
      {isModalOpen === 'apiDtl' && (
        <NewAPIDtlModal val={apiList[0]} /> {/* 确保此处传递有效的 api 对象 */}
      )}
      {isModalOpen === 'selectDeployApi' && (
        <SelectDeployApi val={apiList} changeStatus={changeStatus} setChangeStatus={setChangeStatus} />
      )}

      <ApiDeployJobStartNotification jobId={deployJobId} />
    </div>
  );
}

// 其他辅助函数保持不变

// filter tag
function FilterTags(tags: Defs.Tag[], setTags: any) {
  return (
    <div className="inline-form-group">
      <div className="inline-form-label">分類</div>
      {tags.map((item: Defs.Tag) => (
        <div
          className={`btn btn-tag ${item.select ? 'active' : ''}`}
          key={item.tag_name}
          onClick={() => {
            item.select = !item.select;
            setTags([...tags]);
          }}
        >
          {item.tag_name}
        </div>
      ))}
    </div>
  );
}

function viewList(item: Defs.Api[], tagList: Defs.Tag[], changeStatus: boolean, setChangeStatus: any, setErrorMsg: any, errorMsg: any, setDeployJobId: any) {
  const filteredList = item.filter((api) => {
    const isAnyTagSelected = tagList.some((tag) => tag.select);
    if (!isAnyTagSelected) return true;
    return api.tags.some((apiTag) => tagList.some((tag) => tag.select && tag.tag_name === apiTag));
  });

  const length = Math.ceil(filteredList.length / 3);
  const listItem = length !== 0 ? transpose(new Array(length).fill(0).map((_, i) => filteredList.slice(i * 3, (i + 1) * 3))) : [];

  return (
    <div className="list-wrapper">
      {listItem.map((column: Defs.Api[], columnIndex: number) => (
        <div className="apiList" key={columnIndex}>
          {column.map((val: Defs.Api) => (
            <ApiCard key={val.physical_name} api={val} tagList={tagList} changeStatus={changeStatus} setChangeStatus={setChangeStatus} setErrorMsg={setErrorMsg} errorMsg={errorMsg} setDeployJobId={setDeployJobId} />
          ))}
        </div>
      ))}
    </div>
  );
}

function ApiCard(api: Defs.Api, tagList: Defs.Tag[], changeStatus: boolean, setChangeStatus: any, setErrorMsg: any, errorMsg: any, setDeployJobId: any) {
  let selected = false;
  const isAnyTagSelected = tagList.some((tag) => tag.select);

  tagList.forEach((item: Defs.Tag) => {
    api.tags.forEach((apiTag: string) => {
      if (apiTag === item.tag_name) {
        if (!isAnyTagSelected || item.select) {
          selected = true;
        }
      }
    });
  });

  return (
    <div className={`card ${selected ? 'card-primary' : 'card-not-applicable not-applicable'} card-collapse-sample`}>
      <div className="card-header collapsed" data-toggle="collapse" data-target={`#card-body-${api.physical_name}`}>
        <div
          className="api-name-font"
          style={{
            fontSize: `clamp(0.6rem, ${29 / `${api.physical_name} / ${api.logical_name}`.length}vw, 1rem)`,
          }}
        >
          <i className={`fa fa-circle provide-icon-size ${selected ? (api.provide ? 'provide-icon-color' : 'no-provide-icon-color') : undefined}`} aria-hidden="true" />
          {Spacer({ size: 10, horizontal: true })}
          {`${api.physical_name} / ${api.logical_name}`}
        </div>
      </div>
      <div className="card-body collapse" id={`#card-body-${api.physical_name}`}>
        <div className="api-list-title">
          <p>データセット 連携項目 一覧</p>
          {Spacer({ size: 20 })}
        </div>
        {api.outinfo.map((val: Defs.TableInfo) => {
          const res: JSX.Element[] = [];
          val.column.map((item: Defs.Column) => {
            res.push(
              <div className="list-contents-grid" key={item.physicalName}>
                <p>{item.logicalName}</p>
              </div>
            );
          });
          return res;
        })}
        {Spacer({ size: 20 })}
        <div className="text-danger error-msg">
          {errorMsg[api.physical_name]}
        </div>
        <br />
        {api.provide ? (
          <>
            <div className="btn-center">
              <button
                className="btn btn-secondary btn-secondary"
                onClick={() => handleModalOpen(`${api.physical_name}-dtl-modal`)}
              >
                サンプルデータ挿入 / データセット連携項目追加
              </button>
            </div>
            {isModalOpen === `${api.physical_name}-dtl-modal` && (
              <>
                <NewAPIDtlModal val={api} />
                <NewSampleDataModal id={api.physical_name} logicalName={api.logical_name} />
                <ApiUpdateResultModal val={api} />
              </>
            )}
          </>
        ) : (
          <div className="btn-center">
            <button
              className="btn btn-secondary btn-secondary"
              onClick={() => {
                putDeploy(api, changeStatus, setChangeStatus, setErrorMsg, setDeployJobId);
              }}
            >
              有効化
            </button>
          </div>
        )}
      </div>
    </div>
  );
}

// transpose columns and rows
const transpose = (a: any[]) => a[0].map((_: any, c: string | number) => a.map((r) => r[c]).filter(Boolean));

// put api(deploy)
function putDeploy(api: Defs.Api, changeStatus: boolean, setChangeStatus: any, setErrorMsg: any, setDeployJobId: any) {
  if (api) {
    if (!api.provide) {
      (async () => {
        const errorMsgList: { [key: string]: string } = {
          [api.physical_name]: '',
        };
        commonAjax
          .axios({ swalFire: false, loading: true })
          .put('/api/api/deploy', { id: [api.physical_name] })
          .then((res) => {
            if (res.data.errorMsg != null) {
              const { errorMsg } = res.data;
              console.log({ errorMsg });
              errorMsgList[api.physical_name] = errorMsg;
              setErrorMsg(errorMsgList);
            } else {
              setChangeStatus(!changeStatus);
              setErrorMsg(errorMsgList);
              setDeployJobId(res.data.jobId);
            }
          });
      })();
    }
  }
}

function ApiDeployJobStartNotification({ jobId }: { jobId?: string }) {
  return (
    <DialogModalOK id="api-deploy-job-start-notification" title="処理を開始しました">
      <p>
        データセット有効化処理を開始しました。
        <br />
        完了には時間がかかりますのでお待ちください。
        <br />
        処理の状況は下記画面で確認できます。
      </p>
      <Link to={`/jobs/${jobId}`} className="btn btn-secondary" target="_blank" onClick={() => hideDialog('api-deploy-job-start-notification')}>
        ジョブステータス確認
      </Link>
    </DialogModalOK>
  );
}

export default NewApiList;
kirin-ri commented 1 month ago
import React, { useEffect, useLayoutEffect, useState } from 'react';
import { Link } from 'react-router-dom';
import { commonAjax } from '../../../components/commonAjax';
import { Spacer } from '../spacer';
import * as Defs from './apiDefs';
import ApiUpdateResultModal from './apiUpdateResultModal';
import CreateNewApi from './createNewApi';
import { DialogModalOK, hideDialog, showDialog } from './dialogModal';
import EditApiList from './editApiList';
import NewAPIDtlModal from './newAPIDtlModal';
import NewSampleDataModal from './newSampleDataModal';
import SelectDeployApi from './selectDeployApi';

// main
function NewApiList() {
  // useState
  const [apiList, setApiList] = useState<Defs.Api[]>([]);
  const [tagList, setTags] = useState<Defs.Tag[]>([]);
  const [changeStatus, setChangeStatus] = useState<boolean>(false);
  const [enableDplApi, setEnableDplApi] = useState<boolean>(false);
  const [errorMsg, setErrorMsg] = useState<{}>({});
  const [deployJobId, setDeployJobId] = useState<string>('');
  const [isModalOpen, setIsModalOpen] = useState<string | null>(null); // 定义 isModalOpen 和 setIsModalOpen

  /* API情報取得のリクエスト */
  useEffect(() => {
    (async () => {
      commonAjax
        .axios({ swalFire: false, loading: true })
        .get('/api/api')
        .then((res) => {
          const data = res.data.api;
          const resTags: React.SetStateAction<Defs.Tag[]> = [];
          let tmpTags: string[] = [];
          data.forEach((item: any) => {
            tmpTags = tmpTags.concat(item.tags);
          });
          tmpTags = Array.from(new Set(tmpTags));
          tmpTags.forEach((t: string) => {
            resTags.push({ tag_name: t, select: false });
          });
          setApiList(data);
          setTags(resTags);
          setEnableDplApi(res.data.enableDplApi);
        });
    })();
  }, [changeStatus]);

  useLayoutEffect(() => {
    if (deployJobId) {
      showDialog('api-deploy-job-start-notification');
    }
  }, [deployJobId]);

  const metricsProcess = process.env.REACT_APP_METRICS_PROCESS;
  const title = `${metricsProcess} データセット一覧`;

  // 当组件卸载或更新时,清空 modal 状态
  useEffect(() => {
    return () => {
      setIsModalOpen(null);
    };
  }, []);

  // 控制 modal 打开和关闭的函数
  const handleModalOpen = (modalType: string) => {
    if (isModalOpen !== modalType) {
      setIsModalOpen(modalType);
    } else {
      setIsModalOpen(null); // 如果同样的 modal 已经打开,则关闭
    }
  };

  return (
    <div className="content-wrapper api-list">
      <section className="page-cover">
        <h1>{title}</h1>
      </section>
      <section className="content-header">
        <div className="content-header-left">
          <h1>{title}</h1>
          <div className="content-header-desc">現在、設定されているデータセット一覧です</div>
        </div>
        {enableDplApi && (
          <div className="content-header-right">
            <button type="button" className="btn btn-secondary" onClick={() => handleModalOpen('editApiList')}>
              編集
            </button>
            <button type="button" className="btn btn-primary" onClick={() => handleModalOpen('apiDtl')}>
              追加
            </button>
            <button type="button" className="btn-long-text btn-primary" onClick={() => handleModalOpen('selectDeployApi')}>
              一括有効化
            </button>
          </div>
        )}
      </section>
      <section className="content">
        {/* filter tag */}
        <div className="inline-form">
          <div className="inline-form-cat">フィルタータグ</div>
          {FilterTags(tagList, setTags)}
        </div>
        {Spacer({ size: 30 })}
        {/* API List */}
        {viewList(apiList, tagList, changeStatus, setChangeStatus, setErrorMsg, errorMsg, setDeployJobId)}
      </section>

      {/* 根据 isModalOpen 的状态来渲染不同的 modal */}
      {isModalOpen === 'editApiList' && <EditApiList val={apiList} changeStatus={changeStatus} setChangeStatus={setChangeStatus} />}
      {isModalOpen === 'apiDtl' && (
        <NewAPIDtlModal val={apiList[0]} /> {/* 确保此处传递有效的 api 对象 */}
      )}
      {isModalOpen === 'selectDeployApi' && (
        <SelectDeployApi val={apiList} changeStatus={changeStatus} setChangeStatus={setChangeStatus} />
      )}

      <ApiDeployJobStartNotification jobId={deployJobId} />
    </div>
  );
}

// filter tag
function FilterTags(tags: Defs.Tag[], setTags: any) {
  return (
    <div className="inline-form-group">
      <div className="inline-form-label">分類</div>
      {tags.map((item: Defs.Tag) => (
        <div
          className={`btn btn-tag ${item.select ? 'active' : ''}`}
          key={item.tag_name}
          onClick={() => {
            item.select = !item.select;
            setTags([...tags]);
          }}
        >
          {item.tag_name}
        </div>
      ))}
    </div>
  );
}

function viewList(item: Defs.Api[], tagList: Defs.Tag[], changeStatus: boolean, setChangeStatus: any, setErrorMsg: any, errorMsg: any, setDeployJobId: any) {
  const filteredList = item.filter((api) => {
    const isAnyTagSelected = tagList.some((tag) => tag.select);
    if (!isAnyTagSelected) return true;
    return api.tags.some((apiTag) => tagList.some((tag) => tag.select && tag.tag_name === apiTag));
  });

  const length = Math.ceil(filteredList.length / 3);
  const listItem = length !== 0 ? transpose(new Array(length).fill(0).map((_, i) => filteredList.slice(i * 3, (i + 1) * 3))) : [];

  return (
    <div className="list-wrapper">
      {listItem.map((column: Defs.Api[], columnIndex: number) => (
        <div className="apiList" key={columnIndex}>
          {column.map((val: Defs.Api) => (
            <ApiCard key={val.physical_name} api={val} tagList={tagList} changeStatus={changeStatus} setChangeStatus={setChangeStatus} setErrorMsg={setErrorMsg} errorMsg={errorMsg} setDeployJobId={setDeployJobId} />
          ))}
        </div>
      ))}
    </div>
  );
}

function ApiCard(api: Defs.Api, tagList: Defs.Tag[], changeStatus: boolean, setChangeStatus: any, setErrorMsg: any, errorMsg: any, setDeployJobId: any) {
  let selected = false;
  const isAnyTagSelected = tagList.some((tag) => tag.select);

  tagList.forEach((item: Defs.Tag) => {
    api.tags.forEach((apiTag: string) => {
      if (apiTag === item.tag_name) {
        if (!isAnyTagSelected || item.select) {
          selected = true;
        }
      }
    });
  });

  return (
    <div className={`card ${selected ? 'card-primary' : 'card-not-applicable not-applicable'} card-collapse-sample`}>
      <div className="card-header collapsed" data-toggle="collapse" data-target={`#card-body-${api.physical_name}`}>
        <div
          className="api-name-font"
          style={{
            fontSize: `clamp(0.6rem, ${29 / `${api.physical_name} / ${api.logical_name}`.length}vw, 1rem)`,
          }}
        >
          <i className={`fa fa-circle provide-icon-size ${selected ? (api.provide ? 'provide-icon-color' : 'no-provide-icon-color') : undefined}`} aria-hidden="true" />
          {Spacer({ size: 10, horizontal: true })}
          {`${api.physical_name} / ${api.logical_name}`}
        </div>
      </div>
      <div className="card-body collapse" id={`card-body-${api.physical_name}`}>
        <div className="api-list-title">
          <p>データセット 連携項目 一覧</p>
          {Spacer({ size: 20 })}
        </div>
        {api.outinfo.map((val: Defs.TableInfo) => {
          const res: JSX.Element[] = [];
          val.column.map((item: Defs.Column) => {
            res.push(
              <div className="list-contents-grid" key={item.physicalName}>
                <p>{item.logicalName}</p>
              </div>
            );
          });
          return res;
        })}
        {Spacer({ size: 20 })}
        <div className="text-danger error-msg">
          {errorMsg[api.physical_name]}
        </div>
        <br />
        {api.provide ? (
          <>
            <div className="btn-center">
              <button
                className="btn btn-secondary btn-secondary"
                onClick={() => handleModalOpen(`${api.physical_name}-dtl-modal`)}
              >
                サンプルデータ挿入 / データセット連携項目追加
              </button>
            </div>
            {isModalOpen === `${api.physical_name}-dtl-modal` && (
              <>
                <NewAPIDtlModal val={api} />
                <NewSampleDataModal id={api.physical_name} logicalName={api.logical_name} />
                <ApiUpdateResultModal val={api} />
              </>
            )}
          </>
        ) : (
          <div className="btn-center">
            <button
              className="btn btn-secondary btn-secondary"
              onClick={() => {
                putDeploy(api, changeStatus, setChangeStatus, setErrorMsg, setDeployJobId);
              }}
            >
              有効化
            </button>
          </div>
        )}
      </div>
    </div>
  );
}

// transpose columns and rows
const transpose = (a: any[]) => a[0].map((_: any, c: string | number) => a.map((r) => r[c]).filter(Boolean));

// put api(deploy)
function putDeploy(api: Defs.Api, changeStatus: boolean, setChangeStatus: any, setErrorMsg: any, setDeployJobId: any) {
  if (api) {
    if (!api.provide) {
      (async () => {
        const errorMsgList: { [key: string]: string } = {
          [api.physical_name]: '',
        };
        commonAjax
          .axios({ swalFire: false, loading: true })
          .put('/api/api/deploy', { id: [api.physical_name] })
          .then((res) => {
            if (res.data.errorMsg != null) {
              const { errorMsg } = res.data;
              console.log({ errorMsg });
              errorMsgList[api.physical_name] = errorMsg;
              setErrorMsg(errorMsgList);
            } else {
              setChangeStatus(!changeStatus);
              setErrorMsg(errorMsgList);
              setDeployJobId(res.data.jobId);
            }
          });
      })();
    }
  }
}

function ApiDeployJobStartNotification({ jobId }: { jobId?: string }) {
  return (
    <DialogModalOK id="api-deploy-job-start-notification" title="処理を開始しました">
      <p>
        データセット有効化処理を開始しました。
        <br />
        完了には時間がかかりますのでお待ちください。
        <br />
        処理の状況は下記画面で確認できます。
      </p>
      <Link to={`/jobs/${jobId}`} className="btn btn-secondary" target="_blank" onClick={() => hideDialog('api-deploy-job-start-notification')}>
        ジョブステータス確認
      </Link>
    </DialogModalOK>
  );
}

export default NewApiList;
kirin-ri commented 1 month ago
import React, { useEffect, useLayoutEffect, useState } from 'react';
import { Link } from 'react-router-dom';
import { commonAjax } from '../../../components/commonAjax';
import { Spacer } from '../spacer';
import * as Defs from './apiDefs';
import ApiUpdateResultModal from './apiUpdateResultModal';
import CreateNewApi from './createNewApi';
import { DialogModalOK, hideDialog, showDialog } from './dialogModal';
import EditApiList from './editApiList';
import NewAPIDtlModal from './newAPIDtlModal';
import NewSampleDataModal from './newSampleDataModal';
import SelectDeployApi from './selectDeployApi';

// main
function NewApiList() {
  // useState
  const [apiList, setApiList] = useState<Defs.Api[]>([]);
  const [tagList, setTags] = useState<Defs.Tag[]>([]);
  const [changeStatus, setChangeStatus] = useState<boolean>(false);
  const [enableDplApi, setEnableDplApi] = useState<boolean>(false);
  const [errorMsg, setErrorMsg] = useState<{}>({});
  const [deployJobId, setDeployJobId] = useState<string>('');
  const [isModalOpen, setIsModalOpen] = useState(false);

  /* API情報取得のリクエスト */
  useEffect(() => {
    (async () => {
      commonAjax
        .axios({ swalFire: false, loading: true })
        .get('/api/api')
        .then((res) => {
          const data = res.data.api;
          const resTags: React.SetStateAction<Defs.Tag[]> = [];
          let tmpTags: string[] = [];
          data.map((item: any) => {
            tmpTags = tmpTags.concat(item.tags);
          });
          tmpTags = Array.from(new Set(tmpTags));
          tmpTags.map((t: string) => {
            resTags.push({ tag_name: t, select: false });
          });
          setApiList(data);
          setTags(resTags);
          setEnableDplApi(res.data.enableDplApi);
        });
    })();
  }, [changeStatus]);

  useLayoutEffect(() => {
    if (deployJobId) {
      // ジョブ開始ダイアログ表示
      showDialog('api-deploy-job-start-notification');
    }
  }, [deployJobId]);

  const metricsProcess = process.env.REACT_APP_METRICS_PROCESS;
  const title = `${metricsProcess} データセット一覧`;

  return (
    <div className="content-wrapper api-list">
      <section className="page-cover">
        <h1>{title}</h1>
      </section>
      {/* Content Header (Page header) */}
      <section className="content-header">
        <div className="content-header-left">
          <h1>{title}</h1>
          <div className="content-header-desc">
            現在、設定されているデータセット一覧です
          </div>
        </div>
        {enableDplApi && (
          <div className="content-header-right">
            <button
              type="button"
              className="btn btn-secondary"
              data-toggle="modal"
              data-target="#editApiListModal"
              onClick={() => {
                setIsModalOpen(true);
              }}
            >
              編集
            </button>
            <button
              type="button"
              className="btn btn-primary"
              data-toggle="modal"
              data-target="#api-dtl-modal"
              onClick={() => {
                setIsModalOpen(true);
              }}
            >
              追加
            </button>
            <button
              type="button"
              className="btn-long-text btn-primary"
              data-toggle="modal"
              data-target="#selectDeployApiModal"
              onClick={() => {
                setIsModalOpen(true);
              }}
            >
              一括有効化
            </button>
          </div>
        )}
      </section>
      <section className="content">
        {/* filter tag */}
        <div className="inline-form">
          <div className="inline-form-cat">フィルタータグ</div>
          {FilterTags(tagList, setTags)}
        </div>
        {Spacer({ size: 30 })}
        {/* API List (Grid) */}
        {viewList(
          apiList,
          tagList,
          changeStatus,
          setChangeStatus,
          setErrorMsg,
          errorMsg,
          setDeployJobId,
        )}
      </section>
      {isModalOpen && (
        <>
          <CreateNewApi
            changeStatus={changeStatus}
            setChangeStatus={setChangeStatus}
          />
          <EditApiList
            val={apiList}
            changeStatus={changeStatus}
            setChangeStatus={setChangeStatus}
          />
          <SelectDeployApi
            val={apiList}
            changeStatus={changeStatus}
            setChangeStatus={setChangeStatus}
          />
        </>
      )}
      <ApiDeployJobStartNotification jobId={deployJobId} />
    </div>
  );
}

// fillter tag
function FilterTags(tags: Defs.Tag[], setTags: any) {
  return (
    <div className="inline-form-group">
      <div className="inline-form-label">分類</div>
      {tags.map((item: Defs.Tag) => {
        if (item.select) {
          return (
            <div
              className="btn btn-tag active"
              key={item.tag_name}
              onClick={() => {
                item.select = false;
                const t = tags.slice(0, tags.length);
                setTags(t);
              }}
            >
              {item.tag_name}
            </div>
          );
        }
        return (
          <div
            className="btn btn-tag"
            onClick={() => {
              item.select = true;
              const t = tags.slice(0, tags.length);
              setTags(t);
            }}
          >
            {item.tag_name}
          </div>
        );
      })}
    </div>
  );
}

function viewList(
  item: Defs.Api[],
  tagList: Defs.Tag[],
  changeStatus: boolean,
  setChangeStatus: any,
  setErrorMsg: any,
  errorMsg: any,
  setDeployJobId: any,
) {
  // Filter the API list based on selected tags
  const filteredList = item.filter((api) => {
    // If no tags are selected, show all
    const isAnyTagSelected = tagList.some((tag) => tag.select);
    if (!isAnyTagSelected) return true;

    // Show only the APIs that match at least one selected tag
    return api.tags.some((apiTag) =>
      tagList.some((tag) => tag.select && tag.tag_name === apiTag),
    );
  });

  const length = Math.ceil(filteredList.length / 3);
  let listItem: any = [];
  if (length !== 0) {
    listItem = transpose(
      new Array(length)
        .fill(0)
        .map((_, i) => filteredList.slice(i * 3, (i + 1) * 3)),
    );
  }

  return (
    <div className="list-wrapper">
      {listItem.map((column: Defs.Api[], columnIndex: number) => (
        <div className="apiList" key={columnIndex}>
          {column.map((val: Defs.Api) =>
            ApiCard(
              val,
              tagList,
              changeStatus,
              setChangeStatus,
              setErrorMsg,
              errorMsg,
              setDeployJobId,
            ),
          )}
        </div>
      ))}
    </div>
  );
}

// API Card
function ApiCard(
  api: Defs.Api,
  tagList: Defs.Tag[],
  changeStatus: boolean,
  setChangeStatus: any,
  setErrorMsg: any,
  errorMsg: any,
  setDeployJobId: any,
) {
  let selected = false;

  // タグが1つでも選択されているか確認
  const isAnyTagSelected = tagList.some((tag) => tag.select);

  tagList.map((item: Defs.Tag) => {
    api.tags.map((apiTag: string) => {
      if (apiTag === item.tag_name) {
        if (!isAnyTagSelected || item.select) {
          // タグが未選択の時、全て選択と同じ扱いにする
          selected = true;
        }
      }
    });
  });

  if (!errorMsg) {
    $(`#${api.physical_name}`).collapse('hide');
  }

  return (
    <div
      className={`card ${
        selected ? 'card-primary' : 'card-not-applicable not-applicable'
      } card-collapse-sample`}
    >
      <div
        className="card-header collapsed"
        data-toggle="collapse"
        data-target={`#card-body-${api.physical_name}`}
      >
        <div
          className="api-name-font"
          style={{
            fontSize: `clamp(0.6rem, ${
              29 / `${api.physical_name} / ${api.logical_name}`.length
            }vw, 1rem)`,
          }}
        >
          <i
            className={`fa fa-circle provide-icon-size ${
              selected
                ? api.provide
                  ? 'provide-icon-color'
                  : 'no-provide-icon-color'
                : undefined
            }`}
            aria-hidden="true"
          />
          {Spacer({ size: 10, horizontal: true })}
          {`${api.physical_name} / ${api.logical_name}`}
        </div>
      </div>
      <div className="card-body collapse" id={`card-body-${api.physical_name}`}>
        <div className="api-list-title">
          <p>データセット 連携項目 一覧</p>
          {Spacer({ size: 20 })}
        </div>
        {api.outinfo.map((val: Defs.TableInfo) => {
          const res: JSX.Element[] = [];
          val.column.map((item: Defs.Column) => {
            res.push(
              <div className="list-contents-grid" key={item.physicalName}>
                <p>{item.logicalName}</p>
              </div>,
            );
          });
          return res;
        })}
        {Spacer({ size: 20 })}
        <div className="text-danger error-msg">
          {errorMsg[api.physical_name]}
        </div>
        <br />
        {api.provide ? (
          <>
            <div className="btn-center">
              <button
                className="btn btn-secondary btn-secondary"
                data-toggle="modal"
                data-target={`#${api.physical_name}-dtl-modal`}
                data-backdrop="true"
              >
                サンプルデータ挿入 / データセット連携項目追加
              </button>
            </div>
            <NewAPIDtlModal val={api} />
            <NewSampleDataModal
              id={api.physical_name}
              logicalName={api.logical_name}
            />
            <ApiUpdateResultModal val={api} />
          </>
        ) : (
          <div className="btn-center">
            <button
              className="btn btn-secondary btn-secondary"
              onClick={() => {
                putDeploy(
                  api,
                  changeStatus,
                  setChangeStatus,
                  setErrorMsg,
                  setDeployJobId,
                );
              }}
            >
              有効化
            </button>
          </div>
        )}
      </div>
    </div>
  );
}

// transpose columns and rows
const transpose = (a: any[]) =>
  a[0].map((_: any, c: string | number) => a.map((r) => r[c]).filter(Boolean));

// put api(deploy)
function putDeploy(
  api: Defs.Api,
  changeStatus: boolean,
  setChangeStatus: any,
  setErrorMsg: any,
  setDeployJobId: any,
) {
  if (api) {
    if (!api.provide) {
      (async () => {
        const errorMsgList: { [key: string]: string } = {
          [api.physical_name]: '',
        };
        commonAjax
          .axios({ swalFire: false, loading: true })
          .put('/api/api/deploy', { id: [api.physical_name] })
          .then((res) => {
            if (res.data.errorMsg != null) {
              const { errorMsg } = res.data;
              console.log({ errorMsg });
              errorMsgList[api.physical_name] = errorMsg;
              setErrorMsg(errorMsgList);
            } else {
              setChangeStatus(!changeStatus);
              setErrorMsg(errorMsgList);
              setDeployJobId(res.data.jobId);
            }
          });
      })();
    }
  }
}

function ApiDeployJobStartNotification({ jobId }: { jobId?: string }) {
  return (
    <DialogModalOK
      id="api-deploy-job-start-notification"
      title="処理を開始しました"
    >
      <p>
        データセット有効化処理を開始しました。
        <br />
        完了には時間がかかりますのでお待ちください。
        <br />
        処理の状況は下記画面で確認できます。
      </p>
      <Link
        to={`/jobs/${jobId}`}
        className="btn btn-secondary"
        target="_blank"
        onClick={() => hideDialog('api-deploy-job-start-notification')}
      >
        ジョブステータス確認
      </Link>
    </DialogModalOK>
  );
}

export default NewApiList;
kirin-ri commented 1 month ago
import React, { useEffect, useLayoutEffect, useState } from 'react';
import { Link } from 'react-router-dom';
import { commonAjax } from '../../../components/commonAjax';
import { Spacer } from '../spacer';
import * as Defs from './apiDefs';
import ApiUpdateResultModal from './apiUpdateResultModal';
import CreateNewApi from './createNewApi';
import { DialogModalOK, hideDialog, showDialog } from './dialogModal';
import EditApiList from './editApiList';
import NewAPIDtlModal from './newAPIDtlModal';
import NewSampleDataModal from './newSampleDataModal';
import SelectDeployApi from './selectDeployApi';

// main
function NewApiList() {
  // useState
  const [apiList, setApiList] = useState<Defs.Api[]>([]);
  const [tagList, setTags] = useState<Defs.Tag[]>([]);
  const [changeStatus, setChangeStatus] = useState<boolean>(false);
  const [enableDplApi, setEnableDplApi] = useState<boolean>(false);
  const [errorMsg, setErrorMsg] = useState<{}>({});
  const [deployJobId, setDeployJobId] = useState<string>('');
  const [isModalOpen, setIsModalOpen] = useState<string | null>(null); // 用于控制不同模态框

  /* API情報取得のリクエスト */
  useEffect(() => {
    (async () => {
      commonAjax
        .axios({ swalFire: false, loading: true })
        .get('/api/api')
        .then((res) => {
          const data = res.data.api;
          const resTags: React.SetStateAction<Defs.Tag[]> = [];
          let tmpTags: string[] = [];
          data.map((item: any) => {
            tmpTags = tmpTags.concat(item.tags);
          });
          tmpTags = Array.from(new Set(tmpTags));
          tmpTags.map((t: string) => {
            resTags.push({ tag_name: t, select: false });
          });
          setApiList(data);
          setTags(resTags);
          setEnableDplApi(res.data.enableDplApi);
        });
    })();
  }, [changeStatus]);

  useLayoutEffect(() => {
    if (deployJobId) {
      // ジョブ開始ダイアログ表示
      showDialog('api-deploy-job-start-notification');
    }
  }, [deployJobId]);

  const metricsProcess = process.env.REACT_APP_METRICS_PROCESS;
  const title = `${metricsProcess} データセット一覧`;

  // 控制模态框的打开和关闭
  const handleModalOpen = (modalType: string) => {
    setIsModalOpen(modalType); // 打开对应的模态框
  };

  return (
    <div className="content-wrapper api-list">
      <section className="page-cover">
        <h1>{title}</h1>
      </section>
      {/* Content Header (Page header) */}
      <section className="content-header">
        <div className="content-header-left">
          <h1>{title}</h1>
          <div className="content-header-desc">
            現在、設定されているデータセット一覧です
          </div>
        </div>
        {enableDplApi && (
          <div className="content-header-right">
            <button
              type="button"
              className="btn btn-secondary"
              onClick={() => handleModalOpen('editApiList')}
            >
              編集
            </button>
            <button
              type="button"
              className="btn btn-primary"
              onClick={() => handleModalOpen('apiDtl')}
            >
              追加
            </button>
            <button
              type="button"
              className="btn-long-text btn-primary"
              onClick={() => handleModalOpen('selectDeployApi')}
            >
              一括有効化
            </button>
          </div>
        )}
      </section>
      <section className="content">
        {/* filter tag */}
        <div className="inline-form">
          <div className="inline-form-cat">フィルタータグ</div>
          {FilterTags(tagList, setTags)}
        </div>
        {Spacer({ size: 30 })}
        {/* API List (Grid) */}
        {viewList(
          apiList,
          tagList,
          changeStatus,
          setChangeStatus,
          setErrorMsg,
          errorMsg,
          setDeployJobId,
        )}
      </section>

      {/* 根据 isModalOpen 渲染不同模态框 */}
      {isModalOpen === 'editApiList' && (
        <EditApiList
          val={apiList}
          changeStatus={changeStatus}
          setChangeStatus={setChangeStatus}
        />
      )}
      {isModalOpen === 'apiDtl' && (
        <NewAPIDtlModal
          val={apiList[0]}  // 确保传递有效的api对象
        />
      )}
      {isModalOpen === 'selectDeployApi' && (
        <SelectDeployApi
          val={apiList}
          changeStatus={changeStatus}
          setChangeStatus={setChangeStatus}
        />
      )}
      <ApiDeployJobStartNotification jobId={deployJobId} />
    </div>
  );
}

// fillter tag
function FilterTags(tags: Defs.Tag[], setTags: any) {
  return (
    <div className="inline-form-group">
      <div className="inline-form-label">分類</div>
      {tags.map((item: Defs.Tag) => (
        <div
          className={`btn btn-tag ${item.select ? 'active' : ''}`}
          key={item.tag_name}
          onClick={() => {
            item.select = !item.select;
            setTags([...tags]);
          }}
        >
          {item.tag_name}
        </div>
      ))}
    </div>
  );
}

function viewList(
  item: Defs.Api[],
  tagList: Defs.Tag[],
  changeStatus: boolean,
  setChangeStatus: any,
  setErrorMsg: any,
  errorMsg: any,
  setDeployJobId: any,
) {
  // Filter the API list based on selected tags
  const filteredList = item.filter((api) => {
    const isAnyTagSelected = tagList.some((tag) => tag.select);
    if (!isAnyTagSelected) return true;
    return api.tags.some((apiTag) =>
      tagList.some((tag) => tag.select && tag.tag_name === apiTag),
    );
  });

  const length = Math.ceil(filteredList.length / 3);
  let listItem: any = [];
  if (length !== 0) {
    listItem = transpose(
      new Array(length)
        .fill(0)
        .map((_, i) => filteredList.slice(i * 3, (i + 1) * 3)),
    );
  }

  return (
    <div className="list-wrapper">
      {listItem.map((column: Defs.Api[], columnIndex: number) => (
        <div className="apiList" key={columnIndex}>
          {column.map((val: Defs.Api) =>
            ApiCard(
              val,
              tagList,
              changeStatus,
              setChangeStatus,
              setErrorMsg,
              errorMsg,
              setDeployJobId,
            ),
          )}
        </div>
      ))}
    </div>
  );
}

// 其他功能和组件保持不变...

export default NewApiList;
kirin-ri commented 1 month ago

console.log(Opening modal: ${modalType});

kirin-ri commented 1 month ago
import React, { useEffect, useLayoutEffect, useState } from 'react';
import { Link } from 'react-router-dom';
import { commonAjax } from '../../../components/commonAjax';
import { Spacer } from '../spacer';
import * as Defs from './apiDefs';
import ApiUpdateResultModal from './apiUpdateResultModal';
import CreateNewApi from './createNewApi';
import { DialogModalOK, hideDialog, showDialog } from './dialogModal';
import EditApiList from './editApiList';
import NewAPIDtlModal from './newAPIDtlModal';
import NewSampleDataModal from './newSampleDataModal';
import SelectDeployApi from './selectDeployApi';

// main
function NewApiList() {
  // useState
  const [apiList, setApiList] = useState<Defs.Api[]>([]);
  const [tagList, setTags] = useState<Defs.Tag[]>([]);
  const [changeStatus, setChangeStatus] = useState<boolean>(false);
  const [enableDplApi, setEnableDplApi] = useState<boolean>(false);
  const [errorMsg, setErrorMsg] = useState<{}>({});
  const [deployJobId, setDeployJobId] = useState<string>('');
  const [isModalOpen, setIsModalOpen] = useState<string | null>(null); // 用于控制不同模态框

  /* API情報取得のリクエスト */
  useEffect(() => {
    (async () => {
      commonAjax
        .axios({ swalFire: false, loading: true })
        .get('/api/api')
        .then((res) => {
          const data = res.data.api;
          const resTags: React.SetStateAction<Defs.Tag[]> = [];
          let tmpTags: string[] = [];
          data.map((item: any) => {
            tmpTags = tmpTags.concat(item.tags);
          });
          tmpTags = Array.from(new Set(tmpTags));
          tmpTags.map((t: string) => {
            resTags.push({ tag_name: t, select: false });
          });
          setApiList(data);
          setTags(resTags);
          setEnableDplApi(res.data.enableDplApi);
        });
    })();
  }, [changeStatus]);

  useLayoutEffect(() => {
    if (deployJobId) {
      // ジョブ開始ダイアログ表示
      showDialog('api-deploy-job-start-notification');
    }
  }, [deployJobId]);

  const metricsProcess = process.env.REACT_APP_METRICS_PROCESS;
  const title = `${metricsProcess} データセット一覧`;

  // 控制模态框的打开和关闭
  const handleModalOpen = (modalType: string) => {
    console.log(`Opening modal: ${modalType}`); // 调试日志,确认点击动作
    setIsModalOpen(modalType); // 打开对应的模态框
  };

  return (
    <div className="content-wrapper api-list">
      <section className="page-cover">
        <h1>{title}</h1>
      </section>
      {/* Content Header (Page header) */}
      <section className="content-header">
        <div className="content-header-left">
          <h1>{title}</h1>
          <div className="content-header-desc">
            現在、設定されているデータセット一覧です
          </div>
        </div>
        {enableDplApi && (
          <div className="content-header-right">
            <button
              type="button"
              className="btn btn-secondary"
              onClick={() => handleModalOpen('editApiList')}
            >
              編集
            </button>
            <button
              type="button"
              className="btn btn-primary"
              onClick={() => handleModalOpen('apiDtl')}
            >
              追加
            </button>
            <button
              type="button"
              className="btn-long-text btn-primary"
              onClick={() => handleModalOpen('selectDeployApi')}
            >
              一括有効化
            </button>
          </div>
        )}
      </section>
      <section className="content">
        {/* filter tag */}
        <div className="inline-form">
          <div className="inline-form-cat">フィルタータグ</div>
          {FilterTags(tagList, setTags)}
        </div>
        {Spacer({ size: 30 })}
        {/* API List (Grid) */}
        {viewList(
          apiList,
          tagList,
          changeStatus,
          setChangeStatus,
          setErrorMsg,
          errorMsg,
          setDeployJobId,
        )}
      </section>

      {/* 根据 isModalOpen 渲染不同模态框 */}
      {isModalOpen === 'editApiList' && (
        <EditApiList
          val={apiList}
          changeStatus={changeStatus}
          setChangeStatus={setChangeStatus}
        />
      )}
      {isModalOpen === 'apiDtl' && (
        <NewAPIDtlModal
          val={apiList[0]}  // 确保传递有效的api对象
        />
      )}
      {isModalOpen === 'selectDeployApi' && (
        <SelectDeployApi
          val={apiList}
          changeStatus={changeStatus}
          setChangeStatus={setChangeStatus}
        />
      )}
      <ApiDeployJobStartNotification jobId={deployJobId} />
    </div>
  );
}
kirin-ri commented 1 month ago
import React, { useEffect, useLayoutEffect, useState } from 'react';
import { Link } from 'react-router-dom';
import { commonAjax } from '../../../components/commonAjax';
import { Spacer } from '../spacer';
import * as Defs from './apiDefs';
import ApiUpdateResultModal from './apiUpdateResultModal';
import CreateNewApi from './createNewApi';
import { DialogModalOK, hideDialog, showDialog } from './dialogModal';
import EditApiList from './editApiList';
import NewAPIDtlModal from './newAPIDtlModal';
import NewSampleDataModal from './newSampleDataModal';
import SelectDeployApi from './selectDeployApi';

// main
function NewApiList() {
  // useState
  const [apiList, setApiList] = useState<Defs.Api[]>([]);
  const [tagList, setTags] = useState<Defs.Tag[]>([]);
  const [changeStatus, setChangeStatus] = useState<boolean>(false);
  const [enableDplApi, setEnableDplApi] = useState<boolean>(false);
  const [errorMsg, setErrorMsg] = useState<{}>({});
  const [deployJobId, setDeployJobId] = useState<string>('');
  const [isModalOpen, setIsModalOpen] = useState<string | null>(null); // 控制模态框

  /* API情報取得のリクエスト */
  useEffect(() => {
    (async () => {
      commonAjax
        .axios({ swalFire: false, loading: true })
        .get('/api/api')
        .then((res) => {
          const data = res.data.api;
          const resTags: React.SetStateAction<Defs.Tag[]> = [];
          let tmpTags: string[] = [];
          data.map((item: any) => {
            tmpTags = tmpTags.concat(item.tags);
          });
          tmpTags = Array.from(new Set(tmpTags));
          tmpTags.map((t: string) => {
            resTags.push({ tag_name: t, select: false });
          });
          setApiList(data);
          setTags(resTags);
          setEnableDplApi(res.data.enableDplApi);
        });
    })();
  }, [changeStatus]);

  useLayoutEffect(() => {
    if (deployJobId) {
      showDialog('api-deploy-job-start-notification');
    }
  }, [deployJobId]);

  const metricsProcess = process.env.REACT_APP_METRICS_PROCESS;
  const title = `${metricsProcess} データセット一覧`;

  // 控制模态框的打开和关闭
  const handleModalOpen = (modalType: string) => {
    console.log(`Opening modal: ${modalType}`);
    setIsModalOpen(modalType); // 打开对应的模态框
  };

  // 关闭模态框的函数
  const handleModalClose = () => {
    setIsModalOpen(null); // 关闭所有模态框
  };

  return (
    <div className="content-wrapper api-list">
      <section className="page-cover">
        <h1>{title}</h1>
      </section>
      {/* Content Header (Page header) */}
      <section className="content-header">
        <div className="content-header-left">
          <h1>{title}</h1>
          <div className="content-header-desc">
            現在、設定されているデータセット一覧です
          </div>
        </div>
        {enableDplApi && (
          <div className="content-header-right">
            <button
              type="button"
              className="btn btn-secondary"
              onClick={() => handleModalOpen('editApiList')}
            >
              編集
            </button>
            <button
              type="button"
              className="btn btn-primary"
              onClick={() => handleModalOpen('apiDtl')}
            >
              追加
            </button>
            <button
              type="button"
              className="btn-long-text btn-primary"
              onClick={() => handleModalOpen('selectDeployApi')}
            >
              一括有効化
            </button>
          </div>
        )}
      </section>
      <section className="content">
        {/* filter tag */}
        <div className="inline-form">
          <div className="inline-form-cat">フィルタータグ</div>
          {FilterTags(tagList, setTags)}
        </div>
        {Spacer({ size: 30 })}
        {/* API List (Grid) */}
        {viewList(
          apiList,
          tagList,
          changeStatus,
          setChangeStatus,
          setErrorMsg,
          errorMsg,
          setDeployJobId,
        )}
      </section>

      {/* 根据 isModalOpen 渲染不同模态框 */}
      {isModalOpen === 'editApiList' && (
        <EditApiList
          val={apiList}
          changeStatus={changeStatus}
          setChangeStatus={setChangeStatus}
          onClose={handleModalClose} // 传递关闭模态框函数
        />
      )}
      {isModalOpen === 'apiDtl' && (
        <NewAPIDtlModal
          val={apiList[0]}  // 确保传递有效的api对象
          onClose={handleModalClose} // 传递关闭模态框函数
        />
      )}
      {isModalOpen === 'selectDeployApi' && (
        <SelectDeployApi
          val={apiList}
          changeStatus={changeStatus}
          setChangeStatus={setChangeStatus}
          onClose={handleModalClose} // 传递关闭模态框函数
        />
      )}
      <ApiDeployJobStartNotification jobId={deployJobId} />
    </div>
  );
}

// fillter tag
function FilterTags(tags: Defs.Tag[], setTags: any) {
  return (
    <div className="inline-form-group">
      <div className="inline-form-label">分類</div>
      {tags.map((item: Defs.Tag) => (
        <div
          className={`btn btn-tag ${item.select ? 'active' : ''}`}
          key={item.tag_name}
          onClick={() => {
            item.select = !item.select;
            setTags([...tags]);
          }}
        >
          {item.tag_name}
        </div>
      ))}
    </div>
  );
}

function viewList(
  item: Defs.Api[],
  tagList: Defs.Tag[],
  changeStatus: boolean,
  setChangeStatus: any,
  setErrorMsg: any,
  errorMsg: any,
  setDeployJobId: any,
) {
  // Filter the API list based on selected tags
  const filteredList = item.filter((api) => {
    const isAnyTagSelected = tagList.some((tag) => tag.select);
    if (!isAnyTagSelected) return true;
    return api.tags.some((apiTag) =>
      tagList.some((tag) => tag.select && tag.tag_name === apiTag),
    );
  });

  const length = Math.ceil(filteredList.length / 3);
  let listItem: any = [];
  if (length !== 0) {
    listItem = transpose(
      new Array(length)
        .fill(0)
        .map((_, i) => filteredList.slice(i * 3, (i + 1) * 3)),
    );
  }

  return (
    <div className="list-wrapper">
      {listItem.map((column: Defs.Api[], columnIndex: number) => (
        <div className="apiList" key={columnIndex}>
          {column.map((val: Defs.Api) =>
            ApiCard(
              val,
              tagList,
              changeStatus,
              setChangeStatus,
              setErrorMsg,
              errorMsg,
              setDeployJobId,
            ),
          )}
        </div>
      ))}
    </div>
  );
}

function transpose(a: any[]) {
  return a[0].map((_: any, c: string | number) => a.map((r) => r[c]).filter(Boolean));
}
kirin-ri commented 1 month ago
import React, { useEffect, useLayoutEffect, useState } from 'react';
import { Link } from 'react-router-dom';
import { commonAjax } from '../../../components/commonAjax';
import { Spacer } from '../spacer';
import * as Defs from './apiDefs';
import ApiUpdateResultModal from './apiUpdateResultModal';
import CreateNewApi from './createNewApi';
import { DialogModalOK, hideDialog, showDialog } from './dialogModal';
import EditApiList from './editApiList';
import NewAPIDtlModal from './newAPIDtlModal';
import NewSampleDataModal from './newSampleDataModal';
import SelectDeployApi from './selectDeployApi';

// main
function NewApiList() {
  // useState
  const [apiList, setApiList] = useState<Defs.Api[]>([]);
  const [tagList, setTags] = useState<Defs.Tag[]>([]);
  const [changeStatus, setChangeStatus] = useState<boolean>(false);
  const [enableDplApi, setEnableDplApi] = useState<boolean>(false);
  const [errorMsg, setErrorMsg] = useState<{}>({});
  const [deployJobId, setDeployJobId] = useState<string>('');
  const [isModalOpen, setIsModalOpen] = useState(false);

  /* API情報取得のリクエスト */
  useEffect(() => {
    (async () => {
      commonAjax
        .axios({ swalFire: false, loading: true })
        .get('/api/api')
        .then((res) => {
          const data = res.data.api;
          const resTags: React.SetStateAction<Defs.Tag[]> = [];
          let tmpTags: string[] = [];
          data.map((item: any) => {
            tmpTags = tmpTags.concat(item.tags);
          });
          tmpTags = Array.from(new Set(tmpTags));
          tmpTags.map((t: string) => {
            resTags.push({ tag_name: t, select: false });
          });
          setApiList(data);
          setTags(resTags);
          setEnableDplApi(res.data.enableDplApi);
        });
    })();
  }, [changeStatus]);

  useLayoutEffect(() => {
    if (deployJobId) {
      // ジョブ開始ダイアログ表示
      showDialog('api-deploy-job-start-notification');
    }
  }, [deployJobId]);

  const metricsProcess = process.env.REACT_APP_METRICS_PROCESS;
  const title = `${metricsProcess} データセット一覧`;

  return (
    <div className="content-wrapper api-list">
      <section className="page-cover">
        <h1>{title}</h1>
      </section>
      {/* Content Header (Page header) */}
      <section className="content-header">
        <div className="content-header-left">
          <h1>{title}</h1>
          <div className="content-header-desc">
            現在、設定されているデータセット一覧です
          </div>
        </div>
        {enableDplApi && (
          <div className="content-header-right">
            <button
              type="button"
              className="btn btn-secondary"
              data-toggle="modal"
              data-target="#editApiListModal"
              onClick={() => {
                setIsModalOpen(true);
              }}
            >
              編集
            </button>
            <button
              type="button"
              className="btn btn-primary"
              data-toggle="modal"
              data-target="#api-dtl-modal"
              onClick={() => {
                setIsModalOpen(true);
              }}
            >
              追加
            </button>
            <button
              type="button"
              className="btn-long-text btn-primary"
              data-toggle="modal"
              data-target="#selectDeployApiModal"
              onClick={() => {
                setIsModalOpen(true);
              }}
            >
              一括有効化
            </button>
          </div>
        )}
      </section>
      <section className="content">
        {/* filter tag */}
        <div className="inline-form">
          <div className="inline-form-cat">フィルタータグ</div>
          {FilterTags(tagList, setTags)}
        </div>
        {Spacer({ size: 30 })}
        {/* API List (Grid) */}
        {viewList(
          apiList,
          tagList,
          changeStatus,
          setChangeStatus,
          setErrorMsg,
          errorMsg,
          setDeployJobId,
        )}
      </section>
      {isModalOpen && (
        <>
          <CreateNewApi
            changeStatus={changeStatus}
            setChangeStatus={setChangeStatus}
          />
          <EditApiList
            val={apiList}
            changeStatus={changeStatus}
            setChangeStatus={setChangeStatus}
          />
          <SelectDeployApi
            val={apiList}
            changeStatus={changeStatus}
            setChangeStatus={setChangeStatus}
          />
        </>
      )}
      <ApiDeployJobStartNotification jobId={deployJobId} />
    </div>
  );
}

// fillter tag
function FilterTags(tags: Defs.Tag[], setTags: any) {
  return (
    <div className="inline-form-group">
      <div className="inline-form-label">分類</div>
      {tags.map((item: Defs.Tag) => {
        if (item.select) {
          return (
            <div
              className="btn btn-tag active"
              key={item.tag_name}
              onClick={() => {
                item.select = false;
                const t = tags.slice(0, tags.length);
                setTags(t);
              }}
            >
              {item.tag_name}
            </div>
          );
        }
        return (
          <div
            className="btn btn-tag"
            onClick={() => {
              item.select = true;
              const t = tags.slice(0, tags.length);
              setTags(t);
            }}
          >
            {item.tag_name}
          </div>
        );
      })}
    </div>
  );
}

function viewList(
  item: Defs.Api[],
  tagList: Defs.Tag[],
  changeStatus: boolean,
  setChangeStatus: any,
  setErrorMsg: any,
  errorMsg: any,
  setDeployJobId: any,
) {
  // Filter the API list based on selected tags
  const filteredList = item.filter((api) => {
    // If no tags are selected, show all
    const isAnyTagSelected = tagList.some((tag) => tag.select);
    if (!isAnyTagSelected) return true;

    // Show only the APIs that match at least one selected tag
    return api.tags.some((apiTag) =>
      tagList.some((tag) => tag.select && tag.tag_name === apiTag),
    );
  });

  const length = Math.ceil(filteredList.length / 3);
  let listItem: any = [];
  if (length !== 0) {
    listItem = transpose(
      new Array(length)
        .fill(0)
        .map((_, i) => filteredList.slice(i * 3, (i + 1) * 3)),
    );
  }

  return (
    <div className="list-wrapper">
      {listItem.map((column: Defs.Api[], columnIndex: number) => (
        <div className="apiList" key={columnIndex}>
          {column.map((val: Defs.Api) =>
            ApiCard(
              val,
              tagList,
              changeStatus,
              setChangeStatus,
              setErrorMsg,
              errorMsg,
              setDeployJobId,
            ),
          )}
        </div>
      ))}
    </div>
  );
}

// API Card
function ApiCard(
  api: Defs.Api,
  tagList: Defs.Tag[],
  changeStatus: boolean,
  setChangeStatus: any,
  setErrorMsg: any,
  errorMsg: any,
  setDeployJobId: any,
) {
  let selected = false;

  // タグが1つでも選択されているか確認
  const isAnyTagSelected = tagList.some((tag) => tag.select);

  tagList.map((item: Defs.Tag) => {
    api.tags.map((apiTag: string) => {
      if (apiTag === item.tag_name) {
        if (!isAnyTagSelected || item.select) {
          // タグが未選択の時、全て選択と同じ扱いにする
          selected = true;
        }
      }
    });
  });

  if (!errorMsg) {
    $(`#${api.physical_name}`).collapse('hide');
  }

  return (
    <div
      className={`card ${
        selected ? 'card-primary' : 'card-not-applicable not-applicable'
      } card-collapse-sample`}
    >
      <div
        className="card-header collapsed"
        data-toggle="collapse"
        data-target={`#card-body-${api.physical_name}`}
      >
        <div
          className="api-name-font"
          style={{
            fontSize: `clamp(0.6rem, ${
              29 / `${api.physical_name} / ${api.logical_name}`.length
            }vw, 1rem)`,
          }}
        >
          <i
            className={`fa fa-circle provide-icon-size ${
              selected
                ? api.provide
                  ? 'provide-icon-color'
                  : 'no-provide-icon-color'
                : undefined
            }`}
            aria-hidden="true"
          />
          {Spacer({ size: 10, horizontal: true })}
          {`${api.physical_name} / ${api.logical_name}`}
        </div>
      </div>
      <div className="card-body collapse" id={`card-body-${api.physical_name}`}>
        <div className="api-list-title">
          <p>データセット 連携項目 一覧</p>
          {Spacer({ size: 20 })}
        </div>
        {api.outinfo.map((val: Defs.TableInfo) => {
          const res: JSX.Element[] = [];
          val.column.map((item: Defs.Column) => {
            res.push(
              <div className="list-contents-grid" key={item.physicalName}>
                <p>{item.logicalName}</p>
              </div>,
            );
          });
          return res;
        })}
        {Spacer({ size: 20 })}
        <div className="text-danger error-msg">
          {errorMsg[api.physical_name]}
        </div>
        <br />
        {api.provide ? (
          <>
            <div className="btn-center">
              <button
                className="btn btn-secondary btn-secondary"
                data-toggle="modal"
                data-target={`#${api.physical_name}-dtl-modal`}
                data-backdrop="true"
              >
                サンプルデータ挿入 / データセット連携項目追加
              </button>
            </div>
            <NewAPIDtlModal val={api} />
            <NewSampleDataModal
              id={api.physical_name}
              logicalName={api.logical_name}
            />
            <ApiUpdateResultModal val={api} />
          </>
        ) : (
          <div className="btn-center">
            <button
              className="btn btn-secondary btn-secondary"
              onClick={() => {
                putDeploy(
                  api,
                  changeStatus,
                  setChangeStatus,
                  setErrorMsg,
                  setDeployJobId,
                );
              }}
            >
              有効化
            </button>
          </div>
        )}
      </div>
    </div>
  );
}

// transpose columns and rows
const transpose = (a: any[]) =>
  a[0].map((_: any, c: string | number) => a.map((r) => r[c]).filter(Boolean));

// put api(deploy)
function putDeploy(
  api: Defs.Api,
  changeStatus: boolean,
  setChangeStatus: any,
  setErrorMsg: any,
  setDeployJobId: any,
) {
  if (api) {
    if (!api.provide) {
      (async () => {
        const errorMsgList: { [key: string]: string } = {
          [api.physical_name]: '',
        };
        commonAjax
          .axios({ swalFire: false, loading: true })
          .put('/api/api/deploy', { id: [api.physical_name] })
          .then((res) => {
            if (res.data.errorMsg != null) {
              const { errorMsg } = res.data;
              console.log({ errorMsg });
              errorMsgList[api.physical_name] = errorMsg;
              setErrorMsg(errorMsgList);
            } else {
              setChangeStatus(!changeStatus);
              setErrorMsg(errorMsgList);
              setDeployJobId(res.data.jobId);
            }
          });
      })();
    }
  }
}

function ApiDeployJobStartNotification({ jobId }: { jobId?: string }) {
  return (
    <DialogModalOK
      id="api-deploy-job-start-notification"
      title="処理を開始しました"
    >
      <p>
        データセット有効化処理を開始しました。
        <br />
        完了には時間がかかりますのでお待ちください。
        <br />
        処理の状況は下記画面で確認できます。
      </p>
      <Link
        to={`/jobs/${jobId}`}
        className="btn btn-secondary"
        target="_blank"
        onClick={() => hideDialog('api-deploy-job-start-notification')}
      >
        ジョブステータス確認
      </Link>
    </DialogModalOK>
  );
}

export default NewApiList;
kirin-ri commented 1 month ago
const [isEditApiListModalOpen, setIsEditApiListModalOpen] = useState(false);
const [isApiDtlModalOpen, setIsApiDtlModalOpen] = useState(false);
const [isSelectDeployApiModalOpen, setIsSelectDeployApiModalOpen] = useState(false);
const [isSampleDataModalOpen, setIsSampleDataModalOpen] = useState(false);
kirin-ri commented 1 month ago
<button
  className="btn btn-secondary"
  onClick={() => {
    setIsSampleDataModalOpen(true);
  }}
>
  サンプルデータ挿入 / データセット連携項目追加
</button>
kirin-ri commented 1 month ago
{isSampleDataModalOpen && (
  <NewSampleDataModal
    id={api.physical_name}
    logicalName={api.logical_name}
    onClose={() => setIsSampleDataModalOpen(false)} // 添加关闭模态的回调
  />
)}

{isEditApiListModalOpen && (
  <EditApiList
    val={apiList}
    changeStatus={changeStatus}
    setChangeStatus={setChangeStatus}
    onClose={() => setIsEditApiListModalOpen(false)} // 添加关闭模态的回调
  />
)}

{isApiDtlModalOpen && (
  <CreateNewApi
    changeStatus={changeStatus}
    setChangeStatus={setChangeStatus}
    onClose={() => setIsApiDtlModalOpen(false)} // 添加关闭模态的回调
  />
)}
kirin-ri commented 1 month ago
<button
  className="btn btn-secondary"
  onClick={() => setIsEditApiListModalOpen(true)}
>
  編集
</button>

<button
  className="btn btn-primary"
  onClick={() => setIsApiDtlModalOpen(true)}
>
  追加
</button>

<button
  className="btn-long-text btn-primary"
  onClick={() => setIsSelectDeployApiModalOpen(true)}
>
  一括有効化
</button>
kirin-ri commented 1 month ago
function NewSampleDataModal({ id, logicalName, onClose }) {
  return (
    <div className="modal">
      <div className="modal-content">
        <h2>{logicalName}</h2>
        <button onClick={onClose}>閉じる</button> {/* 添加关闭按钮 */}
      </div>
    </div>
  );
}
kirin-ri commented 1 month ago

const [isSampleDataModalOpen, setIsSampleDataModalOpen] = useState(false);

kirin-ri commented 1 month ago
<button
  className="btn btn-secondary"
  onClick={() => setIsSampleDataModalOpen(true)} // 打开模态窗口
>
  サンプルデータ挿入 / データセット連携項目追加
</button>
kirin-ri commented 1 month ago
{isSampleDataModalOpen && (
  <NewSampleDataModal
    id={api.physical_name}
    logicalName={api.logical_name}
    onClose={() => setIsSampleDataModalOpen(false)} // 关闭模态窗口
  />
)}
kirin-ri commented 1 month ago
import React, { useEffect, useLayoutEffect, useState } from 'react';
import { Link } from 'react-router-dom';
import { commonAjax } from '../../../components/commonAjax';
import { Spacer } from '../spacer';
import * as Defs from './apiDefs';
import ApiUpdateResultModal from './apiUpdateResultModal';
import CreateNewApi from './createNewApi';
import { DialogModalOK, hideDialog, showDialog } from './dialogModal';
import EditApiList from './editApiList';
import NewAPIDtlModal from './newAPIDtlModal';
import NewSampleDataModal from './newSampleDataModal';
import SelectDeployApi from './selectDeployApi';

// main
function NewApiList() {
  // useState
  const [apiList, setApiList] = useState<Defs.Api[]>([]);
  const [tagList, setTags] = useState<Defs.Tag[]>([]);
  const [changeStatus, setChangeStatus] = useState<boolean>(false);
  const [enableDplApi, setEnableDplApi] = useState<boolean>(false);
  const [errorMsg, setErrorMsg] = useState<{}>({});
  const [deployJobId, setDeployJobId] = useState<string>('');
  const [isModalOpen, setIsModalOpen] = useState(false);

  /* API情報取得のリクエスト */
  useEffect(() => {
    (async () => {
      commonAjax
        .axios({ swalFire: false, loading: true })
        .get('/api/api')
        .then((res) => {
          const data = res.data.api;
          const resTags: React.SetStateAction<Defs.Tag[]> = [];
          let tmpTags: string[] = [];
          data.map((item: any) => {
            tmpTags = tmpTags.concat(item.tags);
          });
          tmpTags = Array.from(new Set(tmpTags));
          tmpTags.map((t: string) => {
            resTags.push({ tag_name: t, select: false });
          });
          setApiList(data);
          setTags(resTags);
          setEnableDplApi(res.data.enableDplApi);
        });
    })();
  }, [changeStatus]);

  useLayoutEffect(() => {
    if (deployJobId) {
      // ジョブ開始ダイアログ表示
      showDialog('api-deploy-job-start-notification');
    }
  }, [deployJobId]);

  const metricsProcess = process.env.REACT_APP_METRICS_PROCESS;
  const title = `${metricsProcess} データセット一覧`;

  return (
    <div className="content-wrapper api-list">
      <section className="page-cover">
        <h1>{title}</h1>
      </section>
      {/* Content Header (Page header) */}
      <section className="content-header">
        <div className="content-header-left">
          <h1>{title}</h1>
          <div className="content-header-desc">
            現在、設定されているデータセット一覧です
          </div>
        </div>
        {enableDplApi && (
          <div className="content-header-right">
            <button
              type="button"
              className="btn btn-secondary"
              data-toggle="modal"
              data-target="#editApiListModal"
              onClick={() => {
                setIsModalOpen(true);
              }}
            >
              編集
            </button>
            <button
              type="button"
              className="btn btn-primary"
              data-toggle="modal"
              data-target="#api-dtl-modal"
              onClick={() => {
                setIsModalOpen(true);
              }}
            >
              追加
            </button>
            <button
              type="button"
              className="btn-long-text btn-primary"
              data-toggle="modal"
              data-target="#selectDeployApiModal"
              onClick={() => {
                setIsModalOpen(true);
              }}
            >
              一括有効化
            </button>
          </div>
        )}
      </section>
      <section className="content">
        {/* filter tag */}
        <div className="inline-form">
          <div className="inline-form-cat">フィルタータグ</div>
          {FilterTags(tagList, setTags)}
        </div>
        {Spacer({ size: 30 })}
        {/* API List (Grid) */}
        {viewList(
          apiList,
          tagList,
          changeStatus,
          setChangeStatus,
          setErrorMsg,
          errorMsg,
          setDeployJobId,
        )}
      </section>
      {isModalOpen && (
        <>
          <CreateNewApi
            changeStatus={changeStatus}
            setChangeStatus={setChangeStatus}
          />
          <EditApiList
            val={apiList}
            changeStatus={changeStatus}
            setChangeStatus={setChangeStatus}
          />
          <SelectDeployApi
            val={apiList}
            changeStatus={changeStatus}
            setChangeStatus={setChangeStatus}
          />
        </>
      )}
      <ApiDeployJobStartNotification jobId={deployJobId} />
    </div>
  );
}

// fillter tag
function FilterTags(tags: Defs.Tag[], setTags: any) {
  return (
    <div className="inline-form-group">
      <div className="inline-form-label">分類</div>
      {tags.map((item: Defs.Tag) => {
        if (item.select) {
          return (
            <div
              className="btn btn-tag active"
              key={item.tag_name}
              onClick={() => {
                item.select = false;
                const t = tags.slice(0, tags.length);
                setTags(t);
              }}
            >
              {item.tag_name}
            </div>
          );
        }
        return (
          <div
            className="btn btn-tag"
            onClick={() => {
              item.select = true;
              const t = tags.slice(0, tags.length);
              setTags(t);
            }}
          >
            {item.tag_name}
          </div>
        );
      })}
    </div>
  );
}

function viewList(
  item: Defs.Api[],
  tagList: Defs.Tag[],
  changeStatus: boolean,
  setChangeStatus: any,
  setErrorMsg: any,
  errorMsg: any,
  setDeployJobId: any,
) {
  // Filter the API list based on selected tags
  const filteredList = item.filter((api) => {
    // If no tags are selected, show all
    const isAnyTagSelected = tagList.some((tag) => tag.select);
    if (!isAnyTagSelected) return true;

    // Show only the APIs that match at least one selected tag
    return api.tags.some((apiTag) =>
      tagList.some((tag) => tag.select && tag.tag_name === apiTag),
    );
  });

  const length = Math.ceil(filteredList.length / 3);
  let listItem: any = [];
  if (length !== 0) {
    listItem = transpose(
      new Array(length)
        .fill(0)
        .map((_, i) => filteredList.slice(i * 3, (i + 1) * 3)),
    );
  }

  return (
    <div className="list-wrapper">
      {listItem.map((column: Defs.Api[], columnIndex: number) => (
        <div className="apiList" key={columnIndex}>
          {column.map((val: Defs.Api) =>
            ApiCard(
              val,
              tagList,
              changeStatus,
              setChangeStatus,
              setErrorMsg,
              errorMsg,
              setDeployJobId,
            ),
          )}
        </div>
      ))}
    </div>
  );
}

// API Card
function ApiCard(
  api: Defs.Api,
  tagList: Defs.Tag[],
  changeStatus: boolean,
  setChangeStatus: any,
  setErrorMsg: any,
  errorMsg: any,
  setDeployJobId: any,
) {
  let selected = false;

  // タグが1つでも選択されているか確認
  const isAnyTagSelected = tagList.some((tag) => tag.select);

  tagList.map((item: Defs.Tag) => {
    api.tags.map((apiTag: string) => {
      if (apiTag === item.tag_name) {
        if (!isAnyTagSelected || item.select) {
          // タグが未選択の時、全て選択と同じ扱いにする
          selected = true;
        }
      }
    });
  });

  if (!errorMsg) {
    $(`#${api.physical_name}`).collapse('hide');
  }

  return (
    <div
      className={`card ${
        selected ? 'card-primary' : 'card-not-applicable not-applicable'
      } card-collapse-sample`}
    >
      <div
        className="card-header collapsed"
        data-toggle="collapse"
        data-target={`#card-body-${api.physical_name}`}
      >
        <div
          className="api-name-font"
          style={{
            fontSize: `clamp(0.6rem, ${
              29 / `${api.physical_name} / ${api.logical_name}`.length
            }vw, 1rem)`,
          }}
        >
          <i
            className={`fa fa-circle provide-icon-size ${
              selected
                ? api.provide
                  ? 'provide-icon-color'
                  : 'no-provide-icon-color'
                : undefined
            }`}
            aria-hidden="true"
          />
          {Spacer({ size: 10, horizontal: true })}
          {`${api.physical_name} / ${api.logical_name}`}
        </div>
      </div>
      <div className="card-body collapse" id={`card-body-${api.physical_name}`}>
        <div className="api-list-title">
          <p>データセット 連携項目 一覧</p>
          {Spacer({ size: 20 })}
        </div>
        {api.outinfo.map((val: Defs.TableInfo) => {
          const res: JSX.Element[] = [];
          val.column.map((item: Defs.Column) => {
            res.push(
              <div className="list-contents-grid" key={item.physicalName}>
                <p>{item.logicalName}</p>
              </div>,
            );
          });
          return res;
        })}
        {Spacer({ size: 20 })}
        <div className="text-danger error-msg">
          {errorMsg[api.physical_name]}
        </div>
        <br />
        {api.provide ? (
          <>
            <div className="btn-center">
              <button
                className="btn btn-secondary btn-secondary"
                data-toggle="modal"
                data-target={`#${api.physical_name}-dtl-modal`}
                data-backdrop="true"
              >
                サンプルデータ挿入 / データセット連携項目追加
              </button>
            </div>
            <NewAPIDtlModal val={api} />
            <NewSampleDataModal
              id={api.physical_name}
              logicalName={api.logical_name}
            />
            <ApiUpdateResultModal val={api} />
          </>
        ) : (
          <div className="btn-center">
            <button
              className="btn btn-secondary btn-secondary"
              onClick={() => {
                putDeploy(
                  api,
                  changeStatus,
                  setChangeStatus,
                  setErrorMsg,
                  setDeployJobId,
                );
              }}
            >
              有効化
            </button>
          </div>
        )}
      </div>
    </div>
  );
}

// transpose columns and rows
const transpose = (a: any[]) =>
  a[0].map((_: any, c: string | number) => a.map((r) => r[c]).filter(Boolean));

// put api(deploy)
function putDeploy(
  api: Defs.Api,
  changeStatus: boolean,
  setChangeStatus: any,
  setErrorMsg: any,
  setDeployJobId: any,
) {
  if (api) {
    if (!api.provide) {
      (async () => {
        const errorMsgList: { [key: string]: string } = {
          [api.physical_name]: '',
        };
        commonAjax
          .axios({ swalFire: false, loading: true })
          .put('/api/api/deploy', { id: [api.physical_name] })
          .then((res) => {
            if (res.data.errorMsg != null) {
              const { errorMsg } = res.data;
              console.log({ errorMsg });
              errorMsgList[api.physical_name] = errorMsg;
              setErrorMsg(errorMsgList);
            } else {
              setChangeStatus(!changeStatus);
              setErrorMsg(errorMsgList);
              setDeployJobId(res.data.jobId);
            }
          });
      })();
    }
  }
}

function ApiDeployJobStartNotification({ jobId }: { jobId?: string }) {
  return (
    <DialogModalOK
      id="api-deploy-job-start-notification"
      title="処理を開始しました"
    >
      <p>
        データセット有効化処理を開始しました。
        <br />
        完了には時間がかかりますのでお待ちください。
        <br />
        処理の状況は下記画面で確認できます。
      </p>
      <Link
        to={`/jobs/${jobId}`}
        className="btn btn-secondary"
        target="_blank"
        onClick={() => hideDialog('api-deploy-job-start-notification')}
      >
        ジョブステータス確認
      </Link>
    </DialogModalOK>
  );
}

export default NewApiList;
kirin-ri commented 1 month ago
import React, { useEffect, useLayoutEffect, useState } from 'react';
import { Link } from 'react-router-dom';
import { commonAjax } from '../../../components/commonAjax';
import { Spacer } from '../spacer';
import * as Defs from './apiDefs';
import ApiUpdateResultModal from './apiUpdateResultModal';
import CreateNewApi from './createNewApi';
import { DialogModalOK, hideDialog, showDialog } from './dialogModal';
import EditApiList from './editApiList';
import NewAPIDtlModal from './newAPIDtlModal';
import NewSampleDataModal from './newSampleDataModal';
import SelectDeployApi from './selectDeployApi';

function NewApiList() {
  const [apiList, setApiList] = useState<Defs.Api[]>([]);
  const [tagList, setTags] = useState<Defs.Tag[]>([]);
  const [changeStatus, setChangeStatus] = useState<boolean>(false);
  const [enableDplApi, setEnableDplApi] = useState<boolean>(false);
  const [errorMsg, setErrorMsg] = useState<{}>({});
  const [deployJobId, setDeployJobId] = useState<string>('');
  const [isModalOpen, setIsModalOpen] = useState(false);

  useEffect(() => {
    (async () => {
      commonAjax
        .axios({ swalFire: false, loading: true })
        .get('/api/api')
        .then((res) => {
          const data = res.data.api;
          const resTags: React.SetStateAction<Defs.Tag[]> = [];
          let tmpTags: string[] = [];
          data.map((item: any) => {
            tmpTags = tmpTags.concat(item.tags);
          });
          tmpTags = Array.from(new Set(tmpTags));
          tmpTags.map((t: string) => {
            resTags.push({ tag_name: t, select: false });
          });
          setApiList(data);
          setTags(resTags);
          setEnableDplApi(res.data.enableDplApi);
        });
    })();
  }, [changeStatus]);

  useLayoutEffect(() => {
    if (deployJobId) {
      showDialog('api-deploy-job-start-notification');
    }
  }, [deployJobId]);

  const metricsProcess = process.env.REACT_APP_METRICS_PROCESS;
  const title = `${metricsProcess} データセット一覧`;

  const filteredList = apiList.filter((api) => {
    const isAnyTagSelected = tagList.some((tag) => tag.select);
    if (!isAnyTagSelected) return true;

    return api.tags.some((apiTag) =>
      tagList.some((tag) => tag.select && tag.tag_name === apiTag),
    );
  });

  // 添加日志查看过滤后的 API 列表
  useEffect(() => {
    console.log("Filtered API list after tag selection:", filteredList);
  }, [tagList, apiList]);

  return (
    <div className="content-wrapper api-list">
      <section className="page-cover">
        <h1>{title}</h1>
      </section>

      <section className="content-header">
        <div className="content-header-left">
          <h1>{title}</h1>
        </div>

        {enableDplApi && (
          <div className="content-header-right">
            <button
              type="button"
              className="btn btn-secondary"
              onClick={() => setIsModalOpen(true)}
            >
              追加
            </button>
          </div>
        )}
      </section>

      <section className="content">
        <div className="inline-form">
          <div className="inline-form-cat">フィルタータグ</div>
          {FilterTags(tagList, setTags)}
        </div>

        {Spacer({ size: 30 })}

        {filteredList.length === 0 ? (
          <p>没有匹配的API,请选择其他标签。</p>
        ) : (
          viewList(
            filteredList,
            tagList,
            changeStatus,
            setChangeStatus,
            setErrorMsg,
            errorMsg,
            setDeployJobId,
          )
        )}
      </section>

      {isModalOpen && (
        <>
          <CreateNewApi
            changeStatus={changeStatus}
            setChangeStatus={setChangeStatus}
          />
          <NewSampleDataModal
            id={'sample-data'}
            logicalName={'Sample Data'}
            onClose={() => setIsModalOpen(false)}
          />
        </>
      )}
      <ApiDeployJobStartNotification jobId={deployJobId} />
    </div>
  );
}

export default NewApiList;
kirin-ri commented 1 month ago

import $ from 'jquery'; // 使用 jQuery 以便于折叠操作

kirin-ri commented 1 month ago

useEffect(() => { // 每次 tagList 发生变化时折叠所有展开的部件 $('.collapse').collapse('hide'); }, [tagList]);

kirin-ri commented 1 month ago

const [expandedCards, setExpandedCards] = useState<{ [key: string]: boolean }>({});

kirin-ri commented 1 month ago
import React, { useEffect, useState } from 'react';
import { Link } from 'react-router-dom';
import { commonAjax } from '../../../components/commonAjax';
import { Spacer } from '../spacer';
import * as Defs from './apiDefs';
import ApiUpdateResultModal from './apiUpdateResultModal';
import CreateNewApi from './createNewApi';
import { DialogModalOK, hideDialog, showDialog } from './dialogModal';
import EditApiList from './editApiList';
import NewAPIDtlModal from './newAPIDtlModal';
import NewSampleDataModal from './newSampleDataModal';
import SelectDeployApi from './selectDeployApi';

function NewApiList() {
  const [apiList, setApiList] = useState<Defs.Api[]>([]);
  const [tagList, setTags] = useState<Defs.Tag[]>([]);
  const [changeStatus, setChangeStatus] = useState<boolean>(false);
  const [enableDplApi, setEnableDplApi] = useState<boolean>(false);
  const [errorMsg, setErrorMsg] = useState<{}>({});
  const [deployJobId, setDeployJobId] = useState<string>('');
  const [isModalOpen, setIsModalOpen] = useState(false);

  // 新增:用于控制每个API卡片的折叠状态
  const [expandedCards, setExpandedCards] = useState<{ [key: string]: boolean }>({});

  // API 情報取得のリクエスト
  useEffect(() => {
    (async () => {
      commonAjax
        .axios({ swalFire: false, loading: true })
        .get('/api/api')
        .then((res) => {
          const data = res.data.api;
          const resTags: React.SetStateAction<Defs.Tag[]> = [];
          let tmpTags: string[] = [];
          data.map((item: any) => {
            tmpTags = tmpTags.concat(item.tags);
          });
          tmpTags = Array.from(new Set(tmpTags));
          tmpTags.map((t: string) => {
            resTags.push({ tag_name: t, select: false });
          });
          setApiList(data);
          setTags(resTags);
          setEnableDplApi(res.data.enableDplApi);
        });
    })();
  }, [changeStatus]);

  // 监听标签变化时重置所有卡片的展开状态为收起
  useEffect(() => {
    setExpandedCards({});
  }, [tagList]);

  const toggleCard = (apiName: string) => {
    setExpandedCards((prev) => ({
      ...prev,
      [apiName]: !prev[apiName], // 切换展开状态
    }));
  };

  const metricsProcess = process.env.REACT_APP_METRICS_PROCESS;
  const title = `${metricsProcess} データセット一覧`;

  return (
    <div className="content-wrapper api-list">
      <section className="page-cover">
        <h1>{title}</h1>
      </section>
      <section className="content-header">
        {/* 省略其他代码 */}
      </section>
      <section className="content">
        {/* 过滤标签 */}
        <div className="inline-form">
          <div className="inline-form-cat">フィルタータグ</div>
          {FilterTags(tagList, setTags)}
        </div>
        {Spacer({ size: 30 })}
        {/* API 列表 (网格) */}
        {viewList(
          apiList,
          tagList,
          changeStatus,
          setChangeStatus,
          setErrorMsg,
          errorMsg,
          setDeployJobId,
          expandedCards,  // 传递展开状态
          toggleCard      // 传递展开切换函数
        )}
      </section>
      {/* 其他代码保持不变 */}
    </div>
  );
}

// 更新 viewList 函数,接收 expandedCards 和 toggleCard 参数
function viewList(
  item: Defs.Api[],
  tagList: Defs.Tag[],
  changeStatus: boolean,
  setChangeStatus: any,
  setErrorMsg: any,
  errorMsg: any,
  setDeployJobId: any,
  expandedCards: { [key: string]: boolean }, // 传递的展开状态
  toggleCard: (apiName: string) => void      // 传递的切换函数
) {
  // Filter the API list based on selected tags
  const filteredList = item.filter((api) => {
    const isAnyTagSelected = tagList.some((tag) => tag.select);
    if (!isAnyTagSelected) return true;
    return api.tags.some((apiTag) =>
      tagList.some((tag) => tag.select && tag.tag_name === apiTag)
    );
  });

  const length = Math.ceil(filteredList.length / 3);
  let listItem: any = [];
  if (length !== 0) {
    listItem = transpose(
      new Array(length)
        .fill(0)
        .map((_, i) => filteredList.slice(i * 3, (i + 1) * 3))
    );
  }

  return (
    <div className="list-wrapper">
      {listItem.map((column: Defs.Api[], columnIndex: number) => (
        <div className="apiList" key={columnIndex}>
          {column.map((val: Defs.Api) =>
            ApiCard(
              val,
              tagList,
              changeStatus,
              setChangeStatus,
              setErrorMsg,
              errorMsg,
              setDeployJobId,
              expandedCards[val.physical_name],  // 使用展开状态
              toggleCard                         // 传递切换函数
            ),
          )}
        </div>
      ))}
    </div>
  );
}

// 更新 ApiCard 函数,接收 expanded 和 toggleCard 参数
function ApiCard(
  api: Defs.Api,
  tagList: Defs.Tag[],
  changeStatus: boolean,
  setChangeStatus: any,
  setErrorMsg: any,
  errorMsg: any,
  setDeployJobId: any,
  expanded: boolean,  // 接收的展开状态
  toggleCard: (apiName: string) => void  // 接收的切换函数
) {
  const selected = tagList.some((tag) =>
    api.tags.includes(tag.tag_name) && tag.select
  );

  return (
    <div
      className={`card ${selected ? 'card-primary' : 'card-not-applicable'}`}
    >
      <div
        className="card-header"
        onClick={() => toggleCard(api.physical_name)}  // 点击切换展开状态
      >
        {api.physical_name} / {api.logical_name}
      </div>
      <div className={`card-body ${expanded ? 'show' : 'collapse'}`}>
        {/* 卡片内容 */}
      </div>
    </div>
  );
}
kirin-ri commented 1 month ago
import React, { useEffect, useLayoutEffect, useState } from 'react';
import { Link } from 'react-router-dom';
import { commonAjax } from '../../../components/commonAjax';
import { Spacer } from '../spacer';
import * as Defs from './apiDefs';
import ApiUpdateResultModal from './apiUpdateResultModal';
import CreateNewApi from './createNewApi';
import { DialogModalOK, hideDialog, showDialog } from './dialogModal';
import EditApiList from './editApiList';
import NewAPIDtlModal from './newAPIDtlModal';
import NewSampleDataModal from './newSampleDataModal';
import SelectDeployApi from './selectDeployApi';

// main
function NewApiList() {
  // useState
  const [apiList, setApiList] = useState<Defs.Api[]>([]);
  const [tagList, setTags] = useState<Defs.Tag[]>([]);
  const [changeStatus, setChangeStatus] = useState<boolean>(false);
  const [enableDplApi, setEnableDplApi] = useState<boolean>(false);
  const [errorMsg, setErrorMsg] = useState<{}>({});
  const [deployJobId, setDeployJobId] = useState<string>('');
  const [isModalOpen, setIsModalOpen] = useState(false);

  /* API情報取得のリクエスト */
  useEffect(() => {
    (async () => {
      commonAjax
        .axios({ swalFire: false, loading: true })
        .get('/api/api')
        .then((res) => {
          const data = res.data.api;
          const resTags: React.SetStateAction<Defs.Tag[]> = [];
          let tmpTags: string[] = [];
          data.map((item: any) => {
            tmpTags = tmpTags.concat(item.tags);
          });
          tmpTags = Array.from(new Set(tmpTags));
          tmpTags.map((t: string) => {
            resTags.push({ tag_name: t, select: false });
          });
          setApiList(data);
          setTags(resTags);
          setEnableDplApi(res.data.enableDplApi);
        });
    })();
  }, [changeStatus]);

  useLayoutEffect(() => {
    if (deployJobId) {
      // ジョブ開始ダイアログ表示
      showDialog('api-deploy-job-start-notification');
    }
  }, [deployJobId]);

  const metricsProcess = process.env.REACT_APP_METRICS_PROCESS;
  const title = `${metricsProcess} データセット一覧`;

  return (
    <div className="content-wrapper api-list">
      <section className="page-cover">
        <h1>{title}</h1>
      </section>
      {/* Content Header (Page header) */}
      <section className="content-header">
        <div className="content-header-left">
          <h1>{title}</h1>
          <div className="content-header-desc">
            現在、設定されているデータセット一覧です
          </div>
        </div>
        {enableDplApi && (
          <div className="content-header-right">
            <button
              type="button"
              className="btn btn-secondary"
              data-toggle="modal"
              data-target="#editApiListModal"
              onClick={() => {
                setIsModalOpen(true);
              }}
            >
              編集
            </button>
            <button
              type="button"
              className="btn btn-primary"
              data-toggle="modal"
              data-target="#api-dtl-modal"
              onClick={() => {
                setIsModalOpen(true);
              }}
            >
              追加
            </button>
            <button
              type="button"
              className="btn-long-text btn-primary"
              data-toggle="modal"
              data-target="#selectDeployApiModal"
              onClick={() => {
                setIsModalOpen(true);
              }}
            >
              一括有効化
            </button>
          </div>
        )}
      </section>
      <section className="content">
        {/* filter tag */}
        <div className="inline-form">
          <div className="inline-form-cat">フィルタータグ</div>
          {FilterTags(tagList, setTags)}
        </div>
        {Spacer({ size: 30 })}
        {/* API List (Grid) */}
        {viewList(
          apiList,
          tagList,
          changeStatus,
          setChangeStatus,
          setErrorMsg,
          errorMsg,
          setDeployJobId,
        )}
      </section>
      {isModalOpen && (
        <>
          <CreateNewApi
            changeStatus={changeStatus}
            setChangeStatus={setChangeStatus}
          />
          <EditApiList
            val={apiList}
            changeStatus={changeStatus}
            setChangeStatus={setChangeStatus}
          />
          <SelectDeployApi
            val={apiList}
            changeStatus={changeStatus}
            setChangeStatus={setChangeStatus}
          />
        </>
      )}
      <ApiDeployJobStartNotification jobId={deployJobId} />
    </div>
  );
}

// fillter tag
function FilterTags(tags: Defs.Tag[], setTags: any) {
  return (
    <div className="inline-form-group">
      <div className="inline-form-label">分類</div>
      {tags.map((item: Defs.Tag) => {
        if (item.select) {
          return (
            <div
              className="btn btn-tag active"
              key={item.tag_name}
              onClick={() => {
                item.select = false;
                const t = tags.slice(0, tags.length);
                setTags(t);
              }}
            >
              {item.tag_name}
            </div>
          );
        }
        return (
          <div
            className="btn btn-tag"
            onClick={() => {
              item.select = true;
              const t = tags.slice(0, tags.length);
              setTags(t);
            }}
          >
            {item.tag_name}
          </div>
        );
      })}
    </div>
  );
}

function viewList(
  item: Defs.Api[],
  tagList: Defs.Tag[],
  changeStatus: boolean,
  setChangeStatus: any,
  setErrorMsg: any,
  errorMsg: any,
  setDeployJobId: any,
) {
  // Filter the API list based on selected tags
  const filteredList = item.filter((api) => {
    // If no tags are selected, show all
    const isAnyTagSelected = tagList.some((tag) => tag.select);
    if (!isAnyTagSelected) return true;

    // Show only the APIs that match at least one selected tag
    return api.tags.some((apiTag) =>
      tagList.some((tag) => tag.select && tag.tag_name === apiTag),
    );
  });

  const length = Math.ceil(filteredList.length / 3);
  let listItem: any = [];
  if (length !== 0) {
    listItem = transpose(
      new Array(length)
        .fill(0)
        .map((_, i) => filteredList.slice(i * 3, (i + 1) * 3)),
    );
  }

  return (
    <div className="list-wrapper">
      {listItem.map((column: Defs.Api[], columnIndex: number) => (
        <div className="apiList" key={columnIndex}>
          {column.map((val: Defs.Api) =>
            ApiCard(
              val,
              tagList,
              changeStatus,
              setChangeStatus,
              setErrorMsg,
              errorMsg,
              setDeployJobId,
            ),
          )}
        </div>
      ))}
    </div>
  );
}

// API Card
function ApiCard(
  api: Defs.Api,
  tagList: Defs.Tag[],
  changeStatus: boolean,
  setChangeStatus: any,
  setErrorMsg: any,
  errorMsg: any,
  setDeployJobId: any,
) {
  let selected = false;

  // タグが1つでも選択されているか確認
  const isAnyTagSelected = tagList.some((tag) => tag.select);

  tagList.map((item: Defs.Tag) => {
    api.tags.map((apiTag: string) => {
      if (apiTag === item.tag_name) {
        if (!isAnyTagSelected || item.select) {
          // タグが未選択の時、全て選択と同じ扱いにする
          selected = true;
        }
      }
    });
  });

  if (!errorMsg) {
    $(`#${api.physical_name}`).collapse('hide');
  }

  return (
    <div
      className={`card ${
        selected ? 'card-primary' : 'card-not-applicable not-applicable'
      } card-collapse-sample`}
    >
      <div
        className="card-header collapsed"
        data-toggle="collapse"
        data-target={`#card-body-${api.physical_name}`}
      >
        <div
          className="api-name-font"
          style={{
            fontSize: `clamp(0.6rem, ${
              29 / `${api.physical_name} / ${api.logical_name}`.length
            }vw, 1rem)`,
          }}
        >
          <i
            className={`fa fa-circle provide-icon-size ${
              selected
                ? api.provide
                  ? 'provide-icon-color'
                  : 'no-provide-icon-color'
                : undefined
            }`}
            aria-hidden="true"
          />
          {Spacer({ size: 10, horizontal: true })}
          {`${api.physical_name} / ${api.logical_name}`}
        </div>
      </div>
      <div className="card-body collapse" id={`card-body-${api.physical_name}`}>
        <div className="api-list-title">
          <p>データセット 連携項目 一覧</p>
          {Spacer({ size: 20 })}
        </div>
        {api.outinfo.map((val: Defs.TableInfo) => {
          const res: JSX.Element[] = [];
          val.column.map((item: Defs.Column) => {
            res.push(
              <div className="list-contents-grid" key={item.physicalName}>
                <p>{item.logicalName}</p>
              </div>,
            );
          });
          return res;
        })}
        {Spacer({ size: 20 })}
        <div className="text-danger error-msg">
          {errorMsg[api.physical_name]}
        </div>
        <br />
        {api.provide ? (
          <>
            <div className="btn-center">
              <button
                className="btn btn-secondary btn-secondary"
                data-toggle="modal"
                data-target={`#${api.physical_name}-dtl-modal`}
                data-backdrop="true"
              >
                サンプルデータ挿入 / データセット連携項目追加
              </button>
            </div>
            <NewAPIDtlModal val={api} />
            <NewSampleDataModal
              id={api.physical_name}
              logicalName={api.logical_name}
            />
            <ApiUpdateResultModal val={api} />
          </>
        ) : (
          <div className="btn-center">
            <button
              className="btn btn-secondary btn-secondary"
              onClick={() => {
                putDeploy(
                  api,
                  changeStatus,
                  setChangeStatus,
                  setErrorMsg,
                  setDeployJobId,
                );
              }}
            >
              有効化
            </button>
          </div>
        )}
      </div>
    </div>
  );
}

// transpose columns and rows
const transpose = (a: any[]) =>
  a[0].map((_: any, c: string | number) => a.map((r) => r[c]).filter(Boolean));

// put api(deploy)
function putDeploy(
  api: Defs.Api,
  changeStatus: boolean,
  setChangeStatus: any,
  setErrorMsg: any,
  setDeployJobId: any,
) {
  if (api) {
    if (!api.provide) {
      (async () => {
        const errorMsgList: { [key: string]: string } = {
          [api.physical_name]: '',
        };
        commonAjax
          .axios({ swalFire: false, loading: true })
          .put('/api/api/deploy', { id: [api.physical_name] })
          .then((res) => {
            if (res.data.errorMsg != null) {
              const { errorMsg } = res.data;
              console.log({ errorMsg });
              errorMsgList[api.physical_name] = errorMsg;
              setErrorMsg(errorMsgList);
            } else {
              setChangeStatus(!changeStatus);
              setErrorMsg(errorMsgList);
              setDeployJobId(res.data.jobId);
            }
          });
      })();
    }
  }
}

function ApiDeployJobStartNotification({ jobId }: { jobId?: string }) {
  return (
    <DialogModalOK
      id="api-deploy-job-start-notification"
      title="処理を開始しました"
    >
      <p>
        データセット有効化処理を開始しました。
        <br />
        完了には時間がかかりますのでお待ちください。
        <br />
        処理の状況は下記画面で確認できます。
      </p>
      <Link
        to={`/jobs/${jobId}`}
        className="btn btn-secondary"
        target="_blank"
        onClick={() => hideDialog('api-deploy-job-start-notification')}
      >
        ジョブステータス確認
      </Link>
    </DialogModalOK>
  );
}

export default NewApiList;
kirin-ri commented 1 month ago
import React, { useEffect, useLayoutEffect, useState } from 'react';
import { Link } from 'react-router-dom';
import { commonAjax } from '../../../components/commonAjax';
import { Spacer } from '../spacer';
import * as Defs from './apiDefs';
import ApiUpdateResultModal from './apiUpdateResultModal';
import CreateNewApi from './createNewApi';
import { DialogModalOK, hideDialog, showDialog } from './dialogModal';
import EditApiList from './editApiList';
import NewAPIDtlModal from './newAPIDtlModal';
import NewSampleDataModal from './newSampleDataModal';
import SelectDeployApi from './selectDeployApi';

// main
function NewApiList() {
  // useState
  const [apiList, setApiList] = useState<Defs.Api[]>([]);
  const [tagList, setTags] = useState<Defs.Tag[]>([]);
  const [changeStatus, setChangeStatus] = useState<boolean>(false);
  const [enableDplApi, setEnableDplApi] = useState<boolean>(false);
  const [errorMsg, setErrorMsg] = useState<{}>({});
  const [deployJobId, setDeployJobId] = useState<string>('');
  const [isModalOpen, setIsModalOpen] = useState(false);

  /* API情報取得のリクエスト */
  useEffect(() => {
    (async () => {
      commonAjax
        .axios({ swalFire: false, loading: true })
        .get('/api/api')
        .then((res) => {
          const data = res.data.api;
          const resTags: React.SetStateAction<Defs.Tag[]> = [];
          let tmpTags: string[] = [];
          data.map((item: any) => {
            tmpTags = tmpTags.concat(item.tags);
          });
          tmpTags = Array.from(new Set(tmpTags));
          tmpTags.map((t: string) => {
            resTags.push({ tag_name: t, select: true });
          });
          setApiList(data);
          setTags(resTags);
          setEnableDplApi(res.data.enableDplApi);
        });
    })();
  }, [changeStatus]);

  useLayoutEffect(() => {
    if (deployJobId) {
      // ジョブ開始ダイアログ表示
      showDialog('api-deploy-job-start-notification');
    }
  }, [deployJobId]);

  const metricsProcess = process.env.REACT_APP_METRICS_PROCESS;
  const title = `${metricsProcess} データセット一覧`;

  return (
    <div className="content-wrapper api-list">
      <section className="page-cover">
        <h1>{title}</h1>
      </section>
      {/* Content Header (Page header) */}
      <section className="content-header">
        <div className="content-header-left">
          <h1>{title}</h1>
          <div className="content-header-desc">
            現在、設定されているデータセット一覧です
          </div>
        </div>
        {enableDplApi && (
          <div className="content-header-right">
            <button
              type="button"
              className="btn btn-secondary"
              data-toggle="modal"
              data-target="#editApiListModal"
              onClick={() => {
                setIsModalOpen(true);
              }}
            >
              編集
            </button>
            <button
              type="button"
              className="btn btn-primary"
              data-toggle="modal"
              data-target="#api-dtl-modal"
              onClick={() => {
                setIsModalOpen(true);
              }}
            >
              追加
            </button>
            <button
              type="button"
              className="btn-long-text btn-primary"
              data-toggle="modal"
              data-target="#selectDeployApiModal"
              onClick={() => {
                setIsModalOpen(true);
              }}
            >
              一括有効化
            </button>
          </div>
        )}
      </section>
      <section className="content">
        {/* filter tag */}
        <div className="inline-form">
          <div className="inline-form-cat">フィルタータグ</div>
          {FilterTags(tagList, setTags)}
        </div>
        {Spacer({ size: 30 })}
        {/* API List (Grid) */}
        {viewList(
          apiList,
          tagList,
          changeStatus,
          setChangeStatus,
          setErrorMsg,
          errorMsg,
          setDeployJobId,
        )}
      </section>
      {isModalOpen && (
        <>
          <CreateNewApi
            changeStatus={changeStatus}
            setChangeStatus={setChangeStatus}
          />
          <EditApiList
            val={apiList}
            changeStatus={changeStatus}
            setChangeStatus={setChangeStatus}
          />
          <SelectDeployApi
            val={apiList}
            changeStatus={changeStatus}
            setChangeStatus={setChangeStatus}
          />
        </>
      )}
      <ApiDeployJobStartNotification jobId={deployJobId} />
    </div>
  );
}

// fillter tag
function FilterTags(tags: Defs.Tag[], setTags: any) {
  return (
    <div className="inline-form-group">
      <div className="inline-form-label">分類</div>
      {tags.map((item: Defs.Tag) => {
        if (item.select) {
          return (
            <div
              className="btn btn-tag active"
              key={item.tag_name}
              onClick={() => {
                item.select = false;
                const t = tags.slice(0, tags.length);
                setTags(t);
              }}
            >
              {item.tag_name}
            </div>
          );
        }
        return (
          <div
            className="btn btn-tag"
            onClick={() => {
              item.select = true;
              const t = tags.slice(0, tags.length);
              setTags(t);
            }}
          >
            {item.tag_name}
          </div>
        );
      })}
    </div>
  );
}

function viewList(
  item: Defs.Api[],
  tagList: Defs.Tag[],
  changeStatus: boolean,
  setChangeStatus: any,
  setErrorMsg: any,
  errorMsg: any,
  setDeployJobId: any,
) {
  // if(item.length){
  const length = Math.ceil(item.length / 3);
  let listItem: any = [];
  if (length != 0) {
    listItem = transpose(
      new Array(length).fill(0).map((_, i) => item.slice(i * 3, (i + 1) * 3)),
    );
  }

  return (
    <div className="list-wrapper">
      <div className="apiList">
        {0 >= 0 && listItem.length > 0
          ? listItem[0].map((val: Defs.Api) =>
              ApiCard(
                val,
                tagList,
                changeStatus,
                setChangeStatus,
                setErrorMsg,
                errorMsg,
                setDeployJobId,
              ),
            )
          : null}
      </div>
      <div className="apiList">
        {1 >= 0 && listItem.length > 1
          ? listItem[1].map((val: Defs.Api) =>
              ApiCard(
                val,
                tagList,
                changeStatus,
                setChangeStatus,
                setErrorMsg,
                errorMsg,
                setDeployJobId,
              ),
            )
          : null}
      </div>
      <div className="apiList">
        {2 >= 0 && listItem.length > 2
          ? listItem[2].map((val: Defs.Api) =>
              ApiCard(
                val,
                tagList,
                changeStatus,
                setChangeStatus,
                setErrorMsg,
                errorMsg,
                setDeployJobId,
              ),
            )
          : null}
      </div>
    </div>
  );
  // }
}

// API Card
function ApiCard(
  api: Defs.Api,
  tagList: Defs.Tag[],
  changeStatus: boolean,
  setChangeStatus: any,
  setErrorMsg: any,
  errorMsg: any,
  setDeployJobId: any,
) {
  let selected = false;
  tagList.map((item: Defs.Tag) => {
    api.tags.map((apiTag: string) => {
      if (apiTag == item.tag_name) {
        if (item.select) {
          selected = true;
        }
      }
    });
  });
  if (!errorMsg) {
    $(`#${api.physical_name}`).collapse('hide');
  }

  return (
    <div
      className={`card ${
        selected ? 'card-primary' : 'card-not-applicable not-applicable'
      } card-collapse-sample`}
    >
      <div
        className="card-header collapsed"
        data-toggle="collapse"
        data-target={`#card-body-${api.physical_name}`}
      >
        <div
          className="api-name-font"
          style={{
            fontSize: `clamp(0.6rem, ${
              29 / `${api.physical_name} / ${api.logical_name}`.length
            }vw, 1rem)`,
          }}
        >
          <i
            className={`fa fa-circle provide-icon-size ${
              selected
                ? api.provide
                  ? 'provide-icon-color'
                  : 'no-provide-icon-color'
                : undefined
            }`}
            aria-hidden="true"
          />
          {Spacer({ size: 10, horizontal: true })}
          {`${api.physical_name} / ${api.logical_name}`}
        </div>
      </div>
      <div className="card-body collapse" id={`card-body-${api.physical_name}`}>
        <div className="api-list-title">
          <p>データセット 連携項目 一覧</p>
          {Spacer({ size: 20 })}
        </div>
        {api.outinfo.map((val: Defs.TableInfo) => {
          const res: JSX.Element[] = [];
          val.column.map((item: Defs.Column) => {
            res.push(
              <div className="list-contents-grid" key={item.physicalName}>
                <p>{item.logicalName}</p>
              </div>,
            );
          });
          return res;
        })}
        {Spacer({ size: 20 })}
        <div className="text-danger error-msg">
          {errorMsg[api.physical_name]}
        </div>
        <br />
        {api.provide ? (
          <>
            <div className="btn-center">
              <button
                className="btn btn-secondary btn-secondary"
                data-toggle="modal"
                data-target={`#${api.physical_name}-dtl-modal`}
                data-backdrop="true"
              >
                サンプルデータ挿入 / データセット連携項目追加
              </button>
            </div>
            <NewAPIDtlModal val={api} />
            <NewSampleDataModal
              id={api.physical_name}
              logicalName={api.logical_name}
            />
            <ApiUpdateResultModal val={api} />
          </>
        ) : (
          <div className="btn-center">
            <button
              className="btn btn-secondary btn-secondary"
              onClick={() => {
                putDeploy(
                  api,
                  changeStatus,
                  setChangeStatus,
                  setErrorMsg,
                  setDeployJobId,
                );
              }}
            >
              有効化
            </button>
          </div>
        )}
      </div>
    </div>
  );
}

// transpose columns and rows
const transpose = (a: any[]) =>
  a[0].map((_: any, c: string | number) => a.map((r) => r[c]).filter(Boolean));

// put api(deploy)
function putDeploy(
  api: Defs.Api,
  changeStatus: boolean,
  setChangeStatus: any,
  setErrorMsg: any,
  setDeployJobId: any,
) {
  if (api) {
    if (!api.provide) {
      (async () => {
        const errorMsgList: { [key: string]: string } = {
          [api.physical_name]: '',
        };
        commonAjax
          .axios({ swalFire: false, loading: true })
          .put('/api/api/deploy', { id: [api.physical_name] })
          .then((res) => {
            if (res.data.errorMsg != null) {
              const { errorMsg } = res.data;
              console.log({ errorMsg });
              errorMsgList[api.physical_name] = errorMsg;
              setErrorMsg(errorMsgList);
            } else {
              setChangeStatus(!changeStatus);
              setErrorMsg(errorMsgList);
              setDeployJobId(res.data.jobId);
            }
          });
      })();
    }
  }
}

function ApiDeployJobStartNotification({ jobId }: { jobId?: string }) {
  return (
    <DialogModalOK
      id="api-deploy-job-start-notification"
      title="処理を開始しました"
    >
      <p>
        データセット有効化処理を開始しました。
        <br />
        完了には時間がかかりますのでお待ちください。
        <br />
        処理の状況は下記画面で確認できます。
      </p>
      <Link
        to={`/jobs/${jobId}`}
        className="btn btn-secondary"
        target="_blank"
        onClick={() => hideDialog('api-deploy-job-start-notification')}
      >
        ジョブステータス確認
      </Link>
    </DialogModalOK>
  );
}

export default NewApiList;
kirin-ri commented 1 month ago
function NewApiList() {
  // useState
  const [apiList, setApiList] = useState<Defs.Api[]>([]);
  const [tagList, setTags] = useState<Defs.Tag[]>([]);
  const [changeStatus, setChangeStatus] = useState<boolean>(false);
  const [enableDplApi, setEnableDplApi] = useState<boolean>(false);
  const [errorMsg, setErrorMsg] = useState<{}>({});
  const [deployJobId, setDeployJobId] = useState<string>('');
  const [isModalOpen, setIsModalOpen] = useState(false);

  /* API情報取得のリクエスト */
  useEffect(() => {
    (async () => {
      commonAjax
        .axios({ swalFire: false, loading: true })
        .get('/api/api')
        .then((res) => {
          const data = res.data.api;
          const resTags: React.SetStateAction<Defs.Tag[]> = [];
          let tmpTags: string[] = [];
          data.map((item: any) => {
            tmpTags = tmpTags.concat(item.tags);
          });
          tmpTags = Array.from(new Set(tmpTags));
          tmpTags.map((t: string) => {
            resTags.push({ tag_name: t, select: false }); // 初期状态设为 false
          });
          setApiList(data);
          setTags(resTags);
          setEnableDplApi(res.data.enableDplApi);
        });
    })();
  }, [changeStatus]);

  useLayoutEffect(() => {
    if (deployJobId) {
      // ジョブ開始ダイアログ表示
      showDialog('api-deploy-job-start-notification');
    }
  }, [deployJobId]);

  const metricsProcess = process.env.REACT_APP_METRICS_PROCESS;
  const title = `${metricsProcess} データセット一覧`;

  return (
    <div className="content-wrapper api-list">
      <section className="page-cover">
        <h1>{title}</h1>
      </section>
      {/* Content Header (Page header) */}
      <section className="content-header">
        <div className="content-header-left">
          <h1>{title}</h1>
          <div className="content-header-desc">
            現在、設定されているデータセット一覧です
          </div>
        </div>
        {enableDplApi && (
          <div className="content-header-right">
            <button
              type="button"
              className="btn btn-secondary"
              data-toggle="modal"
              data-target="#editApiListModal"
              onClick={() => {
                setIsModalOpen(true);
              }}
            >
              編集
            </button>
            <button
              type="button"
              className="btn btn-primary"
              data-toggle="modal"
              data-target="#api-dtl-modal"
              onClick={() => {
                setIsModalOpen(true);
              }}
            >
              追加
            </button>
            <button
              type="button"
              className="btn-long-text btn-primary"
              data-toggle="modal"
              data-target="#selectDeployApiModal"
              onClick={() => {
                setIsModalOpen(true);
              }}
            >
              一括有効化
            </button>
          </div>
        )}
      </section>
      <section className="content">
        {/* filter tag */}
        <div className="inline-form">
          <div className="inline-form-cat">フィルタータグ</div>
          {FilterTags(tagList, setTags)}
        </div>
        {Spacer({ size: 30 })}
        {/* API List (Grid) */}
        {viewList(
          apiList,
          tagList,
          changeStatus,
          setChangeStatus,
          setErrorMsg,
          errorMsg,
          setDeployJobId,
        )}
      </section>
      {isModalOpen && (
        <>
          <CreateNewApi
            changeStatus={changeStatus}
            setChangeStatus={setChangeStatus}
          />
          <EditApiList
            val={apiList}
            changeStatus={changeStatus}
            setChangeStatus={setChangeStatus}
          />
          <SelectDeployApi
            val={apiList}
            changeStatus={changeStatus}
            setChangeStatus={setChangeStatus}
          />
        </>
      )}
      <ApiDeployJobStartNotification jobId={deployJobId} />
    </div>
  );
}

// filter tags 逻辑修改
function FilterTags(tags: Defs.Tag[], setTags: any) {
  return (
    <div className="inline-form-group">
      <div className="inline-form-label">分類</div>
      {tags.map((item: Defs.Tag) => (
        <div
          className={`btn btn-tag ${item.select ? 'active' : ''}`}
          key={item.tag_name}
          onClick={() => {
            item.select = !item.select; // 切换选择状态
            const t = tags.slice(0, tags.length);
            setTags(t);
          }}
        >
          {item.tag_name}
        </div>
      ))}
    </div>
  );
}

// 过滤显示逻辑修改
function viewList(
  apiList: Defs.Api[],
  tagList: Defs.Tag[],
  changeStatus: boolean,
  setChangeStatus: any,
  setErrorMsg: any,
  errorMsg: any,
  setDeployJobId: any,
) {
  let filteredList = apiList;

  // 检查是否有选中的标签
  const selectedTags = tagList.filter((tag) => tag.select).map((tag) => tag.tag_name);

  // 如果有选中的标签,按标签过滤API列表
  if (selectedTags.length > 0) {
    filteredList = apiList.filter((api) =>
      api.tags.some((tag) => selectedTags.includes(tag)),
    );
  }

  // 保留原有的 list 渲染逻辑
  const length = Math.ceil(filteredList.length / 3);
  let listItem: any = [];
  if (length != 0) {
    listItem = transpose(
      new Array(length).fill(0).map((_, i) => filteredList.slice(i * 3, (i + 1) * 3)),
    );
  }

  return (
    <div className="list-wrapper">
      <div className="apiList">
        {0 >= 0 && listItem.length > 0
          ? listItem[0].map((val: Defs.Api) =>
              ApiCard(
                val,
                tagList,
                changeStatus,
                setChangeStatus,
                setErrorMsg,
                errorMsg,
                setDeployJobId,
              ),
            )
          : null}
      </div>
      <div className="apiList">
        {1 >= 0 && listItem.length > 1
          ? listItem[1].map((val: Defs.Api) =>
              ApiCard(
                val,
                tagList,
                changeStatus,
                setChangeStatus,
                setErrorMsg,
                errorMsg,
                setDeployJobId,
              ),
            )
          : null}
      </div>
      <div className="apiList">
        {2 >= 0 && listItem.length > 2
          ? listItem[2].map((val: Defs.Api) =>
              ApiCard(
                val,
                tagList,
                changeStatus,
                setChangeStatus,
                setErrorMsg,
                errorMsg,
                setDeployJobId,
              ),
            )
          : null}
      </div>
    </div>
  );
}
kirin-ri commented 1 month ago
function ApiCard(
  api: Defs.Api,
  tagList: Defs.Tag[],
  changeStatus: boolean,
  setChangeStatus: any,
  setErrorMsg: any,
  errorMsg: any,
  setDeployJobId: any,
) {
  let selected = false;

  // 确保每个 API 项目都检查它的 tags,并根据所选 tags 决定是否显示
  tagList.forEach((item: Defs.Tag) => {
    api.tags.forEach((apiTag: string) => {
      if (apiTag === item.tag_name && item.select) {
        selected = true;
      }
    });
  });

  // 确保未被选中时,仍可以打开 modal
  if (!errorMsg) {
    $(`#${api.physical_name}`).collapse('hide');
  }

  return (
    <div
      className={`card ${
        selected ? 'card-primary' : 'card-not-applicable not-applicable'
      } card-collapse-sample`}
    >
      <div
        className="card-header collapsed"
        data-toggle="collapse"
        data-target={`#card-body-${api.physical_name}`}
      >
        <div
          className="api-name-font"
          style={{
            fontSize: `clamp(0.6rem, ${
              29 / `${api.physical_name} / ${api.logical_name}`.length
            }vw, 1rem)`,
          }}
        >
          <i
            className={`fa fa-circle provide-icon-size ${
              selected
                ? api.provide
                  ? 'provide-icon-color'
                  : 'no-provide-icon-color'
                : ''
            }`}
            aria-hidden="true"
          />
          {Spacer({ size: 10, horizontal: true })}
          {`${api.physical_name} / ${api.logical_name}`}
        </div>
      </div>
      <div className="card-body collapse" id={`card-body-${api.physical_name}`}>
        <div className="api-list-title">
          <p>データセット 連携項目 一覧</p>
          {Spacer({ size: 20 })}
        </div>
        {api.outinfo.map((val: Defs.TableInfo) => {
          return val.column.map((item: Defs.Column) => (
            <div className="list-contents-grid" key={item.physicalName}>
              <p>{item.logicalName}</p>
            </div>
          ));
        })}
        {Spacer({ size: 20 })}
        <div className="text-danger error-msg">
          {errorMsg[api.physical_name]}
        </div>
        <br />
        {api.provide ? (
          <>
            <div className="btn-center">
              <button
                className="btn btn-secondary btn-secondary"
                data-toggle="modal"
                data-target={`#${api.physical_name}-dtl-modal`} // 确保 modal 的 target id 绑定正确
                data-backdrop="true"
              >
                サンプルデータ挿入 / データセット連携項目追加
              </button>
            </div>
            {/* Modal 部分 */}
            <NewAPIDtlModal val={api} />
            <NewSampleDataModal
              id={api.physical_name}
              logicalName={api.logical_name}
            />
            <ApiUpdateResultModal val={api} />
          </>
        ) : (
          <div className="btn-center">
            <button
              className="btn btn-secondary btn-secondary"
              onClick={() => {
                putDeploy(
                  api,
                  changeStatus,
                  setChangeStatus,
                  setErrorMsg,
                  setDeployJobId,
                );
              }}
            >
              有効化
            </button>
          </div>
        )}
      </div>
    </div>
  );
}
kirin-ri commented 1 month ago
function NewApiList() {
  // useState
  const [apiList, setApiList] = useState<Defs.Api[]>([]);
  const [tagList, setTags] = useState<Defs.Tag[]>([]);
  const [changeStatus, setChangeStatus] = useState<boolean>(false);
  const [enableDplApi, setEnableDplApi] = useState<boolean>(false);
  const [errorMsg, setErrorMsg] = useState<{}>({});
  const [deployJobId, setDeployJobId] = useState<string>('');
  const [isModalOpen, setIsModalOpen] = useState(false);

  /* API情報取得のリクエスト */
  useEffect(() => {
    (async () => {
      commonAjax
        .axios({ swalFire: false, loading: true })
        .get('/api/api')
        .then((res) => {
          const data = res.data.api;
          const resTags: React.SetStateAction<Defs.Tag[]> = [];
          let tmpTags: string[] = [];
          data.map((item: any) => {
            tmpTags = tmpTags.concat(item.tags);
          });
          tmpTags = Array.from(new Set(tmpTags));
          tmpTags.map((t: string) => {
            resTags.push({ tag_name: t, select: false }); // 初期设为 false
          });
          setApiList(data);
          setTags(resTags);
          setEnableDplApi(res.data.enableDplApi);
        });
    })();
  }, [changeStatus]);

  useLayoutEffect(() => {
    if (deployJobId) {
      // ジョブ開始ダイアログ表示
      showDialog('api-deploy-job-start-notification');
    }
  }, [deployJobId]);

  const metricsProcess = process.env.REACT_APP_METRICS_PROCESS;
  const title = `${metricsProcess} データセット一覧`;

  return (
    <div className="content-wrapper api-list">
      <section className="page-cover">
        <h1>{title}</h1>
      </section>
      {/* Content Header (Page header) */}
      <section className="content-header">
        <div className="content-header-left">
          <h1>{title}</h1>
          <div className="content-header-desc">
            現在、設定されているデータセット一覧です
          </div>
        </div>
        {enableDplApi && (
          <div className="content-header-right">
            <button
              type="button"
              className="btn btn-secondary"
              data-toggle="modal"
              data-target="#editApiListModal"
              onClick={() => {
                setIsModalOpen(true);
              }}
            >
              編集
            </button>
            <button
              type="button"
              className="btn btn-primary"
              data-toggle="modal"
              data-target="#api-dtl-modal"
              onClick={() => {
                setIsModalOpen(true);
              }}
            >
              追加
            </button>
            <button
              type="button"
              className="btn-long-text btn-primary"
              data-toggle="modal"
              data-target="#selectDeployApiModal"
              onClick={() => {
                setIsModalOpen(true);
              }}
            >
              一括有効化
            </button>
          </div>
        )}
      </section>
      <section className="content">
        {/* filter tag */}
        <div className="inline-form">
          <div className="inline-form-cat">フィルタータグ</div>
          {FilterTags(tagList, setTags)}
        </div>
        {Spacer({ size: 30 })}
        {/* API List (Grid) */}
        {viewList(
          apiList,
          tagList,
          changeStatus,
          setChangeStatus,
          setErrorMsg,
          errorMsg,
          setDeployJobId,
        )}
      </section>
      {isModalOpen && (
        <>
          <CreateNewApi
            changeStatus={changeStatus}
            setChangeStatus={setChangeStatus}
          />
          <EditApiList
            val={apiList}
            changeStatus={changeStatus}
            setChangeStatus={setChangeStatus}
          />
          <SelectDeployApi
            val={apiList}
            changeStatus={changeStatus}
            setChangeStatus={setChangeStatus}
          />
        </>
      )}
      <ApiDeployJobStartNotification jobId={deployJobId} />
    </div>
  );
}

// filter tags 逻辑修改
function FilterTags(tags: Defs.Tag[], setTags: any) {
  return (
    <div className="inline-form-group">
      <div className="inline-form-label">分類</div>
      {tags.map((item: Defs.Tag) => (
        <div
          className={`btn btn-tag ${item.select ? 'active' : ''}`}
          key={item.tag_name}
          onClick={() => {
            item.select = !item.select; // 切换选择状态
            const t = tags.slice(0, tags.length);
            setTags(t);
          }}
        >
          {item.tag_name}
        </div>
      ))}
    </div>
  );
}

// 过滤显示逻辑修改
function viewList(
  apiList: Defs.Api[],
  tagList: Defs.Tag[],
  changeStatus: boolean,
  setChangeStatus: any,
  setErrorMsg: any,
  errorMsg: any,
  setDeployJobId: any,
) {
  // 先不做过滤,直接显示所有 API
  let filteredList = apiList;

  // 检查是否有选中的标签
  const selectedTags = tagList.filter((tag) => tag.select).map((tag) => tag.tag_name);

  // 如果有选中的标签,按标签过滤API列表
  if (selectedTags.length > 0) {
    filteredList = apiList.filter((api) =>
      api.tags.some((tag) => selectedTags.includes(tag)),
    );
  }

  const length = Math.ceil(filteredList.length / 3);
  let listItem: any = [];
  if (length != 0) {
    listItem = transpose(
      new Array(length).fill(0).map((_, i) => filteredList.slice(i * 3, (i + 1) * 3)),
    );
  }

  return (
    <div className="list-wrapper">
      <div className="apiList">
        {0 >= 0 && listItem.length > 0
          ? listItem[0].map((val: Defs.Api) =>
              ApiCard(
                val,
                tagList,
                changeStatus,
                setChangeStatus,
                setErrorMsg,
                errorMsg,
                setDeployJobId,
              ),
            )
          : null}
      </div>
      <div className="apiList">
        {1 >= 0 && listItem.length > 1
          ? listItem[1].map((val: Defs.Api) =>
              ApiCard(
                val,
                tagList,
                changeStatus,
                setChangeStatus,
                setErrorMsg,
                errorMsg,
                setDeployJobId,
              ),
            )
          : null}
      </div>
      <div className="apiList">
        {2 >= 0 && listItem.length > 2
          ? listItem[2].map((val: Defs.Api) =>
              ApiCard(
                val,
                tagList,
                changeStatus,
                setChangeStatus,
                setErrorMsg,
                errorMsg,
                setDeployJobId,
              ),
            )
          : null}
      </div>
    </div>
  );
}

// API Card
function ApiCard(
  api: Defs.Api,
  tagList: Defs.Tag[],
  changeStatus: boolean,
  setChangeStatus: any,
  setErrorMsg: any,
  errorMsg: any,
  setDeployJobId: any,
) {
  let selected = false;

  // 确保每个 API 项目都检查它的 tags,并根据所选 tags 决定是否显示
  tagList.forEach((item: Defs.Tag) => {
    api.tags.forEach((apiTag: string) => {
      if (apiTag === item.tag_name && item.select) {
        selected = true;
      }
    });
  });

  // 确保未被选中时,仍可以打开 modal
  if (!errorMsg) {
    $(`#${api.physical_name}`).collapse('hide');
  }

  return (
    <div
      className={`card ${
        selected ? 'card-primary' : 'card-not-applicable not-applicable'
      } card-collapse-sample`}
    >
      <div
        className="card-header collapsed"
        data-toggle="collapse"
        data-target={`#card-body-${api.physical_name}`}
      >
        <div
          className="api-name-font"
          style={{
            fontSize: `clamp(0.6rem, ${
              29 / `${api.physical_name} / ${api.logical_name}`.length
            }vw, 1rem)`,
          }}
        >
          <i
            className={`fa fa-circle provide-icon-size ${
              selected
                ? api.provide
                  ? 'provide-icon-color'
                  : 'no-provide-icon-color'
                : ''
            }`}
            aria-hidden="true"
          />
          {Spacer({ size: 10, horizontal: true })}
          {`${api.physical_name} / ${api.logical_name}`}
        </div>
      </div>
      <div className="card-body collapse" id={`card-body-${api.physical_name}`}>
        <div className="api-list-title">
          <p>データセット 連携項目 一覧</p>
          {Spacer({ size: 20 })}
        </div>
        {api.outinfo.map((val: Defs.TableInfo) => {
          return val.column.map((item: Defs.Column) => (
            <div className="list-contents-grid" key={item.physicalName}>
              <p>{item.logicalName}</p>
            </div>
          ));
        })}
        {Spacer({ size: 20 })}
        <div className="text-danger error-msg">
          {errorMsg[api.physical_name]}
        </div>
        <br />
        {api.provide ? (
          <>
            <div className="btn-center">
              <button
                className="btn btn-secondary btn-secondary"
                data-toggle="modal"
                data-target={`#${api.physical_name}-dtl-modal`} // 确保 modal 的 target id 绑定正确
                data-backdrop="true"
              >
                サンプルデータ挿入 / データセット連携項目追加
              </button>
            </div>
            {/* Modal 部分 */}
            <NewAPIDtlModal val={api} />
            <NewSampleDataModal
              id={api.physical_name}
              logicalName={api.logical_name}
            />
            <ApiUpdateResultModal val={api} />
          </>
        ) : (
          <div className="btn-center">
            <button
              className="btn btn-secondary btn-secondary"
              onClick={() => {
                putDeploy(
                  api,
                  changeStatus,
                  setChangeStatus,
                  setErrorMsg,
                  setDeployJobId,
                );
              }}
            >
              有効化
            </button>
          </div>
        )}
      </div>
    </div>
  );
}
kirin-ri commented 1 month ago
function NewApiList() {
  // useState
  const [apiList, setApiList] = useState<Defs.Api[]>([]);
  const [tagList, setTags] = useState<Defs.Tag[]>([]);
  const [changeStatus, setChangeStatus] = useState<boolean>(false);
  const [enableDplApi, setEnableDplApi] = useState<boolean>(false);
  const [errorMsg, setErrorMsg] = useState<{}>({});
  const [deployJobId, setDeployJobId] = useState<string>('');
  const [isModalOpen, setIsModalOpen] = useState(false);

  /* API情報取得のリクエスト */
  useEffect(() => {
    (async () => {
      commonAjax
        .axios({ swalFire: false, loading: true })
        .get('/api/api')
        .then((res) => {
          const data = res.data.api;
          const resTags: React.SetStateAction<Defs.Tag[]> = [];
          let tmpTags: string[] = [];
          data.map((item: any) => {
            tmpTags = tmpTags.concat(item.tags);
          });
          tmpTags = Array.from(new Set(tmpTags));
          tmpTags.map((t: string) => {
            resTags.push({ tag_name: t, select: false }); // 初期设为 false
          });
          setApiList(data);
          setTags(resTags);
          setEnableDplApi(res.data.enableDplApi);
        });
    })();
  }, [changeStatus]);

  useLayoutEffect(() => {
    if (deployJobId) {
      // ジョブ開始ダイアログ表示
      showDialog('api-deploy-job-start-notification');
    }
  }, [deployJobId]);

  const metricsProcess = process.env.REACT_APP_METRICS_PROCESS;
  const title = `${metricsProcess} データセット一覧`;

  return (
    <div className="content-wrapper api-list">
      <section className="page-cover">
        <h1>{title}</h1>
      </section>
      {/* Content Header (Page header) */}
      <section className="content-header">
        <div className="content-header-left">
          <h1>{title}</h1>
          <div className="content-header-desc">
            現在、設定されているデータセット一覧です
          </div>
        </div>
        {enableDplApi && (
          <div className="content-header-right">
            <button
              type="button"
              className="btn btn-secondary"
              data-toggle="modal"
              data-target="#editApiListModal"
              onClick={() => {
                setIsModalOpen(true);
              }}
            >
              編集
            </button>
            <button
              type="button"
              className="btn btn-primary"
              data-toggle="modal"
              data-target="#api-dtl-modal"
              onClick={() => {
                setIsModalOpen(true);
              }}
            >
              追加
            </button>
            <button
              type="button"
              className="btn-long-text btn-primary"
              data-toggle="modal"
              data-target="#selectDeployApiModal"
              onClick={() => {
                setIsModalOpen(true);
              }}
            >
              一括有効化
            </button>
          </div>
        )}
      </section>
      <section className="content">
        {/* filter tag */}
        <div className="inline-form">
          <div className="inline-form-cat">フィルタータグ</div>
          {FilterTags(tagList, setTags)}
        </div>
        {Spacer({ size: 30 })}
        {/* API List (Grid) */}
        {viewList(
          apiList,
          tagList,
          changeStatus,
          setChangeStatus,
          setErrorMsg,
          errorMsg,
          setDeployJobId,
        )}
      </section>
      {isModalOpen && (
        <>
          <CreateNewApi
            changeStatus={changeStatus}
            setChangeStatus={setChangeStatus}
          />
          <EditApiList
            val={apiList}
            changeStatus={changeStatus}
            setChangeStatus={setChangeStatus}
          />
          <SelectDeployApi
            val={apiList}
            changeStatus={changeStatus}
            setChangeStatus={setChangeStatus}
          />
        </>
      )}
      <ApiDeployJobStartNotification jobId={deployJobId} />
    </div>
  );
}

// filter tags 逻辑修改
function FilterTags(tags: Defs.Tag[], setTags: any) {
  return (
    <div className="inline-form-group">
      <div className="inline-form-label">分類</div>
      {tags.map((item: Defs.Tag) => (
        <div
          className={`btn btn-tag ${item.select ? 'active' : ''}`}
          key={item.tag_name}
          onClick={() => {
            item.select = !item.select; // 切换选择状态
            const t = tags.slice(0, tags.length);
            setTags(t);
          }}
        >
          {item.tag_name}
        </div>
      ))}
    </div>
  );
}

// 过滤显示逻辑修改
function viewList(
  apiList: Defs.Api[],
  tagList: Defs.Tag[],
  changeStatus: boolean,
  setChangeStatus: any,
  setErrorMsg: any,
  errorMsg: any,
  setDeployJobId: any,
) {
  let filteredList = apiList;

  // 检查是否有选中的标签
  const selectedTags = tagList.filter((tag) => tag.select).map((tag) => tag.tag_name);

  // 如果有选中的标签,按标签过滤API列表
  if (selectedTags.length > 0) {
    filteredList = apiList.filter((api) =>
      api.tags.some((tag) => selectedTags.includes(tag)),
    );
  }

  // 渲染所有 Modal 保证初始化
  const modals = apiList.map((val) => (
    <>
      <NewAPIDtlModal val={val} />
      <NewSampleDataModal
        id={val.physical_name}
        logicalName={val.logical_name}
      />
      <ApiUpdateResultModal val={val} />
    </>
  ));

  const length = Math.ceil(filteredList.length / 3);
  let listItem: any = [];
  if (length != 0) {
    listItem = transpose(
      new Array(length).fill(0).map((_, i) => filteredList.slice(i * 3, (i + 1) * 3)),
    );
  }

  return (
    <div className="list-wrapper">
      <div className="apiList">
        {0 >= 0 && listItem.length > 0
          ? listItem[0].map((val: Defs.Api) =>
              ApiCard(
                val,
                tagList,
                changeStatus,
                setChangeStatus,
                setErrorMsg,
                errorMsg,
                setDeployJobId,
              ),
            )
          : null}
      </div>
      <div className="apiList">
        {1 >= 0 && listItem.length > 1
          ? listItem[1].map((val: Defs.Api) =>
              ApiCard(
                val,
                tagList,
                changeStatus,
                setChangeStatus,
                setErrorMsg,
                errorMsg,
                setDeployJobId,
              ),
            )
          : null}
      </div>
      <div className="apiList">
        {2 >= 0 && listItem.length > 2
          ? listItem[2].map((val: Defs.Api) =>
              ApiCard(
                val,
                tagList,
                changeStatus,
                setChangeStatus,
                setErrorMsg,
                errorMsg,
                setDeployJobId,
              ),
            )
          : null}
      </div>
      {/* 渲染所有 Modal */}
      {modals}
    </div>
  );
}

// API Card
function ApiCard(
  api: Defs.Api,
  tagList: Defs.Tag[],
  changeStatus: boolean,
  setChangeStatus: any,
  setErrorMsg: any,
  errorMsg: any,
  setDeployJobId: any,
) {
  let selected = false;

  // 确保每个 API 项目都检查它的 tags,并根据所选 tags 决定是否显示
  tagList.forEach((item: Defs.Tag) => {
    api.tags.forEach((apiTag: string) => {
      if (apiTag === item.tag_name && item.select) {
        selected = true;
      }
    });
  });

  // 确保未被选中时,仍可以打开 modal
  if (!errorMsg) {
    $(`#${api.physical_name}`).collapse('hide');
  }

  return (
    <div
      className={`card ${
        selected ? 'card-primary' : 'card-not-applicable not-applicable'
      } card-collapse-sample`}
    >
      <div
        className="card-header collapsed"
        data-toggle="collapse"
        data-target={`#card-body-${api.physical_name}`}
      >
        <div
          className="api-name-font"
          style={{
            fontSize: `clamp(0.6rem, ${
              29 / `${api.physical_name} / ${api.logical_name}`.length
            }vw, 1rem)`,
          }}
        >
          <i
            className={`fa fa-circle provide-icon-size ${
              selected
                ? api.provide
                  ? 'provide-icon-color'
                  : 'no-provide-icon-color'
                : ''
            }`}
            aria-hidden="true"
          />
          {Spacer({ size: 10, horizontal: true })}
          {`${api.physical_name} / ${api.logical_name}`}
        </div>
      </div>
      <div className="card-body collapse" id={`card-body-${api.physical_name}`}>
        <div className="api-list-title">
          <p>データセット 連携項目 一覧</p>
          {Spacer({ size: 20 })}
        </div>
        {api.outinfo.map((val: Defs.TableInfo) => {
          return val.column.map((item: Defs.Column) => (
            <div className="list-contents-grid" key={item.physicalName}>
              <p>{item.logicalName}</p>
            </div>
          ));
        })}
        {Spacer({ size: 20 })}
        <div className="text-danger error-msg">
          {errorMsg[api.physical_name]}
        </div>
        <br />
        {api.provide ? (
          <>
            <div className="btn-center">
              <button
                className="btn btn-secondary btn-secondary"
                data-toggle="modal"
                data-target={`#${api.physical_name}-dtl-modal`} // 确保 modal 的 target id 绑定正确
                data-backdrop="true"
              >
                サンプルデータ挿入 / データセット連携項目追加
              </button>
            </div>
          </>
        ) : (
          <div className="btn-center">
            <button
              className="btn btn-secondary btn-secondary"
              onClick={() => {
                putDeploy(
                  api,
                  changeStatus,
                  setChangeStatus,
                  setErrorMsg,
                  setDeployJobId,
                );
              }}
            >
              有効化
            </button>
          </div>
        )}
      </div>
    </div>
  );
}