2023年9月29日金曜日

パッケージDBMS_CLOUD_PIPELINEを使用してビューAPEX_WORKSPACE_ACTIVITY_LOGの内容をオフロードする

Oracle APEXでは、詳細なアクティビティのログをビューAPEX_WORKSPACE_ACTIVITY_LOGから参照できるようになっています。このログの保存期間は、APEXの管理サービスのインスタンスの設定ログ間隔の管理から変更できます。


デフォルトでは、APEX_WORKSPACE_ACTIVITY_LOGは14日間ごとにログの切り替えが発生します。


これらのログを長期間保管したいという要件はあるかと思います。単純にログ切替えまでの経過日数を増やすとパフォーマンスに悪い影響がでます。また、APEXがインストールされているスキーマの表領域に(大抵はSYSAUXを使っていると思います)多くの容量が必要になります。

Autonomous Databaseでは、パッケージDBMS_CLOUD_PIPELINEが提供されています。このパッケージを使用し、ビューAPEX_WORKSPACE_ACTIVITY_LOGの内容をオブジェクト・ストレージにエクスポートできます。

APEXのワークスペース・スキーマにDBMS_CLOUD_PIPELINEの実行権限を与えます。

grant execute on dbms_cloud_pipeline to <APEXワークスペース・スキーマ>;


ビューAPEX_WORKSPACE_ACTIVITY_LOGをAPEXのワークスペース・スキーマから検索すると、検索される対象のログは、そのワークスペースに限定されます。内部ワークスペース(管理ツールや開発ツールのログを含む)を含んだすべてのワークスペースのアクティビティ・ログをエクスポートする場合は、スキーマADMINでDBMS_CLOUD_PIPELINEのパイプラインを作成し、実行します。

今回は作業をAPEXのSQLコマンドで実施したいので、ワークスペースに限定してログのオフロードを行います。

最初にログのエクスポート先となるバケットを、オブジェクト・ストレージに作成します。

Oracle Cloudのコンソールより、ストレージバケットを開きます。


バケットの作成を実行します。今回はAPEX_WORKSPACE_ACTITIVY_LOGというバケットを作成しました。


クリデンシャルの作成方法やリソース・プリンシパルの設定方法は他の説明を参照していただき、今回は手っ取り早く事前認証済リクエストを使います。

右端のアイコンよりメニューを開き、事前承認済リクエストの作成を実行します。


事前承認済リクエスト・ターゲットとしてバケットを選択します。アクセス・タイプとしてオブジェクトの書込みを許可(事前承認済ターゲットからオブジェクトを読むことはありません)を選択します。有効期限も確認します。

事前承認済リクエストの作成をクリックします。


事前承認済リクエストが作成されます。現在のURLは非推奨とのことなので、推奨されるURLをコピーしておきます。


APEXのSQLコマンドよりパイプラインAPEX_ACTIVITY_LOG_EXPORTを作成します。DBMS_CLOUD_PIPELINE.CREATE_PIPELINEを呼び出します。

以下のコードを実行します。

事前承認済リクエストの部分は、作成したURLで置き換えます。ログはそのバケットの下にプレフィックスactivityが付与されたファイルとして作成されます。

テストなのでintervalとして2分を指定しています。table_nameAPEX_WORKSPACE_ACTIVITY_LOGkey_columnVIEW_TIMESTAMPです。その他のattributeの詳細は、パッケージDBMS_CLOUD_PIPELINEの説明を参照してください。

declare
l_format clob;
l_attributes clob;
l_location varchar2(4000);
begin
l_location := '事前承認済リクエスト' || 'activity';
select json_object(
'type' value 'json'
) into l_format
from dual;
select json_object(
-- 'credential_name' value 'MY_OCI_CRED'
'format' value l_format -- format jsonを付けると認識されない。
,'interval' value 2
,'key_column' value 'VIEW_TIMESTAMP'
,'location' value l_location
,'priority' value 'LOW'
,'table_name' value 'APEX_WORKSPACE_ACTIVITY_LOG'
) into l_attributes
from dual;
dbms_output.put_line(l_attributes);
dbms_cloud_pipeline.create_pipeline(
pipeline_name => 'APEX_ACTIVITY_LOG_EXPORT'
,pipeline_type => 'EXPORT'
,attributes => l_attributes
);
end;



作成したパイプラインを確認します。ビューUSER_CLOUD_PIPELINESを検索します。

select * from user_cloud_pipelines;


パイプラインをスタートします。DBMS_CLOUD_PIPELINE.START_PIPELINEを呼び出します。

begin
dbms_cloud_pipeline.start_pipeline(
pipeline_name => 'APEX_ACTIVITY_LOG_EXPORT'
,start_date => systimestamp
);
end;
start_dateとして指定した日時よりintervalで指定した時間が経過した時刻が、初回実行の時刻となります。


パイプラインの実行履歴は、ビューUSER_CLOUD_PIPELINE_HISOTRYより確認できます。

select * from user_cloud_pipeline_history;


バケットの一覧を見ると、ファイルが作成されていることが確認できます。

初回実行時は列VIEW_TIMESTAMPによる制限がかかっていないため、formatのmaxfilesizeのデフォルトである10MiBのファイルが作成されています。


