Oracle APEX Graph Visualization Plug-in Enhancement - Drill Down into Details
APEX Graph Visualization Plug-inはOracle Graphの開発チームより提供されている、APEXのアプリケーションに組み込めるグラフを表示するプラグインです。この動画では、グラフ・プラグインに表示されているノードのツールチップ上に、APEXアプリケーションのページを開くボタンを作る方法を解説しています。
本記事では、この動画の内容にそって、Graph Visualization Plug-inのサンプル・アプリケーションに、ページを開くボタンを追加します。
以前にGraph Visualization Plug-inのサンプル・アプリケーションの導入方法を紹介しています。記事「Graph Visualization Plug-inのサンプル・アプリケーションをOracle Database 23aiのAPEXにインストールする」です。こちらの記事ではOracle APEX 24.1のサンプル・アプリケーションのブランチを参照しています。今回のYouTubeの動画では24.2のブランチを参照しています。
Oracle Database 23ai FreeにOracle APEX 24.2の組み合わせで、サンプル・アプリケーションにドロワーの表示を組み込んでみます。
追加した機能は以下のGIF動画のように動作します。
先に紹介した記事の作業を行い、Sample Graph Visualizationsのアプリケーションが作成済みであることを前提とします。
ドロワーを開く機能は、Interactionのページに実装します。
グラフに表示されているノード上で右クリックすると、ツールチップが表示されます。このツールチップ上にボタンを追加します。
アプリケーション・デザイナよりページの作成をクリックします。
フォームを選択します。
ページの名前はEmployee Detailとし、ページ・モードにドロワーを選択します。データ・ソースの表/ビューの名前に、EBA_GRAPHVIZ_EMPLOYEESを指定します。この表はサンプル・アプリケーションのインストール時に、サポートするオブジェクトとしてデータベースにインストールされます。
次へ進みます。
主キー列1はEMPLOYEE_ID(Number)になります。
ページの作成をクリックします。
ドロワーのページが作成されます。
ページ・プロパティの識別の別名を確認します。通常はページ名がEmployee Detailであれば別名はemployee-detailになります。この値をAPEX_PAGE.GET_URLの引数p_pageに与えます。
以上でドロワーのページは完成です。
静的アプリケーション・ファイルとして、今回の機能拡張を実装したJavaScriptファイルを作成します。
セミナー講師が準備したGitHubリポジトリより、graphDrillDown.jsとgraphDrillDownPageLoad.jsの内容を組み込みます。
上記のファイルとほとんど同じですが、graphDrillDownPageLoad.jsの内容をDOMContentLoadedのイベントで実行するようにして、graphDrillDown.jsに追加しました。
それとgraphDrillDown.jsの68行目の以下の行を、
window.location.href = response.url; // uncomment this line to enable redirection
APEXのドロワーを開く、以下のコードに変更しています。
apex.navigation.dialog(response.url, {} );
このファイルを静的アプリケーション・ファイルとして作成します。
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// global variable declaration | |
// define the icon class to be used for the button icon | |
// change the value to customize the icon (reference: oracle icon gallery) | |
const iconClass = "oj-ux-ico-arrow-circle-down"; | |
// define the key identifier for extracting the required value from the tooltip. What element will the getKeyValue search for? | |
const keyIdentifier = "EMPLOYEE_ID"; | |
// reference the name of the apex ajax callback process that will handle the request | |
const ajaxCallback = "REDIRECT_USER"; | |
// button label and title | |
const buttonTitle = "Drill Down - View Details"; | |
// (leave unchanged) selector string to identify the tooltip modal in the DOM | |
const toolTipQuerySelector = 'section.ovis.oj-popup.svelte-1wc7iob'; | |
// function to add a button to the tooltip footer | |
function addButtonToTooltip(toolTip) { | |
//console.log('Button Added to ToolTip'); | |
if (buttonAdded) return; // prevent adding multiple buttons | |
// create the button and append it to the tooltip footer | |
const button = createButton(); | |
toolTip.querySelector('footer').appendChild(button); | |
// attach a click event listener to handle button click | |
button.addEventListener('click', () => handleButtonClick(toolTip)); | |
} | |
// function to create a styled button element | |
function createButton() { | |
//console.log('Button Created'); | |
// create a div element and apply oracle jet (oj) button classes | |
const button = document.createElement('div'); | |
button.classList.add('oj-button', 'normal', 'oj-button-half-chrome', 'oj-button-icon-only', 'oj-complete', 'svelte-16rn59g', 'oj-default', 'oj-enabled'); | |
// define the inner html structure with an icon | |
button.innerHTML = ` | |
<button class="oj-button-button" aria-label="${buttonTitle}" title="${buttonTitle}"> | |
<div class="oj-button-label"> | |
<span class="oj-button-icon oj-start oj-ux-icon ${iconClass}"></span> | |
</div> | |
</button> | |
`; | |
return button; | |
} | |
// function to handle the user's button click event | |
async function handleButtonClick(toolTip) { | |
// retrieve the key value from the tooltip (e.g., employee id) | |
const keyValue = getKeyValue(toolTip); | |
// if no key value is found, alert a message and exit the function | |
if (!keyValue) { | |
apex.message.showErrors( [ | |
{ | |
type: "error", | |
location: [ "page"], | |
message: `Key "${keyIdentifier}" not found in tooltip. Ensure it is declared in the keyIdentifier constant.`, | |
unsafe: false | |
} | |
] ); | |
return; | |
} | |
//console.log(`${keyIdentifier} = value:`, keyValue); | |
try { | |
// send an asynchronous ajax request to the oracle apex server | |
const response = await apex.server.process(ajaxCallback, { | |
x01: keyValue // send the extracted key value as parameter x01 | |
}, { dataType: 'JSON' }); // expect a json response | |
//console.log('server response:', response); | |
// check if the server response contains a url to redirect the user | |
if (response.url) { | |
// window.location.href = response.url; // uncomment this line to enable redirection | |
apex.navigation.dialog(response.url, {} ); | |
} | |
// check if the server returned an error message | |
else if (response.error) { | |
apex.message.showErrors( [ | |
{ | |
type: "error", | |
location: [ "page"], | |
message: `An error occurred during the AJAX request.Server Error: ${response.error}`, | |
unsafe: false | |
} | |
] ); // show an alert with the error message | |
} | |
} catch (error) { | |
// handle any errors that occur during the ajax request | |
console.error('ajax request failed:', error); | |
apex.message.showErrors([ | |
{ | |
type: "error", | |
location: ["page"], | |
message: "An error occurred during the AJAX request.", | |
unsafe: false | |
} | |
]); // display an error alert | |
} | |
} | |
// function to extract the key value from the tooltip table | |
function getKeyValue(toolTip) { | |
// find the row where the key identifier is located | |
const keyValueRow = Array.from(toolTip.querySelectorAll('tr')).find(row => { | |
const labelCell = row.querySelector('td b'); | |
return labelCell && labelCell.textContent.trim() === keyIdentifier; | |
}); | |
if (keyValueRow) { | |
// retrieve the corresponding value from the second cell in the row. i.e the value for that identifier | |
const keyValueCell = keyValueRow.querySelectorAll('td')[1]; | |
return keyValueCell ? keyValueCell.textContent.trim() : null; | |
} | |
return null; // return null if the key value is not found | |
} | |
/* | |
* graphDrillDownPageLoad.jsの内容。 | |
*/ | |
document.addEventListener("DOMContentLoaded", function () { | |
// create a new mutation observer to monitor changes in the DOM reference:https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver | |
const observer = new MutationObserver((mutationsList) => { | |
// iterate through all observed mutations | |
for (const mutation of mutationsList) { | |
// check if the mutation type is 'childList', meaning elements were added or removed | |
if (mutation.type === 'childList') { | |
// attempt to find the tooltip (modal) in the DOM | |
const toolTip = document.querySelector(toolTipQuerySelector); | |
if (toolTip) { | |
addButtonToTooltip(toolTip); // call function to add the button | |
} else { | |
buttonAdded = false; // reset flag when tooltip is removed or closed | |
} | |
} | |
} | |
}); | |
// start observing the body element for child node changes (added or removed elements) | |
observer.observe(document.body, { | |
childList: true, // watch for direct additions or removals of child elements | |
subtree: false // do not monitor changes deep within child elements | |
}); | |
}); |
共有コンポーネントの静的アプリケーション・ファイルを開きます。
ファイルの作成をクリックします。
ディクレクトリはjs、ファイル名はgraphDrillDown.jsとします。
作成をクリックします。
本記事に掲載したgraphDrillDown.jsのコードを貼り付け、変更の保存をクリックします。
ファイルが保存され、縮小されました。とメッセージが表示されます。参照をコピーします。この参照の値をページ・プロパティのJavaScriptのファイルURLに設定します。
取消をクリックし、スクリプト・エディタを閉じます。
ページ・デザイナに戻り、ページ・プロパティのJavaScriptのファイルURLに以下の値を設定します。
[defer]#APP_FILES#js/graphDrillDown#MIN#.js
ドロワーのページを開くURLを取得するために呼び出すAjaxコールバックを作成します。
名前はREDIRECT_USERとします。この名前はgraphDrillDown.jsの定数ajaxCallbackに設定されているため、変更する場合はgraphDrillDown.jsも変更が必要です。
こちらのコードは元の動画のコードより、かなり簡素化しています。
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
DECLARE | |
-- variable to store the final redirect url | |
l_url VARCHAR2(32767); | |
BEGIN | |
/* | |
* p_pageに開くページのページ名、p_itemsに主キーのアイテム名を指定する。 | |
*/ | |
l_url := apex_page.get_url( | |
p_page => 'employee-detail' | |
,p_items => 'P2_EMPLOYEE_ID' | |
,p_values => APEX_APPLICATION.G_X01 | |
); | |
-- return the generated url as a json response | |
apex_json.open_object; | |
apex_json.write('url', l_url); | |
apex_json.close_object; | |
EXCEPTION | |
-- handle any unexpected errors and return an error message in json format | |
WHEN OTHERS THEN | |
apex_debug.message('Error! ' || SQLERRM); -- log error for debugging | |
apex_json.open_object; | |
apex_json.write('error', 'an error occurred: ' || SQLERRM); | |
apex_json.close_object; | |
END; |
以上でアプリケーションは完成です。アプリケーションを実行すると、記事の先頭のGIF動画のように、ツールチップからドロワーを開くことができます。
開いたページはドロワーなので、ドロワーを閉じたときにダイアログ・クローズのイベントが発生します。
Graph Visualization Plug-inのリージョンに静的IDとして、例えばGRAPHVIZを設定します。
URLの取得時に呼び出すAPEX_PAGE.GET_URLの引数p_triggering_elementに、上記の静的IDをセレクタとした#GRAPHVIZを渡すと、ダイアログのクローズ時に動的アクションを実行できます。
/*
* p_pageに開くページのページ名、p_itemsに主キーのアイテム名を指定する。
*/
l_url := apex_page.get_url(
p_page => 'employee-detail'
,p_items => 'P2_EMPLOYEE_ID'
,p_values => APEX_APPLICATION.G_X01
,p_triggering_element => '#GRAPHVIZ'
);
しかし、Graph Visualization Plug-inにはリージョンをリフレッシュするメソッドが未実装(に見える)なため、ダイアログ・クローズにイベントを受け取ってもグラフを更新できません。
操作感は良くありませんが、リージョンをリフレッシュする代わりにwindow.location.reload()を呼び出すことで、グラフを更新することができます。
今回の記事は以上になります。
今回作成したAPEXアプリケーションのエクスポートを以下に置きました。
https://github.com/ujnak/apexapps/blob/master/exports/sample-graph-visualization-with-drilldown.zip
Oracle APEXのアプリケーション作成の参考になれば幸いです。
完