1. はじめに
みなさん、SSOしていますか?
AWS Identity Center を使って複数アカウント/ロールを切り替える運用で、誤ったアカウントで操作するミスを減らすために、コンソール上部ナビバーをアカウント/ロールごとに色分けする Chrome/Edge(Chromium系)拡張機能 「AWS access portal Colorizer」 を作成しました🖥️
コードをそのままコピーして chrome://extensions/ からパッケージ化されていない拡張機能として読み込むだけで動きます▶️
コードを全公開しておりますので、良ければ参考にしてください🌞
2. 機能説明
アカウントIDとロールの組み合わせで、AWS Consoleのナビバーに好きなカラーを付けることができます。
カラー設定前のConsole画面
カラー設定後のConsole画面
- 機能の詳細説明
- アカウントID+ロール名+カラーのマッピングを保存
- ワイルドカード保存: ロール名を
*にするとアカウント全体に色を適用 - 保存済みマッピングの一覧表示・編集・削除・有効/無効トグル(ON/OFF)
- 開いたConsoleページで自動的にバーのカラーを適用
- 保存済みカラー設定ファイルをエクスポート・インポート可能
3. 動作環境
- Chromium 系ブラウザ(Chrome / Edge / Brave 等)
- 対象ホスト
*.console.aws.amazon.com*.awsapps.com*.signin.aws.amazon.com
各ホストの具体的な役割は以下です。
-
*.console.aws.amazon.com:AWS マネジメントコンソール本体のホスト -
*.awsapps.com:IAM ユーザー用のサインイン専用ホスト -
*.signin.aws.amazon.com:AWS IAM Identity Center アクセスポータル用ホスト
4. コード一式
GitHubにコードを置いていますのでダウンロードください。
または、以下からコピーも可能です。
manifest.json
{
"manifest_version": 3,
"name": "AWS access portal Colorizer",
"version": "1.0.0",
"description": "Identity Center (SSO) 環境で AWS access portal の上部バーの色をアカウント/ロールごとに変更します。",
"permissions": ["storage", "activeTab"],
"host_permissions": [
"*://*.console.aws.amazon.com/*",
"*://*.awsapps.com/*",
"*://*.signin.aws.amazon.com/*"
],
"action": {
"default_popup": "popup.html",
"default_title": "AWS access portal Colorizer"
},
"content_scripts": [
{
"matches": [
"*://*.console.aws.amazon.com/*",
"*://*.awsapps.com/*",
"*://*.signin.aws.amazon.com/*"
],
"js": ["content.js"],
"run_at": "document_idle"
}
],
"background": {
"service_worker": "background.js"
}
}
content.js
(function () {
'use strict';
const STORAGE_PREFIX = 'awscc_';
const DISABLED_PREFIX = 'awsccd_';
const STYLE_ID = 'awscc-injected-style';
const DATA_ATTR = 'data-awscc-colored';
const KNOWN_SELECTORS = [
'#awsc-nav-bar',
'#nav-bar',
'[data-testid="awsc-nav-bar"]',
'[data-analytics-id="nav-bar"]',
];
function findNavBar() {
for (const sel of KNOWN_SELECTORS) {
const el = document.querySelector(sel);
if (el) return el;
}
const probeY = 24;
const probeXList = [
Math.round(window.innerWidth * 0.1),
Math.round(window.innerWidth * 0.5),
Math.round(window.innerWidth * 0.9),
];
for (const px of probeXList) {
let el = document.elementFromPoint(px, probeY);
while (el && el !== document.documentElement) {
const rect = el.getBoundingClientRect();
if (
rect.width >= window.innerWidth * 0.7 &&
rect.top <= 10 &&
rect.height > 20 &&
rect.height < 130
) {
return el;
}
el = el.parentElement;
}
}
return null;
}
let _currentColor = null;
function applyColor(color) {
_currentColor = color || null;
let styleEl = document.getElementById(STYLE_ID);
if (!color) {
if (styleEl) styleEl.remove();
document.querySelectorAll('[' + DATA_ATTR + ']').forEach(function (el) {
el.removeAttribute(DATA_ATTR);
el.style.removeProperty('background-color');
});
return;
}
if (!styleEl) {
styleEl = document.createElement('style');
styleEl.id = STYLE_ID;
document.head.appendChild(styleEl);
}
styleEl.textContent = '[' + DATA_ATTR + '] { background-color: ' + color + ' !important; }';
const nav = findNavBar();
if (nav) {
nav.setAttribute(DATA_ATTR, 'true');
nav.style.setProperty('background-color', color, 'important');
if (nav.firstElementChild) {
nav.firstElementChild.style.setProperty('background-color', color, 'important');
}
}
}
var _reapplyObserver = null;
function startReapplyObserver() {
if (_reapplyObserver) return;
_reapplyObserver = new MutationObserver(function () {
if (!_currentColor) return;
var nav = findNavBar();
if (nav && !nav.hasAttribute(DATA_ATTR)) {
applyColor(_currentColor);
}
});
_reapplyObserver.observe(document.documentElement, {
attributes: true,
childList: true,
subtree: true,
attributeFilter: [DATA_ATTR, 'style', 'class'],
});
}
function extractAccountInfo() {
var html = document.documentElement.innerHTML;
var text = document.body ? document.body.innerText : '';
var arnPattern =
/arn:aws:(?:sts|iam)::(\d{12}):(?:assumed-role|role)\/(AWSReservedSSO_[A-Za-z0-9_]+(?:\/[A-Za-z0-9._@-]+)?)/;
var arnMatch = html.match(arnPattern) || text.match(arnPattern);
if (arnMatch) {
return { accountId: arnMatch[1], roleName: arnMatch[2] };
}
var rolePattern = /AWSReservedSSO_[A-Za-z0-9_]+(?:\/[A-Za-z0-9._@-]+)?/;
var roleMatch = text.match(rolePattern) || html.match(rolePattern);
var accountPattern = /\b(\d{12})\b/;
var accountMatch = text.match(accountPattern) || html.match(accountPattern);
return {
accountId: accountMatch ? accountMatch[1] : null,
roleName: roleMatch ? roleMatch[0] : null,
};
}
function makeStorageKey(accountId, roleName) {
return STORAGE_PREFIX + accountId + '_' + roleName;
}
function loadAndApplyColor() {
var info = extractAccountInfo();
if (!info.accountId) return;
var exactColorKey = info.roleName ? makeStorageKey(info.accountId, info.roleName) : null;
var wildcardColorKey = makeStorageKey(info.accountId, '*');
var exactDisabledKey = exactColorKey ? DISABLED_PREFIX + info.accountId + '_' + info.roleName : null;
var wildcardDisabledKey = DISABLED_PREFIX + info.accountId + '_*';
var keysToFetch = [wildcardColorKey, wildcardDisabledKey];
if (exactColorKey) keysToFetch.push(exactColorKey, exactDisabledKey);
chrome.storage.sync.get(keysToFetch, function (result) {
var colorToApply = null;
var disabledKeyToCheck = null;
if (exactColorKey && result[exactColorKey]) {
colorToApply = result[exactColorKey];
disabledKeyToCheck = exactDisabledKey;
} else if (result[wildcardColorKey]) {
colorToApply = result[wildcardColorKey];
disabledKeyToCheck = wildcardDisabledKey;
}
if (!colorToApply) return;
if (disabledKeyToCheck && result[disabledKeyToCheck]) {
applyColor('');
} else {
applyColor(colorToApply);
startReapplyObserver();
}
});
}
chrome.runtime.onMessage.addListener(function (message, _sender, sendResponse) {
if (message.action === 'getAccountInfo') {
sendResponse(extractAccountInfo());
} else if (message.action === 'applyColor') {
applyColor(message.color);
if (message.color) startReapplyObserver();
sendResponse({ success: true });
}
});
function init() {
function tryApply() {
if (findNavBar()) {
loadAndApplyColor();
return true;
}
return false;
}
if (!tryApply()) {
var waitObserver = new MutationObserver(function () {
if (tryApply()) {
waitObserver.disconnect();
}
});
waitObserver.observe(document.documentElement, { childList: true, subtree: true });
setTimeout(function () { waitObserver.disconnect(); }, 15000);
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();
background.js
chrome.runtime.onInstalled.addListener(() => {
console.log('AWS access portal Colorizer がインストールされました。');
});
popup.js
'use strict';
const STORAGE_PREFIX = 'awscc_';
const DISABLED_PREFIX = 'awsccd_';
const PRESET_COLORS = [
{ color: '#37475a', label: 'AWS Dark' },
{ color: '#2e3a6e', label: 'Navy' },
{ color: '#1a5ca8', label: 'Blue' },
{ color: '#2d6a4f', label: 'Green' },
{ color: '#a63a20', label: 'Red' },
{ color: '#6a2fbd', label: 'Purple' },
{ color: '#b85c00', label: 'Orange' },
{ color: '#8e2e7a', label: 'Magenta' },
{ color: '#455a64', label: 'Gray' },
];
let detectedAccountId = null;
let detectedRoleName = null;
let currentColor = '#232f3e';
function makeStorageKey(accountId, roleName) {
return STORAGE_PREFIX + accountId + '_' + roleName;
}
function parseStorageKey(key) {
if (!key.startsWith(STORAGE_PREFIX)) return null;
const body = key.slice(STORAGE_PREFIX.length);
const sepIdx = body.indexOf('_');
if (sepIdx === -1) return null;
return {
accountId: body.slice(0, sepIdx),
roleName: body.slice(sepIdx + 1),
};
}
function showStatus(message, isError = false) {
const el = document.getElementById('status-message');
el.textContent = message;
el.className = isError ? 'error' : '';
setTimeout(() => {
el.textContent = '';
el.className = '';
}, 3000);
}
function isValidHex(str) {
return /^#[0-9a-fA-F]{6}$/.test(str);
}
function setColorUI(color) {
currentColor = color;
document.getElementById('color-picker').value = color;
document.getElementById('color-hex').value = color;
document.getElementById('color-hex').classList.remove('invalid');
document.getElementById('nav-preview').style.backgroundColor = color;
document.querySelectorAll('.preset-color').forEach((el) => {
el.classList.toggle('selected', el.dataset.color === color);
});
}
function buildPresets() {
const container = document.getElementById('preset-colors');
PRESET_COLORS.forEach(({ color, label }) => {
const btn = document.createElement('button');
btn.className = 'preset-color';
btn.dataset.color = color;
btn.title = label + ' ' + color;
btn.style.backgroundColor = color;
btn.addEventListener('click', () => setColorUI(color));
container.appendChild(btn);
});
}
function updateAccountInfoUI(accountId, roleName) {
const accountEl = document.getElementById('detected-account');
const roleEl = document.getElementById('detected-role');
const saveBtn = document.getElementById('btn-save');
if (accountId) {
accountEl.textContent = accountId;
accountEl.classList.remove('not-detected');
} else {
accountEl.textContent = '未検出';
accountEl.classList.add('not-detected');
}
if (roleName) {
roleEl.textContent = roleName;
roleEl.classList.remove('not-detected');
} else {
roleEl.textContent = '未検出';
roleEl.classList.add('not-detected');
}
saveBtn.disabled = !(accountId && roleName);
}
function renderMappings(items, disabledSet = new Set()) {
const list = document.getElementById('mapping-list');
list.innerHTML = '';
if (items.length === 0) {
const empty = document.createElement('div');
empty.className = 'empty-message';
empty.textContent = '保存されたカラーはありません';
list.appendChild(empty);
return;
}
items.forEach(({ key, accountId, roleName, color }) => {
const disabled = disabledSet.has(key.slice(STORAGE_PREFIX.length));
const item = document.createElement('div');
item.className = disabled ? 'mapping-item mapping-item--disabled' : 'mapping-item';
const swatch = document.createElement('div');
swatch.className = 'mapping-swatch';
swatch.style.backgroundColor = color;
const text = document.createElement('div');
text.className = 'mapping-text';
const accountSpan = document.createElement('div');
accountSpan.className = 'mapping-account';
accountSpan.textContent = accountId;
const roleSpan = document.createElement('div');
roleSpan.className = 'mapping-role';
roleSpan.title = roleName;
roleSpan.textContent = roleName;
text.appendChild(accountSpan);
text.appendChild(roleSpan);
const editBtn = document.createElement('button');
editBtn.className = 'mapping-edit';
editBtn.textContent = '✎';
editBtn.title = '編集';
editBtn.addEventListener('click', () => {
item.innerHTML = '';
item.classList.add('mapping-item--editing');
const form = document.createElement('div');
form.className = 'mapping-edit-form';
const accountInput = document.createElement('input');
accountInput.type = 'text';
accountInput.className = 'mapping-edit-input';
accountInput.value = accountId;
accountInput.placeholder = 'アカウント ID';
const roleInput = document.createElement('input');
roleInput.type = 'text';
roleInput.className = 'mapping-edit-input';
roleInput.value = roleName;
roleInput.placeholder = 'ロール名';
const colorRow = document.createElement('div');
colorRow.className = 'mapping-edit-color-row';
const colorInput = document.createElement('input');
colorInput.type = 'color';
colorInput.className = 'mapping-edit-color';
colorInput.value = color;
const hexInput = document.createElement('input');
hexInput.type = 'text';
hexInput.className = 'mapping-edit-input';
hexInput.value = color;
hexInput.placeholder = '#rrggbb';
hexInput.maxLength = 7;
colorInput.addEventListener('input', () => { hexInput.value = colorInput.value; });
hexInput.addEventListener('input', () => {
if (isValidHex(hexInput.value)) colorInput.value = hexInput.value;
});
colorRow.appendChild(colorInput);
colorRow.appendChild(hexInput);
const actions = document.createElement('div');
actions.className = 'mapping-edit-actions';
const saveBtn = document.createElement('button');
saveBtn.className = 'mapping-edit-save';
saveBtn.textContent = '保存';
saveBtn.addEventListener('click', () => {
const newAccountId = accountInput.value.trim();
const newRoleName = roleInput.value.trim();
const newColor = isValidHex(hexInput.value) ? hexInput.value : colorInput.value;
if (!newAccountId || !newRoleName) {
showStatus('アカウント ID とロール名は必須です', true);
return;
}
const newKey = makeStorageKey(newAccountId, newRoleName);
const oldDisabledKey = DISABLED_PREFIX + key.slice(STORAGE_PREFIX.length);
chrome.storage.sync.remove([key, oldDisabledKey], () => {
chrome.storage.sync.set({ [newKey]: newColor }, () => {
loadMappings();
showStatus('更新しました');
});
});
});
const cancelBtn = document.createElement('button');
cancelBtn.className = 'mapping-edit-cancel';
cancelBtn.textContent = 'キャンセル';
cancelBtn.addEventListener('click', () => { loadMappings(); });
actions.appendChild(saveBtn);
actions.appendChild(cancelBtn);
form.appendChild(accountInput);
form.appendChild(roleInput);
form.appendChild(colorRow);
form.appendChild(actions);
item.appendChild(form);
});
const deleteBtn = document.createElement('button');
deleteBtn.className = 'mapping-delete';
deleteBtn.textContent = '×';
deleteBtn.title = '削除';
deleteBtn.addEventListener('click', () => {
const disabledKey = DISABLED_PREFIX + key.slice(STORAGE_PREFIX.length);
chrome.storage.sync.remove([key, disabledKey], () => {
loadMappings();
showStatus('マッピングを削除しました');
});
});
const toggleBtn = document.createElement('button');
toggleBtn.className = 'mapping-toggle ' + (disabled ? 'off' : 'on');
toggleBtn.textContent = disabled ? 'OFF' : 'ON';
toggleBtn.title = disabled ? 'クリックして有効にする' : 'クリックして無効にする';
toggleBtn.addEventListener('click', () => {
const body = key.slice(STORAGE_PREFIX.length);
const disabledKey = DISABLED_PREFIX + body;
if (disabled) {
chrome.storage.sync.remove([disabledKey], () => loadMappings());
} else {
chrome.storage.sync.set({ [disabledKey]: true }, () => loadMappings());
}
});
item.appendChild(swatch);
item.appendChild(text);
item.appendChild(toggleBtn);
item.appendChild(editBtn);
item.appendChild(deleteBtn);
list.appendChild(item);
});
}
function loadMappings() {
chrome.storage.sync.get(null, (allItems) => {
const mappings = [];
const disabledSet = new Set();
Object.keys(allItems).forEach((key) => {
if (key.startsWith(DISABLED_PREFIX)) {
disabledSet.add(key.slice(DISABLED_PREFIX.length));
return;
}
if (!key.startsWith(STORAGE_PREFIX)) return;
const parsed = parseStorageKey(key);
if (!parsed) return;
mappings.push({
key,
accountId: parsed.accountId,
roleName: parsed.roleName,
color: allItems[key],
});
});
renderMappings(mappings, disabledSet);
});
}
function exportMappings() {
chrome.storage.sync.get(null, (allItems) => {
const data = [];
Object.keys(allItems).forEach((key) => {
if (!key.startsWith(STORAGE_PREFIX)) return;
const parsed = parseStorageKey(key);
if (!parsed) return;
data.push({
accountId: parsed.accountId,
roleName: parsed.roleName,
color: allItems[key],
});
});
if (data.length === 0) {
showStatus('エクスポートするデータがありません', true);
return;
}
const json = JSON.stringify(data, null, 2);
const blob = new Blob([json], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'awscc-colors.json';
a.click();
URL.revokeObjectURL(url);
showStatus(`${data.length} 件をエクスポートしました`);
});
}
async function getActiveTab() {
return new Promise((resolve) => {
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
resolve(tabs[0] || null);
});
});
}
async function sendToContentScript(message) {
const tab = await getActiveTab();
if (!tab) return null;
return new Promise((resolve) => {
chrome.tabs.sendMessage(tab.id, message, (response) => {
if (chrome.runtime.lastError) {
resolve(null);
} else {
resolve(response);
}
});
});
}
async function init() {
buildPresets();
setColorUI(currentColor);
const info = await sendToContentScript({ action: 'getAccountInfo' });
if (info) {
detectedAccountId = info.accountId;
detectedRoleName = info.roleName;
updateAccountInfoUI(info.accountId, info.roleName);
if (info.accountId) {
const exactKey = info.roleName ? makeStorageKey(info.accountId, info.roleName) : null;
const wildcardKey = makeStorageKey(info.accountId, '*');
const keysToFetch = [...(exactKey ? [exactKey] : []), wildcardKey];
chrome.storage.sync.get(keysToFetch, (result) => {
if (exactKey && result[exactKey]) {
setColorUI(result[exactKey]);
} else if (result[wildcardKey]) {
setColorUI(result[wildcardKey]);
}
});
}
} else {
updateAccountInfoUI(null, null);
showStatus('このページでは情報を検出できませんでした', true);
}
loadMappings();
document.getElementById('color-picker').addEventListener('input', (e) => {
setColorUI(e.target.value);
});
document.getElementById('color-hex').addEventListener('input', (e) => {
const val = e.target.value.trim();
const hexEl = document.getElementById('color-hex');
if (isValidHex(val)) {
hexEl.classList.remove('invalid');
currentColor = val;
document.getElementById('color-picker').value = val;
document.getElementById('nav-preview').style.backgroundColor = val;
document.querySelectorAll('.preset-color').forEach((el) => {
el.classList.toggle('selected', el.dataset.color === val);
});
} else {
hexEl.classList.add('invalid');
}
});
document.getElementById('btn-apply').addEventListener('click', async () => {
const resp = await sendToContentScript({ action: 'applyColor', color: currentColor });
if (resp && resp.success) {
showStatus('色を適用しました');
} else {
showStatus('適用に失敗しました。AWS コンソールのページで試してください。', true);
}
});
document.getElementById('btn-save').addEventListener('click', () => {
if (!detectedAccountId || !detectedRoleName) {
showStatus('アカウント情報が検出できていません', true);
return;
}
const key = makeStorageKey(detectedAccountId, detectedRoleName);
chrome.storage.sync.set({ [key]: currentColor }, () => {
showStatus('保存しました!次回ログイン時から自動適用されます');
loadMappings();
});
});
document.getElementById('btn-export').addEventListener('click', exportMappings);
document.getElementById('import-file-input').addEventListener('change', (e) => {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (evt) => {
try {
const data = JSON.parse(evt.target.result);
if (!Array.isArray(data)) throw new Error('invalid format');
const validEntries = [];
let skipped = 0;
data.forEach((entry) => {
const { accountId, roleName, color } = entry;
if (
typeof accountId === 'string' && accountId.length > 0 &&
typeof roleName === 'string' && roleName.length > 0 &&
isValidHex(color)
) {
validEntries.push({ key: makeStorageKey(accountId, roleName), accountId, roleName, color });
} else {
skipped++;
}
});
if (validEntries.length === 0) {
showStatus('有効なデータが見つかりませんでした', true);
e.target.value = '';
return;
}
const allKeys = validEntries.map((v) => v.key);
chrome.storage.sync.get(allKeys, (existing) => {
const overwriteEntries = validEntries.filter((v) => v.key in existing);
const doImport = () => {
const toStore = {};
validEntries.forEach((v) => { toStore[v.key] = v.color; });
chrome.storage.sync.set(toStore, () => {
loadMappings();
const total = validEntries.length;
const msg = skipped > 0
? `${total} 件をインポートしました(${skipped} 件スキップ)`
: `${total} 件をインポートしました`;
showStatus(msg);
});
};
if (overwriteEntries.length === 0) {
doImport();
} else {
const lines = overwriteEntries.map((v) => `・${v.accountId} / ${v.roleName}`).join('\n');
const confirmed = window.confirm(
`以下 ${overwriteEntries.length} 件は既存の保存済みカラーを上書きします。よろしいですか?\n\n${lines}`
);
if (confirmed) {
doImport();
} else {
showStatus('インポートをキャンセルしました');
}
}
});
} catch (_err) {
showStatus('インポートに失敗しました(不正な形式)', true);
}
e.target.value = '';
};
reader.readAsText(file);
});
}
document.addEventListener('DOMContentLoaded', init);
popup.html
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>AWS access portal Colorizer</title>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
font-size: 13px;
width: 320px;
background: #1a1a2e;
color: #e0e0e0;
}
header {
background: #16213e;
padding: 12px 16px;
border-bottom: 2px solid #e47911;
display: flex;
align-items: center;
gap: 8px;
}
header h1 {
font-size: 14px;
font-weight: 600;
color: #e47911;
}
.aws-icon {
width: 20px;
height: 20px;
background: #e47911;
border-radius: 3px;
display: flex;
align-items: center;
justify-content: center;
font-size: 10px;
font-weight: bold;
color: #fff;
}
.section {
padding: 12px 16px;
border-bottom: 1px solid #2a2a4a;
}
.section-title {
font-size: 11px;
font-weight: 600;
color: #888;
text-transform: uppercase;
letter-spacing: 0.05em;
margin-bottom: 8px;
}
.account-info {
background: #0f3460;
border-radius: 6px;
padding: 10px 12px;
}
.info-row {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 4px;
}
.info-row:last-child {
margin-bottom: 0;
}
.info-label {
color: #888;
font-size: 11px;
white-space: nowrap;
margin-right: 8px;
padding-top: 1px;
}
.info-value {
color: #e0e0e0;
font-size: 12px;
font-family: monospace;
word-break: break-all;
text-align: right;
}
.info-value.not-detected {
color: #666;
font-style: italic;
font-family: inherit;
}
.nav-preview {
height: 36px;
border-radius: 6px;
margin-bottom: 10px;
display: flex;
align-items: center;
padding: 0 12px;
gap: 8px;
transition: background-color 0.2s;
border: 1px solid #333;
font-size: 11px;
color: rgba(255,255,255,0.7);
}
.nav-preview-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: rgba(255,255,255,0.4);
}
.color-picker-row {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 10px;
}
.color-picker-wrap {
position: relative;
width: 48px;
height: 48px;
flex-shrink: 0;
}
input[type="color"] {
width: 48px;
height: 48px;
border: 2px solid #555;
border-radius: 8px;
cursor: pointer;
padding: 2px;
background: #0f3460;
flex-shrink: 0;
}
input[type="color"]:hover {
border-color: #e47911;
}
.color-picker-wrap:hover input[type="color"] {
border-color: #e47911;
}
.eyedropper-icon {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 22px;
height: 22px;
pointer-events: none;
filter: drop-shadow(0 0 2px rgba(0,0,0,0.8));
opacity: 0.9;
}
.color-hex-wrap {
flex: 1;
display: flex;
flex-direction: column;
gap: 4px;
}
.color-hex-label {
font-size: 10px;
color: #888;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.color-hex-input {
width: 100%;
background: #0f3460;
border: 1px solid #444;
border-radius: 4px;
padding: 8px 10px;
color: #e0e0e0;
font-family: monospace;
font-size: 15px;
letter-spacing: 0.05em;
}
.color-hex-input:focus {
outline: none;
border-color: #e47911;
}
.color-hex-input.invalid {
border-color: #e05555;
color: #e05555;
}
.preset-colors {
display: flex;
gap: 6px;
flex-wrap: wrap;
justify-content: center;
align-items: center;
}
.preset-color {
width: 24px;
height: 24px;
border-radius: 4px;
border: 2px solid transparent;
cursor: pointer;
transition: border-color 0.2s, transform 0.1s;
}
.preset-color:hover {
border-color: #fff;
transform: scale(1.1);
}
.preset-color.selected {
border-color: #e47911;
}
.btn {
display: block;
width: 100%;
padding: 9px 12px;
border: none;
border-radius: 5px;
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: opacity 0.2s, transform 0.1s;
margin-bottom: 8px;
}
.btn:last-child {
margin-bottom: 0;
}
.btn:hover {
opacity: 0.9;
}
.btn:active {
transform: scale(0.98);
}
.btn-apply {
background: #16213e;
color: #e0e0e0;
border: 1px solid #444;
}
.btn-save {
background: #e47911;
color: #fff;
}
.btn-save:disabled {
background: #555;
cursor: not-allowed;
opacity: 0.6;
}
.mapping-list {
max-height: 240px;
overflow-y: auto;
}
.mapping-list::-webkit-scrollbar {
width: 4px;
}
.mapping-list::-webkit-scrollbar-thumb {
background: #444;
border-radius: 2px;
}
.mapping-item {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 8px;
border-radius: 5px;
margin-bottom: 4px;
background: #0f3460;
}
.mapping-swatch {
width: 16px;
height: 16px;
border-radius: 3px;
flex-shrink: 0;
border: 1px solid #444;
}
.mapping-text {
flex: 1;
overflow: hidden;
}
.mapping-account {
font-family: monospace;
font-size: 12px;
color: #e0e0e0;
}
.mapping-role {
font-size: 11px;
color: #888;
word-break: break-all;
}
.mapping-delete {
background: none;
border: none;
color: #666;
cursor: pointer;
font-size: 14px;
line-height: 1;
padding: 2px 4px;
border-radius: 3px;
}
.mapping-delete:hover {
color: #e05555;
background: #2a1a1a;
}
.empty-message {
color: #555;
font-style: italic;
font-size: 12px;
text-align: center;
padding: 8px 0;
}
.mapping-edit {
background: none;
border: none;
color: #666;
cursor: pointer;
font-size: 14px;
line-height: 1;
padding: 2px 4px;
border-radius: 3px;
}
.mapping-edit:hover {
color: #e47911;
background: #2a2010;
}
.mapping-toggle {
background: none;
border: 1px solid #444;
border-radius: 10px;
cursor: pointer;
font-size: 10px;
font-weight: 700;
padding: 2px 6px;
line-height: 1.4;
min-width: 34px;
text-align: center;
flex-shrink: 0;
transition: color 0.2s, border-color 0.2s;
}
.mapping-toggle.on {
color: #4caf50;
border-color: #4caf50;
}
.mapping-toggle.off {
color: #555;
border-color: #444;
}
.mapping-toggle:hover {
opacity: 0.8;
}
.mapping-item--disabled .mapping-swatch {
opacity: 0.35;
}
.mapping-item--disabled .mapping-account,
.mapping-item--disabled .mapping-role {
opacity: 0.4;
}
.mapping-item--editing {
flex-direction: column;
align-items: stretch;
}
.mapping-edit-form {
display: flex;
flex-direction: column;
gap: 6px;
width: 100%;
}
.mapping-edit-input {
width: 100%;
background: #1a1a2e;
border: 1px solid #444;
border-radius: 4px;
padding: 5px 8px;
color: #e0e0e0;
font-family: monospace;
font-size: 12px;
}
.mapping-edit-input:focus {
outline: none;
border-color: #e47911;
}
.mapping-edit-color-row {
display: flex;
align-items: center;
gap: 8px;
}
.mapping-edit-color {
width: 36px;
height: 30px;
border: 1px solid #444;
border-radius: 4px;
cursor: pointer;
padding: 1px;
background: #1a1a2e;
flex-shrink: 0;
}
.mapping-edit-actions {
display: flex;
gap: 6px;
}
.mapping-edit-save {
flex: 1;
background: #e47911;
color: #fff;
border: none;
border-radius: 4px;
padding: 5px 0;
cursor: pointer;
font-size: 12px;
font-weight: 600;
}
.mapping-edit-save:hover {
background: #f5a623;
}
.mapping-edit-cancel {
flex: 1;
background: transparent;
color: #888;
border: 1px solid #333;
border-radius: 4px;
padding: 5px 0;
cursor: pointer;
font-size: 12px;
}
.mapping-edit-cancel:hover {
color: #e0e0e0;
border-color: #555;
}
.save-note {
font-size: 11px;
color: #7a7a7a;
margin-top: 6px;
margin-bottom: 10px;
text-align: left;
line-height: 1.3;
}
#status-message {
font-size: 12px;
padding: 4px 0;
min-height: 18px;
color: #4caf50;
text-align: center;
}
#status-message.error {
color: #e05555;
}
.export-import-row {
display: flex;
gap: 8px;
}
.export-import-row .btn,
.export-import-row .btn-import-label {
flex: 1;
min-width: 0;
padding: 9px 12px;
display: inline-flex;
align-items: center;
justify-content: center;
margin-bottom: 0;
width: auto;
box-sizing: border-box;
}
.btn-export {
background: #16213e;
color: #e0e0e0;
border: 1px solid #444;
}
.btn-import-label {
display: inline-flex;
border: 1px solid #444;
border-radius: 5px;
font-size: 13px;
font-weight: 500;
text-align: center;
cursor: pointer;
background: #16213e;
color: #e0e0e0;
transition: opacity 0.2s;
}
.btn-import-label:hover {
opacity: 0.9;
}
</style>
</head>
<body>
<header>
<div class="aws-icon">AWS</div>
<h1>AWS access portal Colorizer</h1>
</header>
<div class="section">
<div class="section-title">現在ログイン中のアカウント情報</div>
<div class="account-info">
<div class="info-row">
<span class="info-label">アカウント ID</span>
<span class="info-value not-detected" id="detected-account">検出中...</span>
</div>
<div class="info-row">
<span class="info-label">ロール名</span>
<span class="info-value not-detected" id="detected-role">検出中...</span>
</div>
</div>
</div>
<div class="section">
<div class="section-title">カラー選択</div>
<div class="nav-preview" id="nav-preview">
<div class="nav-preview-dot"></div>
<div class="nav-preview-dot"></div>
<div class="nav-preview-dot"></div>
<span>ナビバープレビュー</span>
</div>
<div class="color-picker-row">
<div class="color-picker-wrap">
<input type="color" id="color-picker" value="#1a1a2e" title="クリックしてカラーピッカーを開く" />
<svg class="eyedropper-icon" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<path d="M20.71 5.63l-2.34-2.34a1 1 0 0 0-1.41 0l-3.12 3.12-1.41-1.42-1.42 1.42 1.41 1.41-6.6 6.6A2 2 0 0 0 5 15.88V19a1 1 0 0 0 1 1h3.12a2 2 0 0 0 1.42-.59l6.6-6.6 1.41 1.41 1.42-1.42-1.42-1.41 3.12-3.12a1 1 0 0 0 0-1.64z" fill="white"/>
<circle cx="4" cy="20" r="2" fill="white"/>
</svg>
</div>
<div class="color-hex-wrap">
<span class="color-hex-label">カラーコード(HEX)</span>
<input type="text" class="color-hex-input" id="color-hex" value="#1a1a2e" maxlength="7" placeholder="#rrggbb" spellcheck="false" />
</div>
</div>
<div class="preset-colors" id="preset-colors">
</div>
</div>
<div class="section">
<button class="btn btn-apply" id="btn-apply">現在のタブに適用してみる</button>
<button class="btn btn-save" id="btn-save" disabled>このアカウント/ロールのカラーを保存</button>
<div id="status-message"></div>
</div>
<div class="section">
<div class="section-title">保存済みカラー</div>
<div class="save-note">(注) ロール名を「*」として保存すると、そのアカウントにログインしているどのロールであっても保存したカラーが適用されます。</div>
<div class="mapping-list" id="mapping-list">
<div class="empty-message">保存されたカラーはありません</div>
</div>
</div>
<div class="section">
<div class="section-title">保存済みカラーのエクスポート / インポート</div>
<div class="export-import-row">
<button class="btn btn-export" id="btn-export">エクスポート</button>
<label class="btn-import-label" for="import-file-input">インポート</label>
<input type="file" id="import-file-input" accept=".json" style="display:none" />
</div>
</div>
<script src="popup.js"></script>
</body>
</html>
5. インストール方法
- コードを用意する(リポジトリをダウンロードもしくはコードをコピーし、任意のパスのローカルフォルダに格納する)
- ブラウザで
chrome://extensions/を開き、右上で「デベロッパーモード」を有効化 - 「パッケージ化されていない拡張機能を読み込む」を選択し、ダウンロードまたはコピーしてきた
AWS-access-portal-Colorizerフォルダを指定
- Manifest V3 を利用しているため、Service Worker(
background.js)やhost_permissionsが必要です。ブラウザ側のポリシーや企業ポリシーでロード不可となる場合があります。 - プライベート(シークレット)ウィンドウで動かしたい場合は、拡張機能の詳細で 「シークレットモードでの実行を許可」 を有効化してください。
6. 使い方
AWS access portal からメンバーアカウントにログイン後の画面で、該当の拡張アイコンをクリックしてポップアップを開くと、以下のような画面が表示されます。
7. さいごに
「AWS access portal Colorizer」が、日々のAWS操作でアカウント切り替えミスを減らし、作業効率向上の一助になれば嬉しいです🐈