以上でビューAPEX_WORKSPACE_ACTIVITY_LOGのエクスポートが確認できました。

パイプラインの停止はDBMS_CLOUD_PIPELINE.STOP_PIPELINE、削除はDBMS_CLOUD_PIPELINE.DROP_PIPELINEを呼び出します。

パッケージDBMS_CLOUD_PIPELINEの紹介は以上になります。

2023年9月27日水曜日

サンプル・アプリケーションSample Graph Visualizationsの紹介

 Oracle APEXのサンプル・アプリケーションとしてSample Graph Visualizationsが追加されています。インストールできるのは、APEX 23.1以降です。

https://oracle.github.io/apex/


最近のAPEX Office HourにてOracle社のSpatial and Graph Product Management TeamのRahul Taskerさんが、APEX and Property Graphs in Oracle Database 23cという題でオンライン・セミナーをされています。録画はこちら

Property GraphはOracle Database 23cから提供される機能ですし、その話だから使えるのはまだ先かな、と思って聞いていたところ、Jayant Sharmaさんが「Sample Graph VisualizationsはADB Serverlessでも使えるよ。」とのこと。

ということで、サンプルをダウンロードとしてAlways FreeのADB Serverlessにインストールしてみました。問題なく動きました。

アプリケーションを実行するとログイン画面が表示されます。


サインインすると、沢山のサンプルが表示されます。


Basic Graphを開いてみると、以下のように表示されます。


Graph Studioで使われているビジュアリゼーションのJavaScriptコンポーネントを、APEXのプラグインとして移植しているように見えます。


Sample Graph Visualizationsでは、グラフを表示するリージョンのタイプGraph Visualizations (Preview)となっています。

将来は標準のリージョンになるかもしれません。


このグラフのソースとなっているQueryを確認した範囲では、ソースはverticesとedgesの属性を、それぞれ配列として含むJSONドキュメントであれば良いみたいです。つまり、必ずしもOracle Database 23cのProperty Graphの機能が無くても、このプラグインは使えます。


Stylingを開くと以下のようなグラフが確認できます。


Interactionでは、APEXの動的アクションを使った実装がされています。


Selectionでは、さらに凝った実装が行われています。


Network Evolutionでは、チャートを重ねて表示しています。


その他にSaving Graph State、Keyboard Navigation Shortcutsといったページがあります。

Oracle Database 23cにProperty Graphが実装されても、APEXで使用するにはビジュアリゼーションがネックだと思っていたので、このようなコンポーネントが使えるようになっているのは、とてもありがたいです。

ちなみにAPEX 23.1とOracle Database 23cを組み合わせた場合は、ソースタイプとしてプロパティ・グラフを指定できるようになります。


SELECT文全体を記述する必要がなくなり、若干ソースの設定が容易になります。

以上です。

Oracle APEXのアプリケーション作成の参考になれば幸いです。

動的アクションから開いたモーダル・ダイアログのページのクローズ・イベントを取る方法

動的アクションよりモーダル・ダイアログのページ(以下フォームと記します)を開いた場合、イベントのダイアログのクローズが取れない(TRUEアクションが実行されない)との相談がありました。

原因と対処方法について紹介します。

最初に検証に使用する簡単なアプリケーションを作成します。

アプリケーション作成ウィザードを起動します。アプリケーションの名前Dialog Close Eventとします。

ホーム・ページは削除し、サンプル・データセットのEMP/DEPTに含まれる表EMPをソースとした、対話モード・レポートとフォームのページを追加します。


対話モード・レポートのページ名EMPとし、表またはビューとしてEMPを選択します。フォームを含めるチェックをいれ、ページの追加を行います。


以上で、アプリケーションを作成します。

ページ・デザイナで、ページ番号の表EMPの対話モード・レポートのページを開きます。


動的アクションでフォームを開く実装を追加します。

フォームを開くURLを保持するページ・アイテムとしてP1_URLを作成します。

一般的な実装ではタイプとして非表示を選択しますが、今回は検証なのでテキスト・フィールドを選択します。


フォームを開く際の引数となる従業員番号を保持するページ・アイテムとしてP1_EMPNOを作成します。タイプテキスト・フィールドです。


フォームを開くボタンとしてOPENを作成します。動作アクションとして動的アクションで定義を選択します。


ボタンOPENに動的アクションを作成します。

識別名前onClick OPENとします。タイミングイベントはデフォルトのクリックです。


最初のTRUEアクションで、フォームを開くためのURLを生成します。

アクションとしてサーバー側のコードを実行を選択し、設定PL/SQLコードとして以下を記述します。
:P1_URL := apex_page.get_url(p_page => 2, p_items => 'P2_EMPNO', p_values => :P1_EMPNO);
追記: 以下のようにapex_page.get_urlの引数p_triggering_elementとして対話モード・レポートの静的IDを指定すると、JavaScriptによるワークアラウンドは不要でした。
:P1_URL := apex_page.get_url(p_page => 2, p_items => 'P2_EMPNO', p_values => :P1_EMPNO, p_triggering_element => 'emp');

送信するアイテムとしてP1_EMPNOを指定し、戻すアイテムとしてP1_URLを指定します。

