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.

