506 lines
14 KiB
ObjectPascal
506 lines
14 KiB
ObjectPascal
unit dddToolsAdminMain;
|
|
|
|
{$I Synopse.inc} // define HASINLINE CPU32 CPU64 OWNNORMTOUPPER
|
|
|
|
interface
|
|
|
|
uses
|
|
Windows,
|
|
Messages,
|
|
SysUtils,
|
|
Variants,
|
|
Classes,
|
|
Graphics,
|
|
Controls,
|
|
Forms,
|
|
Dialogs,
|
|
Clipbrd,
|
|
mORMotUI,
|
|
mORMotUILogin,
|
|
mORMotToolbar,
|
|
SynTaskDialog,
|
|
SynCommons,
|
|
mORMot,
|
|
mORMotHttpClient,
|
|
mORMotDDD,
|
|
dddInfraApps,
|
|
dddToolsAdminDB,
|
|
dddToolsAdminLog;
|
|
|
|
type
|
|
TAdminSaveOrExport = (expSaveGrid, expCopyGrid, expCopyRow);
|
|
|
|
TAdminControl = class(TWinControl)
|
|
protected
|
|
fClient: TSQLHttpClientWebsockets;
|
|
fAdmin: IAdministratedDaemon;
|
|
fDatabases: TRawUTF8DynArray;
|
|
fPage: TSynPager;
|
|
fPages: array of TSynPage;
|
|
fLogFrame: TLogFrame;
|
|
fLogFrames: TLogFrameDynArray;
|
|
fChatPage: TSynPage;
|
|
fChatFrame: TLogFrame;
|
|
fDBFrame: TDBFrameDynArray;
|
|
fDefinition: TDDDRestClientSettings;
|
|
fDlgSave: TSaveDialog;
|
|
public
|
|
LogFrameClass: TLogFrameClass;
|
|
DBFrameClass: TDBFrameClass;
|
|
State: record
|
|
raw: TDocVariantData;
|
|
daemon: RawUTF8;
|
|
version: RawUTF8;
|
|
mem: RawUTF8;
|
|
clients: integer;
|
|
exceptions: TRawUTF8DynArray;
|
|
lasttix: Int64;
|
|
end;
|
|
SavePrefix: TFileName;
|
|
OnBeforeExecute: TOnExecute;
|
|
OnAfterExecute: TOnExecute;
|
|
OnAfterGetState: TNotifyEvent;
|
|
destructor Destroy; override;
|
|
function Open(Definition: TDDDRestClientSettings; Model: TSQLModel = nil): boolean; virtual;
|
|
procedure Show; virtual;
|
|
procedure GetState;
|
|
function AddPage(const aCaption: RawUTF8): TSynPage; virtual;
|
|
function AddDBFrame(const aCaption, aDatabaseName: RawUTF8; aClass:
|
|
TDBFrameClass): TDBFrame; virtual;
|
|
function AddLogFrame(page: TSynPage; const aCaption, aEvents, aPattern: RawUTF8;
|
|
aClass: TLogFrameClass): TLogFrame; virtual;
|
|
procedure EndLog(aLogFrame: TLogFrame); virtual;
|
|
procedure OnPageChange(Sender: TObject); virtual;
|
|
function CurrentDBFrame: TDBFrame;
|
|
function FindDBFrame(const aDatabaseName: RawUTF8): TDBFrame;
|
|
procedure FormKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState); virtual;
|
|
procedure SaveOrExport(Fmt: TAdminSaveOrExport; const ContextName: string = '';
|
|
DB: TDBFrame = nil);
|
|
property Client: TSQLHttpClientWebsockets read fClient;
|
|
property Page: TSynPager read fPage;
|
|
property LogFrame: TLogFrame read fLogFrame;
|
|
property DBFrame: TDBFrameDynArray read fDBFrame;
|
|
property ChatPage: TSynPage read fChatPage;
|
|
property ChatFrame: TLogFrame read fChatFrame;
|
|
property Admin: IAdministratedDaemon read fAdmin;
|
|
end;
|
|
|
|
TAdminForm = class(TSynForm)
|
|
procedure FormCreate(Sender: TObject);
|
|
procedure FormShow(Sender: TObject);
|
|
protected
|
|
fFrame: TAdminControl;
|
|
public
|
|
property Frame: TAdminControl read fFrame;
|
|
end;
|
|
|
|
var
|
|
AdminForm: TAdminForm;
|
|
|
|
function AskForUserIfVoid(Definition: TDDDRestClientSettings): boolean;
|
|
|
|
implementation
|
|
|
|
{$R *.dfm}
|
|
|
|
function AskForUserIfVoid(Definition: TDDDRestClientSettings): boolean;
|
|
var
|
|
U, P: string;
|
|
begin
|
|
result := false;
|
|
if Definition.ORM.User = '' then
|
|
if TLoginForm.Login(Application.Mainform.Caption, FormatString(
|
|
'Credentials for %', [Definition.ORM.ServerName]), U, P, true, '') then begin
|
|
Definition.ORM.User := StringToUTF8(U);
|
|
Definition.ORM.PasswordPlain := StringToUTF8(P);
|
|
end
|
|
else
|
|
exit;
|
|
result := true;
|
|
end;
|
|
|
|
var
|
|
AdminControlConnecting: TForm; // only one Open() attempt at once
|
|
|
|
function TAdminControl.Open(Definition: TDDDRestClientSettings; Model: TSQLModel): boolean;
|
|
begin
|
|
result := false;
|
|
if Assigned(fAdmin) or (Definition.Orm.User = '') or Assigned(AdminControlConnecting) then
|
|
exit;
|
|
try
|
|
AdminControlConnecting := CreateTempForm('Connecting to ' + string(Definition.ORM.ServerName));
|
|
try
|
|
Application.ProcessMessages;
|
|
if Model = nil then
|
|
Model := TSQLModel.Create([], '');
|
|
Model.OnClientIdle := TLoginForm.OnIdleProcess; // allow basic UI interactivity
|
|
fClient := AdministratedDaemonClient(Definition, Model);
|
|
if not fClient.Services.Resolve(IAdministratedDaemon, fAdmin) then
|
|
raise EDDDRestClient.CreateUTF8('Resolve(IAdministratedDaemon)=false: check % version',
|
|
[Definition.ORM.ServerName]);
|
|
GetState;
|
|
fDefinition := Definition;
|
|
result := true;
|
|
finally
|
|
FreeAndNil(AdminControlConnecting);
|
|
if fClient <> nil then
|
|
fClient.OnIdle := nil; // back to default blocking behavior (safer UI)
|
|
end;
|
|
except
|
|
on E: Exception do begin
|
|
ShowException(E);
|
|
FreeAndNil(fClient);
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
procedure TAdminControl.GetState;
|
|
var
|
|
exec: TServiceCustomAnswer;
|
|
begin
|
|
if self = nil then
|
|
exit;
|
|
try
|
|
if fAdmin <> nil then begin
|
|
State.raw.Clear;
|
|
exec := fAdmin.DatabaseExecute('', '#info');
|
|
if (exec.Content = '') or (exec.Content[1] <> '{') then
|
|
exec := fAdmin.DatabaseExecute('', '#state'); // backward compatibility
|
|
State.raw.InitJSONInPlace(pointer(exec.Content), JSON_OPTIONS_FAST);
|
|
State.raw.GetAsRawUTF8('daemon', State.daemon);
|
|
if not State.raw.GetAsRawUTF8('version', State.version) then
|
|
State.version := fClient.SessionVersion;
|
|
State.mem := State.raw.U['memused'];
|
|
if State.mem = '' then
|
|
KBU(state.Raw.O['SystemMemory'].O['Allocated'].I['Used'] shl 10, State.mem);
|
|
State.clients := State.raw.I['clients'];
|
|
State.raw.GetAsDocVariantSafe('exception')^.ToRawUTF8DynArray(State.exceptions);
|
|
State.raw.AddValue('remoteip', fClient.Server + ':' + fClient.Port);
|
|
State.lasttix := GetTickCount64;
|
|
end;
|
|
if Assigned(OnAfterGetState) then
|
|
OnAfterGetState(self);
|
|
except
|
|
Finalize(State);
|
|
end;
|
|
end;
|
|
|
|
procedure TAdminControl.Show;
|
|
var
|
|
i, n: integer;
|
|
f: TDBFrame;
|
|
begin
|
|
if (fClient = nil) or (fAdmin = nil) or (fPage <> nil) then
|
|
exit; // show again after hide
|
|
if LogFrameClass = nil then
|
|
LogFrameClass := TLogFrame;
|
|
if DBFrameClass = nil then
|
|
DBFrameClass := TDBFrame;
|
|
fDatabases := fAdmin.DatabaseList;
|
|
fPage := TSynPager.Create(self);
|
|
fPage.ControlStyle := fPage.ControlStyle + [csClickEvents]; // enable OnDblClick
|
|
fPage.Parent := self;
|
|
fPage.Align := alClient;
|
|
fPage.OnChange := OnPageChange;
|
|
n := length(fDatabases);
|
|
fLogFrame := AddLogFrame(nil, 'log', '', '', LogFrameClass);
|
|
if n > 0 then begin
|
|
for i := 0 to n - 1 do begin
|
|
f := AddDBFrame(fDatabases[i], fDatabases[i], DBFrameClass);
|
|
f.Open;
|
|
if i = 0 then begin
|
|
fPage.ActivePageIndex := 1;
|
|
f.SetResult(State.raw.ToJSON('', '', jsonUnquotedPropName));
|
|
end;
|
|
end;
|
|
Application.ProcessMessages;
|
|
fDBFrame[0].mmoSQL.SetFocus;
|
|
end;
|
|
fChatPage := AddPage('Chat');
|
|
fChatPage.TabVisible := false;
|
|
end;
|
|
|
|
procedure TAdminControl.EndLog(aLogFrame: TLogFrame);
|
|
begin
|
|
if aLogFrame <> nil then
|
|
try
|
|
Screen.Cursor := crHourGlass;
|
|
if aLogFrame.Callback <> nil then begin
|
|
fClient.Services.CallBackUnRegister(aLogFrame.Callback);
|
|
aLogFrame.Callback := nil;
|
|
Sleep(10);
|
|
end;
|
|
aLogFrame.Closing;
|
|
finally
|
|
Screen.Cursor := crDefault;
|
|
end;
|
|
end;
|
|
|
|
destructor TAdminControl.Destroy;
|
|
var
|
|
i: integer;
|
|
begin
|
|
if fClient <> nil then
|
|
fClient.OnIdle := TLoginForm.OnIdleProcess; // allow basic UI interactivity
|
|
for i := 0 to high(fLogFrames) do begin
|
|
EndLog(fLogFrames[i]);
|
|
fLogFrames[i].Admin := nil;
|
|
fLogFrames[i] := nil;
|
|
end;
|
|
Finalize(fLogFrames);
|
|
for i := 0 to high(fDBFrame) do
|
|
fDBFrame[i].Admin := nil;
|
|
fDBFrame := nil;
|
|
fAdmin := nil;
|
|
fDefinition.Free;
|
|
if fClient <> nil then begin
|
|
for i := 1 to 5 do begin
|
|
Sleep(50); // leave some time to flush all pending CallBackUnRegister()
|
|
Application.ProcessMessages;
|
|
end;
|
|
FreeAndNil(fClient);
|
|
end;
|
|
inherited Destroy;
|
|
end;
|
|
|
|
procedure TAdminControl.FormKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState);
|
|
|
|
procedure LogKeys(aLogFrame: TLogFrame);
|
|
var ch: char;
|
|
begin
|
|
if aLogFrame <> nil then
|
|
case Key of
|
|
VK_F3:
|
|
if Shift = [] then
|
|
aLogFrame.btnSearchNextClick(aLogFrame.btnSearchNext)
|
|
else
|
|
aLogFrame.btnSearchNextClick(aLogFrame.btnSearchPrevious);
|
|
ord('A')..ord('Z'), ord('0')..ord('9'), 32:
|
|
if (Shift = []) and (aLogFrame.ClassType <> TLogFrameChat) and not
|
|
aLogFrame.edtSearch.Focused then begin
|
|
ch := Char(Key);
|
|
if (Key in [ord('A')..ord('Z')]) and (GetKeyState(VK_CAPITAL) and 1=0) then
|
|
inc(ch,32); // emulate capslock behavior
|
|
aLogFrame.edtSearch.Text := aLogFrame.edtSearch.Text + string(ch);
|
|
end
|
|
else if (key = ord('F')) and (ssCtrl in Shift) then begin
|
|
aLogFrame.edtSearch.SelectAll;
|
|
aLogFrame.edtSearch.SetFocus;
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
var
|
|
page: TControl;
|
|
ndx: integer;
|
|
begin
|
|
page := fPage.ActivePage;
|
|
if page = nil then
|
|
exit;
|
|
ndx := page.Tag;
|
|
if ndx > 0 then begin
|
|
ndx := ndx - 1; // see AddDBFrame()
|
|
if cardinal(ndx) < cardinal(length(fDBFrame)) then
|
|
with fDBFrame[ndx] do
|
|
case Key of
|
|
VK_F5:
|
|
btnCmdClick(btnCmd);
|
|
VK_F9:
|
|
btnExecClick(btnExec);
|
|
ord('A'):
|
|
if ssCtrl in Shift then begin
|
|
mmoSQL.SelectAll;
|
|
mmoSQL.SetFocus;
|
|
end;
|
|
ord('H'):
|
|
if ssCtrl in Shift then
|
|
btnHistoryClick(btnHistory);
|
|
end
|
|
end
|
|
else if ndx < 0 then begin
|
|
ndx := -(ndx + 1); // see AddLogFrame()
|
|
if cardinal(ndx) < cardinal(length(fLogFrames)) then
|
|
LogKeys(fLogFrames[ndx]);
|
|
end;
|
|
end;
|
|
|
|
function TAdminControl.AddPage(const aCaption: RawUTF8): TSynPage;
|
|
var
|
|
n: integer;
|
|
begin
|
|
n := length(fPages);
|
|
SetLength(fPages, n + 1);
|
|
result := TSynPage.Create(self);
|
|
result.Caption := UTF8ToString(aCaption);
|
|
result.PageControl := fPage;
|
|
fPages[n] := result;
|
|
end;
|
|
|
|
function TAdminControl.AddDBFrame(const aCaption, aDatabaseName: RawUTF8;
|
|
aClass: TDBFrameClass): TDBFrame;
|
|
var
|
|
page: TSynPage;
|
|
n: integer;
|
|
begin
|
|
page := AddPage(aCaption);
|
|
n := length(fDBFrame);
|
|
SetLength(fDBFrame, n + 1);
|
|
result := aClass.Create(self);
|
|
result.Name := FormatString('DBFrame%', [aCaption]);
|
|
result.Parent := page;
|
|
result.Align := alClient;
|
|
result.Client := fClient;
|
|
result.Admin := fAdmin;
|
|
result.DatabaseName := aDatabaseName;
|
|
result.OnBeforeExecute := OnBeforeExecute;
|
|
result.OnAfterExecute := OnAfterExecute;
|
|
result.SavePrefix := SavePrefix;
|
|
fDBFrame[n] := result;
|
|
page.Tag := n + 1; // Tag>0 -> index in fDBFrame[Tag-1] -> used in FormKeyDown
|
|
end;
|
|
|
|
function TAdminControl.AddLogFrame(page: TSynPage; const aCaption, aEvents,
|
|
aPattern: RawUTF8; aClass: TLogFrameClass): TLogFrame;
|
|
var
|
|
n: integer;
|
|
begin
|
|
if page = nil then begin
|
|
page := AddPage(aCaption);
|
|
fPage.ActivePageIndex := fPage.PageCount - 1;
|
|
end;
|
|
if aEvents = '' then
|
|
result := aClass.Create(self, fAdmin)
|
|
else
|
|
result := aClass.CreateCustom(self, fAdmin, aEvents, aPattern);
|
|
result.Parent := page;
|
|
result.Align := alClient;
|
|
n := length(fLogFrames);
|
|
SetLength(fLogFrames, n + 1);
|
|
fLogFrames[n] := result;
|
|
page.Tag := -(n + 1); // Tag<0 -> index in fLogFrames[-(Tag+1)] -> used in FormKeyDown
|
|
end;
|
|
|
|
procedure TAdminControl.OnPageChange(Sender: TObject);
|
|
var
|
|
ndx: cardinal;
|
|
begin
|
|
if fPage.ActivePage = fChatPage then begin
|
|
if fChatFrame = nil then
|
|
fChatFrame := AddLogFrame(fChatPage, '', 'Monitoring', '[CHAT] ', TLogFrameChat);
|
|
exit;
|
|
end;
|
|
ndx := fPage.ActivePageIndex - 1;
|
|
if ndx >= cardinal(Length(fDBFrame)) then
|
|
exit;
|
|
end;
|
|
|
|
function TAdminControl.CurrentDBFrame: TDBFrame;
|
|
var
|
|
ndx: cardinal;
|
|
begin
|
|
ndx := fPage.ActivePageIndex - 1;
|
|
if ndx >= cardinal(Length(fDBFrame)) then
|
|
result := nil
|
|
else
|
|
result := fDBFrame[ndx];
|
|
end;
|
|
|
|
function TAdminControl.FindDBFrame(const aDatabaseName: RawUTF8): TDBFrame;
|
|
var
|
|
i: Integer;
|
|
begin
|
|
for i := 0 to high(fDBFrame) do
|
|
if IdemPropNameU(fDBFrame[i].DatabaseName, aDatabaseName) then begin
|
|
result := fDBFrame[i];
|
|
exit;
|
|
end;
|
|
result := nil;
|
|
end;
|
|
|
|
procedure TAdminControl.SaveOrExport(Fmt: TAdminSaveOrExport;
|
|
const ContextName: string; DB: TDBFrame);
|
|
var
|
|
grid: TSQLTable;
|
|
row: integer;
|
|
name, table: RawUTF8;
|
|
begin
|
|
if DB = nil then
|
|
DB := CurrentDBFrame;
|
|
if DB = nil then
|
|
exit;
|
|
grid := DB.Grid.Table;
|
|
if (grid = nil) or (grid.RowCount = 0) then
|
|
exit;
|
|
if Fmt = expSaveGrid then begin
|
|
if fDlgSave = nil then begin
|
|
fDlgSave := TSaveDialog.Create(Owner);
|
|
fDlgSave.Options := [ofOverwritePrompt, ofHideReadOnly, ofPathMustExist,
|
|
ofEnableSizing];
|
|
fDlgSave.Filter :=
|
|
'JSON (human readable)|*.json|JSON (small)|*.json|CSV (text)|*.txt|Excel/Office (.ods)|*.ods|HTML|*.html';
|
|
fDlgSave.DefaultExt := '.html';
|
|
fDlgSave.FilterIndex := 5;
|
|
fDlgSave.InitialDir := GetShellFolderPath(CSIDL_DOCUMENTS);
|
|
end;
|
|
if PropNameValid(pointer(db.GridLastTableName)) then
|
|
name := db.GridLastTableName;
|
|
fDlgSave.FileName := SysUtils.Trim(FormatString('% % %',
|
|
[ContextName, name, NowToString(false)]));
|
|
if not fDlgSave.Execute then
|
|
exit;
|
|
case fDlgSave.FilterIndex of
|
|
1:
|
|
JSONBufferReformat(pointer(grid.GetJSONValues(true)), table);
|
|
2:
|
|
table := grid.GetJSONValues(true);
|
|
3:
|
|
table := grid.GetCSVValues(true);
|
|
4:
|
|
table := grid.GetODSDocument;
|
|
5:
|
|
table := grid.GetHtmlTable;
|
|
end;
|
|
if table <> '' then
|
|
FileFromString(table, fDlgSave.FileName);
|
|
end
|
|
else begin
|
|
case Fmt of
|
|
expCopyGrid:
|
|
table := grid.GetCSVValues(true);
|
|
expCopyRow:
|
|
begin
|
|
row := db.drwgrdResult.Row;
|
|
if row < 0 then
|
|
exit;
|
|
table := grid.GetCSVValues(true, ',', false, row, row);
|
|
end;
|
|
end;
|
|
if table <> '' then
|
|
Clipboard.AsText := UTF8ToString(table);
|
|
end;
|
|
end;
|
|
|
|
|
|
{ TAdminForm }
|
|
|
|
procedure TAdminForm.FormCreate(Sender: TObject);
|
|
begin
|
|
DefaultFont.Name := 'Tahoma';
|
|
DefaultFont.Size := 9;
|
|
Caption := FormatString('% %', [ExeVersion.ProgramName, ExeVersion.Version.Detailed]);
|
|
fFrame := TAdminControl.Create(self);
|
|
fFrame.Parent := self;
|
|
fFrame.Align := alClient;
|
|
OnKeyDown := fFrame.FormKeyDown;
|
|
end;
|
|
|
|
procedure TAdminForm.FormShow(Sender: TObject);
|
|
begin
|
|
fFrame.Show;
|
|
Caption := FormatString('% - % % via %', [ExeVersion.ProgramName,
|
|
fFrame.State.daemon, fFrame.State.version, fFrame.fDefinition.ORM.ServerName]);
|
|
end;
|
|
|
|
end.
|
|
|