ボタンのアクションとしてこのアプリケーションのページにリダイレクトを選択してフォームを開く場合は、ページが表示された時点でのページ・アイテムの値が引数となります。ページ表示後に変更したページ・アイテムの値を引数とすることはできないため、このような場合は、動的アクションとして実装します。

実行結果を待機はかならずオンにします。初期化時に実行する必要は無いので、これはオフにします。


ページ・アイテムP1_URLとして設定されたページを開くTRUEアクションを作成します。

アクションとしてJavaScriptコードの実行を選択し、設定コードとして以下を記述します。
apex.navigation.redirect(apex.items.P1_URL.value);

以上でアプリケーションを実行します。

Empnoとして7788を入力し、ボタンOPENをクリックします。


指定した従業員番号でフォームが開きます。動的アクションは適切に実装されていることがわかります。

Commissionなどを変更し、変更の適用をクリックします。


変更の適用をクリックするとフォームが閉じますが、対話モード・レポートがリフレッシュされないため、変更した値がレポートに反映されません。


このようになる理由は、ダイアログのクローズに登録されている動的アクションとファンクションapex.navigation.dialog(apex.theme42.dialog)呼び出し時のイベントのターゲットの不一致にあります。

ページ作成ウィザードで対話モード・レポートとフォームのページを作成すると、レポートの編集 - ダイアログのクローズという名前で、レポートをリフレッシュする動的アクションが作成されます。

この動的アクションのタイミング選択タイプとしてリージョンリージョンとして対話モード・レポートのリージョンであるEmployeesが選択されています。


ページ・アイテムP1_URLに実際にフォームを開くURLが設定されています。こちらを確認すると、以下のようになっています。
javascript:apex.theme42.dialog('\u002Fords\u002Fr\u002Fapexdev\u002Fdialog-close-event\u002Femployee?p2_empno=7788\u0026session=107992989464019\u0026cs=3mbMpJtQdUFyCb1YAXSH6hLtMuzCcsW93-WP8d5oMfp9J5S4Dw5nDFMEHLvOPgfUVORO3IxljtqC_NTpKInotEw\u0026dialogCs=kwCn_71eCWwNlEi-AzXtGDzAVWhhenYNbvXNDRUKCKLRDluOCNLUMotdGi_ZlKcPv5qzmDil0lWfd4yV41Z1bA',{title:'Employee',w:'720',mxw:'960',modal:true,dialog:null,dlgCls:'t-Drawer-page--standard '+''},' js-dialog-class-t-Drawer--pullOutEnd',this)
JavaScript APIのapex.theme42.dialogを呼び出していますが、APIリファレンスのapex.navigation.dialogを同等なので、以下のリファレンスを参照します。

https://docs.oracle.com/en/database/oracle/apex/23.1/aexjs/apex.navigation.html#.fn:dialog

第4引数のpTriggeringElementに指定されたHTML要素にapexafterclosedialog、apexafterclosecanceldialogのイベントが発行されると記載されています。つまり、動的アクションのタイミングとして設定されている要素がpTriggeringElementとして指定されていないと、ダイアログのクローズとして作成されている動的アクションは動作しません。

以上より、ページ・アイテムP1_URLとして生成されたJavaScriptを実行する際のthisを、対話モード・レポートのリージョンEmployeesとなるようにします。

色々な方法があるとは思いますが、今回はcallを使ってみます。

pTriggeringElementとして指定するために、対話モード・レポートのリージョンに静的IDとしてempを設定します。


ページ・プロパティJavaScriptファンクションおよびグローバル変数の宣言として、以下を記述します。
function openDialog(url) {
    eval(url);
}

TRUEアクションJavaScriptコードの実行コードを以下に置き換えます。
openDialog.call(apex.jQuery("#emp"),apex.items.P1_URL.value);


以上で対応は完了です。

フォームをクローズしたときにレポートがリフレッシュされるようになりました。

今回作成したAPEXアプリケーションのエクスポートを以下に置きました。
https://github.com/ujnak/apexapps/blob/master/exports/dialog-close-event.zip

Oracle APEXのアプリケーション作成の参考になれば幸いです。

追記

ダイアログをクローズしたときに、フォームの複数のページ・アイテムの値を元のページに戻す実装を追加してみました。

フォームのページにあるプロセスダイアログを閉じる設定戻すアイテムに、値を戻すページ・アイテムをカンマ区切りで複数設定します。今回の例ではP2_ENAME,P2_JOBとしています。


レポートのページに、フォームから戻されたページ・アイテムの値を保持するページ・アイテムP1_ENAMEP1_JOBを作成しておきます。


イベントがダイアログのクローズのときに実行されるTRUEアクションを作成します。

アクション値の設定設定タイプの設定としてDialog Return Itemを選択します。戻りアイテムP2_ENAME(フォームのページにあるページ・アイテム名)を指定します。

影響を受ける要素選択タイプアイテムを選び、アイテムとしてフォームから戻される値を設定するページ・アイテムP1_ENAMEを指定します。


戻すアイテムが複数ある場合は、TRUEアクションの値の設定を追加します。


2023年9月8日金曜日

Oracle JETのGanttチャートを直接Oracle APEXで操作する

