Open kirin-ri opened 7 months ago
apiVersion: apps/v1
kind: Deployment
metadata:
name: catalog-web-app
spec:
replicas: 1
selector:
matchLabels:
app: catalog-web-app
strategy:
rollingUpdate:
maxSurge: 1
maxUnavailable: 1
minReadySeconds: 5
template:
metadata:
labels:
app: catalog-web-app
spec:
nodeSelector:
'beta.kubernetes.io/os': linux
containers:
- name: catalog-web-app
image: idnaacr.azurecr.io/catalog-web-app:latest
ports:
- containerPort: 5000
resources:
requests:
cpu: 50m
limits:
cpu: 500m
env:
- name: AAD_CLIENT_ID
value: 'd69ba062-cb62-440c-bf7d-4cda629c4e97'
- name: AAD_CLIENT_SECRET
value: 'iUG8Q~xoZNeyaaW023kMVyV4wmdJOQHl1AcDzcmW'
- name: AAD_TENANT_ID
value: '17a6323e-2beb-40b3-817b-97ae9fad5804'
- name: ACCESS_HISTORY_LIMIT_DAYS
value: '14'
- name: ARANGO_URL
value: 'https://4.190.18.243:8529/'
- name: ARANGO_DB
value: 'INDUSTRIAL_DNA_DB'
- name: ARANGO_USER
value: 'ind-arangodb'
- name: ARANGO_PWD
value: 'uH7yYk6|'
- name: BASE_PROJECT
value: 'startpack'
- name: CHAT2QUERY_API_KEY
value: 'asx28iyo4BtqvAK1JG8kMr569zdDbKfse31M5HFx24lOOzBBB0inwk3KLbBCxKFEl'
- name: CHAT2QUERY_TRIES
value: '1'
- name: CHAT2QUERY_URL
value: 'https://data.tidbcloud.com/api/v1beta/apps/chat2query-uoeZOQoN/v1/chat2data'
- name: DATABRICKS_TOKEN
value: 'dapi3104aa04027fefcdbd531acf6cc01839'
- name: DATABRICKS_URL
value: 'https://adb-5718025460118174.14.azuredatabricks.net'
- name: DPB_APP_URI
value: 'http://10.128.1.4:8000'
- name: DPB_RETRY_DELAY
value: '10'
- name: DPB_RETRY_TRIES
value: '300'
- name: DPB_TIMEOUT
value: '30'
- name: DPB_USER
value: 'ind-user'
- name: DPB_USER_DATABASE
value: 'INDUSTRIAL_DNA_DB'
- name: DPB_USER_SCHEMA
value: 'INDUSTRIAL_DNA_SCHEMA'
- name: DPL_HOST
value: 'startpack.dev.ind.prd.is.a.i4square.info'
- name: ENABLE_CREATING_METRICS
value: 'true'
- name: ENABLE_DPL_API
value: 'true'
- name: ENV
value: 'dev'
- name: FAVORITES_MAX_ITEMS
value: '10'
- name: FLASK_APP
value: 'catalog-web-app'
- name: GIT_BRANCH
value: 'master'
- name: GIT_BRANCH_CATALOG_AP
value: 'master'
- name: GIT_BRANCH_SCM_METRICS
value: 'master'
- name: GIT_EMAIL
value: 'ind-dpb@example.com'
- name: GIT_ORGANIZATION
value: 'qmonus-test'
- name: GIT_REPOSITORY
value: 'scm-metrics'
- name: GIT_TOKEN
value: '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'
- name: GIT_USER
value: 'FumiyaOkoshi'
- name: JENKINS_HOST
value: '20.48.61.168:4000'
- name: JENKINS_PWD
value: 'V3f8QuT9'
- name: JENKINS_RETRY_DELAY
value: '10'
- name: JENKINS_RETRY_TRIES
value: '300'
- name: JENKINS_USER
value: 'AdminUser'
- name: PROJECT
value: 'startpack'
- name: SNOW_ACCOUNT
value: 'ld50475.ap-northeast-1.aws'
- name: SNOW_DB
value: 'INDUSTRIAL_DNA_DB'
- name: SNOW_PWD
value: 'Fjt807Gnjkly'
- name: SNOW_ROLE
value: 'SYSADMIN'
- name: SNOW_SCHEMA
value: 'INDUSTRIAL_DNA_SCHEMA'
- name: SNOW_USER
value: 'ACADMI4IND'
- name: SNOW_WH
value: 'TEST_WH'
- name: TENANT
value: 'ind'
- name: TO_PATH
value: 'tmp/i4-dpb'
hostAliases:
- hostnames:
- startpack.dev.ind.prd.is.a.i4square.info # DNA DPL
ip: 4.216.131.132
---
apiVersion: v1
kind: Service
metadata:
name: catalog-web-app
spec:
# loadBalancerIP: 20.48.118.80
type: LoadBalancer
ports:
- name: backend-port
port: 80
targetPort: 5000
protocol: TCP
selector:
app: catalog-web-app
loadBalancerSourceRanges:
# daas and git and webhook ip
- 20.48.61.168/32
# aks ip
# - 40.81.216.40/32
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: catalog-web-app
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
nginx.ingress.kubernetes.io/whitelist-source-range: '20.48.61.168/32'
spec:
tls:
- hosts:
- catalog.mock.i-dna.i4square.net
secretName: kvc-ind-ssl-cert
ingressClassName: nginx
rules:
- host: catalog.mock.i-dna.i4square.net
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: catalog-web-app
port:
number: 80
// ConfigContext.tsx
import React, { createContext, useState, useEffect, useContext } from 'react';
import { Config } from './types';
interface ConfigContextType {
config: Config | null;
}
const ConfigContext = createContext<ConfigContextType | null>(null);
export const ConfigProvider: React.FC = ({ children }) => {
const [config, setConfig] = useState<Config | null>(null);
useEffect(() => {
fetch('/api/config')
.then(response => response.json())
.then((data: Config) => {
setConfig(data);
})
.catch(error => {
console.error('Error fetching config:', error);
});
}, []);
return (
<ConfigContext.Provider value={{ config }}>
{children}
</ConfigContext.Provider>
);
};
export const useConfig = () => {
const context = useContext(ConfigContext);
if (!context) {
throw new Error('useConfig must be used within a ConfigProvider');
}
return context.config;
};
// ConfigContext.tsx
import React, { createContext, useState, useEffect, useContext } from 'react';
// 定义配置类型接口
export interface Config {
aadClientId: string;
}
// 定义上下文类型接口
interface ConfigContextType {
config: Config | null;
}
// 创建上下文
const ConfigContext = createContext<ConfigContextType | null>(null);
// 提供者组件
export const ConfigProvider: React.FC = ({ children }) => {
const [config, setConfig] = useState<Config | null>(null);
useEffect(() => {
fetch('/api/config')
.then(response => response.json())
.then((data: Config) => {
setConfig(data);
})
.catch(error => {
console.error('Error fetching config:', error);
});
}, []);
return (
<ConfigContext.Provider value={{ config }}>
{children}
</ConfigContext.Provider>
);
};
// 自定义钩子
export const useConfig = () => {
const context = useContext(ConfigContext);
if (!context) {
throw new Error('useConfig must be used within a ConfigProvider');
}
return context.config;
};
プロパティ 'children' は型 '{}' に存在しません。
const systemDemo = process.env.REACT_APP_DEMO_PROCESS;
const title = `${systemDemo} メトリクス一覧`
return (
<>
<aside className="main-sidebar sidebar-light-primary elevation-2">
{/* Brand Logo */}
<Link to="/" className="brand-link">
<img
src="/logo_fix.02.png"
alt="iQuattro Logo"
className="brand-image"
/>
<img
src="/logo_fix.02_logo.png"
alt="iQuattro Logo"
className="brand-text"
/>
</Link>
{/* Sidebar */}
<div className="sidebar">
{/* Sidebar Menu */}
<nav>
<ul className="nav nav-pills nav-sidebar flex-column">
<li className="nav-header">
<Link to="/new-metrics-list" className="nav-link">
{title}
</Link>
'use strict';
const fs = require('fs');
const path = require('path');
const paths = require('./paths');
// Make sure that including paths.js after env.js will read .env variables.
delete require.cache[require.resolve('./paths')];
const NODE_ENV = process.env.NODE_ENV;
if (!NODE_ENV) {
throw new Error(
'The NODE_ENV environment variable is required but was not specified.'
);
}
// https://github.com/bkeepers/dotenv#what-other-env-files-can-i-use
const dotenvFiles = [
`${paths.dotenv}.${NODE_ENV}.local`,
// Don't include `.env.local` for `test` environment
// since normally you expect tests to produce the same
// results for everyone
NODE_ENV !== 'test' && `${paths.dotenv}.local`,
`${paths.dotenv}.${NODE_ENV}`,
paths.dotenv,
].filter(Boolean);
// Load environment variables from .env* files. Suppress warnings using silent
// if this file is missing. dotenv will never modify any environment variables
// that have already been set. Variable expansion is supported in .env files.
// https://github.com/motdotla/dotenv
// https://github.com/motdotla/dotenv-expand
dotenvFiles.forEach(dotenvFile => {
if (fs.existsSync(dotenvFile)) {
require('dotenv-expand')(
require('dotenv').config({
path: dotenvFile,
})
);
}
});
// We support resolving modules according to `NODE_PATH`.
// This lets you use absolute paths in imports inside large monorepos:
// https://github.com/facebook/create-react-app/issues/253.
// It works similar to `NODE_PATH` in Node itself:
// https://nodejs.org/api/modules.html#modules_loading_from_the_global_folders
// Note that unlike in Node, only *relative* paths from `NODE_PATH` are honored.
// Otherwise, we risk importing Node.js core modules into an app instead of webpack shims.
// https://github.com/facebook/create-react-app/issues/1023#issuecomment-265344421
// We also resolve them to make sure all tools using them work consistently.
const appDirectory = fs.realpathSync(process.cwd());
process.env.NODE_PATH = (process.env.NODE_PATH || '')
.split(path.delimiter)
.filter(folder => folder && !path.isAbsolute(folder))
.map(folder => path.resolve(appDirectory, folder))
.join(path.delimiter);
// Grab NODE_ENV and REACT_APP_* environment variables and prepare them to be
// injected into the application via DefinePlugin in webpack configuration.
const REACT_APP = /^REACT_APP_/i;
function getClientEnvironment(publicUrl) {
const raw = Object.keys(process.env)
.filter(key => REACT_APP.test(key))
.reduce(
(env, key) => {
env[key] = process.env[key];
return env;
},
{
// Useful for determining whether we’re running in production mode.
// Most importantly, it switches React into the correct mode.
NODE_ENV: process.env.NODE_ENV || 'development',
// Useful for resolving the correct path to static assets in `public`.
// For example, <img src={process.env.PUBLIC_URL + '/img/logo.png'} />.
// This should only be used as an escape hatch. Normally you would put
// images into the `src` and `import` them in code to get their paths.
PUBLIC_URL: publicUrl,
// We support configuring the sockjs pathname during development.
// These settings let a developer run multiple simultaneous projects.
// They are used as the connection `hostname`, `pathname` and `port`
// in webpackHotDevClient. They are used as the `sockHost`, `sockPath`
// and `sockPort` options in webpack-dev-server.
WDS_SOCKET_HOST: process.env.WDS_SOCKET_HOST,
WDS_SOCKET_PATH: process.env.WDS_SOCKET_PATH,
WDS_SOCKET_PORT: process.env.WDS_SOCKET_PORT,
// Whether or not react-refresh is enabled.
// It is defined here so it is available in the webpackHotDevClient.
FAST_REFRESH: process.env.FAST_REFRESH !== 'false',
}
);
// Stringify all values so we can feed into webpack DefinePlugin
const stringified = {
'process.env': Object.keys(raw).reduce((env, key) => {
env[key] = JSON.stringify(raw[key]);
return env;
}, {}),
};
return { raw, stringified };
}
module.exports = getClientEnvironment;
API_META_INFO
28 Rows
API_PHYSICAL_NAME
VARCHAR(16777216)
API_LOGICAL_NAME
VARCHAR(16777216)
PROVIDE
BOOLEAN
API_LINKAGE_ITEMS
VARIANT
TAGS
VARIANT
PRESET
データセット 良品ショット数 稼働時間 段替え時間 段替え回数(日次) 段替え回数(月次) 良品数(手直し含む) MT
(マシンタイム) MCT
(マシンサイクルタイム) CT
(サイクルタイム) 良品加工時間 稼働日数 空回し回数 ATT
:Average Time To Troubleshoot 設備停止時間 可動時間 廃品数 生産数 手直し不良数 工程内不良数 実績生産時間 基準時間 正味人工 配置人数 OPTリリーフ 正味作業工数 時間当たりの生産数 作業者数 良品数(手直し含まず) 出庫数 平均在庫数 故障回数 設備有効性 工程効率 良品率 実働時間 実時間 計画していた作業者数 排出係数 活動量
INSERT INTO API_META_INFO (API_PHYSICAL_NAME, PROVIDE, TAGS, PRESET) VALUES ('良品ショット数', FALSE, PARSE_JSON('[]'), TRUE);
Invalid expression [PARSE_JSON('[]')] in VALUES clause
Error: invalid identifier 'API_LOGICAL_NAME' (line 4)
INSERT INTO IND_ECMDEMO_DEV_DB.IND_ECMDEMO_DEV_SCHEMA.API_META_INFO(API_LINKAGE_ITEMS) select PARSE_JSON('[]') where API_LOGICAL_NAME = '良品ショット数'
API_PHYSICAL_NAME API_LOGICAL_NAME PROVIDE API_LINKAGE_ITEMS TAGS PRESET quality_shot_number 良品ショット数 FALSE [] [] TRUE
INSERT INTO API_META_INFO (API_PHYSICAL_NAME, API_LOGICAL_NAME, PROVIDE, API_LINKAGE_ITEMS, TAGS, PRESET) VALUES ('operation_time', '稼働時間', FALSE, PARSE_JSON('[]'), PARSE_JSON('[]'), TRUE);
良品ショット数: quality_shot_count
稼働時間: operation_time
段替え時間: changeover_time
段替え回数(日次): daily_changeover_count
段替え回数(月次): monthly_changeover_count
良品数(手直し含む): quality_product_including_repairs
MT(マシンタイム): machine_time
MCT(マシンサイクルタイム): machine_cycle_time
CT(サイクルタイム): cycle_time
良品加工時間: quality_processing_time
稼働日数: operating_days
空回し回数: idle_run_count
ATT:Average Time To Troubleshoot: average_time_to_troubleshoot
設備停止時間: equipment_downtime
可動時間: operating_time
廃品数: waste_count
生産数: production_number
手直し不良数: rework_defect_count
工程内不良数: in_process_defect_count
実績生産時間: actual_production_time
基準時間: standard_time
正味人工: net_labor
配置人数: staff_count
OPTリリーフ: opt_relief
正味作業工数: net_working_hours
時間当たりの生産数: production_per_hour
作業者数: worker_count
良品数(手直し含まず): quality_product_excluding_repairs
出庫数: dispatch_count
平均在庫数: average_inventory
故障回数: breakdown_count
設備有効性: equipment_effectiveness
工程効率: process_efficiency
良品率: yield_rate
実働時間: actual_working_time
実時間: actual_time
計画していた作業者数: planned_worker_count
排出係数: emission_factor
活動量: activity_volume
import { useEffect, useRef } from 'react';
import { HashRouter, Route, useLocation } from 'react-router-dom';
import { commonAjax } from '../components/commonAjax';
import AppHeader from './components/block/appHeader';
import AppMenu from './components/block/appMenu';
import routes from './routes/routes';
function App() {
const location = useLocation();
const lastAccessHash = useRef<String>('');
useEffect(() => {
if (location.hash !== lastAccessHash.current) {
recordAccess(location as unknown as Location);
lastAccessHash.current = location.hash;
}
}, [location.hash]);
return <MainComponent />;
}
function recordAccess(location: Location) {
// メトリクス詳細画面
const matches = /#\/metrics-details\/(.+)/.exec(location.hash);
if (matches) {
const id = decodeURIComponent(matches[1]);
commonAjax.axios().put('/api/recordAccess', {
url: `/metrics-details/${id}`,
disp: id,
});
}
}
function MainComponent() {
return (
<div className="wrapper">
<AppHeader />
<HashRouter>
<AppMenu />
<div className="wrapper">
{routes.map((route, idx) => (
<Route
path={route.path}
exact={route.exact}
component={route.component}
key={idx}
/>
))}
</div>
</HashRouter>
<div
id="loading-spinner"
style={{
display: 'none',
position: 'fixed',
top: '0',
left: '0',
width: '100vw',
height: '100vh',
zIndex: 2000,
backgroundColor: '#00000080',
justifyItems: 'center',
alignItems: 'center',
}}
>
<div className="loading-spinner"></div>
</div>
</div>
);
}
export default App;
``
useEffect(() => {
// ルートに応じてタイトルを設定するための基本的なロジック
const baseTitle = process.env.REACT_APP_SITE_TITLE || 'Default App Title';
document.title = baseTitle + ' - ' + location.pathname.replace(/^\/|\/$/g, '');
}, [location]);
if (!settings) {
settings = {
panels: [
{ id: PANEL_ID_UPDATE, order: 1 },
{ id: PANEL_ID_METRICS, order: 2 },
{ id: PANEL_ID_FAV, order: 3 },
],
metricsPanel: {
metricsId: '基準在庫週数',
},
};
saveSettings(settings);
}
// systemdemoの値によってmetricsIdを変更する
let metricsId;
if (systemdemo === 'ecm') {
metricsId = '基准在库';
} else if (systemdemo === 'scm') {
metricsId = '基準在庫週数';
} else {
metricsId = 'demo';
}
// settingsが未定義の場合、初期設定を行う
if (!settings) {
settings = {
panels: [
{ id: PANEL_ID_UPDATE, order: 1 },
{ id: PANEL_ID_METRICS, order: 2 },
{ id: PANEL_ID_FAV, order: 3 },
],
metricsPanel: {
metricsId: metricsId, // 条件に基づいたmetricsIdを使用
},
};
saveSettings(settings);
}
Traceback (most recent call last):
File "/app/tools/metricsUtils.py", line 397, in cloneMetricsRepo
git.Repo.clone_from(url, path, multi_options=["-b", branch])
File "/usr/local/lib/python3.9/site-packages/git/repo/base.py", line 1275, in clone_from
return cls._clone(git, url, to_path, GitCmdObjectDB, progress, multi_options, **kwargs)
File "/usr/local/lib/python3.9/site-packages/git/repo/base.py", line 1169, in _clone
multi = shlex.split(" ".join(multi_options))
TypeError: sequence item 1: expected str instance, NoneType found
Traceback (most recent call last):
File "/app/tools/metricsUtils.py", line 397, in cloneMetricsRepo
git.Repo.clone_from(url, path, multi_options=["-b", branch])
File "/usr/local/lib/python3.9/site-packages/git/repo/base.py", line 1275, in clone_from
return cls._clone(git, url, to_path, GitCmdObjectDB, progress, multi_options, **kwargs)
File "/usr/local/lib/python3.9/site-packages/git/repo/base.py", line 1169, in _clone
multi = shlex.split(" ".join(multi_options))
TypeError: sequence item 1: expected str instance, NoneType found
{metrics.metrics.map((val) => {
return (
<a
href={`#/metrics-details/${val.name}`}
className="link"
>
<p>
<i
className={`fa fa-circle ${
val.provided
? 'metrics-provide-icon'
: 'metrics-no-provide-icon'
}`}
aria-hidden="true"
/>
{val.name}
</p>
</a>
);
})}
</>
}
/* API情報取得のリクエスト */
useEffect(() => {
(async () => {
await commonAjax
.axios({})
.get('/api/metrics')
.then((res) => {
let data: Metrics[] = res.data.metrics;
// ダミーデータ挿入
// data = data.concat(dummyData);
// サイクルを切り取るロジック
// data = data.map((d) => {
// d.name = d.name.split(':')[0];
// return d;
// });
data = Array.from(new Map(data.map((d) => [d.name, d])).values());
const group = data.reduce((result: MetricsList[], current) => {
const element = result.find((p) => {
return p.subCategory === current.subCategory;
});
if (element) {
element.metrics.push(current);
} else {
result.push({
subCategory: current.subCategory,
metrics: [current],
});
}
return result;
}, []);
setMetricsList(group);
setEnableCreatingMetrics(res.data.enableCreatingMetrics);
});
})();
}, []);
useEffect(() => {
(async () => {
await commonAjax
.axios({})
.get('/api/metrics')
.then((res) => {
let data: Metrics[] = res.data.metrics;
// ダミーデータ挿入
// data = data.concat(dummyData);
// サイクルを切り取るロジック
// data = data.map((d) => {
// d.name = d.name.split(':')[0];
// return d;
// });
data = Array.from(new Map(data.map((d) => [d.name, d])).values());
data.sort((a,b) => a.name.localeCompare(b.name,'ja'))
const group = data.reduce((result: MetricsList[], current) => {
const element = result.find((p) => {
return p.subCategory === current.subCategory;
});
if (element) {
element.metrics.push(current);
} else {
result.push({
subCategory: current.subCategory,
metrics: [current],
});
}
return result;
}, []);
setMetricsList(group);
setEnableCreatingMetrics(res.data.enableCreatingMetrics);
});
})();
}, []);
const metricsProcess = process.env.REACT_APP_METRICS_PROCESS;
const title = `${metricsProcess} メトリクス一覧`
return (
<div className="content-wrapper metrics-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>
</section>
{/* Metrics List (Grid) */}
{Spacer({ size: 50 })}
{view(props as any)}
</div>
);
}
// API List
function GridMetricsList(props: MetricsListProps) {
return (
<>
{props.metricsList.map((metrics: MetricsList) =>
MetricsCard(props, metrics)
)}
</>
);
}
// Metrics Card
function MetricsCard(props: MetricsListProps, metrics: MetricsList) {
return (
<>
<div>
<div className="card card-tertiary card-collapse-metrics">
<div className="card-header">
<div
className="name-font"
style={{
fontSize: `clamp(0.65rem, ${
30 / metrics.subCategory.length
}vw, 1rem)`,
}}
>
{metrics.subCategory}
</div>
</div>
<div className="card-body" id={metrics.subCategory}>
{
<>
{metrics.metrics.map((val) => {
return (
<a
href={`#/metrics-details/${val.name}`}
className="link"
>
<p>
<i
className={`fa fa-circle ${
val.provided
? 'metrics-provide-icon'
: 'metrics-no-provide-icon'
}`}
aria-hidden="true"
/>
{val.name}
</p>
</a>
);
})}
</>
}
{props.enableCreatingMetrics && (
<>
{Spacer({ size: 20 })}
<div className="centering-btn">
<button
className="btn btn-secondary"
data-toggle="modal"
data-target="#metrics-dtlModal"
value={metrics.subCategory}
>
メトリクスを追加する +
</button>
</div>
</>
)}
</div>
</div>
<NewMetricsDtlModal/>
</div>
</>
);
}
const view = (props: MetricsListProps) => {
if (props.metricsList.length) {
return (
<>
<section className="content">
<div className="list-wrapper">
{GridMetricsList(props)}
</div>
</section>
</>
);
}
};
当然可以。这里是如何用日语描述您的原始代码状态和修改后的状态:
現在の状態の説明:
現在のコードでは、APIから取得したデータは特定の順序でソートされていません。データはAPIからどのように返されるかに基づいています。このため、表示されるデータの順序はAPIの応答の内容とその順序に依存しており、明確なソート処理は行われていないため、ユーザーインターフェースに表示されるデータの順序は一貫性がない可能性があります。
改良後の状態の説明:
提案する改良では、データをAPIから取得した後、重複を削除し、各サブカテゴリーに基づいてデータをグループ化します。その後、各グループ内のmetricsの数に基づいてグループをソートします。このソートは、metricsの数が多いグループがリストの上部に来るように行われます。これにより、より多くのデータが含まれているカテゴリーをユーザーがすぐに認識できるようになり、ユーザーインターフェースの使いやすさが向上します。
この説明をもとに、上司への報告やプレゼンテーションを行うと、現在のコードの動作と提案された改良点が明確に伝わります。
User
并且说明,为了泛用性,不从api取得里进行修改
ChatGPT
追加説明:API側での修正を行わない理由
APIからデータを取得する際に、そのソート順序を変更しない選択をした理由は、システムの汎用性を高めるためです。API側でデータの順序を固定すると、特定のビューや要件に対して最適化されますが、それがすべての使用シナリオや将来の要件に適合するとは限りません。
フロントエンドでデータを操作してソートするアプローチを採用することで、同一のAPI応答を異なるビューやコンテキストに柔軟に対応させることが可能になります。例えば、あるビューではmetricsの数が多い順に表示する必要があり、別のビューではアルファベット順や作成日時順での表示が求められるかもしれません。
このように、フロントエンドでのデータ処理を選ぶことは、アプリケーションの再利用性と拡張性を保ちながら、異なるユーザー要件に柔軟に対応できる設計を可能にします。APIは可能な限りシンプルに保ち、データの提供に集中することで、システム全体の複雑さを避け、メンテナンスの容易さやパフォーマンスの最適化を図ることができます。
現在のコードでは、APIから取得したカテゴリは特定の順序でソートされていません。データはAPIからどのように返されるかに基づいています。
API側でデータの順序を固定すると、今回の要件に対してだけ最適化されます。
カテゴリの位置を変えるため、各カテゴリ内のメトリクスの数に基づいてソートするように改修しました。メトリクス数が多い順カテゴリが上部に来るように行われます。メトリクス一覧画面の汎用性を高めるため、上記の対応でいかがでしょうか。