Oracle APEXのチャートのひとつにGanttチャートが含まれています。このチャートにはOracle JETのGanttチャートが使われていますが、使用できる機能はかなり限定されています。

Oracle JETのGanttチャートを(Oracle APEXのチャートとしてではなく)、直接APEXアプリケーションに実装してみます。理論上はOracle JETのGanttチャートが提供している機能をすべて利用できるようになります。

作成したアプリケーションは以下のように動作します。


Oracle JET Cookbookで紹介されているGanttのOverviewを実装しています。
https://www.oracle.com/webfolder/technetwork/jet/jetcookbook.html?component=gantt&demo=overview


いくつかのテキストやアイコンが表示されていません。CSS変数の定義の違いに影響されているのだと思われますが、原因については調べていません。

以下より実装について説明します。

Oracle JET CookbookではGanttチャートの表示に使用するデータを、rowData.jsonとdepData.jsonの2つの静的ファイルから読み込んでいます。このデータについては、データベースに表を作成してデータを投入します。

以下のDDLを実行し、表JGANTT_ROWSJGANTT_TASKSJGANTT_REFERENCE_OBJECTSJGANTT_DEPENDENCIESの4つの表を作成します。

-- create tables
create table jgantt_rows (
row_id varchar2(80 char) not null,
parent_row_id varchar2(80 char),
label varchar2(80 char)
)
;
-- table index
create index jgantt_rows_i1 on jgantt_rows (row_id);
create table jgantt_tasks (
task_id varchar2(80 char) not null,
row_id varchar2(80 char) not null,
attribute_desc varchar2(80 char),
svg_class_name varchar2(80 char),
start_date date,
end_date date,
downtime_start_date date,
downtime_end_date date,
overtime_start_date date,
overtime_end_date date
)
;
-- table index
create index jgantt_tasks_i1 on jgantt_tasks (row_id);
create index jgantt_tasks_i52 on jgantt_tasks (task_id);
create table jgantt_reference_objects (
row_id varchar2(80 char) not null,
id number generated by default on null as identity
constraint jgantt_reference_o_id_pk primary key,
start_date date,
end_date date
)
;
-- table index
create index jgantt_reference_o_i1 on jgantt_reference_objects (row_id);
-- create tables
create table jgantt_dependencies (
id varchar2(80 char) not null,
predecessor varchar2(80 char),
successor varchar2(80 char)
)
;
-- load data
SQLワークショップSQLスクリプトから実行します。


表が作成されたら、データをロードします。

ネットワーク経由でrowData.jsonを取得し、JSONをパースして表に投入します。以下のスクリプトを実行します。

declare
l_response clob;
l_response_json json_array_t;
e_load_data_failed exception;
/* 前後のごみ取り */
l_start pls_integer;
l_top_row_count pls_integer;
/* rows object */
l_row_object json_object_t;
procedure process_row(
p_row_object in json_object_t
,p_parent_row_id in jgantt_rows.parent_row_id%type default null
)
as
l_row_id jgantt_rows.row_id%type;
l_label jgantt_rows.label%type;
l_parent_row_id jgantt_rows.parent_row_id%type;
/* referenceObjects */
l_reference_objects json_array_t;
l_reference_object_count pls_integer;
l_reference_object json_object_t;
r_reference_object jgantt_reference_objects%rowtype;
/* task object */
l_tasks json_array_t;
l_task_count pls_integer;
l_task_object json_object_t;
r_task jgantt_tasks%rowtype;
/* row object */
l_rows json_array_t;
l_row_count pls_integer;
l_row_object json_object_t;
r_reference_objects jgantt_reference_objects%rowtype;
begin
l_row_id := p_row_object.get_string('id');
l_label := p_row_object.get_string('label');
insert into jgantt_rows(row_id, label, parent_row_id) values(l_row_id, l_label, p_parent_row_id);
-- referenceObjectsの扱い。
l_reference_objects := p_row_object.get_array('referenceObjects');
if l_reference_objects is not null then
l_reference_object_count := l_reference_objects.get_size();
for j in 1..l_reference_object_count
loop
l_reference_object := treat(l_reference_objects.get(j-1) as json_object_t);
r_reference_objects.row_id := l_row_id;
r_reference_objects.start_date := l_reference_object.get_timestamp('start');
r_reference_objects.end_date := l_reference_object.get_timestamp('end');
insert into jgantt_reference_objects values r_reference_objects;
end loop;
end if;
-- tasksの扱い。
l_tasks := p_row_object.get_array('tasks');
if l_tasks is not null then
l_task_count := l_tasks.get_size();
for j in 1..l_task_count
loop
l_task_object := treat(l_tasks.get(j-1) as json_object_t);
r_task.row_id := l_row_id;
r_task.task_id := l_task_object.get_string('id');
r_task.attribute_desc := l_task_object.get_string('attributeDesc');
r_task.svg_class_name := l_task_object.get_string('svgClassName');
r_task.start_date := l_task_object.get_timestamp('start');
r_task.end_date := l_task_object.get_timestamp('end');
r_task.downtime_start_date := l_task_object.get_timestamp('downtimeStart');
r_task.downtime_end_date := l_task_object.get_timestamp('downtimeEnd');
r_task.overtime_start_date := l_task_object.get_timestamp('overtimeStart');
r_task.overtime_end_date := l_task_object.get_timestamp('overtimeEnd');
insert into jgantt_tasks values r_task;
end loop;
end if;
-- rowsの扱い。
l_rows := p_row_object.get_array('rows');
if l_rows is not null then
l_row_count := l_rows.get_size();
for j in 1..l_row_count
loop
l_row_object := treat(l_rows.get(j-1) as json_object_t);
process_row(l_row_object, l_row_id);
end loop;
end if;
end process_row;
begin
apex_web_service.set_request_headers('Content-Type','application/json');
l_response := apex_web_service.make_rest_request(
p_url => 'https://www.oracle.com/webfolder/technetwork/jet/cookbook/dataVisualizations/gantt/overview/rowData.json'
,p_http_method => 'GET'
);
if apex_web_service.g_status_code <> 200 then
raise e_load_data_failed;
end if;
l_start := instr(l_response, '[');
l_response := substr(l_response, l_start);
-- dbms_output.put_line(substr(l_response,1,100));
l_response_json := json_array_t(l_response);
l_top_row_count := l_response_json.get_size();
for i in 1..l_top_row_count
loop
l_row_object := treat(l_response_json.get(i-1) as json_object_t);
process_row(l_row_object);
end loop;
end;

SQLワークショップSQLコマンドから実行します。


同様にdepData.jsonを取得し、表にロードします。

declare
l_response clob;
l_response_json json_array_t;
e_load_data_failed exception;
/* 前後のごみ取り */
l_start pls_integer;
l_count pls_integer;
/* rows object */
l_object json_object_t;
r_dependencies jgantt_dependencies%rowtype;
begin
apex_web_service.set_request_headers('Content-Type','application/json');
l_response := apex_web_service.make_rest_request(
p_url => 'https://www.oracle.com/webfolder/technetwork/jet/cookbook/dataVisualizations/gantt/overview/depData.json'
,p_http_method => 'GET'
);
if apex_web_service.g_status_code <> 200 then
raise e_load_data_failed;
end if;
l_start := instr(l_response, '[');
l_response := substr(l_response, l_start);
-- dbms_output.put_line(substr(l_response,1,100));
l_response_json := json_array_t(l_response);
l_count := l_response_json.get_size();
for i in 1..l_count
loop
l_object := treat(l_response_json.get(i-1) as json_object_t);
r_dependencies.id := l_object.get_string('id');
r_dependencies.predecessor := l_object.get_string('predecessor');
r_dependencies.successor := l_object.get_string('successor');
insert into jgantt_dependencies values r_dependencies;
end loop;
end;


続いて反対に、表に保存されているデータをJSON形式で取り出すRESTサービスを作成します。返されるデータが大きいため、Ajaxコールバックでは実装できません。

モジュール・ベース・パス/jgantt/URLテンプレートoverviewメソッドGETを指定して、リソース・ハンドラを作成します。

ソース・タイプとしてPL/SQLを選択し、ソースに以下を記述します。

declare
l_response_json json_object_t;
l_response blob;
/* rowData.jsonの生成に使う */
l_rows json_array_t;
l_row_object json_object_t;
/* depData.jsonの生成に使う */
l_deps json_array_t;
l_dep json_object_t;
/*
* rowオブジェクトを生成する
*/
function get_row_object(
p_row_id in jgantt_rows.row_id%type
)
return json_object_t
as
l_row_object json_object_t;
/* referenceObjects */
l_reference_objects json_array_t;
l_reference_object json_object_t;
/* tasks */
l_tasks json_array_t;
l_task json_object_t;
/* rows */
l_rows json_array_t;
begin
l_row_object := json_object_t();
/* row_idでの検索なので、選択されるのは一行だけ */
for r in (select * from jgantt_rows where row_id = p_row_id)
loop
l_row_object.put('id', r.row_id);
l_row_object.put('label', r.label);
/* referenceObjectsの生成 */
l_reference_objects := json_array_t();
for e in (select * from jgantt_reference_objects where row_id = r.row_id)
loop
l_reference_object := json_object_t();
if e.start_date is not null then l_reference_object.put('start', e.start_date); end if;
if e.end_date is not null then l_reference_object.put('end', e.end_date); end if;
l_reference_objects.append(l_reference_object);
end loop;
if l_reference_objects.get_size() > 0 then
l_row_object.put('referenceObjects', l_reference_objects);
end if;
/* tasksの生成 */
l_tasks := json_array_t();
for t in (select * from jgantt_tasks where row_id = r.row_id)
loop
l_task := json_object_t();
l_task.put('id', t.task_id);
if t.attribute_desc is not null then l_task.put('attributeDesc', t.attribute_desc); end if;
if t.svg_class_name is not null then l_task.put('svgClassName', t.svg_class_name); end if;
if t.start_date is not null then l_task.put('start', t.start_date); end if;
if t.end_date is not null then l_task.put('end', t.end_date); end if;
if t.downtime_start_date is not null then l_task.put('downtimeStart', t.downtime_start_date); end if;
if t.downtime_end_date is not null then l_task.put('downtimeEnd', t.downtime_end_date); end if;
if t.overtime_start_date is not null then l_task.put('overtimeStart', t.downtime_start_date); end if;
if t.overtime_end_date is not null then l_task.put('overtimeEnd', t.downtime_end_date); end if;
l_tasks.append(l_task);
end loop;
if l_tasks.get_size() > 0 then
l_row_object.put('tasks', l_tasks);
end if;
/* 子供のrowsの生成 */
l_rows := json_array_t();
for c in (select row_id from jgantt_rows where parent_row_id = r.row_id)
loop
l_rows.append(get_row_object(c.row_id));
end loop;
if l_rows.get_size() > 0 then
l_row_object.put('rows', l_rows);
end if;
end loop;
return l_row_object;
end get_row_object;
begin
/*
* rowData.jsonに相当するデータを生成する
*/
l_rows := json_array_t();
for c in (select row_id from jgantt_rows where parent_row_id is null)
loop
l_rows.append(get_row_object(c.row_id));
end loop;
/*
* depData.jsonに相当するデータを生成する。
*/
l_deps := json_array_t();
for c in (select * from jgantt_dependencies)
loop
l_dep := json_object_t();
l_dep.put('id', c.id);
l_dep.put('predecessor', c.predecessor);
l_dep.put('successor', c.successor);
l_deps.append(l_dep);
end loop;
l_response_json := json_object_t();
l_response_json.put('rowData', l_rows);
l_response_json.put('depData', l_deps);
l_response := l_response_json.to_blob();
owa_util.mime_header('application/json');
wpg_docload.download_file(l_response);
end;
APEXアプリケーションからは完全なURLを呼び出してデータを取得するため、このURLを記録しておきます。


アプリケーション作成ウィザードを起動し、空のアプリケーションを作成します。名前JET Native Ganttとします。作成されたアプリケーションのホーム・ページをページ・デザイナで開きます。

Breadcrumb BarにあるリージョンJET Native Ganttを削除します。その後にBodyに新規にリージョンを作成します。

識別タイトルGanttタイプととして静的コンテンツを選択します。ソースHTMLコードに以下を記述します。これはOracle JET Cookbookのdemo.htmlとほぼ同じで、<head>...</head>を取り除いています。

<html lang="en-us" style="height:100%;" dir="ltr">
<body class="demo-disable-bg-image">
<div id="sampleDemo" class="demo-padding demo-container">
<div id="componentDemoContent" style="width: 1px; min-width: 100%;">
<div id="container">
<div class="oj-panel oj-bg-info-30 oj-sm-margin-4x-bottom">
<h2 id="h1" class="oj-typography-subheading-md">Options To Control The Gantt Below</h2>
<oj-form-layout user-assistance-density="reflow" max-columns="1" direction="row">
<oj-checkboxset
id="task-elements"
class="oj-choice-direction-row"
label-hint="Task elements to show"
value="[[taskElementsDetails]]"
on-value-changed="[[handleTaskElementsSettings]]">
<oj-option value="attribute">Attribute</oj-option>
<oj-option value="overtime">Overtime</oj-option>
<oj-option value="downtime">Downtime</oj-option>
</oj-checkboxset>
<oj-checkboxset
id="toggles"
class="oj-choice-direction-row"
label-hint="Controls"
value="[[togglesDetails]]"
on-value-changed="[[handleTogglesSettings]]">
<oj-option value="timeCursor">Time Cursor</oj-option>
<oj-option value="zooming">Zooming</oj-option>
</oj-checkboxset>
</oj-form-layout>
</div>
<oj-gantt
id="gantt"
start="[[projectStartDate.toISOString()]]"
end="[[projectEndDate.toISOString()]]"
viewport-start="[[viewportStart.toISOString()]]"
viewport-end="[[viewportEnd.toISOString()]]"
gridlines.vertical="visible"
expanded="{{expanded}}"
zooming="[[zooming]]"
time-cursor="[[timeCursor]]"
row-axis.rendered="on"
row-axis.width="210px"
major-axis.scale="days"
major-axis.converter.days="[[dateConverter]]"
major-axis.zoom-order='[[ ["weeks","days",custom8HrScale,"hours"] ]]'
minor-axis.scale="[[custom8HrScale]]"
minor-axis.zoom-order='[[ ["weeks","days",custom8HrScale,"hours"] ]]'
selection-mode="multiple"
selection-behavior="highlightDependencies"
dnd.move.tasks="enabled"
on-oj-move="[[handleMove]]"
task-defaults.resizable="enabled"
on-oj-resize="[[handleResize]]"
task-aggregation="on"
dependency-line-shape="straight"
reference-objects="[[referenceObjects]]"
row-data="[[dataProvider]]"
dependency-data="[[dependenciesDataProvider]]"
:aria-label='[["Gantt Chart. Current date is " + currentDateFormatted ]]'
class="demo-gantt">
<template slot="rowMappingTemplate" data-oj-as="row">
<oj-gantt-row
reference-objects="[[row.data.referenceObjects]]"
tasks="[[row.data.tasks]]"
label="[[row.data.label]]"
short-desc="[[getRowDesc(row)]]"></oj-gantt-row>
</template>
<template slot="taskMappingTemplate" data-oj-as="task">
<oj-gantt-task
task-id="[[task.data.id]]"
start="[[task.data.start]]"
end="[[task.data.end]]"
height="[[task.data.svgClassName === 'demo-gantt-task-hold' ? 12 : null]]"
border-radius="[[task.data.svgClassName === 'demo-gantt-task-hold' ? '0' : null]]"
attribute.rendered="[[!showAttribute() || task.data.svgClassName === 'demo-gantt-task-hold' ? 'off' : 'on']]"
attribute.short-desc="Attribute Description"
downtime.start="[[showDowntime() ? task.data.downtimeStart : null]]"
downtime.end="[[showDowntime() ? task.data.downtimeEnd : null]]"
overtime.start="[[showOvertime() ? task.data.overtimeStart : null]]"
overtime.end="[[showOvertime() ? task.data.overtimeEnd : null]]"
svg-class-name="[[task.data.svgClassName]]"></oj-gantt-task>
</template>
<template slot="dependencyTemplate" data-oj-as="dependency">
<oj-gantt-dependency
predecessor-task-id="[[dependency.data.predecessor]]"
successor-task-id="[[dependency.data.successor]]"></oj-gantt-dependency>
</template>
<template slot="referenceObjectMappingTemplate" data-oj-as="ref">
<oj-gantt-reference-object
start="[[ref.data.start]]"
end="[[ref.data.end]]"></oj-gantt-reference-object>
</template>
<template slot="rowAxisLabelTemplate" data-oj-as="rowAxisLabel">
<svg class="demo-gantt-row-label">
<foreignObject
:x="[[getRowLabelX(rowAxisLabel.maxWidth)]]"
y="0"
:width="[[rowAxisLabel.maxWidth]]"
:height="[[rowAxisLabel.maxHeight]]">
<div class="oj-flex oj-sm-align-items-center demo-full-size">
<span :class='[[ {"oj-typography-semi-bold": !rowAxisLabel.leaf} ]]'>
<oj-bind-text value="[[rowAxisLabel.rowData.label]]"></oj-bind-text>
</span>
<oj-bind-if test="[[!rowAxisLabel.leaf && !expanded().has(rowAxisLabel.rowData.id)]]">
<span class="oj-badge oj-badge-success oj-badge-subtle oj-sm-margin-2x-start">
<oj-bind-text value="[['+' + rowAxisLabel.data.rows.length]]"></oj-bind-text>
</span>
</oj-bind-if>
<oj-bind-if test="[[!rowAxisLabel.leaf]]">
<span
class="oj-icon-color-danger oj-ux-ico-triangle-up-s oj-ux-icon-size-2x oj-sm-margin-2x-start"></span>
</oj-bind-if>
</div>
</foreignObject>
</svg>
</template>
</oj-gantt>
<br />
<p>
Task Action:
<span id="results" class="italic oj-typography-body-md">
<oj-bind-text value="[[dndAction]]"></oj-bind-text>
</span>
</p>
</div>
</div>
</div>
</body>
</html>
外観テンプレートとして、Blank with Attributes (No Grid)を選択しています。


ページ・プロパティJavaScriptファイルURLに以下を記述します。

[require jet]

ファンクションおよびグローバル変数の宣言に以下を記述します。

var view;

ページ・ロード時に実行に以下を記述します。

require(["require", "exports", "knockout", "ojs/ojbootstrap", "ojs/ojarraydataprovider", "ojs/ojarraytreedataprovider", "ojs/ojconverter-datetime", "ojs/ojknockout-keyset", "ojs/ojknockout", "ojs/ojgantt", "ojs/ojcheckboxset", "ojs/ojformlayout"], function (require, exports, ko, ojbootstrap_1, ArrayDataProvider, ArrayTreeDataProvider, ojconverter_datetime_1, ojknockout_keyset_1) {
"use strict";
class DemoCustomScaleNHr {
constructor(N) {
this.converter = new ojconverter_datetime_1.IntlDateTimeConverter({
hour: '2-digit',
hour12: true
});
this.hour = 60 * 60 * 1000;
this.name = `${N}hr`;
this.N = N;
}
formatter(date) {
return this.converter.format(date);
}
getNextDate(date) {
return new Date(new Date(date).getTime() + this.N * this.hour).toISOString();
}
getPreviousDate(date) {
const d = new Date(date);
d.setHours(Math.floor(d.getHours() / this.N) * this.N, 0, 0, 0);
return d.toISOString();
}
}
class ViewModel {
constructor(data) {
this.dataProvider = new ArrayTreeDataProvider(data.rowData, {
keyAttributes: 'id',
childrenAttribute: 'rows'
});
this.dependenciesDataProvider = new ArrayDataProvider(data.depData, {
keyAttributes: 'id'
});
this.taskElementsDetails = [];
this.togglesDetails = [];
this.showAttribute = ko.observable(false);
this.showOvertime = ko.observable(false);
this.showDowntime = ko.observable(false);
this.timeCursor = ko.observable('off');
this.zooming = ko.observable('off');
this.dndAction = ko.observable('(Move or Resize a Task)');
this.handleTaskElementsSettings = (event) => {
this.taskElementsDetails = event.detail.value;
this.handleSettings(this.taskElementsDetails.concat(this.togglesDetails));
};
this.handleTogglesSettings = (event) => {
this.togglesDetails = event.detail.value;
this.handleSettings(this.taskElementsDetails.concat(this.togglesDetails));
};
this.handleSettings = (details) => {
this.showAttribute(details.indexOf('attribute') !== -1);
this.showOvertime(details.indexOf('overtime') !== -1);
this.showDowntime(details.indexOf('downtime') !== -1);
this.timeCursor(details.indexOf('timeCursor') !== -1 ? 'on' : 'off');
this.zooming(details.indexOf('zooming') !== -1 ? 'on' : 'off');
};
this.handleMove = (event) => {
const taskContexts = event.detail.taskContexts;
const rowContext = event.detail.rowContext;
const dropDate = event.detail.value;
this.dndAction(`${taskContexts.length} task(s) dropped on ${rowContext.rowData.label} at ${dropDate}`);
};
this.handleResize = (event) => {
const taskContexts = event.detail.taskContexts;
const dropDate = event.detail.value;
this.dndAction(`${taskContexts.length} task(s) resized to ${dropDate}`);
};
this.expanded = new ojknockout_keyset_1.ObservableKeySet().add(['Mixer A', 'Packaging A']);
this.projectStartDate = new Date('2020-10-01T00:00:00');
this.projectEndDate = new Date('2020-10-31T00:00:00');
// 8 hours scale
this.custom8HrScale = new DemoCustomScaleNHr(8);
// Date converter
this.dateConverter = new ojconverter_datetime_1.IntlDateTimeConverter({
formatType: 'date',
dateFormat: 'long'
});
this.timeConverter = new ojconverter_datetime_1.IntlDateTimeConverter({
formatType: 'time'
});
this.currentDate = new Date('Oct 03, 2020, 17:00:00');
this.currentDateString = this.currentDate.toISOString();
this.currentDateFormatted = this.dateConverter.format(this.currentDateString);
// set viewport to cover two days before and after
this.day = 1000 * 60 * 60 * 24;
this.viewportStart = new Date('Oct 03, 2020');
this.viewportEnd = new Date(this.viewportStart.getTime() + 3 * this.day);
this.referenceObjects = [
{
value: this.currentDateString,
label: this.timeConverter.format(this.currentDateString),
svgClassName: 'demo-current-time-indicator'
}
];
this.getRowDesc = (row) => {
const desc = [row.data.label];
if (row.data.rows) {
desc.push(`${row.data.rows.length} siblings`);
}
desc.push('1 issue');
return desc.join(', ');
};
// Helper function to get appropriate row label x position depending on document reading direction
this.getRowLabelX = (rowAxisWidth) => {
const dir = document.documentElement.getAttribute('dir');
return dir === 'ltr' ? '0' : -rowAxisWidth;
};
}
}
(0, ojbootstrap_1.whenDocumentReady)().then(() => {
fetch("&G_DATA_URL!RAW.")
.then((response) => response.json())
.then( (data) => {
view = new ViewModel(data);
ko.applyBindings(view, document.getElementById('container'));
}
);
});
});
CSSのファイルURLに以下を記述します。

#JET_CSS_DIRECTORY#redwood/oj-redwood-notag-min.css

CSSインラインに以下を記述します。本来はもっと調整が必要な内容です。

.demo-gantt {
width: 100%;
height: 25rem;
}
.demo-full-size {
width: 100%;
height: 100%;
}
.demo-gantt .demo-gantt-row-label {
overflow: visible;
}
.demo-current-time-indicator {
stroke: var(--oj-dvt-danger-color);
}
.demo-gantt-task {
fill: rgb(var(--oj-palette-neutral-rgb-0));
}
.demo-gantt-task-emphasis-low {
fill: rgba(var(--oj-palette-dvt-rgb-2), 0.2);
}
.demo-gantt-task-emphasis-high {
fill: rgb(var(--oj-palette-dvt-rgb-7));
}
.demo-gantt-task-hold {
fill: rgb(var(--oj-palette-neutral-rgb-130));
}


JSONデータを返すRESTサービスのURLは、アプリケーション定義に置換文字列G_DATA_URLとして定義します。


以上でアプリケーションは完成です。アプリケーションを実行すると、記事の先頭のGIF動画のように動作します。

Oracle JET自体はオープンソースのJavaScriptライブラリであり、利用に当たって費用が発生しないかわりに、開発の主体であるオラクルからサポートを受けることもできません。Oracle APEXでは、APEXが使用している範囲であればOracle JETについてもSRを受け付けますが、Oracle JETをサポートしているわけではありません。

世の中には、Ganttチャートを実装したJavaScriptライブラリがいくつがあります。例えばAnyChartのAnyGanttやHighchartsのHighcarts Ganttなどです。これらは有償ですが、技術サポートが含まれています。単純に無料で使えるのでOracle JETを使って実装しようと考える前に、サポートが付いている商用のライブラリの使用を検討しても良いかと思います。

今回作成したAPEXアプリケーションのエクスポートを以下に置きました。
https://github.com/ujnak/apexapps/blob/master/exports/jet-native-gantt.zip

Oracle APEXのアプリケーション作成の参考になれば幸いです。